Skip to content

Commit a3636ad

Browse files
authored
fix(i18n): add onTranslateError and fallback on defaultLocal with con… (#1782)
* fix(i18n): add onTranslateError and fallback on defaultLocal with context value * test(translate): add more translate test
1 parent 69e5c80 commit a3636ad

File tree

9 files changed

+138
-41
lines changed

9 files changed

+138
-41
lines changed

.changeset/curly-trains-draw.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@scaleway/use-i18n": patch
3+
---
4+
5+
add a onTranslateError function in case of desync traduction between language. This will help to focus only on default language for developers

packages/use-i18n/src/__tests__/locales/en.json

Lines changed: 0 additions & 7 deletions
This file was deleted.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export default {
2+
'with.identifier': 'Are you sure you want to delete {identifier}?',
3+
plurals:
4+
'{count, plural, =0 {No file} =1 {{count} file} other {{count} files}}',
5+
subtitle: 'Here is a subtitle',
6+
'tests.test.namespaces': 'test',
7+
title: 'Welcome on @scaelway/ui i18n hook',
8+
'translate.error':
9+
'On translate sync issue with variable between locales {newVariable}',
10+
} as const

packages/use-i18n/src/__tests__/locales/es.json

Lines changed: 0 additions & 6 deletions
This file was deleted.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default {
2+
plurals:
3+
'You have {numPhotos, plural, =0 {no photos.} =1 {one photo.} other {# photos.}}',
4+
subtitle: 'Aquí hay un subtítulo',
5+
'tests.test.namespaces': 'test',
6+
title: 'Bienvenido @scaelway/ui i18n hook',
7+
} as const

packages/use-i18n/src/__tests__/locales/fr.json

Lines changed: 0 additions & 6 deletions
This file was deleted.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export default {
2+
plurals:
3+
'You have {numPhotos, plural, =0 {no photos.} =1 {one photo.} other {# photos.}}',
4+
subtitle: 'Voici un sous-titre',
5+
'tests.test.namespaces': 'test',
6+
title: 'Bienvenue sur @scaelway/ui i18n hook',
7+
'translate.error':
8+
'onTranslateError fonction sera appelé car il manque une variable en français {oldFrenchVariable}',
9+
} as const

packages/use-i18n/src/__tests__/usei18n.tsx

Lines changed: 68 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,31 +9,28 @@ import {
99
import { act, renderHook, waitFor } from '@testing-library/react'
1010
import { enGB, fr as frDateFns } from 'date-fns/locale'
1111
import mockdate from 'mockdate'
12-
import type { ReactNode } from 'react'
12+
import type { ComponentProps, ReactNode } from 'react'
1313
import I18n, { useI18n, useTranslation } from '..'
14-
import en from './locales/en.json'
15-
import es from './locales/es.json'
16-
import fr from './locales/fr.json'
14+
import en from './locales/en'
15+
import es from './locales/es'
16+
import fr from './locales/fr'
1717

1818
const LOCALE_ITEM_STORAGE = 'locales'
1919

20-
type Locale = {
21-
test: 'Test'
22-
'with.identifier': 'Are you sure you want to delete {identifier}?'
23-
plurals: '{numPhotos, plural, =0 {You have one photo.} other {You have # photos.}}'
24-
subtitle: 'Here is a subtitle'
25-
'tests.test.namespaces': 'test'
26-
title: 'Welcome on @scaelway/ui i18n hook'
27-
}
28-
20+
type LocaleEN = typeof en
21+
type Locale = LocaleEN
2922
type NamespaceLocale = {
3023
name: 'Name'
3124
lastName: 'Last Name'
3225
languages: 'Languages'
3326
}
3427

28+
type OnTranslateError = ComponentProps<typeof I18n>['onTranslateError']
29+
3530
const defaultSupportedLocales = ['en', 'fr', 'es']
3631

32+
const defaultOnTranslateError: OnTranslateError = () => {}
33+
3734
const wrapper =
3835
({
3936
loadDateLocaleAsync = async (locale: string) => {
@@ -61,13 +58,14 @@ const wrapper =
6158
return enGB
6259
},
6360
defaultLoad = async ({ locale }: { locale: string }) =>
64-
import(`./locales/${locale}.json`),
61+
import(`./locales/${locale}.ts`),
6562
defaultLocale = 'en',
6663
defaultTranslations = {},
6764
enableDebugKey = false,
6865
enableDefaultLocale = false,
6966
localeItemStorage = LOCALE_ITEM_STORAGE,
7067
supportedLocales = defaultSupportedLocales,
68+
onTranslateError = defaultOnTranslateError,
7169
} = {}) =>
7270
({ children }: { children: ReactNode }) => (
7371
<I18n
@@ -80,6 +78,7 @@ const wrapper =
8078
enableDefaultLocale={enableDefaultLocale}
8179
localeItemStorage={localeItemStorage}
8280
supportedLocales={supportedLocales}
81+
onTranslateError={onTranslateError}
8382
>
8483
{children}
8584
</I18n>
@@ -470,7 +469,7 @@ describe('i18n hook', () => {
470469
expect(localStorage.getItem(LOCALE_ITEM_STORAGE)).toBe('es')
471470
})
472471

473-
it('should translate correctly with enableDebugKey', async () => {
472+
it('should translate correctly with enableDebugKey and return key', async () => {
474473
const { result } = renderHook(() => useI18n<Locale>(), {
475474
wrapper: wrapper({
476475
defaultLocale: 'en',
@@ -479,15 +478,66 @@ describe('i18n hook', () => {
479478
supportedLocales: ['en', 'fr'],
480479
}),
481480
})
481+
482+
// @ts-expect-error this key doesn't exist but enable debug key will return the key
482483
expect(result.current.t('test')).toEqual('test')
483484

484485
await waitFor(() => {
485486
expect(result.current.t('title')).toEqual('title')
486487
expect(result.current.t('subtitle')).toEqual('subtitle')
487-
expect(result.current.t('plurals', { numPhotos: 0 })).toEqual('plurals')
488-
expect(result.current.t('plurals', { numPhotos: 1 })).toEqual('plurals')
489-
expect(result.current.t('plurals', { numPhotos: 2 })).toEqual('plurals')
488+
expect(result.current.t('plurals', { count: 0 })).toEqual('plurals')
489+
expect(result.current.t('plurals', { count: 1 })).toEqual('plurals')
490+
expect(result.current.t('plurals', { count: 2 })).toEqual('plurals')
491+
})
492+
})
493+
494+
it('should call onTranslateError when there is a sync issue to remove/add variable in one traduction of a language', async () => {
495+
const mockOnTranslateError = jest.fn()
496+
497+
const { result } = renderHook(() => useI18n<Locale>(), {
498+
wrapper: wrapper({
499+
defaultLocale: 'en',
500+
defaultTranslations: { en, fr },
501+
supportedLocales: ['en', 'fr'],
502+
onTranslateError: mockOnTranslateError,
503+
}),
504+
})
505+
506+
await act(async () => {
507+
await result.current.switchLocale('fr')
508+
})
509+
510+
waitFor(() => {
511+
expect(result.current.currentLocale).toEqual('fr')
490512
})
513+
514+
const newVariable = 'newVariable'
515+
expect(
516+
result.current.t('translate.error', { newVariable: 'newVariable' }),
517+
).toBe(
518+
`On translate sync issue with variable between locales ${newVariable}`,
519+
)
520+
521+
expect(mockOnTranslateError).toHaveBeenCalledTimes(1)
522+
expect(mockOnTranslateError).toHaveBeenCalledWith({
523+
error: new Error(
524+
'The intl string context variable "oldFrenchVariable" was not provided to the string "onTranslateError fonction sera appelé car il manque une variable en français {oldFrenchVariable}"',
525+
),
526+
currentLocale: 'fr',
527+
key: 'translate.error',
528+
value:
529+
'onTranslateError fonction sera appelé car il manque une variable en français {oldFrenchVariable}',
530+
})
531+
532+
const oldFrenchVariable = 'cette variable fonctionne'
533+
expect(
534+
result.current.t('translate.error', {
535+
// @ts-expect-error this variable doesn't exist in english anymore but still in french locales
536+
oldFrenchVariable,
537+
}),
538+
).toBe(
539+
`onTranslateError fonction sera appelé car il manque une variable en français ${oldFrenchVariable}`,
540+
)
491541
})
492542

493543
it('should use namespaceTranslation', async () => {

packages/use-i18n/src/usei18n.tsx

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ const I18nContextProvider = ({
200200
loadDateLocaleAsync,
201201
localeItemStorage = LOCALE_ITEM_STORAGE,
202202
onLoadDateLocaleError,
203+
onTranslateError,
203204
supportedLocales,
204205
}: {
205206
children: ReactNode
@@ -213,6 +214,17 @@ const I18nContextProvider = ({
213214
enableDebugKey: boolean
214215
localeItemStorage: string
215216
supportedLocales: string[]
217+
onTranslateError?: ({
218+
error,
219+
currentLocale,
220+
value,
221+
key,
222+
}: {
223+
error: Error
224+
currentLocale: string
225+
value: string
226+
key: string
227+
}) => void
216228
}): ReactElement => {
217229
const [currentLocale, setCurrentLocale] = useState<string>(
218230
getCurrentLocale({ defaultLocale, localeItemStorage, supportedLocales }),
@@ -382,6 +394,7 @@ const I18nContextProvider = ({
382394
const translate = useCallback(
383395
(key: string, context?: ReactParamsObject<any>) => {
384396
const value = translations[currentLocale]?.[key] as string
397+
385398
if (enableDebugKey) {
386399
return key
387400
}
@@ -390,14 +403,36 @@ const I18nContextProvider = ({
390403
return ''
391404
}
392405
if (context) {
393-
return formatters
394-
.getTranslationFormat(value, currentLocale)
395-
.format(context) as string
406+
try {
407+
return formatters
408+
.getTranslationFormat(value, currentLocale)
409+
.format(context) as string
410+
} catch (err) {
411+
onTranslateError?.({
412+
error: err as Error,
413+
currentLocale,
414+
value,
415+
key,
416+
})
417+
418+
// with default locale nothing should break or it's normal to not ignore it.
419+
const defaultValue = translations[defaultLocale]?.[key] as string
420+
421+
return formatters
422+
.getTranslationFormat(defaultValue, defaultLocale)
423+
.format(context) as string
424+
}
396425
}
397426

398427
return value
399428
},
400-
[currentLocale, translations, enableDebugKey],
429+
[
430+
currentLocale,
431+
translations,
432+
enableDebugKey,
433+
defaultLocale,
434+
onTranslateError,
435+
],
401436
)
402437

403438
const namespaceTranslation = useCallback(

0 commit comments

Comments
 (0)