From c1364cf766ecdc677c9e2419ee617f48c57e58ff Mon Sep 17 00:00:00 2001 From: QuiiBz Date: Tue, 30 Aug 2022 17:20:13 +0200 Subject: [PATCH 1/4] feat(use-i18): typesafe useTranslation --- packages/use-i18n/src/usei18n.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/use-i18n/src/usei18n.tsx b/packages/use-i18n/src/usei18n.tsx index 036dfba7f..48908b46a 100644 --- a/packages/use-i18n/src/usei18n.tsx +++ b/packages/use-i18n/src/usei18n.tsx @@ -126,10 +126,12 @@ export function useI18n< return context as unknown as Context } -export const useTranslation = ( +export function useTranslation< + Locale extends BaseLocale | undefined = undefined, +>( namespaces: string[] = [], load: LoadTranslationsFn | undefined = undefined, -): Context & { isLoaded: boolean } => { +): Context & { isLoaded: boolean } { const context = useContext(I18nContext) if (context === undefined) { throw new Error('useTranslation must be used within a I18nProvider') @@ -148,7 +150,9 @@ export const useTranslation = ( [loadedNamespaces, namespaces], ) - return { ...context, isLoaded } + return { ...context, isLoaded } as unknown as Context & { + isLoaded: boolean + } } type LoadTranslationsFn = ({ From 4723f0f74e77c56e130e74923b208ac87efa81f7 Mon Sep 17 00:00:00 2001 From: QuiiBz Date: Tue, 30 Aug 2022 17:33:04 +0200 Subject: [PATCH 2/4] refactor: remove prefix key --- .../locales/namespaces/en/profile.json | 5 - .../__tests__/locales/namespaces/en/user.json | 6 - .../locales/namespaces/fr/profile.json | 5 - .../__tests__/locales/namespaces/fr/user.json | 5 - packages/use-i18n/src/__tests__/usei18n.tsx | 107 ------------------ packages/use-i18n/src/usei18n.tsx | 12 +- 6 files changed, 1 insertion(+), 139 deletions(-) delete mode 100644 packages/use-i18n/src/__tests__/locales/namespaces/en/profile.json delete mode 100644 packages/use-i18n/src/__tests__/locales/namespaces/en/user.json delete mode 100644 packages/use-i18n/src/__tests__/locales/namespaces/fr/profile.json delete mode 100644 packages/use-i18n/src/__tests__/locales/namespaces/fr/user.json diff --git a/packages/use-i18n/src/__tests__/locales/namespaces/en/profile.json b/packages/use-i18n/src/__tests__/locales/namespaces/en/profile.json deleted file mode 100644 index a738b9765..000000000 --- a/packages/use-i18n/src/__tests__/locales/namespaces/en/profile.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "prefix": "profile", - "name": "Name", - "lastName": "Last Name" -} diff --git a/packages/use-i18n/src/__tests__/locales/namespaces/en/user.json b/packages/use-i18n/src/__tests__/locales/namespaces/en/user.json deleted file mode 100644 index 5f1bbd56f..000000000 --- a/packages/use-i18n/src/__tests__/locales/namespaces/en/user.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "prefix": "user", - "name": "Name", - "lastName": "Last Name", - "languages": "Languages" -} diff --git a/packages/use-i18n/src/__tests__/locales/namespaces/fr/profile.json b/packages/use-i18n/src/__tests__/locales/namespaces/fr/profile.json deleted file mode 100644 index 5474c8515..000000000 --- a/packages/use-i18n/src/__tests__/locales/namespaces/fr/profile.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "prefix": "profile", - "name": "Prénom", - "lastName": "Nom" -} diff --git a/packages/use-i18n/src/__tests__/locales/namespaces/fr/user.json b/packages/use-i18n/src/__tests__/locales/namespaces/fr/user.json deleted file mode 100644 index 14303b2e6..000000000 --- a/packages/use-i18n/src/__tests__/locales/namespaces/fr/user.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "prefix": "user", - "name": "Prénom", - "lastName": "Nom" -} diff --git a/packages/use-i18n/src/__tests__/usei18n.tsx b/packages/use-i18n/src/__tests__/usei18n.tsx index 8da9de890..cb52d2f7e 100644 --- a/packages/use-i18n/src/__tests__/usei18n.tsx +++ b/packages/use-i18n/src/__tests__/usei18n.tsx @@ -111,113 +111,6 @@ describe('i18n hook', () => { }) }) - it('should use specific load on useTranslation', async () => { - const load = async ({ - locale, - namespace, - }: { - locale: string - namespace: string - }) => import(`./locales/namespaces/${locale}/${namespace}.json`) - - const { result } = renderHook( - () => useTranslation(['user', 'profile'], load), - { - wrapper: wrapper({ - defaultLocale: 'en', - supportedLocales: ['en', 'fr'], - }), - }, - ) - - await waitFor(() => { - expect(result.current.translations).toStrictEqual({ - en: { - 'profile.lastName': 'Last Name', - 'profile.name': 'Name', - 'user.languages': 'Languages', - 'user.lastName': 'Last Name', - 'user.name': 'Name', - }, - }) - }) - - expect(result.current.t('user.name')).toEqual('Name') - expect(result.current.t('user.lastName')).toEqual('Last Name') - expect(result.current.t('user.languages')).toEqual('Languages') - - act(() => { - result.current.switchLocale('fr') - }) - - await waitFor(() => { - expect(result.current.translations).toStrictEqual({ - en: { - 'profile.lastName': 'Last Name', - 'profile.name': 'Name', - 'user.languages': 'Languages', - 'user.lastName': 'Last Name', - 'user.name': 'Name', - }, - fr: { - 'profile.lastName': 'Nom', - 'profile.name': 'Prénom', - 'user.lastName': 'Nom', - 'user.name': 'Prénom', - }, - }) - }) - - expect(result.current.t('user.name')).toEqual('Prénom') - expect(result.current.t('user.lastName')).toEqual('Nom') - expect(result.current.t('user.languages')).toEqual('') - - expect(result.current.t('user')).toEqual('') - expect(result.current.t('user', { test: 'toto' })).toEqual('') - }) - - it("should use specific load and fallback default local if the key doesn't exist", async () => { - const load = async ({ - locale, - namespace, - }: { - locale: string - namespace: string - }) => import(`./locales/namespaces/${locale}/${namespace}.json`) - - const { result } = renderHook(() => useTranslation(['user'], load), { - wrapper: wrapper({ - defaultLocale: 'fr', - enableDefaultLocale: true, - supportedLocales: ['en', 'fr'], - }), - }) - - // current local will be 'en' based on navigator - // await load of locales - act(() => { - result.current.switchLocale('fr') - }) - - await waitFor(() => { - expect(result.current.translations).toStrictEqual({ - en: { - 'user.languages': 'Languages', - 'user.lastName': 'Last Name', - 'user.name': 'Name', - }, - fr: { - 'user.lastName': 'Nom', - 'user.name': 'Prénom', - }, - }) - - expect(result.current.t('user.languages')).toEqual('') - expect(result.current.t('user.lastName')).toEqual('Nom') - expect(result.current.t('user.name')).toEqual('Prénom') - }) - }) - it('should set current locale from navigator languages', async () => { jest.spyOn(window, 'navigator', 'get').mockImplementation( () => diff --git a/packages/use-i18n/src/usei18n.tsx b/packages/use-i18n/src/usei18n.tsx index 48908b46a..a0101978a 100644 --- a/packages/use-i18n/src/usei18n.tsx +++ b/packages/use-i18n/src/usei18n.tsx @@ -35,13 +35,6 @@ export type InitialScopedTranslateFn = ( t?: InitialTranslateFn, ) => InitialTranslateFn -const prefixKeys = (prefix: string) => (obj: { [key: string]: string }) => - Object.keys(obj).reduce((acc: { [key: string]: string }, key) => { - acc[`${prefix}${key}`] = obj[key] - - return acc - }, {}) - const areNamespacesLoaded = ( namespaces: string[], loadedNamespaces: string[] = [], @@ -226,9 +219,6 @@ const I18nContextProvider = ({ ...result[currentLocale].default, } - const { prefix, ...values } = trad - const preparedValues = prefix ? prefixKeys(`${prefix}.`)(values) : values - // avoid a lot of render when async update // This is handled automatically in react 18, but we leave it here for compat // https://github.com/reactwg/react-18/discussions/21#discussioncomment-801703 @@ -238,7 +228,7 @@ const I18nContextProvider = ({ ...{ [currentLocale]: { ...prevState[currentLocale], - ...preparedValues, + ...trad, }, }, })) From 4ef65b615b0f41a0e5fd79ddf3031831e0be9850 Mon Sep 17 00:00:00 2001 From: QuiiBz Date: Wed, 31 Aug 2022 11:10:57 +0200 Subject: [PATCH 3/4] feat: add back tests without prefix --- .../locales/namespaces/en/profile.json | 4 + .../__tests__/locales/namespaces/en/user.json | 5 + .../locales/namespaces/fr/profile.json | 4 + .../__tests__/locales/namespaces/fr/user.json | 4 + packages/use-i18n/src/__tests__/usei18n.tsx | 98 +++++++++++++++++++ 5 files changed, 115 insertions(+) create mode 100644 packages/use-i18n/src/__tests__/locales/namespaces/en/profile.json create mode 100644 packages/use-i18n/src/__tests__/locales/namespaces/en/user.json create mode 100644 packages/use-i18n/src/__tests__/locales/namespaces/fr/profile.json create mode 100644 packages/use-i18n/src/__tests__/locales/namespaces/fr/user.json diff --git a/packages/use-i18n/src/__tests__/locales/namespaces/en/profile.json b/packages/use-i18n/src/__tests__/locales/namespaces/en/profile.json new file mode 100644 index 000000000..ed1f0a507 --- /dev/null +++ b/packages/use-i18n/src/__tests__/locales/namespaces/en/profile.json @@ -0,0 +1,4 @@ +{ + "name": "Name", + "lastName": "Last Name" +} diff --git a/packages/use-i18n/src/__tests__/locales/namespaces/en/user.json b/packages/use-i18n/src/__tests__/locales/namespaces/en/user.json new file mode 100644 index 000000000..ba61042bd --- /dev/null +++ b/packages/use-i18n/src/__tests__/locales/namespaces/en/user.json @@ -0,0 +1,5 @@ +{ + "name": "Name", + "lastName": "Last Name", + "languages": "Languages" +} diff --git a/packages/use-i18n/src/__tests__/locales/namespaces/fr/profile.json b/packages/use-i18n/src/__tests__/locales/namespaces/fr/profile.json new file mode 100644 index 000000000..b04239a81 --- /dev/null +++ b/packages/use-i18n/src/__tests__/locales/namespaces/fr/profile.json @@ -0,0 +1,4 @@ +{ + "name": "Prénom", + "lastName": "Nom" +} diff --git a/packages/use-i18n/src/__tests__/locales/namespaces/fr/user.json b/packages/use-i18n/src/__tests__/locales/namespaces/fr/user.json new file mode 100644 index 000000000..b04239a81 --- /dev/null +++ b/packages/use-i18n/src/__tests__/locales/namespaces/fr/user.json @@ -0,0 +1,4 @@ +{ + "name": "Prénom", + "lastName": "Nom" +} diff --git a/packages/use-i18n/src/__tests__/usei18n.tsx b/packages/use-i18n/src/__tests__/usei18n.tsx index cb52d2f7e..a464b363a 100644 --- a/packages/use-i18n/src/__tests__/usei18n.tsx +++ b/packages/use-i18n/src/__tests__/usei18n.tsx @@ -111,6 +111,104 @@ describe('i18n hook', () => { }) }) + it('should use specific load on useTranslation', async () => { + const load = async ({ + locale, + namespace, + }: { + locale: string + namespace: string + }) => import(`./locales/namespaces/${locale}/${namespace}.json`) + + const { result } = renderHook( + () => useTranslation(['user', 'profile'], load), + { + wrapper: wrapper({ + defaultLocale: 'en', + supportedLocales: ['en', 'fr'], + }), + }, + ) + + await waitFor(() => { + expect(result.current.translations).toStrictEqual({ + en: { + languages: 'Languages', + lastName: 'Last Name', + name: 'Name', + }, + }) + }) + + expect(result.current.t('name')).toEqual('Name') + expect(result.current.t('lastName')).toEqual('Last Name') + expect(result.current.t('languages')).toEqual('Languages') + + act(() => { + result.current.switchLocale('fr') + }) + + await waitFor(() => { + expect(result.current.translations).toStrictEqual({ + en: { + languages: 'Languages', + lastName: 'Last Name', + name: 'Name', + }, + fr: { + lastName: 'Nom', + name: 'Prénom', + }, + }) + }) + + expect(result.current.t('name')).toEqual('Prénom') + expect(result.current.t('lastName')).toEqual('Nom') + expect(result.current.t('languages')).toEqual('') + }) + + it("should use specific load and fallback default local if the key doesn't exist", async () => { + const load = async ({ + locale, + namespace, + }: { + locale: string + namespace: string + }) => import(`./locales/namespaces/${locale}/${namespace}.json`) + + const { result } = renderHook(() => useTranslation(['user'], load), { + wrapper: wrapper({ + defaultLocale: 'fr', + enableDefaultLocale: true, + supportedLocales: ['en', 'fr'], + }), + }) + + // current local will be 'en' based on navigator + // await load of locales + act(() => { + result.current.switchLocale('fr') + }) + + await waitFor(() => { + expect(result.current.translations).toStrictEqual({ + en: { + languages: 'Languages', + lastName: 'Last Name', + name: 'Name', + }, + fr: { + lastName: 'Nom', + name: 'Prénom', + }, + }) + + expect(result.current.t('languages')).toEqual('') + expect(result.current.t('lastName')).toEqual('Nom') + expect(result.current.t('name')).toEqual('Prénom') + }) + }) + it('should set current locale from navigator languages', async () => { jest.spyOn(window, 'navigator', 'get').mockImplementation( () => From ddf06f3492b63a2399a7482cd1b03c11b57f2e4a Mon Sep 17 00:00:00 2001 From: QuiiBz Date: Wed, 31 Aug 2022 16:01:23 +0200 Subject: [PATCH 4/4] chore(deps): bump international-types --- packages/use-i18n/package.json | 4 ++-- pnpm-lock.yaml | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/use-i18n/package.json b/packages/use-i18n/package.json index e1a7e8bd0..6a5911a03 100644 --- a/packages/use-i18n/package.json +++ b/packages/use-i18n/package.json @@ -31,13 +31,13 @@ "@formatjs/fast-memoize": "1.2.6", "date-fns": "2.29.2", "filesize": "9.0.11", - "international-types": "0.3.3", + "international-types": "0.3.4", "intl-messageformat": "10.1.4", "prop-types": "15.8.1" }, "peerDependencies": { "date-fns": "2.x", - "international-types": "0.3.3", + "international-types": "0.3.4", "react": "18.x", "react-dom": "18.x" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 16229d6c8..6fe0d41ee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -158,7 +158,7 @@ importers: '@formatjs/fast-memoize': 1.2.6 date-fns: 2.29.2 filesize: 9.0.11 - international-types: 0.3.3 + international-types: 0.3.4 intl-messageformat: 10.1.4 prop-types: 15.8.1 dependencies: @@ -166,7 +166,7 @@ importers: '@formatjs/fast-memoize': 1.2.6 date-fns: 2.29.2 filesize: 9.0.11 - international-types: 0.3.3 + international-types: 0.3.4 intl-messageformat: 10.1.4 prop-types: 15.8.1 @@ -6998,8 +6998,8 @@ packages: side-channel: 1.0.4 dev: false - /international-types/0.3.3: - resolution: {integrity: sha512-S7XBOB3bncwtqBLvTvG/Bm0GYGDOP5d7RHB1ihHToUAgwrmcluE7uM4pM9puWiT/s+RRfdV8u+Z2sM+WGDx6Fw==} + /international-types/0.3.4: + resolution: {integrity: sha512-4zGZ1Co7H8kfYF4a2a3hpIZL0EqRlxzTGqI0johcyrlg7sKZel4Yi8mZY0TUClRREzNR3L50MjMD2GEx0PRDfg==} dev: false /intl-messageformat/10.1.4: