Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/curly-trains-draw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@scaleway/use-i18n": patch
---

add a onTranslateError function in case of desync traduction between language. This will help to focus only on default language for developers
7 changes: 0 additions & 7 deletions packages/use-i18n/src/__tests__/locales/en.json

This file was deleted.

10 changes: 10 additions & 0 deletions packages/use-i18n/src/__tests__/locales/en.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export default {
'with.identifier': 'Are you sure you want to delete {identifier}?',
plurals:
'{count, plural, =0 {No file} =1 {{count} file} other {{count} files}}',
subtitle: 'Here is a subtitle',
'tests.test.namespaces': 'test',
title: 'Welcome on @scaelway/ui i18n hook',
'translate.error':
'On translate sync issue with variable between locales {newVariable}',
} as const
6 changes: 0 additions & 6 deletions packages/use-i18n/src/__tests__/locales/es.json

This file was deleted.

7 changes: 7 additions & 0 deletions packages/use-i18n/src/__tests__/locales/es.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default {
plurals:
'You have {numPhotos, plural, =0 {no photos.} =1 {one photo.} other {# photos.}}',
subtitle: 'Aquí hay un subtítulo',
'tests.test.namespaces': 'test',
title: 'Bienvenido @scaelway/ui i18n hook',
} as const
6 changes: 0 additions & 6 deletions packages/use-i18n/src/__tests__/locales/fr.json

This file was deleted.

