From 029124990321c2bda1b959fe30f87c4239253634 Mon Sep 17 00:00:00 2001 From: Alexandre Combemorel Date: Tue, 30 Jul 2024 18:03:20 +0200 Subject: [PATCH 1/7] feat: add type to locales and remove automatic mapping from browser type to basic locale --- packages/use-i18n/src/usei18n.tsx | 141 ++++++++++++++---------------- 1 file changed, 67 insertions(+), 74 deletions(-) diff --git a/packages/use-i18n/src/usei18n.tsx b/packages/use-i18n/src/usei18n.tsx index 1cc691dfd..a671b9e61 100644 --- a/packages/use-i18n/src/usei18n.tsx +++ b/packages/use-i18n/src/usei18n.tsx @@ -23,53 +23,49 @@ import type { ReactParamsObject, ScopedTranslateFn, TranslateFn } from './types' const LOCALE_ITEM_STORAGE = 'locale' +type SupportedLocalesType = (locale: string) => locale is T + type TranslationsByLocales = Record -type RequiredGenericContext = - keyof Locale extends never - ? Omit, 't' | 'namespaceTranslation'> & { - t: (str: 'You must pass a generic argument to useI18n()') => void - namespaceTranslation: ( - str: 'You must pass a generic argument to useI18n()', - ) => void - } - : Context +type RequiredGenericContext< + LocaleParam extends BaseLocale, + T extends string, +> = keyof string extends never + ? Omit, 't' | 'namespaceTranslation'> & { + t: (str: 'You must pass a generic argument to useI18n()') => void + namespaceTranslation: ( + str: 'You must pass a generic argument to useI18n()', + ) => void + } + : Context const areNamespacesLoaded = ( namespaces: string[], loadedNamespaces: string[] = [], ) => namespaces.every(n => loadedNamespaces.includes(n)) -const getLocaleFallback = (locale: string) => - locale.split('-')[0]?.split('_')[0] - -const getCurrentLocale = ({ +const getCurrentLocale = ({ defaultLocale, - supportedLocales, + isLocaleSupported, localeItemStorage, }: { - defaultLocale: string - supportedLocales: string[] + defaultLocale: T + isLocaleSupported: SupportedLocalesType localeItemStorage: string -}): string => { +}): T => { if (typeof window !== 'undefined') { - const { languages } = navigator - const browserLocales = [ - ...new Set([...languages.map(getLocaleFallback), ...languages]), - ] + const { languages: browserLocales } = navigator const currentLocalFromlocalStorage = localStorage.getItem(localeItemStorage) if ( currentLocalFromlocalStorage && - supportedLocales.find( - supportedLocale => supportedLocale === currentLocalFromlocalStorage, - ) + isLocaleSupported(currentLocalFromlocalStorage) ) { return currentLocalFromlocalStorage } localStorage.removeItem(localeItemStorage) - const findedBrowserLocal = browserLocales.find( - locale => locale && supportedLocales.includes(locale), + const findedBrowserLocal = browserLocales.find(locale => + isLocaleSupported(locale), ) if (findedBrowserLocal) { @@ -78,12 +74,7 @@ const getCurrentLocale = ({ return findedBrowserLocal } - if ( - defaultLocale && - supportedLocales.find( - supportedLocale => supportedLocale === defaultLocale, - ) - ) { + if (defaultLocale && isLocaleSupported(defaultLocale)) { localStorage.setItem(localeItemStorage, defaultLocale) return defaultLocale @@ -93,8 +84,8 @@ const getCurrentLocale = ({ return defaultLocale } -type Context = { - currentLocale: string +type Context = { + currentLocale: T dateFnsLocale?: DateFnsLocale datetime: ( date: Date | number, @@ -109,11 +100,10 @@ type Context = { formatUnit: (value: number, options: FormatUnitOptions) => string loadTranslations: ( namespace: string, - load?: LoadTranslationsFn, + load?: LoadTranslationsFn, ) => Promise - locales: string[] namespaces: string[] - namespaceTranslation: ScopedTranslateFn + namespaceTranslation: ScopedTranslateFn relativeTime: ( date: Date | number, options?: FormatDistanceToNowOptions, @@ -123,32 +113,36 @@ type Context = { options?: FormatDistanceToNowStrictOptions, ) => string setTranslations: React.Dispatch> - switchLocale: (locale: string) => Promise - t: TranslateFn + switchLocale: (locale: T) => Promise + t: TranslateFn translations: TranslationsByLocales } // It's safe to use any here because the Locale can be anything at this point: // useI18n / useTranslation requires to explicitely give a Locale to use. -const I18nContext = createContext | undefined>(undefined) +const I18nContext = createContext | undefined>(undefined) export function useI18n< // eslint-disable-next-line @typescript-eslint/ban-types - Locale extends BaseLocale = {}, ->(): RequiredGenericContext { + LocaleParam extends BaseLocale = {}, + T extends string = '', +>(): RequiredGenericContext { const context = useContext(I18nContext) if (context === undefined) { throw new Error('useI18n must be used within a I18nProvider') } - return context as unknown as RequiredGenericContext + return context as unknown as RequiredGenericContext } -// eslint-disable-next-line @typescript-eslint/ban-types -export function useTranslation( +export function useTranslation< + // eslint-disable-next-line @typescript-eslint/ban-types + LocaleParam extends BaseLocale = {}, + T extends string = '', +>( namespaces: string[] = [], - load: LoadTranslationsFn | undefined = undefined, -): RequiredGenericContext & { isLoaded: boolean } { + load: LoadTranslationsFn | undefined = undefined, +): RequiredGenericContext & { isLoaded: boolean } { const context = useContext(I18nContext) if (context === undefined) { throw new Error('useTranslation must be used within a I18nProvider') @@ -171,29 +165,26 @@ export function useTranslation( return { ...context, isLoaded, - } as unknown as RequiredGenericContext & { + } as unknown as RequiredGenericContext & { isLoaded: boolean } } -type LoadTranslationsFn = ({ +type LoadTranslationsFn = ({ namespace, locale, }: { namespace: string - locale: string + locale: T }) => Promise<{ default: BaseLocale }> -type LoadLocaleFn = (locale: string) => DateFnsLocale -type LoadLocaleFnAsync = (locale: string) => Promise +type LoadLocaleFn = (locale: T) => DateFnsLocale +type LoadLocaleFnAsync = (locale: T) => Promise type LoadDateLocaleError = (error: Error) => void const initialDefaultTranslations = {} -// TODO: improve type from Provider based on a Generic to have 'fr' | 'en' -type Locale = string - -const I18nContextProvider = ({ +const I18nContextProvider = ({ children, defaultLoad, defaultLocale, @@ -205,19 +196,19 @@ const I18nContextProvider = ({ localeItemStorage = LOCALE_ITEM_STORAGE, onLoadDateLocaleError, onTranslateError, - supportedLocales, + isLocaleSupported, }: { children: ReactNode - defaultLoad: LoadTranslationsFn - loadDateLocale?: LoadLocaleFn - loadDateLocaleAsync: LoadLocaleFnAsync + defaultLoad: LoadTranslationsFn + loadDateLocale?: LoadLocaleFn + loadDateLocaleAsync: LoadLocaleFnAsync onLoadDateLocaleError?: LoadDateLocaleError - defaultLocale: Locale + defaultLocale: T defaultTranslations: TranslationsByLocales enableDefaultLocale: boolean enableDebugKey: boolean - localeItemStorage: Locale - supportedLocales: Locale[] + localeItemStorage: string + isLocaleSupported: SupportedLocalesType onTranslateError?: ({ error, currentLocale, @@ -225,14 +216,18 @@ const I18nContextProvider = ({ key, }: { error: Error - currentLocale: Locale - defaultLocale: Locale + currentLocale: T + defaultLocale: T value: string key: string }) => void }): ReactElement => { - const [currentLocale, setCurrentLocale] = useState( - getCurrentLocale({ defaultLocale, localeItemStorage, supportedLocales }), + const [currentLocale, setCurrentLocale] = useState( + getCurrentLocale({ + defaultLocale, + localeItemStorage, + isLocaleSupported, + }), ) const [translations, setTranslations] = useState(defaultTranslations) @@ -245,7 +240,7 @@ const I18nContextProvider = ({ const loadDateFNS = loadDateLocale ?? loadDateLocaleAsync const setDateFns = useCallback( - async (locale: string) => { + async (locale: T) => { try { const dateFns = await loadDateFNS(locale) setDateFnsLocale(dateFns) @@ -275,7 +270,7 @@ const I18nContextProvider = ({ }, [currentLocale, dateFnsLocale, setDateFns, setDateFnsLocale]) const loadTranslations = useCallback( - async (namespace: string, load: LoadTranslationsFn = defaultLoad) => { + async (namespace: string, load: LoadTranslationsFn = defaultLoad) => { const result = { [currentLocale]: { default: {} }, defaultLocale: { default: {} }, @@ -318,14 +313,14 @@ const I18nContextProvider = ({ ) const switchLocale = useCallback( - async (locale: string) => { - if (supportedLocales.includes(locale)) { + async (locale: T) => { + if (isLocaleSupported(locale)) { localStorage.setItem(localeItemStorage, locale) setCurrentLocale(locale) await setDateFns(locale) } }, - [setDateFns, localeItemStorage, setCurrentLocale, supportedLocales], + [setDateFns, localeItemStorage, setCurrentLocale, isLocaleSupported], ) const formatNumber = useCallback( @@ -457,7 +452,6 @@ const I18nContextProvider = ({ formatNumber, formatUnit, loadTranslations, - locales: supportedLocales, namespaces, namespaceTranslation, relativeTime, @@ -481,7 +475,6 @@ const I18nContextProvider = ({ relativeTime, relativeTimeStrict, setTranslations, - supportedLocales, switchLocale, translate, translations, From 9bf0549c20eee6e261f9a01f76422da590fc81b6 Mon Sep 17 00:00:00 2001 From: Alexandre Combemorel Date: Tue, 30 Jul 2024 18:05:50 +0200 Subject: [PATCH 2/7] docs: add changeset --- .changeset/fuzzy-emus-repeat.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fuzzy-emus-repeat.md diff --git a/.changeset/fuzzy-emus-repeat.md b/.changeset/fuzzy-emus-repeat.md new file mode 100644 index 000000000..7e2269930 --- /dev/null +++ b/.changeset/fuzzy-emus-repeat.md @@ -0,0 +1,5 @@ +--- +"@scaleway/use-i18n": major +--- + +Add typing on locales and remove auto mapping from browser locale to simple locale From 13c6b9929a5c8b2c7c575a82013b8aee57690b1d Mon Sep 17 00:00:00 2001 From: Alexandre Combemorel Date: Mon, 12 Aug 2024 15:13:42 +0200 Subject: [PATCH 3/7] fix: test i18n major update --- .../use-i18n/src/__tests__/usei18n.test.tsx | 64 ++++++++++--------- 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/packages/use-i18n/src/__tests__/usei18n.test.tsx b/packages/use-i18n/src/__tests__/usei18n.test.tsx index b7a617bf8..166450eb1 100644 --- a/packages/use-i18n/src/__tests__/usei18n.test.tsx +++ b/packages/use-i18n/src/__tests__/usei18n.test.tsx @@ -10,8 +10,10 @@ import fr from './locales/fr' const LOCALE_ITEM_STORAGE = 'locales' -type LocaleEN = typeof en -type Locale = LocaleEN +const ListLocales = ['es', 'en', 'fr', 'fr-FR', 'en-GB'] as const +type Locales = (typeof ListLocales)[number] + +type Locale = typeof en type NamespaceLocale = { name: 'Name' lastName: 'Last Name' @@ -20,7 +22,8 @@ type NamespaceLocale = { type OnTranslateError = ComponentProps['onTranslateError'] -const defaultSupportedLocales = ['en', 'fr', 'es'] +const isLocaleSupportedDefault = (locale: string): locale is Locales => + ListLocales.includes(locale as Locales) const defaultOnTranslateError: OnTranslateError = () => {} @@ -57,7 +60,7 @@ const wrapper = enableDebugKey = false, enableDefaultLocale = false, localeItemStorage = LOCALE_ITEM_STORAGE, - supportedLocales = defaultSupportedLocales, + isLocaleSupported = isLocaleSupportedDefault, onTranslateError = defaultOnTranslateError, } = {}) => ({ children }: { children: ReactNode }) => ( @@ -70,7 +73,7 @@ const wrapper = enableDebugKey={enableDebugKey} enableDefaultLocale={enableDefaultLocale} localeItemStorage={localeItemStorage} - supportedLocales={supportedLocales} + isLocaleSupported={isLocaleSupported} onTranslateError={onTranslateError} > {children} @@ -114,7 +117,7 @@ describe('i18n hook', () => { }) it('should use defaultLoad, useTranslation, switch local and translate', async () => { - const { result } = renderHook(() => useTranslation([]), { + const { result } = renderHook(() => useTranslation([]), { wrapper: wrapper({ defaultLocale: 'en' }), }) // first render there is no load @@ -152,11 +155,11 @@ describe('i18n hook', () => { }) => import(`./locales/namespaces/${locale}/${namespace}.json`) const { result } = renderHook( - () => useTranslation(['user', 'profile'], load), + () => useTranslation(['user', 'profile'], load), { wrapper: wrapper({ defaultLocale: 'en', - supportedLocales: ['en', 'fr'], + isLocaleSupported: isLocaleSupportedDefault, }), }, ) @@ -208,12 +211,12 @@ describe('i18n hook', () => { }) => import(`./locales/namespaces/${locale}/${namespace}.json`) const { result } = renderHook( - () => useTranslation(['user'], load), + () => useTranslation(['user'], load), { wrapper: wrapper({ defaultLocale: 'fr', enableDefaultLocale: true, - supportedLocales: ['en', 'fr'], + isLocaleSupported: isLocaleSupportedDefault, }), }, ) @@ -247,7 +250,7 @@ describe('i18n hook', () => { const { result } = renderHook(() => useI18n(), { wrapper: wrapper({ defaultLocale: 'fr', - supportedLocales: ['en', 'fr', 'es'], + isLocaleSupported: isLocaleSupportedDefault, }), }) @@ -303,7 +306,7 @@ describe('i18n hook', () => { const { result } = renderHook(() => useI18n(), { wrapper: wrapper({ defaultLocale: 'es', - supportedLocales: ['en', 'fr', 'es'], + isLocaleSupported: isLocaleSupportedDefault, }), }) @@ -334,7 +337,7 @@ describe('i18n hook', () => { const { result } = renderHook(() => useI18n(), { wrapper: wrapper({ defaultLocale: 'en', - supportedLocales: ['en'], + isLocaleSupported: isLocaleSupportedDefault, }), }) @@ -365,7 +368,7 @@ describe('i18n hook', () => { const { result } = renderHook(() => useI18n(), { wrapper: wrapper({ defaultLocale: 'es', - supportedLocales: ['en', 'fr', 'es'], + isLocaleSupported: isLocaleSupportedDefault, }), }) @@ -394,7 +397,7 @@ describe('i18n hook', () => { const { result } = renderHook(() => useI18n(), { wrapper: wrapper({ defaultLocale: 'es', - supportedLocales: ['en', 'fr', 'es'], + isLocaleSupported: isLocaleSupportedDefault, }), }) @@ -406,10 +409,10 @@ describe('i18n hook', () => { }) it('should switch locale', async () => { - const { result } = renderHook(() => useI18n(), { + const { result } = renderHook(() => useI18n(), { wrapper: wrapper({ defaultLocale: 'en', - supportedLocales: ['en', 'fr', 'es'], + isLocaleSupported: isLocaleSupportedDefault, }), }) expect(result.current.currentLocale).toEqual('en') @@ -434,6 +437,9 @@ describe('i18n hook', () => { expect(localStorage.getItem(LOCALE_ITEM_STORAGE)).toBe('es') await act(async () => { + // we test even if an incorrect typescript value is being passed to the function + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error await result.current.switchLocale('test') }) @@ -449,7 +455,7 @@ describe('i18n hook', () => { defaultLocale: 'en', defaultTranslations: { en }, enableDebugKey: true, - supportedLocales: ['en', 'fr'], + isLocaleSupported: isLocaleSupportedDefault, }), }) @@ -468,11 +474,11 @@ describe('i18n hook', () => { it('should call onTranslateError when there is a sync issue to remove/add variable in one traduction of a language', async () => { const mockOnTranslateError = vi.fn() - const { result } = renderHook(() => useI18n(), { + const { result } = renderHook(() => useI18n(), { wrapper: wrapper({ defaultLocale: 'en', defaultTranslations: { en, fr }, - supportedLocales: ['en', 'fr'], + isLocaleSupported: isLocaleSupportedDefault, onTranslateError: mockOnTranslateError, }), }) @@ -534,7 +540,7 @@ describe('i18n hook', () => { }) it('should use formatNumber', async () => { - const { result } = renderHook(() => useI18n(), { + const { result } = renderHook(() => useI18n(), { wrapper: wrapper({ defaultLocale: 'en', }), @@ -578,7 +584,7 @@ describe('i18n hook', () => { }) it('should use formatList', async () => { - const { result } = renderHook(() => useI18n(), { + const { result } = renderHook(() => useI18n(), { wrapper: wrapper({ defaultLocale: 'en', }), @@ -635,7 +641,7 @@ describe('i18n hook', () => { }) it('should use datetime', async () => { - const { result } = renderHook(() => useI18n(), { + const { result } = renderHook(() => useI18n(), { wrapper: wrapper({ defaultLocale: 'en', }), @@ -707,7 +713,7 @@ describe('i18n hook', () => { }) it('should relativeTime', async () => { - const { result } = renderHook(() => useI18n(), { + const { result } = renderHook(() => useI18n(), { wrapper: wrapper({ defaultLocale: 'en', }), @@ -728,7 +734,7 @@ describe('i18n hook', () => { }) it('should relativeTimeStrict', async () => { - const { result } = renderHook(() => useI18n(), { + const { result } = renderHook(() => useI18n(), { wrapper: wrapper({ defaultLocale: 'en', }), @@ -749,7 +755,7 @@ describe('i18n hook', () => { }) it('should formatUnit', async () => { - const { result } = renderHook(() => useI18n(), { + const { result } = renderHook(() => useI18n(), { wrapper: wrapper({ defaultLocale: 'en', }), @@ -770,7 +776,7 @@ describe('i18n hook', () => { }) it('should formatDate', async () => { - const { result } = renderHook(() => useI18n(), { + const { result } = renderHook(() => useI18n(), { wrapper: wrapper({ defaultLocale: 'en', }), @@ -795,7 +801,7 @@ describe('i18n hook', () => { const { result } = renderHook(() => useI18n(), { wrapper: wrapper({ defaultLocale: 'test', - supportedLocales: ['test'], + isLocaleSupported: isLocaleSupportedDefault, }), }) @@ -823,7 +829,7 @@ describe('i18n hook', () => { const { result } = renderHook(() => useI18n(), { wrapper: wrapper({ defaultLocale: 'es', - supportedLocales: ['en', 'fr', 'es'], + isLocaleSupported: isLocaleSupportedDefault, }), }) From 097a7fd6b1558eb97a1f1fee0941b103b433d0c1 Mon Sep 17 00:00:00 2001 From: Alexandre Combemorel Date: Mon, 12 Aug 2024 16:29:43 +0200 Subject: [PATCH 4/7] fix: namespaceTranslation --- .../namespaceTranslation.test.ts | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/use-i18n/src/__typetests__/namespaceTranslation.test.ts b/packages/use-i18n/src/__typetests__/namespaceTranslation.test.ts index b8feac97a..752c7a190 100644 --- a/packages/use-i18n/src/__typetests__/namespaceTranslation.test.ts +++ b/packages/use-i18n/src/__typetests__/namespaceTranslation.test.ts @@ -2,14 +2,18 @@ import { expect, test } from 'tstyche' import { useI18n } from '../usei18n' +const ListLocales = ['es', 'en', 'fr', 'fr-FR', 'en-GB'] as const +type Locales = (typeof ListLocales)[number] +type Locale = { + hello: 'world' + 'doe.john': 'John Doe' + 'doe.jane': 'Jane Doe' + 'doe.child': 'Child is {name}' + 'describe.john': '{name} is {age} years old' +} + test('i18n - namespaceTranslation', () => { - const { namespaceTranslation } = useI18n<{ - hello: 'world' - 'doe.john': 'John Doe' - 'doe.jane': 'Jane Doe' - 'doe.child': 'Child is {name}' - 'describe.john': '{name} is {age} years old' - }>() + const { namespaceTranslation } = useI18n() // Single key expect(namespaceTranslation('hello')).type.toRaiseError( @@ -62,7 +66,7 @@ test('i18n - namespaceTranslation', () => { expect(namespaceTranslation('describe')('john')).type.toRaiseError() // Required generic - const { namespaceTranslation: namespaceTranslation2 } = useI18n() + const { namespaceTranslation: namespaceTranslation2 } = useI18n() expect(namespaceTranslation2('test')).type.toRaiseError( `Argument of type '"test"' is not assignable to parameter of type`, ) From 793d999c289e720cca5865e4005e610509d0ff88 Mon Sep 17 00:00:00 2001 From: Alexandre Combemorel Date: Tue, 13 Aug 2024 09:30:38 +0200 Subject: [PATCH 5/7] fix: rename check locale function name --- .../use-i18n/src/__tests__/usei18n.test.tsx | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/use-i18n/src/__tests__/usei18n.test.tsx b/packages/use-i18n/src/__tests__/usei18n.test.tsx index 166450eb1..d411cb1bc 100644 --- a/packages/use-i18n/src/__tests__/usei18n.test.tsx +++ b/packages/use-i18n/src/__tests__/usei18n.test.tsx @@ -22,7 +22,7 @@ type NamespaceLocale = { type OnTranslateError = ComponentProps['onTranslateError'] -const isLocaleSupportedDefault = (locale: string): locale is Locales => +const isDefaultLocalesSupported = (locale: string): locale is Locales => ListLocales.includes(locale as Locales) const defaultOnTranslateError: OnTranslateError = () => {} @@ -60,7 +60,7 @@ const wrapper = enableDebugKey = false, enableDefaultLocale = false, localeItemStorage = LOCALE_ITEM_STORAGE, - isLocaleSupported = isLocaleSupportedDefault, + isLocaleSupported = isDefaultLocalesSupported, onTranslateError = defaultOnTranslateError, } = {}) => ({ children }: { children: ReactNode }) => ( @@ -159,7 +159,7 @@ describe('i18n hook', () => { { wrapper: wrapper({ defaultLocale: 'en', - isLocaleSupported: isLocaleSupportedDefault, + isLocaleSupported: isDefaultLocalesSupported, }), }, ) @@ -216,7 +216,7 @@ describe('i18n hook', () => { wrapper: wrapper({ defaultLocale: 'fr', enableDefaultLocale: true, - isLocaleSupported: isLocaleSupportedDefault, + isLocaleSupported: isDefaultLocalesSupported, }), }, ) @@ -250,7 +250,7 @@ describe('i18n hook', () => { const { result } = renderHook(() => useI18n(), { wrapper: wrapper({ defaultLocale: 'fr', - isLocaleSupported: isLocaleSupportedDefault, + isLocaleSupported: isDefaultLocalesSupported, }), }) @@ -306,7 +306,7 @@ describe('i18n hook', () => { const { result } = renderHook(() => useI18n(), { wrapper: wrapper({ defaultLocale: 'es', - isLocaleSupported: isLocaleSupportedDefault, + isLocaleSupported: isDefaultLocalesSupported, }), }) @@ -337,7 +337,7 @@ describe('i18n hook', () => { const { result } = renderHook(() => useI18n(), { wrapper: wrapper({ defaultLocale: 'en', - isLocaleSupported: isLocaleSupportedDefault, + isLocaleSupported: isDefaultLocalesSupported, }), }) @@ -368,7 +368,7 @@ describe('i18n hook', () => { const { result } = renderHook(() => useI18n(), { wrapper: wrapper({ defaultLocale: 'es', - isLocaleSupported: isLocaleSupportedDefault, + isLocaleSupported: isDefaultLocalesSupported, }), }) @@ -397,7 +397,7 @@ describe('i18n hook', () => { const { result } = renderHook(() => useI18n(), { wrapper: wrapper({ defaultLocale: 'es', - isLocaleSupported: isLocaleSupportedDefault, + isLocaleSupported: isDefaultLocalesSupported, }), }) @@ -412,7 +412,7 @@ describe('i18n hook', () => { const { result } = renderHook(() => useI18n(), { wrapper: wrapper({ defaultLocale: 'en', - isLocaleSupported: isLocaleSupportedDefault, + isLocaleSupported: isDefaultLocalesSupported, }), }) expect(result.current.currentLocale).toEqual('en') @@ -455,7 +455,7 @@ describe('i18n hook', () => { defaultLocale: 'en', defaultTranslations: { en }, enableDebugKey: true, - isLocaleSupported: isLocaleSupportedDefault, + isLocaleSupported: isDefaultLocalesSupported, }), }) @@ -478,7 +478,7 @@ describe('i18n hook', () => { wrapper: wrapper({ defaultLocale: 'en', defaultTranslations: { en, fr }, - isLocaleSupported: isLocaleSupportedDefault, + isLocaleSupported: isDefaultLocalesSupported, onTranslateError: mockOnTranslateError, }), }) @@ -801,7 +801,7 @@ describe('i18n hook', () => { const { result } = renderHook(() => useI18n(), { wrapper: wrapper({ defaultLocale: 'test', - isLocaleSupported: isLocaleSupportedDefault, + isLocaleSupported: isDefaultLocalesSupported, }), }) @@ -829,7 +829,7 @@ describe('i18n hook', () => { const { result } = renderHook(() => useI18n(), { wrapper: wrapper({ defaultLocale: 'es', - isLocaleSupported: isLocaleSupportedDefault, + isLocaleSupported: isDefaultLocalesSupported, }), }) From 114514c67644dced881177d4281ea383e2925f33 Mon Sep 17 00:00:00 2001 From: Alexandre Combemorel Date: Tue, 13 Aug 2024 09:54:36 +0200 Subject: [PATCH 6/7] fix: rename T param in more meaningful name --- packages/use-i18n/src/usei18n.tsx | 90 +++++++++++++++++++------------ 1 file changed, 55 insertions(+), 35 deletions(-) diff --git a/packages/use-i18n/src/usei18n.tsx b/packages/use-i18n/src/usei18n.tsx index a671b9e61..5267f357e 100644 --- a/packages/use-i18n/src/usei18n.tsx +++ b/packages/use-i18n/src/usei18n.tsx @@ -23,35 +23,40 @@ import type { ReactParamsObject, ScopedTranslateFn, TranslateFn } from './types' const LOCALE_ITEM_STORAGE = 'locale' -type SupportedLocalesType = (locale: string) => locale is T +type SupportedLocalesType = ( + locale: string, +) => locale is LocalSupportedType type TranslationsByLocales = Record type RequiredGenericContext< LocaleParam extends BaseLocale, - T extends string, + LocalSupportedType extends string, > = keyof string extends never - ? Omit, 't' | 'namespaceTranslation'> & { + ? Omit< + Context, + 't' | 'namespaceTranslation' + > & { t: (str: 'You must pass a generic argument to useI18n()') => void namespaceTranslation: ( str: 'You must pass a generic argument to useI18n()', ) => void } - : Context + : Context const areNamespacesLoaded = ( namespaces: string[], loadedNamespaces: string[] = [], ) => namespaces.every(n => loadedNamespaces.includes(n)) -const getCurrentLocale = ({ +const getCurrentLocale = ({ defaultLocale, isLocaleSupported, localeItemStorage, }: { - defaultLocale: T - isLocaleSupported: SupportedLocalesType + defaultLocale: LocalSupportedType + isLocaleSupported: SupportedLocalesType localeItemStorage: string -}): T => { +}): LocalSupportedType => { if (typeof window !== 'undefined') { const { languages: browserLocales } = navigator const currentLocalFromlocalStorage = localStorage.getItem(localeItemStorage) @@ -84,8 +89,11 @@ const getCurrentLocale = ({ return defaultLocale } -type Context = { - currentLocale: T +type Context< + LocaleParam extends BaseLocale, + LocalSupportedType extends string, +> = { + currentLocale: LocalSupportedType dateFnsLocale?: DateFnsLocale datetime: ( date: Date | number, @@ -100,7 +108,7 @@ type Context = { formatUnit: (value: number, options: FormatUnitOptions) => string loadTranslations: ( namespace: string, - load?: LoadTranslationsFn, + load?: LoadTranslationsFn, ) => Promise namespaces: string[] namespaceTranslation: ScopedTranslateFn @@ -113,7 +121,7 @@ type Context = { options?: FormatDistanceToNowStrictOptions, ) => string setTranslations: React.Dispatch> - switchLocale: (locale: T) => Promise + switchLocale: (locale: LocalSupportedType) => Promise t: TranslateFn translations: TranslationsByLocales } @@ -125,24 +133,29 @@ const I18nContext = createContext | undefined>(undefined) export function useI18n< // eslint-disable-next-line @typescript-eslint/ban-types LocaleParam extends BaseLocale = {}, - T extends string = '', ->(): RequiredGenericContext { + LocalSupportedType extends string = '', +>(): RequiredGenericContext { const context = useContext(I18nContext) if (context === undefined) { throw new Error('useI18n must be used within a I18nProvider') } - return context as unknown as RequiredGenericContext + return context as unknown as RequiredGenericContext< + LocaleParam, + LocalSupportedType + > } export function useTranslation< // eslint-disable-next-line @typescript-eslint/ban-types LocaleParam extends BaseLocale = {}, - T extends string = '', + LocalSupportedType extends string = '', >( namespaces: string[] = [], - load: LoadTranslationsFn | undefined = undefined, -): RequiredGenericContext & { isLoaded: boolean } { + load: LoadTranslationsFn | undefined = undefined, +): RequiredGenericContext & { + isLoaded: boolean +} { const context = useContext(I18nContext) if (context === undefined) { throw new Error('useTranslation must be used within a I18nProvider') @@ -165,26 +178,30 @@ export function useTranslation< return { ...context, isLoaded, - } as unknown as RequiredGenericContext & { + } as unknown as RequiredGenericContext & { isLoaded: boolean } } -type LoadTranslationsFn = ({ +type LoadTranslationsFn = ({ namespace, locale, }: { namespace: string - locale: T + locale: LocalSupportedType }) => Promise<{ default: BaseLocale }> -type LoadLocaleFn = (locale: T) => DateFnsLocale -type LoadLocaleFnAsync = (locale: T) => Promise +type LoadLocaleFn = ( + locale: LocalSupportedType, +) => DateFnsLocale +type LoadLocaleFnAsync = ( + locale: LocalSupportedType, +) => Promise type LoadDateLocaleError = (error: Error) => void const initialDefaultTranslations = {} -const I18nContextProvider = ({ +const I18nContextProvider = ({ children, defaultLoad, defaultLocale, @@ -199,16 +216,16 @@ const I18nContextProvider = ({ isLocaleSupported, }: { children: ReactNode - defaultLoad: LoadTranslationsFn - loadDateLocale?: LoadLocaleFn - loadDateLocaleAsync: LoadLocaleFnAsync + defaultLoad: LoadTranslationsFn + loadDateLocale?: LoadLocaleFn + loadDateLocaleAsync: LoadLocaleFnAsync onLoadDateLocaleError?: LoadDateLocaleError - defaultLocale: T + defaultLocale: LocalSupportedType defaultTranslations: TranslationsByLocales enableDefaultLocale: boolean enableDebugKey: boolean localeItemStorage: string - isLocaleSupported: SupportedLocalesType + isLocaleSupported: SupportedLocalesType onTranslateError?: ({ error, currentLocale, @@ -216,13 +233,13 @@ const I18nContextProvider = ({ key, }: { error: Error - currentLocale: T - defaultLocale: T + currentLocale: LocalSupportedType + defaultLocale: LocalSupportedType value: string key: string }) => void }): ReactElement => { - const [currentLocale, setCurrentLocale] = useState( + const [currentLocale, setCurrentLocale] = useState( getCurrentLocale({ defaultLocale, localeItemStorage, @@ -240,7 +257,7 @@ const I18nContextProvider = ({ const loadDateFNS = loadDateLocale ?? loadDateLocaleAsync const setDateFns = useCallback( - async (locale: T) => { + async (locale: LocalSupportedType) => { try { const dateFns = await loadDateFNS(locale) setDateFnsLocale(dateFns) @@ -270,7 +287,10 @@ const I18nContextProvider = ({ }, [currentLocale, dateFnsLocale, setDateFns, setDateFnsLocale]) const loadTranslations = useCallback( - async (namespace: string, load: LoadTranslationsFn = defaultLoad) => { + async ( + namespace: string, + load: LoadTranslationsFn = defaultLoad, + ) => { const result = { [currentLocale]: { default: {} }, defaultLocale: { default: {} }, @@ -313,7 +333,7 @@ const I18nContextProvider = ({ ) const switchLocale = useCallback( - async (locale: T) => { + async (locale: LocalSupportedType) => { if (isLocaleSupported(locale)) { localStorage.setItem(localeItemStorage, locale) setCurrentLocale(locale) From ec93385dbbe970bc4232c3a5ac833466ad10f4fc Mon Sep 17 00:00:00 2001 From: Alexandre Date: Wed, 14 Aug 2024 10:56:50 +0200 Subject: [PATCH 7/7] Update packages/use-i18n/src/usei18n.tsx Co-authored-by: philibeaux --- packages/use-i18n/src/usei18n.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/use-i18n/src/usei18n.tsx b/packages/use-i18n/src/usei18n.tsx index 5267f357e..b31152c3a 100644 --- a/packages/use-i18n/src/usei18n.tsx +++ b/packages/use-i18n/src/usei18n.tsx @@ -31,7 +31,7 @@ type TranslationsByLocales = Record type RequiredGenericContext< LocaleParam extends BaseLocale, LocalSupportedType extends string, -> = keyof string extends never +> = keyof LocaleParam extends never ? Omit< Context, 't' | 'namespaceTranslation'