Skip to content

Commit 13e2ded

Browse files
authored
fix(i18n): add loaded namespace (#183)
1 parent de2536d commit 13e2ded

File tree

5 files changed

+86
-94
lines changed

5 files changed

+86
-94
lines changed

packages/use-i18n/README.md

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@ Use of local `variables` and `namespace` to dynamically load locales.
3030
your loaders will be:
3131

3232
```js
33-
const load = ({ locale, namespace }) => import(`./locales/${locale}/${namespace}`)
34-
const loadDateLocale = (locale) => import(`date-fns/locale/${locale}/index`)
33+
const load = ({ locale, namespace }) =>
34+
import(`./locales/${locale}/${namespace}`)
35+
const loadDateLocale = locale => import(`date-fns/locale/${locale}/index`)
3536
```
3637

3738
Inside your app you will need to use useTranslation to load namespace locales.
@@ -102,23 +103,16 @@ const App = () => {
102103
}
103104
```
104105
106+
In a case you will need to avoid somes useless re-render. you can wait that all your namespaces are loaded
107+
105108
```js
106-
import { useI18n } from '@scaleway/use-i18n'
109+
import { useTranslation } from '@scaleway/use-i18n'
107110

108111
const App = () => {
109-
const i18n = useTranslation()
110-
const { loadTranslations } = i18n
111112
const namespaces = ['app', 'common']
113+
const { t, isLoaded } = useTranslation(namespaces)
112114

113-
const key = namespaces.join(',')
114-
115-
useEffect(
116-
() =>
117-
key.split(',').map(async namespace => loadTranslations(namespace, load)),
118-
[loadTranslations, key, load],
119-
)
120-
121-
return <>{i18n.t('app.user')}(</>
115+
return isLoaded ? <>{t('app.user')}(</> : null
122116
}
123117
```
124118

packages/use-i18n/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,13 @@
3434
},
3535
"peerDependencies": {
3636
"date-fns": "2.x",
37-
"react": "17.x"
37+
"react": "17.x",
38+
"react-dom": "17.x"
3839
},
3940
"devDependencies": {
4041
"@testing-library/react-hooks": "^6.0.0",
4142
"mockdate": "^3.0.5",
42-
"react": "17.0.2"
43+
"react": "17.0.2",
44+
"react-dom": "17.0.2"
4345
}
4446
}

packages/use-i18n/src/__tests__/index.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,14 @@ describe('i18n hook', () => {
157157
}),
158158
},
159159
)
160+
161+
// current local will be 'en' based on navigator
160162
// await load of locales
163+
act(() => {
164+
result.current.switchLocale('fr')
165+
})
161166
await waitForNextUpdate()
167+
162168
expect(result.current.translations).toStrictEqual({
163169
en: {
164170
'user.languages': 'Languages',
@@ -170,10 +176,6 @@ describe('i18n hook', () => {
170176
'user.name': 'Prénom',
171177
},
172178
})
173-
act(() => {
174-
result.current.switchLocale('fr')
175-
})
176-
await waitForNextUpdate()
177179

178180
expect(result.current.t('user.languages')).toEqual('')
179181
expect(result.current.t('user.lastName')).toEqual('Nom')

packages/use-i18n/src/index.js

Lines changed: 62 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import React, {
1010
useMemo,
1111
useState,
1212
} from 'react'
13+
import ReactDOM from 'react-dom'
1314
import 'intl-pluralrules'
1415

1516
const LOCALE_ITEM_STORAGE = 'locale'
@@ -21,6 +22,27 @@ const prefixKeys = prefix => obj =>
2122
return acc
2223
}, {})
2324

25+
const areNamespacesLoaded = (namespaces, loadedNamespaces) =>
26+
namespaces.every(n => loadedNamespaces.includes(n))
27+
28+
const getLocaleFallback = locale => locale.split('-')[0].split('_')[0]
29+
30+
const getCurrentLocale = ({
31+
defaultLocale,
32+
supportedLocales,
33+
localeItemStorage,
34+
}) => {
35+
const languages = navigator.languages || [navigator.language]
36+
const browserLocales = [...new Set(languages.map(getLocaleFallback))]
37+
const localeStorage = localStorage.getItem(localeItemStorage)
38+
39+
return (
40+
localeStorage ||
41+
browserLocales.find(locale => supportedLocales.includes(locale)) ||
42+
defaultLocale
43+
)
44+
}
45+
2446
const I18nContext = createContext()
2547

2648
export const useI18n = () => {
@@ -37,16 +59,19 @@ export const useTranslation = (namespaces = [], load) => {
3759
if (context === undefined) {
3860
throw new Error('useTranslation must be used within a I18nProvider')
3961
}
40-
const { loadTranslations } = context
62+
const { loadTranslations, namespaces: loadedNamespaces } = context
4163

4264
const key = namespaces.join(',')
43-
useEffect(
44-
() =>
45-
key.split(',').map(async namespace => loadTranslations(namespace, load)),
46-
[loadTranslations, key, load],
65+
useEffect(() => {
66+
key.split(',').map(async namespace => loadTranslations(namespace, load))
67+
}, [loadTranslations, key, load])
68+
69+
const isLoaded = useMemo(
70+
() => areNamespacesLoaded(namespaces, loadedNamespaces),
71+
[loadedNamespaces, namespaces],
4772
)
4873

49-
return context
74+
return { ...context, isLoaded }
5075
}
5176

5277
// https://formatjs.io/docs/intl-messageformat/
@@ -66,27 +91,18 @@ const I18nContextProvider = ({
6691
localeItemStorage,
6792
supportedLocales,
6893
}) => {
69-
const [currentLocale, setCurrentLocale] = useState(defaultLocale)
70-
const [locales, setLocales] = useState(supportedLocales)
94+
const [currentLocale, setCurrentLocale] = useState(
95+
getCurrentLocale({ defaultLocale, localeItemStorage, supportedLocales }),
96+
)
7197
const [translations, setTranslations] = useState(defaultTranslations)
98+
const [namespaces, setNamespaces] = useState([])
7299
const [dateFnsLocale, setDateFnsLocale] = useState()
73100

74-
const getLocaleFallback = useCallback(
75-
locale => locale.split('-')[0].split('_')[0],
76-
[],
77-
)
78-
79-
const getCurrentLocale = useCallback(() => {
80-
const languages = navigator.languages || [navigator.language]
81-
const browserLocales = [...new Set(languages.map(getLocaleFallback))]
82-
const localeStorage = localStorage.getItem(localeItemStorage)
83-
84-
return (
85-
localeStorage ||
86-
browserLocales.find(locale => locales.includes(locale)) ||
87-
defaultLocale
88-
)
89-
}, [defaultLocale, getLocaleFallback, locales, localeItemStorage])
101+
useEffect(() => {
102+
loadDateLocale(currentLocale === 'en' ? 'en-GB' : currentLocale)
103+
.then(setDateFnsLocale)
104+
.catch(() => loadDateLocale('en-GB').then(setDateFnsLocale))
105+
}, [loadDateLocale, currentLocale])
90106

91107
const loadTranslations = useCallback(
92108
async (namespace, load = defaultLoad) => {
@@ -114,15 +130,22 @@ const I18nContextProvider = ({
114130
const { prefix, ...values } = trad
115131
const preparedValues = prefix ? prefixKeys(`${prefix}.`)(values) : values
116132

117-
setTranslations(prevState => ({
118-
...prevState,
119-
...{
120-
[currentLocale]: {
121-
...prevState[currentLocale],
122-
...preparedValues,
133+
// avoid a lot of render when async update
134+
ReactDOM.unstable_batchedUpdates(() => {
135+
setTranslations(prevState => ({
136+
...prevState,
137+
...{
138+
[currentLocale]: {
139+
...prevState[currentLocale],
140+
...preparedValues,
141+
},
123142
},
124-
},
125-
}))
143+
}))
144+
145+
setNamespaces(prevState => [
146+
...new Set([...(prevState || []), namespace]),
147+
])
148+
})
126149

127150
return namespace
128151
},
@@ -131,12 +154,12 @@ const I18nContextProvider = ({
131154

132155
const switchLocale = useCallback(
133156
locale => {
134-
if (locales.includes(locale)) {
157+
if (supportedLocales.includes(locale)) {
135158
localStorage.setItem(localeItemStorage, locale)
136159
setCurrentLocale(locale)
137160
}
138161
},
139-
[setCurrentLocale, locales, localeItemStorage],
162+
[localeItemStorage, setCurrentLocale, supportedLocales],
140163
)
141164

142165
const formatNumber = useCallback(
@@ -205,14 +228,6 @@ const I18nContextProvider = ({
205228
[translate],
206229
)
207230

208-
useEffect(() => {
209-
loadDateLocale(currentLocale === 'en' ? 'en-GB' : currentLocale)
210-
.then(setDateFnsLocale)
211-
.catch(() => loadDateLocale('en-GB').then(setDateFnsLocale))
212-
213-
setCurrentLocale(getCurrentLocale())
214-
}, [loadDateLocale, currentLocale, getCurrentLocale])
215-
216231
const value = useMemo(
217232
() => ({
218233
currentLocale,
@@ -221,11 +236,11 @@ const I18nContextProvider = ({
221236
formatList,
222237
formatNumber,
223238
loadTranslations,
224-
locales,
239+
locales: supportedLocales,
225240
namespaceTranslation,
241+
namespaces,
226242
relativeTime,
227243
relativeTimeStrict,
228-
setLocales,
229244
setTranslations,
230245
switchLocale,
231246
t: translate,
@@ -235,15 +250,15 @@ const I18nContextProvider = ({
235250
currentLocale,
236251
dateFnsLocale,
237252
datetime,
238-
formatNumber,
239253
formatList,
254+
formatNumber,
240255
loadTranslations,
241-
locales,
242256
namespaceTranslation,
257+
namespaces,
243258
relativeTime,
244259
relativeTimeStrict,
245-
setLocales,
246260
setTranslations,
261+
supportedLocales,
247262
switchLocale,
248263
translate,
249264
translations,

yarn.lock

Lines changed: 6 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -126,16 +126,7 @@
126126
dependencies:
127127
"@babel/types" "^7.12.13"
128128

129-
"@babel/helper-function-name@^7.12.13":
130-
version "7.12.13"
131-
resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz#93ad656db3c3c2232559fd7b2c3dbdcbe0eb377a"
132-
integrity sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA==
133-
dependencies:
134-
"@babel/helper-get-function-arity" "^7.12.13"
135-
"@babel/template" "^7.12.13"
136-
"@babel/types" "^7.12.13"
137-
138-
"@babel/helper-function-name@^7.14.2":
129+
"@babel/helper-function-name@^7.12.13", "@babel/helper-function-name@^7.14.2":
139130
version "7.14.2"
140131
resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.14.2.tgz#397688b590760b6ef7725b5f0860c82427ebaac2"
141132
integrity sha512-NYZlkZRydxw+YT56IlhIcS8PAhb+FEUiOzuhFTfqDyPmzAhRge6ua0dQYT/Uh0t/EDHq05/i+e5M2d4XvjgarQ==
@@ -6552,12 +6543,12 @@ [email protected]:
65526543
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
65536544
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
65546545

6555-
[email protected], ms@^2.0.0:
6546+
65566547
version "2.1.2"
65576548
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
65586549
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
65596550

6560-
ms@^2.1.1:
6551+
ms@^2.0.0, ms@^2.1.1:
65616552
version "2.1.3"
65626553
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
65636554
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
@@ -7269,12 +7260,7 @@ performance-now@^2.1.0:
72697260
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
72707261
integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
72717262

7272-
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2:
7273-
version "2.2.2"
7274-
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad"
7275-
integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==
7276-
7277-
picomatch@^2.2.3:
7263+
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3:
72787264
version "2.2.3"
72797265
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.3.tgz#465547f359ccc206d3c48e46a1bcb89bf7ee619d"
72807266
integrity sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg==
@@ -7486,7 +7472,7 @@ quick-lru@^4.0.1:
74867472
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
74877473
integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==
74887474

7489-
react-dom@^17.0.1:
7475+
react-dom@17.0.2, react-dom@^17.0.1:
74907476
version "17.0.2"
74917477
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
74927478
integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==
@@ -7980,14 +7966,7 @@ run-parallel@^1.1.9:
79807966
dependencies:
79817967
queue-microtask "^1.2.2"
79827968

7983-
rxjs@^6.6.0:
7984-
version "6.6.6"
7985-
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.6.tgz#14d8417aa5a07c5e633995b525e1e3c0dec03b70"
7986-
integrity sha512-/oTwee4N4iWzAMAL9xdGKjkEHmIwupR3oXbQjCKywF1BeFohswF3vZdogbmEF6pZkOsXTzWkrZszrWpQTByYVg==
7987-
dependencies:
7988-
tslib "^1.9.0"
7989-
7990-
rxjs@^6.6.7:
7969+
rxjs@^6.6.0, rxjs@^6.6.7:
79917970
version "6.6.7"
79927971
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9"
79937972
integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==

0 commit comments

Comments
 (0)