diff --git a/packages/use-i18n/src/__tests__/usei18n.tsx b/packages/use-i18n/src/__tests__/usei18n.tsx index af0bcbcb3..bc3137404 100644 --- a/packages/use-i18n/src/__tests__/usei18n.tsx +++ b/packages/use-i18n/src/__tests__/usei18n.tsx @@ -8,6 +8,15 @@ import fr from './locales/fr.json' 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' +} + const wrapper = ({ loadDateLocale = async (locale: string) => @@ -285,7 +294,7 @@ describe('i18n hook', () => { }) it('should translate correctly with enableDebugKey', async () => { - const { result } = renderHook(() => useI18n(), { + const { result } = renderHook(() => useI18n(), { wrapper: wrapper({ defaultLocale: 'en', defaultTranslations: { en }, @@ -311,24 +320,21 @@ describe('i18n hook', () => { }) it('should use namespaceTranslation', async () => { - const { result } = renderHook(() => useI18n(), { + const { result } = renderHook(() => useI18n(), { wrapper: wrapper({ defaultLocale: 'en', defaultTranslations: { en }, }), }) await waitFor(() => { - const identiqueTranslate = result.current.namespaceTranslation('') - expect(identiqueTranslate('title')).toEqual(result.current.t('title')) + const identiqueTranslate = result.current.namespaceTranslation('tests') + expect(identiqueTranslate('test.namespaces')).toEqual( + result.current.t('tests.test.namespaces'), + ) }) const translate = result.current.namespaceTranslation('tests.test') expect(translate('namespaces')).toEqual('test') - - // inception - const translate1 = result.current.namespaceTranslation('tests') - const translate2 = result.current.namespaceTranslation('test', translate1) - expect(translate2('namespaces')).toEqual(translate1('test.namespaces')) }) it('should use formatNumber', async () => { diff --git a/packages/use-i18n/src/usei18n.tsx b/packages/use-i18n/src/usei18n.tsx index 560129f70..c1b189aa9 100644 --- a/packages/use-i18n/src/usei18n.tsx +++ b/packages/use-i18n/src/usei18n.tsx @@ -4,7 +4,7 @@ import { formatDistanceToNow, formatDistanceToNowStrict, } from 'date-fns' -import type { BaseLocale, LocaleValue } from 'international-types' +import type { BaseLocale } from 'international-types' import PropTypes from 'prop-types' import { ReactElement, @@ -20,21 +20,12 @@ import ReactDOM from 'react-dom' import dateFormat, { FormatDateOptions } from './formatDate' import unitFormat, { FormatUnitOptions } from './formatUnit' import formatters, { IntlListFormatOptions } from './formatters' -import type { ScopedTranslateFn, TranslateFn } from './types' +import type { ReactParamsObject, ScopedTranslateFn, TranslateFn } from './types' const LOCALE_ITEM_STORAGE = 'locale' type TranslationsByLocales = Record -export type InitialTranslateFn = ( - key: string, - context?: Record, -) => string -export type InitialScopedTranslateFn = ( - namespace: string, - t?: InitialTranslateFn, -) => InitialTranslateFn - const areNamespacesLoaded = ( namespaces: string[], loadedNamespaces: string[] = [], @@ -62,7 +53,7 @@ const getCurrentLocale = ({ ) } -interface Context { +interface Context { currentLocale: string dateFnsLocale?: DateFnsLocale datetime: ( @@ -82,9 +73,7 @@ interface Context { ) => Promise locales: string[] namespaces: string[] - namespaceTranslation: Locale extends BaseLocale - ? ScopedTranslateFn - : InitialScopedTranslateFn + namespaceTranslation: ScopedTranslateFn relativeTime: ( date: Date | number, options?: { @@ -102,15 +91,15 @@ interface Context { ) => string setTranslations: React.Dispatch> switchLocale: (locale: string) => void - t: Locale extends BaseLocale ? TranslateFn : InitialTranslateFn + t: TranslateFn translations: TranslationsByLocales } -const I18nContext = createContext(undefined) +// 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) -export function useI18n< - Locale extends BaseLocale | undefined = undefined, ->(): Context { +export function useI18n(): Context { const context = useContext(I18nContext) if (context === undefined) { throw new Error('useI18n must be used within a I18nProvider') @@ -119,9 +108,7 @@ export function useI18n< return context as unknown as Context } -export function useTranslation< - Locale extends BaseLocale | undefined = undefined, ->( +export function useTranslation( namespaces: string[] = [], load: LoadTranslationsFn | undefined = undefined, ): Context & { isLoaded: boolean } { @@ -323,8 +310,8 @@ const I18nContextProvider = ({ [dateFnsLocale], ) - const translate = useCallback( - (key, context) => { + const translate = useCallback( + (key: string, context?: ReactParamsObject) => { const value = translations[currentLocale]?.[key] as string if (!value) { if (enableDebugKey) { @@ -344,10 +331,9 @@ const I18nContextProvider = ({ [currentLocale, translations, enableDebugKey], ) - const namespaceTranslation = useCallback( - (namespace, t = translate) => - (identifier, context) => - t(`${namespace}.${identifier}`, context) || t(identifier, context), + const namespaceTranslation = useCallback( + (scope: string) => (key: string, context?: ReactParamsObject) => + translate(`${scope}.${key}`, context) || translate(key, context), [translate], )