Skip to content

Commit

Permalink
Decorate resolved language and optimise exercise and collection lookup (
Browse files Browse the repository at this point in the history
  • Loading branch information
gewfy authored Apr 1, 2024
2 parents b52f3f4 + b17e6ff commit b77dce5
Show file tree
Hide file tree
Showing 8 changed files with 141 additions and 61 deletions.
44 changes: 28 additions & 16 deletions client/src/lib/content/hooks/useCollectionById.spec.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,60 @@
import {renderHook} from '@testing-library/react-hooks';
import useCollectionById from './useCollectionById';

const mockT = jest.fn();
jest.mock('react-i18next', () => ({
useTranslation: jest.fn(() => ({
t: mockT,
})),
}));
const mockGetCollectionById = jest
.fn()
.mockReturnValue({name: 'some-collection'});
jest.mock('./useGetCollectionById', () => () => mockGetCollectionById);

afterEach(() => {
jest.clearAllMocks();
});

describe('useCollectionById', () => {
it('returns a translated collection', () => {
mockT.mockReturnValue({});
const {result} = renderHook(() => useCollectionById('some-collection-id'));

expect(mockT).toHaveBeenCalledTimes(1);
expect(mockT).toHaveBeenCalledWith('some-collection-id', {
returnObjects: true,
});
expect(mockGetCollectionById).toHaveBeenCalledTimes(1);
expect(mockGetCollectionById).toHaveBeenCalledWith(
'some-collection-id',
undefined,
undefined,
);

expect(result.current).toEqual({name: 'some-collection'});
});

it('returns a translated collection for a specific language', () => {
const {result} = renderHook(() =>
useCollectionById('some-collection-id', 'sv'),
);

expect(mockGetCollectionById).toHaveBeenCalledTimes(1);
expect(mockGetCollectionById).toHaveBeenCalledWith(
'some-collection-id',
'sv',
undefined,
);

expect(result.current).toEqual({});
expect(result.current).toEqual({name: 'some-collection'});
});

it('returns null when no ID is provided', () => {
mockT.mockReturnValue({});
const {result} = renderHook(() => useCollectionById(undefined));

expect(mockT).toHaveBeenCalledTimes(0);
expect(mockGetCollectionById).toHaveBeenCalledTimes(0);

expect(result.current).toBe(null);
});

it('memoizes the result - as i18next.t is not pure', () => {
mockT.mockReturnValue({});
const {result, rerender} = renderHook(() =>
useCollectionById('some-collection-id'),
);

rerender();

expect(mockT).toHaveBeenCalledTimes(1);
expect(mockGetCollectionById).toHaveBeenCalledTimes(1);
expect(result.all.length).toEqual(2);
});
});
48 changes: 38 additions & 10 deletions client/src/lib/content/hooks/useGetCollectionById.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import {act} from 'react-test-renderer';
import useGetCollectionById from './useGetCollectionById';
import useAppState from '../../appState/state/state';

const mockT = jest.fn().mockReturnValue({name: 'some-collection'});
const mockT = jest.fn().mockReturnValue({
res: {name: 'some-collection'},
usedLng: 'en',
});
jest.mock('react-i18next', () => ({
useTranslation: jest.fn(() => ({
t: mockT,
Expand All @@ -26,18 +29,21 @@ describe('useGetCollectionById', () => {

act(() => {
expect(result.current('some-collection-id')).toEqual({
language: 'en',
name: 'some-collection',
});
});

expect(mockT).toHaveBeenCalledTimes(1);
expect(mockT).toHaveBeenCalledWith('some-collection-id', {
returnObjects: true,
returnDetails: true,
keySeparator: false,
});
});

it('returns null if collection is not found', () => {
mockT.mockReturnValueOnce('some-collection-id');
mockT.mockReturnValueOnce({res: 'some-collection-id'});
const {result} = renderHook(() => useGetCollectionById());

act(() => {
Expand All @@ -46,7 +52,10 @@ describe('useGetCollectionById', () => {
});

it('returns null if collection is locked', () => {
mockT.mockReturnValueOnce({name: 'some-collection-id', locked: true});
mockT.mockReturnValueOnce({
res: {name: 'some-collection-id', locked: true},
usedLng: 'en',
});
const {result} = renderHook(() => useGetCollectionById());

act(() => {
Expand All @@ -55,17 +64,24 @@ describe('useGetCollectionById', () => {
});

it('returns a translated collection for a specific language', () => {
mockT.mockReturnValueOnce({
res: {name: 'some-collection'},
usedLng: 'sv',
});
const {result} = renderHook(() => useGetCollectionById());

act(() => {
expect(result.current('some-collection-id', 'sv')).toEqual({
name: 'some-collection',
language: 'sv',
});
});

expect(mockT).toHaveBeenCalledTimes(1);
expect(mockT).toHaveBeenCalledWith('some-collection-id', {
returnObjects: true,
returnDetails: true,
keySeparator: false,
lng: 'sv',
});
});
Expand All @@ -78,11 +94,15 @@ describe('useGetCollectionById', () => {
showOnboarding: false,
},
});
mockT.mockReturnValueOnce({name: 'some-collection', locked: true});
mockT.mockReturnValueOnce({
res: {name: 'some-collection', locked: true},
usedLng: 'en',
});
const {result} = renderHook(() => useGetCollectionById());

act(() => {
expect(result.current('some-collection-id')).toEqual({
language: 'en',
name: 'some-collection',
locked: true,
});
Expand All @@ -92,14 +112,18 @@ describe('useGetCollectionById', () => {
it('returns locked collection if id is in useUnlockedCollectionIds', () => {
mockUseUnlockedCollectionIds.mockReturnValueOnce(['some-collection-id']);
mockT.mockReturnValueOnce({
id: 'some-collection-id',
name: 'some-collection',
locked: true,
res: {
id: 'some-collection-id',
name: 'some-collection',
locked: true,
},
usedLng: 'en',
});
const {result} = renderHook(() => useGetCollectionById());

act(() => {
expect(result.current('some-collection-id')).toEqual({
language: 'en',
id: 'some-collection-id',
name: 'some-collection',
locked: true,
Expand All @@ -109,14 +133,18 @@ describe('useGetCollectionById', () => {

it('returns locked collection if ignoreLocked = true', () => {
mockT.mockReturnValueOnce({
id: 'some-collection-id',
name: 'some-collection',
locked: true,
res: {
id: 'some-collection-id',
name: 'some-collection',
locked: true,
},
usedLng: 'en',
});
const {result} = renderHook(() => useGetCollectionById());

act(() => {
expect(result.current('some-collection-id', undefined, true)).toEqual({
language: 'en',
id: 'some-collection-id',
name: 'some-collection',
locked: true,
Expand Down
28 changes: 19 additions & 9 deletions client/src/lib/content/hooks/useGetCollectionById.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,28 @@ const useGetCollectionById = () => {

return useCallback(
(id: string, language?: LANGUAGE_TAG, ignoreLocked?: boolean) => {
const collection = t(id, {
returnObjects: true,
const translation = t(id, {
lng: language,
}) as Collection;
returnObjects: true,
returnDetails: true,
keySeparator: false, // prevents object from being copied
});

// i18next fallbacks to the key if no translation is found
if (typeof translation.res !== 'object') {
return null;
}

const collection = {
...(translation.res as Collection),
language: translation.usedLng,
};

if (
// i18next fallbacks to the key if no translation is found
typeof collection !== 'object' ||
(collection.locked &&
!ignoreLocked &&
!showLockedContent &&
!unlockedCollectionIds?.includes(id))
collection.locked &&
!ignoreLocked &&
!showLockedContent &&
!unlockedCollectionIds?.includes(id)
) {
return null;
}
Expand Down
33 changes: 27 additions & 6 deletions client/src/lib/content/hooks/useGetExerciseById.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import {act} from 'react-test-renderer';
import useGetExerciseById from './useGetExerciseById';
import useAppState from '../../appState/state/state';

const mockT = jest.fn().mockReturnValue({name: 'some-exercise'});
const mockT = jest.fn().mockReturnValue({
res: {name: 'some-exercise'},
usedLng: 'en',
});
jest.mock('react-i18next', () => ({
useTranslation: jest.fn(() => ({
t: mockT,
Expand All @@ -26,18 +29,21 @@ describe('useGetExerciseById', () => {

act(() => {
expect(result.current('some-exercise-id')).toEqual({
language: 'en',
name: 'some-exercise',
});
});

expect(mockT).toHaveBeenCalledTimes(1);
expect(mockT).toHaveBeenCalledWith('some-exercise-id', {
returnObjects: true,
returnDetails: true,
keySeparator: false,
});
});

it('returns null if exercise is not found', () => {
mockT.mockReturnValueOnce('some-exercise-id');
mockT.mockReturnValueOnce({res: 'some-exercise-id'});
const {result} = renderHook(() => useGetExerciseById());

act(() => {
Expand All @@ -46,17 +52,24 @@ describe('useGetExerciseById', () => {
});

it('returns a translated exercise for a specific language', () => {
mockT.mockReturnValueOnce({
res: {name: 'some-exercise'},
usedLng: 'sv',
});
const {result} = renderHook(() => useGetExerciseById());

act(() => {
expect(result.current('some-exercise-id', 'sv')).toEqual({
language: 'sv',
name: 'some-exercise',
});
});

expect(mockT).toHaveBeenCalledTimes(1);
expect(mockT).toHaveBeenCalledWith('some-exercise-id', {
returnObjects: true,
returnDetails: true,
keySeparator: false,
lng: 'sv',
});
});
Expand All @@ -78,11 +91,15 @@ describe('useGetExerciseById', () => {
showOnboarding: false,
},
});
mockT.mockReturnValueOnce({name: 'some-exercise', locked: true});
mockT.mockReturnValueOnce({
res: {name: 'some-exercise', locked: true},
usedLng: 'en',
});
const {result} = renderHook(() => useGetExerciseById());

act(() => {
expect(result.current('some-exercise-id')).toEqual({
language: 'en',
name: 'some-exercise',
locked: true,
});
Expand All @@ -92,14 +109,18 @@ describe('useGetExerciseById', () => {
it('returns locked exercise if id is in useUnlockedExerciseIds', () => {
mockUseUnlockedExerciseIds.mockReturnValueOnce(['some-exercise-id']);
mockT.mockReturnValueOnce({
id: 'some-exercise-id',
name: 'some-exercise',
locked: true,
res: {
id: 'some-exercise-id',
name: 'some-exercise',
locked: true,
},
usedLng: 'en',
});
const {result} = renderHook(() => useGetExerciseById());

act(() => {
expect(result.current('some-exercise-id')).toEqual({
language: 'en',
id: 'some-exercise-id',
name: 'some-exercise',
locked: true,
Expand Down
25 changes: 17 additions & 8 deletions client/src/lib/content/hooks/useGetExerciseById.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,26 @@ const useGetExerciseById = () => {

return useCallback(
(id: string, language?: LANGUAGE_TAG) => {
const exercise = t(id, {
returnObjects: true,
const translation = t(id, {
lng: language,
}) as Exercise;
returnObjects: true,
returnDetails: true,
keySeparator: false, // prevents object from being copied
});

// i18next fallbacks to the key if no translation is found
if (typeof translation.res !== 'object') {
return null;
}
const exercise = {
...(translation.res as Exercise),
language: translation.usedLng,
};

if (
// i18next fallbacks to the key if no translation is found
typeof exercise !== 'object' ||
(exercise.locked &&
!showLockedContent &&
!unlockedExerciseIds.includes(exercise.id))
exercise.locked &&
!showLockedContent &&
!unlockedExerciseIds.includes(exercise.id)
) {
return null;
}
Expand Down
4 changes: 2 additions & 2 deletions client/src/lib/i18n/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
DEFAULT_LANGUAGE_TAG,
LANGUAGE_TAGS,
} from '../../../../shared/src/i18n/constants';
import Backend from './backend/backend';
import filterContent from './plugins/filterContent';
import {omitPublishableContent} from './utils/utils';

export * from '../../../../shared/src/i18n/constants';
Expand All @@ -34,7 +34,7 @@ const DEFAULT_24HOUR_LANGUAGE_TAG = 'en-gb';

export const init = () =>
i18next
.use(Backend)
.use(filterContent)
.use(initReactI18next)
.init({
lng: findBestLanguageTag(CLIENT_LANGUAGE_TAGS)?.languageTag,
Expand Down
Loading

0 comments on commit b77dce5

Please sign in to comment.