Skip to content

Commit e028640

Browse files
author
Tom Lienard
authored
feat(use-i18n): required generic (#1059)
* feat(use-i18n): required generic * fix: add generic locale to tests * feat: improve error message by removing overload
1 parent 0f5754c commit e028640

File tree

4 files changed

+50
-17
lines changed

4 files changed

+50
-17
lines changed

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

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ type Locale = {
1717
title: 'Welcome on @scaelway/ui i18n hook'
1818
}
1919

20+
type NamespaceLocale = {
21+
name: 'Name'
22+
lastName: 'Last Name'
23+
languages: 'Languages'
24+
}
25+
2026
const wrapper =
2127
({
2228
loadDateLocale = async (locale: string) =>
@@ -92,7 +98,7 @@ describe('i18n hook', () => {
9298
})
9399

94100
it('should use defaultLoad, useTranslation, switch local and translate', async () => {
95-
const { result } = renderHook(() => useTranslation([]), {
101+
const { result } = renderHook(() => useTranslation<Locale>([]), {
96102
wrapper: wrapper({ defaultLocale: 'en' }),
97103
})
98104
// first render there is no load
@@ -130,7 +136,7 @@ describe('i18n hook', () => {
130136
}) => import(`./locales/namespaces/${locale}/${namespace}.json`)
131137

132138
const { result } = renderHook(
133-
() => useTranslation(['user', 'profile'], load),
139+
() => useTranslation<NamespaceLocale>(['user', 'profile'], load),
134140
{
135141
wrapper: wrapper({
136142
defaultLocale: 'en',
@@ -185,13 +191,16 @@ describe('i18n hook', () => {
185191
namespace: string
186192
}) => import(`./locales/namespaces/${locale}/${namespace}.json`)
187193

188-
const { result } = renderHook(() => useTranslation(['user'], load), {
189-
wrapper: wrapper({
190-
defaultLocale: 'fr',
191-
enableDefaultLocale: true,
192-
supportedLocales: ['en', 'fr'],
193-
}),
194-
})
194+
const { result } = renderHook(
195+
() => useTranslation<NamespaceLocale>(['user'], load),
196+
{
197+
wrapper: wrapper({
198+
defaultLocale: 'fr',
199+
enableDefaultLocale: true,
200+
supportedLocales: ['en', 'fr'],
201+
}),
202+
},
203+
)
195204

196205
// current local will be 'en' based on navigator
197206
// await load of locales

packages/use-i18n/src/__typetests__/namespaceTranslation.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1+
/* eslint-disable react-hooks/rules-of-hooks */
12
import { expectError, expectType } from 'tsd-lite'
23
import { useI18n } from '../usei18n'
34

4-
// eslint-disable-next-line react-hooks/rules-of-hooks
55
const { namespaceTranslation } = useI18n<{
66
hello: 'world'
77
'doe.john': 'John Doe'
@@ -50,3 +50,7 @@ expectType<string>(
5050
)
5151
expectError(scopedT3('john', {}))
5252
expectError(scopedT3('john'))
53+
54+
// Required generic
55+
const { namespaceTranslation: namespaceTranslation2 } = useI18n()
56+
expectError(namespaceTranslation2('test'))

packages/use-i18n/src/__typetests__/t.test.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1+
/* eslint-disable react-hooks/rules-of-hooks */
12
import { expectError, expectType } from 'tsd-lite'
2-
import { useI18n } from '../usei18n'
3+
import { useI18n, useTranslation } from '../usei18n'
34

4-
// eslint-disable-next-line react-hooks/rules-of-hooks
55
const { t } = useI18n<{
66
hello: 'world'
77
'doe.john': 'John Doe'
@@ -58,3 +58,7 @@ expectType<string>(
5858
),
5959
}),
6060
)
61+
62+
// Required generic
63+
const { t: t2 } = useI18n()
64+
expectError(t2('test'))

packages/use-i18n/src/usei18n.tsx

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@ import type { ReactParamsObject, ScopedTranslateFn, TranslateFn } from './types'
2525
const LOCALE_ITEM_STORAGE = 'locale'
2626

2727
type TranslationsByLocales = Record<string, BaseLocale>
28+
type RequiredGenericContext<Locale extends BaseLocale> =
29+
keyof Locale extends never
30+
? Omit<Context<Locale>, 't' | 'namespaceTranslation'> & {
31+
t: (str: 'You must pass a generic argument to useI18n()') => void
32+
namespaceTranslation: (
33+
str: 'You must pass a generic argument to useI18n()',
34+
) => void
35+
}
36+
: Context<Locale>
2837

2938
const areNamespacesLoaded = (
3039
namespaces: string[],
@@ -99,19 +108,23 @@ interface Context<Locale extends BaseLocale> {
99108
// useI18n / useTranslation requires to explicitely give a Locale to use.
100109
const I18nContext = createContext<Context<any> | undefined>(undefined)
101110

102-
export function useI18n<Locale extends BaseLocale>(): Context<Locale> {
111+
export function useI18n<
112+
// eslint-disable-next-line @typescript-eslint/ban-types
113+
Locale extends BaseLocale = {},
114+
>(): RequiredGenericContext<Locale> {
103115
const context = useContext(I18nContext)
104116
if (context === undefined) {
105117
throw new Error('useI18n must be used within a I18nProvider')
106118
}
107119

108-
return context as unknown as Context<Locale>
120+
return context as unknown as RequiredGenericContext<Locale>
109121
}
110122

111-
export function useTranslation<Locale extends BaseLocale>(
123+
// eslint-disable-next-line @typescript-eslint/ban-types
124+
export function useTranslation<Locale extends BaseLocale = {}>(
112125
namespaces: string[] = [],
113126
load: LoadTranslationsFn | undefined = undefined,
114-
): Context<Locale> & { isLoaded: boolean } {
127+
): RequiredGenericContext<Locale> & { isLoaded: boolean } {
115128
const context = useContext(I18nContext)
116129
if (context === undefined) {
117130
throw new Error('useTranslation must be used within a I18nProvider')
@@ -130,7 +143,10 @@ export function useTranslation<Locale extends BaseLocale>(
130143
[loadedNamespaces, namespaces],
131144
)
132145

133-
return { ...context, isLoaded } as unknown as Context<Locale> & {
146+
return {
147+
...context,
148+
isLoaded,
149+
} as unknown as RequiredGenericContext<Locale> & {
134150
isLoaded: boolean
135151
}
136152
}

0 commit comments

Comments
 (0)