diff --git a/packages/use-i18n/src/__tests__/usei18n.tsx b/packages/use-i18n/src/__tests__/usei18n.tsx index bc3137404..c17f177c6 100644 --- a/packages/use-i18n/src/__tests__/usei18n.tsx +++ b/packages/use-i18n/src/__tests__/usei18n.tsx @@ -17,6 +17,12 @@ type Locale = { title: 'Welcome on @scaelway/ui i18n hook' } +type NamespaceLocale = { + name: 'Name' + lastName: 'Last Name' + languages: 'Languages' +} + const wrapper = ({ loadDateLocale = async (locale: string) => @@ -92,7 +98,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 @@ -130,7 +136,7 @@ describe('i18n hook', () => { }) => import(`./locales/namespaces/${locale}/${namespace}.json`) const { result } = renderHook( - () => useTranslation(['user', 'profile'], load), + () => useTranslation(['user', 'profile'], load), { wrapper: wrapper({ defaultLocale: 'en', @@ -185,13 +191,16 @@ describe('i18n hook', () => { namespace: string }) => import(`./locales/namespaces/${locale}/${namespace}.json`) - const { result } = renderHook(() => useTranslation(['user'], load), { - wrapper: wrapper({ - defaultLocale: 'fr', - enableDefaultLocale: true, - supportedLocales: ['en', 'fr'], - }), - }) + 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 diff --git a/packages/use-i18n/src/__typetests__/namespaceTranslation.test.ts b/packages/use-i18n/src/__typetests__/namespaceTranslation.test.ts index 48f427368..538049b55 100644 --- a/packages/use-i18n/src/__typetests__/namespaceTranslation.test.ts +++ b/packages/use-i18n/src/__typetests__/namespaceTranslation.test.ts @@ -1,7 +1,7 @@ +/* eslint-disable react-hooks/rules-of-hooks */ import { expectError, expectType } from 'tsd-lite' import { useI18n } from '../usei18n' -// eslint-disable-next-line react-hooks/rules-of-hooks const { namespaceTranslation } = useI18n<{ hello: 'world' 'doe.john': 'John Doe' @@ -50,3 +50,7 @@ expectType( ) expectError(scopedT3('john', {})) expectError(scopedT3('john')) + +// Required generic +const { namespaceTranslation: namespaceTranslation2 } = useI18n() +expectError(namespaceTranslation2('test')) diff --git a/packages/use-i18n/src/__typetests__/t.test.tsx b/packages/use-i18n/src/__typetests__/t.test.tsx index 3444dc568..9b152ba51 100644 --- a/packages/use-i18n/src/__typetests__/t.test.tsx +++ b/packages/use-i18n/src/__typetests__/t.test.tsx @@ -1,7 +1,7 @@ +/* eslint-disable react-hooks/rules-of-hooks */ import { expectError, expectType } from 'tsd-lite' -import { useI18n } from '../usei18n' +import { useI18n, useTranslation } from '../usei18n' -// eslint-disable-next-line react-hooks/rules-of-hooks const { t } = useI18n<{ hello: 'world' 'doe.john': 'John Doe' @@ -58,3 +58,7 @@ expectType( ), }), ) + +// Required generic +const { t: t2 } = useI18n() +expectError(t2('test')) diff --git a/packages/use-i18n/src/usei18n.tsx b/packages/use-i18n/src/usei18n.tsx index 27e815f2a..6c1c84e86 100644 --- a/packages/use-i18n/src/usei18n.tsx +++ b/packages/use-i18n/src/usei18n.tsx @@ -25,6 +25,15 @@ import type { ReactParamsObject, ScopedTranslateFn, TranslateFn } from './types' const LOCALE_ITEM_STORAGE = 'locale' 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 const areNamespacesLoaded = ( namespaces: string[], @@ -99,19 +108,23 @@ interface Context { // useI18n / useTranslation requires to explicitely give a Locale to use. const I18nContext = createContext | undefined>(undefined) -export function useI18n(): Context { +export function useI18n< + // eslint-disable-next-line @typescript-eslint/ban-types + Locale extends BaseLocale = {}, +>(): RequiredGenericContext { const context = useContext(I18nContext) if (context === undefined) { throw new Error('useI18n must be used within a I18nProvider') } - return context as unknown as Context + return context as unknown as RequiredGenericContext } -export function useTranslation( +// eslint-disable-next-line @typescript-eslint/ban-types +export function useTranslation( namespaces: string[] = [], load: LoadTranslationsFn | undefined = undefined, -): Context & { isLoaded: boolean } { +): RequiredGenericContext & { isLoaded: boolean } { const context = useContext(I18nContext) if (context === undefined) { throw new Error('useTranslation must be used within a I18nProvider') @@ -130,7 +143,10 @@ export function useTranslation( [loadedNamespaces, namespaces], ) - return { ...context, isLoaded } as unknown as Context & { + return { + ...context, + isLoaded, + } as unknown as RequiredGenericContext & { isLoaded: boolean } }