9 changes: 9 additions & 0 deletions packages/use-i18n/src/__tests__/locales/fr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export default {
plurals:
'You have {numPhotos, plural, =0 {no photos.} =1 {one photo.} other {# photos.}}',
subtitle: 'Voici un sous-titre',
'tests.test.namespaces': 'test',
title: 'Bienvenue sur @scaelway/ui i18n hook',
'translate.error':
'onTranslateError fonction sera appelé car il manque une variable en français {oldFrenchVariable}',
} as const
86 changes: 68 additions & 18 deletions packages/use-i18n/src/__tests__/usei18n.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,31 +9,28 @@ import {
import { act, renderHook, waitFor } from '@testing-library/react'
import { enGB, fr as frDateFns } from 'date-fns/locale'
import mockdate from 'mockdate'
import type { ReactNode } from 'react'
import type { ComponentProps, ReactNode } from 'react'
import I18n, { useI18n, useTranslation } from '..'
import en from './locales/en.json'
import es from './locales/es.json'
import fr from './locales/fr.json'
import en from './locales/en'
import es from './locales/es'
import fr from './locales/fr'

const LOCALE_ITEM_STORAGE = 'locales'

type Locale = {
test: 'Test'
'with.identifier': 'Are you sure you want to delete {identifier}?'
plurals: '{numPhotos, plural, =0 {You have one photo.} other {You have # photos.}}'
subtitle: 'Here is a subtitle'
'tests.test.namespaces': 'test'
title: 'Welcome on @scaelway/ui i18n hook'
}

type LocaleEN = typeof en
type Locale = LocaleEN
type NamespaceLocale = {
name: 'Name'
lastName: 'Last Name'
languages: 'Languages'
}

type OnTranslateError = ComponentProps<typeof I18n>['onTranslateError']

const defaultSupportedLocales = ['en', 'fr', 'es']

const defaultOnTranslateError: OnTranslateError = () => {}

const wrapper =
({
loadDateLocaleAsync = async (locale: string) => {
Expand Down Expand Up @@ -61,13 +58,14 @@ const wrapper =
return enGB
},
defaultLoad = async ({ locale }: { locale: string }) =>
import(`./locales/${locale}.json`),
import(`./locales/${locale}.ts`),
defaultLocale = 'en',
defaultTranslations = {},
enableDebugKey = false,
enableDefaultLocale = false,
localeItemStorage = LOCALE_ITEM_STORAGE,
supportedLocales = defaultSupportedLocales,
onTranslateError = defaultOnTranslateError,
} = {}) =>
({ children }: { children: ReactNode }) => (
<I18n
Expand All @@ -80,6 +78,7 @@ const wrapper =
enableDefaultLocale={enableDefaultLocale}
localeItemStorage={localeItemStorage}
supportedLocales={supportedLocales}
onTranslateError={onTranslateError}
>
{children}
</I18n>
Expand Down Expand Up @@ -470,7 +469,7 @@ describe('i18n hook', () => {
expect(localStorage.getItem(LOCALE_ITEM_STORAGE)).toBe('es')
})

it('should translate correctly with enableDebugKey', async () => {
it('should translate correctly with enableDebugKey and return key', async () => {
const { result } = renderHook(() => useI18n<Locale>(), {
wrapper: wrapper({
defaultLocale: 'en',
Expand All @@ -479,15 +478,66 @@ describe('i18n hook', () => {
supportedLocales: ['en', 'fr'],
}),
})

// @ts-expect-error this key doesn't exist but enable debug key will return the key
expect(result.current.t('test')).toEqual('test')

await waitFor(() => {
expect(result.current.t('title')).toEqual('title')
expect(result.current.t('subtitle')).toEqual('subtitle')
expect(result.current.t('plurals', { numPhotos: 0 })).toEqual('plurals')
expect(result.current.t('plurals', { numPhotos: 1 })).toEqual('plurals')
expect(result.current.t('plurals', { numPhotos: 2 })).toEqual('plurals')
expect(result.current.t('plurals', { count: 0 })).toEqual('plurals')
expect(result.current.t('plurals', { count: 1 })).toEqual('plurals')
expect(result.current.t('plurals', { count: 2 })).toEqual('plurals')
})
})

it('should call onTranslateError when there is a sync issue to remove/add variable in one traduction of a language', async () => {
const mockOnTranslateError = jest.fn()

const { result } = renderHook(() => useI18n<Locale>(), {
wrapper: wrapper({
defaultLocale: 'en',
defaultTranslations: { en, fr },
supportedLocales: ['en', 'fr'],
onTranslateError: mockOnTranslateError,
}),
})

await act(async () => {
await result.current.switchLocale('fr')
})

waitFor(() => {
expect(result.current.currentLocale).toEqual('fr')
})

const newVariable = 'newVariable'
expect(
result.current.t('translate.error', { newVariable: 'newVariable' }),
).toBe(
`On translate sync issue with variable between locales ${newVariable}`,
)

expect(mockOnTranslateError).toHaveBeenCalledTimes(1)
expect(mockOnTranslateError).toHaveBeenCalledWith({
error: new Error(
'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}"',
),
currentLocale: 'fr',
key: 'translate.error',
value:
'onTranslateError fonction sera appelé car il manque une variable en français {oldFrenchVariable}',
})

const oldFrenchVariable = 'cette variable fonctionne'
expect(
result.current.t('translate.error', {
// @ts-expect-error this variable doesn't exist in english anymore but still in french locales
oldFrenchVariable,
}),
).toBe(
`onTranslateError fonction sera appelé car il manque une variable en français ${oldFrenchVariable}`,
)
})

it('should use namespaceTranslation', async () => {
Expand Down
43 changes: 39 additions & 4 deletions packages/use-i18n/src/usei18n.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ const I18nContextProvider = ({
loadDateLocaleAsync,
localeItemStorage = LOCALE_ITEM_STORAGE,
onLoadDateLocaleError,
onTranslateError,
supportedLocales,
}: {
children: ReactNode
Expand All @@ -213,6 +214,17 @@ const I18nContextProvider = ({
enableDebugKey: boolean
localeItemStorage: string
supportedLocales: string[]
onTranslateError?: ({
error,
currentLocale,
value,
key,
}: {
error: Error
currentLocale: string
value: string
key: string
}) => void
}): ReactElement => {
const [currentLocale, setCurrentLocale] = useState<string>(
getCurrentLocale({ defaultLocale, localeItemStorage, supportedLocales }),
Expand Down Expand Up @@ -382,6 +394,7 @@ const I18nContextProvider = ({
const translate = useCallback(
(key: string, context?: ReactParamsObject<any>) => {
const value = translations[currentLocale]?.[key] as string

if (enableDebugKey) {
return key
}
Expand All @@ -390,14 +403,36 @@ const I18nContextProvider = ({
return ''
}
if (context) {
return formatters
.getTranslationFormat(value, currentLocale)
.format(context) as string
try {
return formatters
.getTranslationFormat(value, currentLocale)
.format(context) as string
} catch (err) {
onTranslateError?.({
error: err as Error,
currentLocale,
value,
key,
})

// with default locale nothing should break or it's normal to not ignore it.
const defaultValue = translations[defaultLocale]?.[key] as string

return formatters
.getTranslationFormat(defaultValue, defaultLocale)
.format(context) as string
}
}

return value
},
[currentLocale, translations, enableDebugKey],
[
currentLocale,
translations,
enableDebugKey,
defaultLocale,
onTranslateError,
],
)

const namespaceTranslation = useCallback(
Expand Down