Skip to content

Commit cfe9757

Browse files
author
Tom Lienard
authored
feat(use-i18): typesafe useTranslation (#956)
* feat(use-i18): typesafe useTranslation * refactor: remove prefix key * feat: add back tests without prefix * chore(deps): bump international-types
1 parent 71bf390 commit cfe9757

File tree

8 files changed

+36
-55
lines changed

8 files changed

+36
-55
lines changed

packages/use-i18n/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,13 @@
3131
"@formatjs/fast-memoize": "1.2.6",
3232
"date-fns": "2.29.2",
3333
"filesize": "9.0.11",
34-
"international-types": "0.3.3",
34+
"international-types": "0.3.4",
3535
"intl-messageformat": "10.1.4",
3636
"prop-types": "15.8.1"
3737
},
3838
"peerDependencies": {
3939
"date-fns": "2.x",
40-
"international-types": "0.3.3",
40+
"international-types": "0.3.4",
4141
"react": "18.x",
4242
"react-dom": "18.x"
4343
}
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
{
2-
"prefix": "profile",
32
"name": "Name",
43
"lastName": "Last Name"
54
}

packages/use-i18n/src/__tests__/locales/namespaces/en/user.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
{
2-
"prefix": "user",
32
"name": "Name",
43
"lastName": "Last Name",
54
"languages": "Languages"
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
{
2-
"prefix": "profile",
32
"name": "Prénom",
43
"lastName": "Nom"
54
}
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
{
2-
"prefix": "user",
32
"name": "Prénom",
43
"lastName": "Nom"
54
}

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

Lines changed: 22 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -133,18 +133,16 @@ describe('i18n hook', () => {
133133
await waitFor(() => {
134134
expect(result.current.translations).toStrictEqual({
135135
en: {
136-
'profile.lastName': 'Last Name',
137-
'profile.name': 'Name',
138-
'user.languages': 'Languages',
139-
'user.lastName': 'Last Name',
140-
'user.name': 'Name',
136+
languages: 'Languages',
137+
lastName: 'Last Name',
138+
name: 'Name',
141139
},
142140
})
143141
})
144142

145-
expect(result.current.t('user.name')).toEqual('Name')
146-
expect(result.current.t('user.lastName')).toEqual('Last Name')
147-
expect(result.current.t('user.languages')).toEqual('Languages')
143+
expect(result.current.t('name')).toEqual('Name')
144+
expect(result.current.t('lastName')).toEqual('Last Name')
145+
expect(result.current.t('languages')).toEqual('Languages')
148146

149147
act(() => {
150148
result.current.switchLocale('fr')
@@ -153,27 +151,20 @@ describe('i18n hook', () => {
153151
await waitFor(() => {
154152
expect(result.current.translations).toStrictEqual({
155153
en: {
156-
'profile.lastName': 'Last Name',
157-
'profile.name': 'Name',
158-
'user.languages': 'Languages',
159-
'user.lastName': 'Last Name',
160-
'user.name': 'Name',
154+
languages: 'Languages',
155+
lastName: 'Last Name',
156+
name: 'Name',
161157
},
162158
fr: {
163-
'profile.lastName': 'Nom',
164-
'profile.name': 'Prénom',
165-
'user.lastName': 'Nom',
166-
'user.name': 'Prénom',
159+
lastName: 'Nom',
160+
name: 'Prénom',
167161
},
168162
})
169163
})
170164

171-
expect(result.current.t('user.name')).toEqual('Prénom')
172-
expect(result.current.t('user.lastName')).toEqual('Nom')
173-
expect(result.current.t('user.languages')).toEqual('')
174-
175-
expect(result.current.t('user')).toEqual('')
176-
expect(result.current.t('user', { test: 'toto' })).toEqual('')
165+
expect(result.current.t('name')).toEqual('Prénom')
166+
expect(result.current.t('lastName')).toEqual('Nom')
167+
expect(result.current.t('languages')).toEqual('')
177168
})
178169

179170
it("should use specific load and fallback default local if the key doesn't exist", async () => {
@@ -202,19 +193,19 @@ describe('i18n hook', () => {
202193
await waitFor(() => {
203194
expect(result.current.translations).toStrictEqual({
204195
en: {
205-
'user.languages': 'Languages',
206-
'user.lastName': 'Last Name',
207-
'user.name': 'Name',
196+
languages: 'Languages',
197+
lastName: 'Last Name',
198+
name: 'Name',
208199
},
209200
fr: {
210-
'user.lastName': 'Nom',
211-
'user.name': 'Prénom',
201+
lastName: 'Nom',
202+
name: 'Prénom',
212203
},
213204
})
214205

215-
expect(result.current.t('user.languages')).toEqual('')
216-
expect(result.current.t('user.lastName')).toEqual('Nom')
217-
expect(result.current.t('user.name')).toEqual('Prénom')
206+
expect(result.current.t('languages')).toEqual('')
207+
expect(result.current.t('lastName')).toEqual('Nom')
208+
expect(result.current.t('name')).toEqual('Prénom')
218209
})
219210
})
220211

packages/use-i18n/src/usei18n.tsx

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,6 @@ export type InitialScopedTranslateFn = (
3535
t?: InitialTranslateFn,
3636
) => InitialTranslateFn
3737

38-
const prefixKeys = (prefix: string) => (obj: { [key: string]: string }) =>
39-
Object.keys(obj).reduce((acc: { [key: string]: string }, key) => {
40-
acc[`${prefix}${key}`] = obj[key]
41-
42-
return acc
43-
}, {})
44-
4538
const areNamespacesLoaded = (
4639
namespaces: string[],
4740
loadedNamespaces: string[] = [],
@@ -126,10 +119,12 @@ export function useI18n<
126119
return context as unknown as Context<Locale>
127120
}
128121

129-
export const useTranslation = (
122+
export function useTranslation<
123+
Locale extends BaseLocale | undefined = undefined,
124+
>(
130125
namespaces: string[] = [],
131126
load: LoadTranslationsFn | undefined = undefined,
132-
): Context & { isLoaded: boolean } => {
127+
): Context<Locale> & { isLoaded: boolean } {
133128
const context = useContext(I18nContext)
134129
if (context === undefined) {
135130
throw new Error('useTranslation must be used within a I18nProvider')
@@ -148,7 +143,9 @@ export const useTranslation = (
148143
[loadedNamespaces, namespaces],
149144
)
150145

151-
return { ...context, isLoaded }
146+
return { ...context, isLoaded } as unknown as Context<Locale> & {
147+
isLoaded: boolean
148+
}
152149
}
153150

154151
type LoadTranslationsFn = ({
@@ -222,9 +219,6 @@ const I18nContextProvider = ({
222219
...result[currentLocale].default,
223220
}
224221

225-
const { prefix, ...values } = trad
226-
const preparedValues = prefix ? prefixKeys(`${prefix}.`)(values) : values
227-
228222
// avoid a lot of render when async update
229223
// This is handled automatically in react 18, but we leave it here for compat
230224
// https://github.com/reactwg/react-18/discussions/21#discussioncomment-801703
@@ -234,7 +228,7 @@ const I18nContextProvider = ({
234228
...{
235229
[currentLocale]: {
236230
...prevState[currentLocale],
237-
...preparedValues,
231+
...trad,
238232
},
239233
},
240234
}))

pnpm-lock.yaml

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)