Skip to content

Commit 849da5c

Browse files
author
Tom Lienard
authored
feat(i18n): always required Locale generic (#1044)
* feat(i18n): always required Locale generic * fix: type errors in useI18n * fix: tests error
1 parent 597cfa9 commit 849da5c

File tree

2 files changed

+30
-38
lines changed

2 files changed

+30
-38
lines changed

packages/use-i18n/src/__tests__/usei18n.tsx

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ import fr from './locales/fr.json'
88

99
const LOCALE_ITEM_STORAGE = 'locales'
1010

11+
type Locale = {
12+
test: 'Test'
13+
'with.identifier': 'Are you sure you want to delete {identifier}?'
14+
plurals: '{numPhotos, plural, =0 {You have one photo.} other {You have # photos.}}'
15+
subtitle: 'Here is a subtitle'
16+
'tests.test.namespaces': 'test'
17+
title: 'Welcome on @scaelway/ui i18n hook'
18+
}
19+
1120
const wrapper =
1221
({
1322
loadDateLocale = async (locale: string) =>
@@ -285,7 +294,7 @@ describe('i18n hook', () => {
285294
})
286295

287296
it('should translate correctly with enableDebugKey', async () => {
288-
const { result } = renderHook(() => useI18n(), {
297+
const { result } = renderHook(() => useI18n<Locale>(), {
289298
wrapper: wrapper({
290299
defaultLocale: 'en',
291300
defaultTranslations: { en },
@@ -311,24 +320,21 @@ describe('i18n hook', () => {
311320
})
312321

313322
it('should use namespaceTranslation', async () => {
314-
const { result } = renderHook(() => useI18n(), {
323+
const { result } = renderHook(() => useI18n<Locale>(), {
315324
wrapper: wrapper({
316325
defaultLocale: 'en',
317326
defaultTranslations: { en },
318327
}),
319328
})
320329
await waitFor(() => {
321-
const identiqueTranslate = result.current.namespaceTranslation('')
322-
expect(identiqueTranslate('title')).toEqual(result.current.t('title'))
330+
const identiqueTranslate = result.current.namespaceTranslation('tests')
331+
expect(identiqueTranslate('test.namespaces')).toEqual(
332+
result.current.t('tests.test.namespaces'),
333+
)
323334
})
324335

325336
const translate = result.current.namespaceTranslation('tests.test')
326337
expect(translate('namespaces')).toEqual('test')
327-
328-
// inception
329-
const translate1 = result.current.namespaceTranslation('tests')
330-
const translate2 = result.current.namespaceTranslation('test', translate1)
331-
expect(translate2('namespaces')).toEqual(translate1('test.namespaces'))
332338
})
333339

334340
it('should use formatNumber', async () => {

packages/use-i18n/src/usei18n.tsx

Lines changed: 15 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
formatDistanceToNow,
55
formatDistanceToNowStrict,
66
} from 'date-fns'
7-
import type { BaseLocale, LocaleValue } from 'international-types'
7+
import type { BaseLocale } from 'international-types'
88
import PropTypes from 'prop-types'
99
import {
1010
ReactElement,
@@ -20,21 +20,12 @@ import ReactDOM from 'react-dom'
2020
import dateFormat, { FormatDateOptions } from './formatDate'
2121
import unitFormat, { FormatUnitOptions } from './formatUnit'
2222
import formatters, { IntlListFormatOptions } from './formatters'
23-
import type { ScopedTranslateFn, TranslateFn } from './types'
23+
import type { ReactParamsObject, ScopedTranslateFn, TranslateFn } from './types'
2424

2525
const LOCALE_ITEM_STORAGE = 'locale'
2626

2727
type TranslationsByLocales = Record<string, BaseLocale>
2828

29-
export type InitialTranslateFn = (
30-
key: string,
31-
context?: Record<string, LocaleValue | JSX.Element>,
32-
) => string
33-
export type InitialScopedTranslateFn = (
34-
namespace: string,
35-
t?: InitialTranslateFn,
36-
) => InitialTranslateFn
37-
3829
const areNamespacesLoaded = (
3930
namespaces: string[],
4031
loadedNamespaces: string[] = [],
@@ -62,7 +53,7 @@ const getCurrentLocale = ({
6253
)
6354
}
6455

65-
interface Context<Locale extends BaseLocale | undefined = undefined> {
56+
interface Context<Locale extends BaseLocale> {
6657
currentLocale: string
6758
dateFnsLocale?: DateFnsLocale
6859
datetime: (
@@ -82,9 +73,7 @@ interface Context<Locale extends BaseLocale | undefined = undefined> {
8273
) => Promise<string>
8374
locales: string[]
8475
namespaces: string[]
85-
namespaceTranslation: Locale extends BaseLocale
86-
? ScopedTranslateFn<Locale>
87-
: InitialScopedTranslateFn
76+
namespaceTranslation: ScopedTranslateFn<Locale>
8877
relativeTime: (
8978
date: Date | number,
9079
options?: {
@@ -102,15 +91,15 @@ interface Context<Locale extends BaseLocale | undefined = undefined> {
10291
) => string
10392
setTranslations: React.Dispatch<React.SetStateAction<TranslationsByLocales>>
10493
switchLocale: (locale: string) => void
105-
t: Locale extends BaseLocale ? TranslateFn<Locale> : InitialTranslateFn
94+
t: TranslateFn<Locale>
10695
translations: TranslationsByLocales
10796
}
10897

109-
const I18nContext = createContext<Context | undefined>(undefined)
98+
// It's safe to use any here because the Locale can be anything at this point:
99+
// useI18n / useTranslation requires to explicitely give a Locale to use.
100+
const I18nContext = createContext<Context<any> | undefined>(undefined)
110101

111-
export function useI18n<
112-
Locale extends BaseLocale | undefined = undefined,
113-
>(): Context<Locale> {
102+
export function useI18n<Locale extends BaseLocale>(): Context<Locale> {
114103
const context = useContext(I18nContext)
115104
if (context === undefined) {
116105
throw new Error('useI18n must be used within a I18nProvider')
@@ -119,9 +108,7 @@ export function useI18n<
119108
return context as unknown as Context<Locale>
120109
}
121110

122-
export function useTranslation<
123-
Locale extends BaseLocale | undefined = undefined,
124-
>(
111+
export function useTranslation<Locale extends BaseLocale>(
125112
namespaces: string[] = [],
126113
load: LoadTranslationsFn | undefined = undefined,
127114
): Context<Locale> & { isLoaded: boolean } {
@@ -323,8 +310,8 @@ const I18nContextProvider = ({
323310
[dateFnsLocale],
324311
)
325312

326-
const translate = useCallback<InitialTranslateFn>(
327-
(key, context) => {
313+
const translate = useCallback(
314+
(key: string, context?: ReactParamsObject<any>) => {
328315
const value = translations[currentLocale]?.[key] as string
329316
if (!value) {
330317
if (enableDebugKey) {
@@ -344,10 +331,9 @@ const I18nContextProvider = ({
344331
[currentLocale, translations, enableDebugKey],
345332
)
346333

347-
const namespaceTranslation = useCallback<InitialScopedTranslateFn>(
348-
(namespace, t = translate) =>
349-
(identifier, context) =>
350-
t(`${namespace}.${identifier}`, context) || t(identifier, context),
334+
const namespaceTranslation = useCallback(
335+
(scope: string) => (key: string, context?: ReactParamsObject<any>) =>
336+
translate(`${scope}.${key}`, context) || translate(key, context),
351337
[translate],
352338
)
353339

0 commit comments

Comments
 (0)