From 394f39c36bd9de2a4040e6a3ea66a3c479b0f23a Mon Sep 17 00:00:00 2001 From: QuiiBz Date: Wed, 19 Oct 2022 16:24:33 +0200 Subject: [PATCH 1/3] feat(i18n): always required Locale generic --- packages/use-i18n/src/__tests__/usei18n.tsx | 11 ++++- packages/use-i18n/src/types.ts | 27 ++++++++---- packages/use-i18n/src/usei18n.tsx | 46 ++++++++------------- 3 files changed, 46 insertions(+), 38 deletions(-) diff --git a/packages/use-i18n/src/__tests__/usei18n.tsx b/packages/use-i18n/src/__tests__/usei18n.tsx index af0bcbcb3..59ecac1de 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 }, diff --git a/packages/use-i18n/src/types.ts b/packages/use-i18n/src/types.ts index 2c6ea1bb6..545cdaa84 100644 --- a/packages/use-i18n/src/types.ts +++ b/packages/use-i18n/src/types.ts @@ -8,27 +8,38 @@ import type { } from 'international-types' import type { ReactNode } from 'react' -export type ReactParamsObject = Record< +export type ReactParamsObject< + Value extends LocaleValue, + ReactParams extends boolean = true, +> = Record< Params[number], - LocaleValue | ReactNode + ReactParams extends true ? LocaleValue | ReactNode : LocaleValue > -export type TranslateFn = < +export type TranslateFn< + Locale extends BaseLocale, + ReactParams extends boolean = true, +> = < Key extends LocaleKeys, Value extends LocaleValue = ScopedValue, >( key: Key, - ...params: Params['length'] extends 0 ? [] : [ReactParamsObject] + ...params: Params['length'] extends 0 + ? [] + : [ReactParamsObject] ) => string -export type ScopedTranslateFn = < - Scope extends Scopes, ->( +export type ScopedTranslateFn< + Locale extends BaseLocale, + ReactParams extends boolean = true, +> = >( scope: Scope, ) => < Key extends LocaleKeys, Value extends LocaleValue = ScopedValue, >( key: Key, - ...params: Params['length'] extends 0 ? [] : [ReactParamsObject] + ...params: Params['length'] extends 0 + ? [] + : [ReactParamsObject] ) => string diff --git a/packages/use-i18n/src/usei18n.tsx b/packages/use-i18n/src/usei18n.tsx index 560129f70..e42e61c40 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, @@ -26,15 +26,6 @@ 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, ...context) => { const value = translations[currentLocale]?.[key] as string if (!value) { if (enableDebugKey) { @@ -336,7 +323,7 @@ const I18nContextProvider = ({ if (context) { return formatters .getTranslationFormat(value, currentLocale) - .format(context) as string + .format(...context) as string } return value @@ -344,10 +331,11 @@ const I18nContextProvider = ({ [currentLocale, translations, enableDebugKey], ) - const namespaceTranslation = useCallback( - (namespace, t = translate) => - (identifier, context) => - t(`${namespace}.${identifier}`, context) || t(identifier, context), + const namespaceTranslation = useCallback>( + namespace => + (identifier, ...context) => + translate(`${namespace}.${identifier}`, ...context) || + translate(identifier, ...context), [translate], ) From 1541719d9deb2034bc8da2b2caf9017959a57c50 Mon Sep 17 00:00:00 2001 From: QuiiBz Date: Thu, 20 Oct 2022 14:21:20 +0200 Subject: [PATCH 2/3] fix: type errors in useI18n --- packages/use-i18n/src/types.ts | 27 ++++++++------------------- packages/use-i18n/src/usei18n.tsx | 20 +++++++++----------- 2 files changed, 17 insertions(+), 30 deletions(-) diff --git a/packages/use-i18n/src/types.ts b/packages/use-i18n/src/types.ts index 545cdaa84..2c6ea1bb6 100644 --- a/packages/use-i18n/src/types.ts +++ b/packages/use-i18n/src/types.ts @@ -8,38 +8,27 @@ import type { } from 'international-types' import type { ReactNode } from 'react' -export type ReactParamsObject< - Value extends LocaleValue, - ReactParams extends boolean = true, -> = Record< +export type ReactParamsObject = Record< Params[number], - ReactParams extends true ? LocaleValue | ReactNode : LocaleValue + LocaleValue | ReactNode > -export type TranslateFn< - Locale extends BaseLocale, - ReactParams extends boolean = true, -> = < +export type TranslateFn = < Key extends LocaleKeys, Value extends LocaleValue = ScopedValue, >( key: Key, - ...params: Params['length'] extends 0 - ? [] - : [ReactParamsObject] + ...params: Params['length'] extends 0 ? [] : [ReactParamsObject] ) => string -export type ScopedTranslateFn< - Locale extends BaseLocale, - ReactParams extends boolean = true, -> = >( +export type ScopedTranslateFn = < + Scope extends Scopes, +>( scope: Scope, ) => < Key extends LocaleKeys, Value extends LocaleValue = ScopedValue, >( key: Key, - ...params: Params['length'] extends 0 - ? [] - : [ReactParamsObject] + ...params: Params['length'] extends 0 ? [] : [ReactParamsObject] ) => string diff --git a/packages/use-i18n/src/usei18n.tsx b/packages/use-i18n/src/usei18n.tsx index e42e61c40..c1b189aa9 100644 --- a/packages/use-i18n/src/usei18n.tsx +++ b/packages/use-i18n/src/usei18n.tsx @@ -20,7 +20,7 @@ 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' @@ -73,7 +73,7 @@ interface Context { ) => Promise locales: string[] namespaces: string[] - namespaceTranslation: ScopedTranslateFn + namespaceTranslation: ScopedTranslateFn relativeTime: ( date: Date | number, options?: { @@ -91,7 +91,7 @@ interface Context { ) => string setTranslations: React.Dispatch> switchLocale: (locale: string) => void - t: TranslateFn + t: TranslateFn translations: TranslationsByLocales } @@ -310,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) { @@ -323,7 +323,7 @@ const I18nContextProvider = ({ if (context) { return formatters .getTranslationFormat(value, currentLocale) - .format(...context) as string + .format(context) as string } return value @@ -331,11 +331,9 @@ const I18nContextProvider = ({ [currentLocale, translations, enableDebugKey], ) - const namespaceTranslation = useCallback>( - namespace => - (identifier, ...context) => - translate(`${namespace}.${identifier}`, ...context) || - translate(identifier, ...context), + const namespaceTranslation = useCallback( + (scope: string) => (key: string, context?: ReactParamsObject) => + translate(`${scope}.${key}`, context) || translate(key, context), [translate], ) From fa5524c032405acc56068bb1f5a5acdcb542c625 Mon Sep 17 00:00:00 2001 From: QuiiBz Date: Thu, 20 Oct 2022 14:24:32 +0200 Subject: [PATCH 3/3] fix: tests error --- packages/use-i18n/src/__tests__/usei18n.tsx | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/use-i18n/src/__tests__/usei18n.tsx b/packages/use-i18n/src/__tests__/usei18n.tsx index 59ecac1de..bc3137404 100644 --- a/packages/use-i18n/src/__tests__/usei18n.tsx +++ b/packages/use-i18n/src/__tests__/usei18n.tsx @@ -320,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 () => {