Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 8 additions & 14 deletions packages/use-i18n/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ Use of local `variables` and `namespace` to dynamically load locales.
your loaders will be:

```js
const load = ({ locale, namespace }) => import(`./locales/${locale}/${namespace}`)
const loadDateLocale = (locale) => import(`date-fns/locale/${locale}/index`)
const load = ({ locale, namespace }) =>
import(`./locales/${locale}/${namespace}`)
const loadDateLocale = locale => import(`date-fns/locale/${locale}/index`)
```

Inside your app you will need to use useTranslation to load namespace locales.
Expand Down Expand Up @@ -102,23 +103,16 @@ const App = () => {
}
```

In a case you will need to avoid somes useless re-render. you can wait that all your namespaces are loaded

```js
import { useI18n } from '@scaleway/use-i18n'
import { useTranslation } from '@scaleway/use-i18n'

const App = () => {
const i18n = useTranslation()
const { loadTranslations } = i18n
const namespaces = ['app', 'common']
const { t, isLoaded } = useTranslation(namespaces)

const key = namespaces.join(',')

useEffect(
() =>
key.split(',').map(async namespace => loadTranslations(namespace, load)),
[loadTranslations, key, load],
)

return <>{i18n.t('app.user')}(</>
return isLoaded ? <>{t('app.user')}(</> : null
}
```

Expand Down
6 changes: 4 additions & 2 deletions packages/use-i18n/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,13 @@
},
"peerDependencies": {
"date-fns": "2.x",
"react": "17.x"
"react": "17.x",
"react-dom": "17.x"
},
"devDependencies": {
"@testing-library/react-hooks": "^6.0.0",
"mockdate": "^3.0.5",
"react": "17.0.2"
"react": "17.0.2",
"react-dom": "17.0.2"
}
}
10 changes: 6 additions & 4 deletions packages/use-i18n/src/__tests__/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,14 @@ describe('i18n hook', () => {
}),
},
)

// current local will be 'en' based on navigator
// await load of locales
act(() => {
result.current.switchLocale('fr')
})
await waitForNextUpdate()

expect(result.current.translations).toStrictEqual({
en: {
'user.languages': 'Languages',
Expand All @@ -170,10 +176,6 @@ describe('i18n hook', () => {
'user.name': 'Prénom',
},
})
act(() => {
result.current.switchLocale('fr')
})
await waitForNextUpdate()

expect(result.current.t('user.languages')).toEqual('')
expect(result.current.t('user.lastName')).toEqual('Nom')
Expand Down
109 changes: 62 additions & 47 deletions packages/use-i18n/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import React, {
useMemo,
useState,
} from 'react'
import ReactDOM from 'react-dom'
import 'intl-pluralrules'

const LOCALE_ITEM_STORAGE = 'locale'
Expand All @@ -21,6 +22,27 @@ const prefixKeys = prefix => obj =>
return acc
}, {})

const areNamespacesLoaded = (namespaces, loadedNamespaces) =>
namespaces.every(n => loadedNamespaces.includes(n))

const getLocaleFallback = locale => locale.split('-')[0].split('_')[0]

const getCurrentLocale = ({
defaultLocale,
supportedLocales,
localeItemStorage,
}) => {
const languages = navigator.languages || [navigator.language]
const browserLocales = [...new Set(languages.map(getLocaleFallback))]
const localeStorage = localStorage.getItem(localeItemStorage)

return (
localeStorage ||
browserLocales.find(locale => supportedLocales.includes(locale)) ||
defaultLocale
)
}

const I18nContext = createContext()

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

const key = namespaces.join(',')
useEffect(
() =>
key.split(',').map(async namespace => loadTranslations(namespace, load)),
[loadTranslations, key, load],
useEffect(() => {
key.split(',').map(async namespace => loadTranslations(namespace, load))
}, [loadTranslations, key, load])

const isLoaded = useMemo(
() => areNamespacesLoaded(namespaces, loadedNamespaces),
[loadedNamespaces, namespaces],
)

return context
return { ...context, isLoaded }
}

// https://formatjs.io/docs/intl-messageformat/
Expand All @@ -66,27 +91,18 @@ const I18nContextProvider = ({
localeItemStorage,
supportedLocales,
}) => {
const [currentLocale, setCurrentLocale] = useState(defaultLocale)
const [locales, setLocales] = useState(supportedLocales)
const [currentLocale, setCurrentLocale] = useState(
getCurrentLocale({ defaultLocale, localeItemStorage, supportedLocales }),
)
const [translations, setTranslations] = useState(defaultTranslations)
const [namespaces, setNamespaces] = useState([])
const [dateFnsLocale, setDateFnsLocale] = useState()

const getLocaleFallback = useCallback(
locale => locale.split('-')[0].split('_')[0],
[],
)

const getCurrentLocale = useCallback(() => {
const languages = navigator.languages || [navigator.language]
const browserLocales = [...new Set(languages.map(getLocaleFallback))]
const localeStorage = localStorage.getItem(localeItemStorage)

return (
localeStorage ||
browserLocales.find(locale => locales.includes(locale)) ||
defaultLocale
)
}, [defaultLocale, getLocaleFallback, locales, localeItemStorage])
useEffect(() => {
loadDateLocale(currentLocale === 'en' ? 'en-GB' : currentLocale)
.then(setDateFnsLocale)
.catch(() => loadDateLocale('en-GB').then(setDateFnsLocale))
}, [loadDateLocale, currentLocale])

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

setTranslations(prevState => ({
...prevState,
...{
[currentLocale]: {
...prevState[currentLocale],
...preparedValues,
// avoid a lot of render when async update
ReactDOM.unstable_batchedUpdates(() => {
setTranslations(prevState => ({
...prevState,
...{
[currentLocale]: {
...prevState[currentLocale],
...preparedValues,
},
},
},
}))
}))

setNamespaces(prevState => [
...new Set([...(prevState || []), namespace]),
])
})

return namespace
},
Expand All @@ -131,12 +154,12 @@ const I18nContextProvider = ({

const switchLocale = useCallback(
locale => {
if (locales.includes(locale)) {
if (supportedLocales.includes(locale)) {
localStorage.setItem(localeItemStorage, locale)
setCurrentLocale(locale)
}
},
[setCurrentLocale, locales, localeItemStorage],
[localeItemStorage, setCurrentLocale, supportedLocales],
)

const formatNumber = useCallback(
Expand Down Expand Up @@ -205,14 +228,6 @@ const I18nContextProvider = ({
[translate],
)

useEffect(() => {
loadDateLocale(currentLocale === 'en' ? 'en-GB' : currentLocale)
.then(setDateFnsLocale)
.catch(() => loadDateLocale('en-GB').then(setDateFnsLocale))

setCurrentLocale(getCurrentLocale())
}, [loadDateLocale, currentLocale, getCurrentLocale])

const value = useMemo(
() => ({
currentLocale,
Expand All @@ -221,11 +236,11 @@ const I18nContextProvider = ({
formatList,
formatNumber,
loadTranslations,
locales,
locales: supportedLocales,
namespaceTranslation,
namespaces,
relativeTime,
relativeTimeStrict,
setLocales,
setTranslations,
switchLocale,
t: translate,
Expand All @@ -235,15 +250,15 @@ const I18nContextProvider = ({
currentLocale,
dateFnsLocale,
datetime,
formatNumber,
formatList,
formatNumber,
loadTranslations,
locales,
namespaceTranslation,
namespaces,
relativeTime,
relativeTimeStrict,
setLocales,
setTranslations,
supportedLocales,
switchLocale,
translate,
translations,
Expand Down
33 changes: 6 additions & 27 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -126,16 +126,7 @@
dependencies:
"@babel/types" "^7.12.13"

"@babel/helper-function-name@^7.12.13":
version "7.12.13"
resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz#93ad656db3c3c2232559fd7b2c3dbdcbe0eb377a"
integrity sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA==
dependencies:
"@babel/helper-get-function-arity" "^7.12.13"
"@babel/template" "^7.12.13"
"@babel/types" "^7.12.13"

"@babel/helper-function-name@^7.14.2":
"@babel/helper-function-name@^7.12.13", "@babel/helper-function-name@^7.14.2":
version "7.14.2"
resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.14.2.tgz#397688b590760b6ef7725b5f0860c82427ebaac2"
integrity sha512-NYZlkZRydxw+YT56IlhIcS8PAhb+FEUiOzuhFTfqDyPmzAhRge6ua0dQYT/Uh0t/EDHq05/i+e5M2d4XvjgarQ==
Expand Down Expand Up @@ -6552,12 +6543,12 @@ ms@2.0.0:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=

ms@2.1.2, ms@^2.0.0:
ms@2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==

ms@^2.1.1:
ms@^2.0.0, ms@^2.1.1:
version "2.1.3"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
Expand Down Expand Up @@ -7269,12 +7260,7 @@ performance-now@^2.1.0:
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=

picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad"
integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==

picomatch@^2.2.3:
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.3.tgz#465547f359ccc206d3c48e46a1bcb89bf7ee619d"
integrity sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg==
Expand Down Expand Up @@ -7486,7 +7472,7 @@ quick-lru@^4.0.1:
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==

react-dom@^17.0.1:
react-dom@17.0.2, react-dom@^17.0.1:
version "17.0.2"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==
Expand Down Expand Up @@ -7980,14 +7966,7 @@ run-parallel@^1.1.9:
dependencies:
queue-microtask "^1.2.2"

rxjs@^6.6.0:
version "6.6.6"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.6.tgz#14d8417aa5a07c5e633995b525e1e3c0dec03b70"
integrity sha512-/oTwee4N4iWzAMAL9xdGKjkEHmIwupR3oXbQjCKywF1BeFohswF3vZdogbmEF6pZkOsXTzWkrZszrWpQTByYVg==
dependencies:
tslib "^1.9.0"

rxjs@^6.6.7:
rxjs@^6.6.0, rxjs@^6.6.7:
version "6.6.7"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9"
integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==
Expand Down