From 7af4689b6892e587ab840eed221b49c9a5d34fe2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 1 Jan 2024 12:05:32 +0000 Subject: [PATCH 1/4] chore(deps): update dependency date-fns to v3 --- packages/use-i18n/package.json | 4 ++-- pnpm-lock.yaml | 11 ++++------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/use-i18n/package.json b/packages/use-i18n/package.json index 93db10edd..e324f16df 100644 --- a/packages/use-i18n/package.json +++ b/packages/use-i18n/package.json @@ -31,13 +31,13 @@ "dependencies": { "@formatjs/ecma402-abstract": "1.18.0", "@formatjs/fast-memoize": "2.2.0", - "date-fns": "2.30.0", + "date-fns": "3.0.0", "filesize": "10.1.0", "international-types": "0.8.1", "intl-messageformat": "10.5.8" }, "peerDependencies": { - "date-fns": "2.x", + "date-fns": "2.x || 3.x", "react": "18.x || 18", "react-dom": "18.x || 18" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6727005c2..950e4c5cd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -267,8 +267,8 @@ importers: specifier: 2.2.0 version: 2.2.0 date-fns: - specifier: 2.30.0 - version: 2.30.0 + specifier: 3.0.0 + version: 3.0.0 filesize: specifier: 10.1.0 version: 10.1.0 @@ -5206,11 +5206,8 @@ packages: resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==} dev: true - /date-fns@2.30.0: - resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} - engines: {node: '>=0.11'} - dependencies: - '@babel/runtime': 7.21.5 + /date-fns@3.0.0: + resolution: {integrity: sha512-xjDz3rNN9jp+Lh3P/4MeY4E5HkaRnEnrJCcrdRZnKdn42gJlIe6hwrrwVXePRwVR2kh1UcMnz00erYBnHF8PFA==} dev: false /debug@2.6.9: From c9ec804f3bece163753b7a25740b9393d19012af Mon Sep 17 00:00:00 2001 From: Scaleway Bot Date: Mon, 1 Jan 2024 12:05:57 +0000 Subject: [PATCH 2/4] chore: add changeset renovate-2327f3b --- .changeset/renovate-2327f3b.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/renovate-2327f3b.md diff --git a/.changeset/renovate-2327f3b.md b/.changeset/renovate-2327f3b.md new file mode 100644 index 000000000..7fd2abddd --- /dev/null +++ b/.changeset/renovate-2327f3b.md @@ -0,0 +1,5 @@ +--- +'@scaleway/use-i18n': patch +--- + +Updated dependency `date-fns` to `2.x || 3.x`. From 4bf0ea6d7910ee5781642b0b879f5eec8beea280 Mon Sep 17 00:00:00 2001 From: Alexandre Philibeaux Date: Wed, 3 Jan 2024 11:45:36 +0100 Subject: [PATCH 3/4] fix(use-i18n): load date-fns --- packages/use-i18n/package.json | 2 +- packages/use-i18n/src/usei18n.tsx | 37 ++++++++++++++++++------------- pnpm-lock.yaml | 8 +++---- 3 files changed, 27 insertions(+), 20 deletions(-) diff --git a/packages/use-i18n/package.json b/packages/use-i18n/package.json index e324f16df..be7f6c889 100644 --- a/packages/use-i18n/package.json +++ b/packages/use-i18n/package.json @@ -31,7 +31,7 @@ "dependencies": { "@formatjs/ecma402-abstract": "1.18.0", "@formatjs/fast-memoize": "2.2.0", - "date-fns": "3.0.0", + "date-fns": "3.0.6", "filesize": "10.1.0", "international-types": "0.8.1", "intl-messageformat": "10.5.8" diff --git a/packages/use-i18n/src/usei18n.tsx b/packages/use-i18n/src/usei18n.tsx index 67499ca7d..40900ff31 100644 --- a/packages/use-i18n/src/usei18n.tsx +++ b/packages/use-i18n/src/usei18n.tsx @@ -1,6 +1,10 @@ import type { NumberFormatOptions } from '@formatjs/ecma402-abstract' -import type { Locale as DateFnsLocale } from 'date-fns' -import { formatDistanceToNow, formatDistanceToNowStrict } from 'date-fns' +import { + type Locale as DateFnsLocale, + formatDistanceToNow, + formatDistanceToNowStrict, +} from 'date-fns' +import { enGB } from 'date-fns/locale' import type { BaseLocale } from 'international-types' import type { ReactElement, ReactNode } from 'react' import { @@ -12,12 +16,9 @@ import { useState, } from 'react' import ReactDOM from 'react-dom' -import type { FormatDateOptions } from './formatDate' -import dateFormat from './formatDate' -import type { FormatUnitOptions } from './formatUnit' -import unitFormat from './formatUnit' -import type { IntlListFormatOptions } from './formatters' -import formatters from './formatters' +import dateFormat, { type FormatDateOptions } from './formatDate' +import unitFormat, { type FormatUnitOptions } from './formatUnit' +import formatters, { type IntlListFormatOptions } from './formatters' import type { ReactParamsObject, ScopedTranslateFn, TranslateFn } from './types' const LOCALE_ITEM_STORAGE = 'locale' @@ -163,7 +164,8 @@ type LoadTranslationsFn = ({ namespace: string locale: string }) => Promise<{ default: BaseLocale }> -type LoadLocaleFn = (locale: string) => Promise + +type LoadLocaleFn = (locale: string) => Promise | DateFnsLocale const initialDefaultTranslations = {} @@ -182,7 +184,7 @@ const I18nContextProvider = ({ children: ReactNode defaultLoad: LoadTranslationsFn loadDateLocale?: LoadLocaleFn - defaultDateLocale?: Locale + defaultDateLocale?: DateFnsLocale defaultLocale: string defaultTranslations: TranslationsByLocales enableDefaultLocale: boolean @@ -196,14 +198,19 @@ const I18nContextProvider = ({ const [translations, setTranslations] = useState(defaultTranslations) const [namespaces, setNamespaces] = useState([]) - const [dateFnsLocale, setDateFnsLocale] = useState( - defaultDateLocale ?? undefined, + const [dateFnsLocale, setDateFnsLocale] = useState( + defaultDateLocale ?? enGB, ) useEffect(() => { - loadDateLocale?.(currentLocale === 'en' ? 'en-GB' : currentLocale) - .then(setDateFnsLocale) - .catch(() => loadDateLocale('en-GB').then(setDateFnsLocale)) + const loadDateFnsLocale = async () => { + const dateFns = await loadDateLocale?.( + currentLocale === 'en' ? 'en-GB' : currentLocale, + ) + setDateFnsLocale(dateFns) + } + + loadDateFnsLocale().catch(() => setDateFnsLocale(enGB)) }, [loadDateLocale, currentLocale]) const loadTranslations = useCallback( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 950e4c5cd..ece2e7747 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -267,8 +267,8 @@ importers: specifier: 2.2.0 version: 2.2.0 date-fns: - specifier: 3.0.0 - version: 3.0.0 + specifier: 3.0.6 + version: 3.0.6 filesize: specifier: 10.1.0 version: 10.1.0 @@ -5206,8 +5206,8 @@ packages: resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==} dev: true - /date-fns@3.0.0: - resolution: {integrity: sha512-xjDz3rNN9jp+Lh3P/4MeY4E5HkaRnEnrJCcrdRZnKdn42gJlIe6hwrrwVXePRwVR2kh1UcMnz00erYBnHF8PFA==} + /date-fns@3.0.6: + resolution: {integrity: sha512-W+G99rycpKMMF2/YD064b2lE7jJGUe+EjOES7Q8BIGY8sbNdbgcs9XFTZwvzc9Jx1f3k7LB7gZaZa7f8Agzljg==} dev: false /debug@2.6.9: From 327a48dead77a5be5d333f073dc710877a2552d4 Mon Sep 17 00:00:00 2001 From: Alexandre Philibeaux Date: Wed, 3 Jan 2024 18:56:24 +0100 Subject: [PATCH 4/4] fix(use-i18n): load date-fns --- .changeset/lovely-knives-judge.md | 5 + packages/use-i18n/package.json | 4 +- packages/use-i18n/src/__tests__/usei18n.tsx | 174 +++++++++++++++----- packages/use-i18n/src/usei18n.tsx | 134 +++++++++------ 4 files changed, 223 insertions(+), 94 deletions(-) create mode 100644 .changeset/lovely-knives-judge.md diff --git a/.changeset/lovely-knives-judge.md b/.changeset/lovely-knives-judge.md new file mode 100644 index 000000000..fa1d6d7d7 --- /dev/null +++ b/.changeset/lovely-knives-judge.md @@ -0,0 +1,5 @@ +--- +"@scaleway/use-i18n": major +--- + +Add a onError function for load date-fns locale, fix a bug when a local is removed diff --git a/packages/use-i18n/package.json b/packages/use-i18n/package.json index be7f6c889..53f916c84 100644 --- a/packages/use-i18n/package.json +++ b/packages/use-i18n/package.json @@ -4,7 +4,7 @@ "description": "A small hook to handle i18n", "type": "module", "engines": { - "node": ">=14.x" + "node": ">=18.x" }, "sideEffects": false, "exports": { @@ -37,7 +37,7 @@ "intl-messageformat": "10.5.8" }, "peerDependencies": { - "date-fns": "2.x || 3.x", + "date-fns": "3.x", "react": "18.x || 18", "react-dom": "18.x || 18" } diff --git a/packages/use-i18n/src/__tests__/usei18n.tsx b/packages/use-i18n/src/__tests__/usei18n.tsx index 288413755..831ddc458 100644 --- a/packages/use-i18n/src/__tests__/usei18n.tsx +++ b/packages/use-i18n/src/__tests__/usei18n.tsx @@ -31,10 +31,24 @@ type NamespaceLocale = { languages: 'Languages' } +const defaultSupportedLocales = ['en', 'fr', 'es'] + const wrapper = ({ - loadDateLocale = async (locale: string) => - import(`date-fns/locale/${locale}/index`), + loadDateLocale = async (locale: string) => { + if (locale === 'en') { + return (await import('date-fns/locale/en-GB')).enGB + } + if (locale === 'fr') { + return (await import('date-fns/locale/fr')).fr + } + + if (locale === 'es') { + return (await import('date-fns/locale/es')).es + } + + return (await import(`date-fns/locale/en-GB`)).enGB + }, defaultLoad = async ({ locale }: { locale: string }) => import(`./locales/${locale}.json`), defaultLocale = 'en', @@ -42,7 +56,7 @@ const wrapper = enableDebugKey = false, enableDefaultLocale = false, localeItemStorage = LOCALE_ITEM_STORAGE, - supportedLocales = ['en', 'fr', 'es'], + supportedLocales = defaultSupportedLocales, } = {}) => ({ children }: { children: ReactNode }) => ( { expect(result.current.t('title')).toEqual(en.title) }) - act(() => { - result.current.switchLocale('fr') + await act(async () => { + await result.current.switchLocale('fr') }) await waitFor(() => { expect(result.current.t('title')).toEqual(fr.title) }) - act(() => { - result.current.switchLocale('es') + await act(async () => { + await result.current.switchLocale('es') }) await waitFor(() => { @@ -176,8 +190,8 @@ describe('i18n hook', () => { expect(result.current.t('lastName')).toEqual('Last Name') expect(result.current.t('languages')).toEqual('Languages') - act(() => { - result.current.switchLocale('fr') + await act(async () => { + await result.current.switchLocale('fr') }) await waitFor(() => { @@ -221,8 +235,8 @@ describe('i18n hook', () => { // current local will be 'en' based on navigator // await load of locales - act(() => { - result.current.switchLocale('fr') + await act(async () => { + await result.current.switchLocale('fr') }) await waitFor(() => { @@ -264,11 +278,13 @@ describe('i18n hook', () => { } as unknown as Navigator) const mockGetItem = jest.fn().mockImplementation(() => 'en') const mockSetItem = jest.fn() + const mockRemoveItem = jest.fn() const localStorageMock = jest .spyOn(global, 'localStorage', 'get') .mockReturnValue({ getItem: mockGetItem, setItem: mockSetItem, + removeItem: mockRemoveItem, clear: jest.fn(), } as unknown as Storage) @@ -281,7 +297,38 @@ describe('i18n hook', () => { await waitFor(() => { expect(result.current.currentLocale).toEqual('en') - expect(mockGetItem).toHaveBeenCalledTimes(2) + expect(mockGetItem).toHaveBeenCalledTimes(1) + expect(mockGetItem).toHaveBeenCalledWith(LOCALE_ITEM_STORAGE) + }) + localStorageMock.mockRestore() + }) + + it('should not set current locale from localStorage when this value is not supported', async () => { + jest.spyOn(global, 'navigator', 'get').mockReturnValueOnce({ + languages: ['bz'], + } as unknown as Navigator) + const mockGetItem = jest.fn().mockImplementation(() => 're') + const mockSetItem = jest.fn() + const mockRemoveItem = jest.fn() + const localStorageMock = jest + .spyOn(global, 'localStorage', 'get') + .mockReturnValue({ + getItem: mockGetItem, + setItem: mockSetItem, + removeItem: mockRemoveItem, + clear: jest.fn(), + } as unknown as Storage) + + const { result } = renderHook(() => useI18n(), { + wrapper: wrapper({ + defaultLocale: 'en', + supportedLocales: ['en'], + }), + }) + + await waitFor(() => { + expect(result.current.currentLocale).toEqual('en') + expect(mockGetItem).toHaveBeenCalledTimes(1) expect(mockGetItem).toHaveBeenCalledWith(LOCALE_ITEM_STORAGE) }) localStorageMock.mockRestore() @@ -293,11 +340,13 @@ describe('i18n hook', () => { } as unknown as Navigator) const mockGetItem = jest.fn() const mockSetItem = jest.fn() + const mockRemoveItem = jest.fn() const localStorageMock = jest .spyOn(global, 'localStorage', 'get') .mockReturnValueOnce({ getItem: mockGetItem, setItem: mockSetItem, + removeItem: mockRemoveItem, clear: jest.fn(), } as unknown as Storage) @@ -320,11 +369,13 @@ describe('i18n hook', () => { } as unknown as Navigator) const mockGetItem = jest.fn() const mockSetItem = jest.fn() + const mockRemoveItem = jest.fn() const localStorageMock = jest .spyOn(global, 'localStorage', 'get') .mockReturnValueOnce({ getItem: mockGetItem, setItem: mockSetItem, + removeItem: mockRemoveItem, clear: jest.fn(), } as unknown as Storage) @@ -350,10 +401,10 @@ describe('i18n hook', () => { }), }) expect(result.current.currentLocale).toEqual('en') - expect(localStorage.getItem(LOCALE_ITEM_STORAGE)).toBe(null) + expect(localStorage.getItem(LOCALE_ITEM_STORAGE)).toBe('en') - act(() => { - result.current.switchLocale('fr') + await act(async () => { + await result.current.switchLocale('fr') }) await waitFor(() => { @@ -361,8 +412,8 @@ describe('i18n hook', () => { }) expect(localStorage.getItem(LOCALE_ITEM_STORAGE)).toBe('fr') - act(() => { - result.current.switchLocale('es') + await act(async () => { + await result.current.switchLocale('es') }) await waitFor(() => { @@ -370,8 +421,8 @@ describe('i18n hook', () => { }) expect(localStorage.getItem(LOCALE_ITEM_STORAGE)).toBe('es') - act(() => { - result.current.switchLocale('test') + await act(async () => { + await result.current.switchLocale('test') }) await waitFor(() => { @@ -441,8 +492,8 @@ describe('i18n hook', () => { }), ).toEqual('$2.00') - act(() => { - result.current.switchLocale('fr') + await act(async () => { + await result.current.switchLocale('fr') }) // https://stackoverflow.com/questions/58769806/identical-strings-not-matching-in-jest @@ -491,8 +542,8 @@ describe('i18n hook', () => { }), ).toEqual('Motorcycle Bus Car') - act(() => { - result.current.switchLocale('fr') + await act(async () => { + await result.current.switchLocale('fr') }) await waitFor(() => { @@ -565,8 +616,8 @@ describe('i18n hook', () => { }), ).toEqual('12/17/1995') - act(() => { - result.current.switchLocale('fr') + await act(async () => { + await result.current.switchLocale('fr') }) await waitFor(() => { @@ -602,11 +653,12 @@ describe('i18n hook', () => { expect(result.current.relativeTime(date)).toEqual('over 20 years ago') - act(() => { - result.current.switchLocale('fr') + await act(async () => { + await result.current.switchLocale('fr') }) await waitFor(() => { + expect(result.current.dateFnsLocale?.code).toBe('fr') expect(result.current.relativeTime(date)).toEqual('il y a plus de 20 ans') }) }) @@ -621,8 +673,8 @@ describe('i18n hook', () => { const date = new Date('September 13, 2011 15:15:00') expect(result.current.relativeTimeStrict(date)).toEqual('3499 days ago') - act(() => { - result.current.switchLocale('fr') + await act(async () => { + await result.current.switchLocale('fr') }) await waitFor(() => { @@ -642,8 +694,8 @@ describe('i18n hook', () => { expect( result.current.formatUnit(12, { short: false, unit: 'byte' }), ).toEqual('12 bytes') - act(() => { - result.current.switchLocale('fr') + await act(async () => { + await result.current.switchLocale('fr') }) await waitFor(() => { @@ -663,8 +715,8 @@ describe('i18n hook', () => { expect( result.current.formatDate(new Date(2020, 1, 13, 16, 28), 'numericHour'), ).toEqual('2020-02-13 4:28 PM') - act(() => { - result.current.switchLocale('fr') + await act(async () => { + await result.current.switchLocale('fr') }) await waitFor(() => { @@ -674,17 +726,55 @@ describe('i18n hook', () => { }) }) - it('should load default datefns locales', async () => { - const { result } = renderHook(() => useI18n(), { - wrapper: wrapper({ - defaultLocale: 'test', - supportedLocales: ['test'], - }), + describe('date-fns', () => { + it('should load default date-fns locales', async () => { + const { result } = renderHook(() => useI18n(), { + wrapper: wrapper({ + defaultLocale: 'test', + supportedLocales: ['test'], + }), + }) + + await waitFor(() => { + expect(result.current.dateFnsLocale?.code).toEqual('en-GB') + }) }) - expect(result.current.dateFnsLocale).toBe(undefined) - await waitFor(() => { - expect(result.current.dateFnsLocale?.code).toEqual('en-GB') + it('should load correct date-fns based on current local', async () => { + jest.spyOn(global, 'navigator', 'get').mockReturnValueOnce({ + languages: ['fr'], + } as unknown as Navigator) + const mockGetItem = jest.fn().mockImplementation(() => 'fr') + const mockSetItem = jest.fn() + const mockRemoveItem = jest.fn() + const localStorageMock = jest + .spyOn(global, 'localStorage', 'get') + .mockReturnValue({ + getItem: mockGetItem, + setItem: mockSetItem, + removeItem: mockRemoveItem, + clear: jest.fn(), + } as unknown as Storage) + + const { result } = renderHook(() => useI18n(), { + wrapper: wrapper({ + defaultLocale: 'es', + supportedLocales: ['en', 'fr', 'es'], + }), + }) + + await waitFor(() => { + expect(result.current.currentLocale).toEqual('fr') + expect(mockGetItem).toHaveBeenCalledTimes(2) + expect(mockGetItem).toHaveBeenCalledWith(LOCALE_ITEM_STORAGE) + }) + + await waitFor(() => { + expect(result.current.dateFnsLocale?.code).toEqual('fr') + expect(result.current.dateFnsLocale).toMatchObject({ code: 'fr' }) + }) + + localStorageMock.mockRestore() }) }) diff --git a/packages/use-i18n/src/usei18n.tsx b/packages/use-i18n/src/usei18n.tsx index 40900ff31..f73d799e5 100644 --- a/packages/use-i18n/src/usei18n.tsx +++ b/packages/use-i18n/src/usei18n.tsx @@ -1,10 +1,11 @@ import type { NumberFormatOptions } from '@formatjs/ecma402-abstract' -import { - type Locale as DateFnsLocale, - formatDistanceToNow, - formatDistanceToNowStrict, +import type { + Locale as DateFnsLocale, + FormatDistanceToNowOptions, + FormatDistanceToNowStrictOptions, } from 'date-fns' -import { enGB } from 'date-fns/locale' +import { formatDistanceToNow } from 'date-fns/formatDistanceToNow' +import { formatDistanceToNowStrict } from 'date-fns/formatDistanceToNowStrict' import type { BaseLocale } from 'international-types' import type { ReactElement, ReactNode } from 'react' import { @@ -21,7 +22,7 @@ import unitFormat, { type FormatUnitOptions } from './formatUnit' import formatters, { type IntlListFormatOptions } from './formatters' import type { ReactParamsObject, ScopedTranslateFn, TranslateFn } from './types' -const LOCALE_ITEM_STORAGE = 'locale' +const LOCALE_ITEM_STORAGE = 'locale' as const type TranslationsByLocales = Record type RequiredGenericContext = @@ -54,15 +55,38 @@ const getCurrentLocale = ({ if (typeof window !== 'undefined') { const { languages } = navigator const browserLocales = [...new Set(languages.map(getLocaleFallback))] - const localeStorage = localStorage.getItem(localeItemStorage) - - return ( - localeStorage || - browserLocales.find( - locale => locale && supportedLocales.includes(locale), - ) || - defaultLocale + const currentLocalFromlocalStorage = localStorage.getItem(localeItemStorage) + + if ( + currentLocalFromlocalStorage && + supportedLocales.find( + supportedLocale => supportedLocale === currentLocalFromlocalStorage, + ) + ) { + return currentLocalFromlocalStorage + } + localStorage.removeItem(localeItemStorage) + + const findedBrowserLocal = browserLocales.find( + locale => locale && supportedLocales.includes(locale), ) + + if (findedBrowserLocal) { + localStorage.setItem(localeItemStorage, findedBrowserLocal) + + return findedBrowserLocal + } + + if ( + defaultLocale && + supportedLocales.find( + supportedLocale => supportedLocale === defaultLocale, + ) + ) { + localStorage.setItem(localeItemStorage, defaultLocale) + + return defaultLocale + } } return defaultLocale @@ -91,21 +115,14 @@ type Context = { namespaceTranslation: ScopedTranslateFn relativeTime: ( date: Date | number, - options?: { - includeSeconds?: boolean - addSuffix?: boolean - }, + options?: FormatDistanceToNowOptions, ) => string relativeTimeStrict: ( date: Date | number, - options?: { - addSuffix?: boolean - unit?: 'second' | 'minute' | 'hour' | 'day' | 'month' | 'year' - roundingMethod?: 'floor' | 'ceil' | 'round' - }, + options?: FormatDistanceToNowStrictOptions, ) => string setTranslations: React.Dispatch> - switchLocale: (locale: string) => void + switchLocale: (locale: string) => Promise t: TranslateFn translations: TranslationsByLocales } @@ -165,26 +182,27 @@ type LoadTranslationsFn = ({ locale: string }) => Promise<{ default: BaseLocale }> -type LoadLocaleFn = (locale: string) => Promise | DateFnsLocale +type LoadLocaleFn = (locale: string) => Promise +type LoadDateLocaleError = (error: Error) => void const initialDefaultTranslations = {} const I18nContextProvider = ({ children, defaultLoad, - loadDateLocale, - defaultDateLocale, defaultLocale, defaultTranslations = initialDefaultTranslations, - enableDefaultLocale = false, enableDebugKey = false, + enableDefaultLocale = false, + loadDateLocale, localeItemStorage = LOCALE_ITEM_STORAGE, + onLoadDateLocaleError, supportedLocales, }: { children: ReactNode defaultLoad: LoadTranslationsFn - loadDateLocale?: LoadLocaleFn - defaultDateLocale?: DateFnsLocale + loadDateLocale: LoadLocaleFn + onLoadDateLocaleError?: LoadDateLocaleError defaultLocale: string defaultTranslations: TranslationsByLocales enableDefaultLocale: boolean @@ -199,19 +217,38 @@ const I18nContextProvider = ({ useState(defaultTranslations) const [namespaces, setNamespaces] = useState([]) const [dateFnsLocale, setDateFnsLocale] = useState( - defaultDateLocale ?? enGB, + undefined, + ) + + const setDateFns = useCallback( + async (locale: string) => { + try { + const dateFns = await loadDateLocale(locale) + setDateFnsLocale(dateFns) + } catch (err) { + if (err instanceof Error && onLoadDateLocaleError) { + onLoadDateLocaleError(err) + } + + setDateFnsLocale(dateFnsLocale) + } + }, + [loadDateLocale, setDateFnsLocale, onLoadDateLocaleError, dateFnsLocale], ) + /** + * At first render when we find a local on the localStorage which is not the same as the default, + * we should switch also the date-fns local related to the current local. + * As the method is async, we obviously need a useEffect to apply this change... + * */ + useEffect(() => { - const loadDateFnsLocale = async () => { - const dateFns = await loadDateLocale?.( - currentLocale === 'en' ? 'en-GB' : currentLocale, - ) - setDateFnsLocale(dateFns) + if (!dateFnsLocale) { + setDateFns(currentLocale) + .then() + .catch(() => null) } - - loadDateFnsLocale().catch(() => setDateFnsLocale(enGB)) - }, [loadDateLocale, currentLocale]) + }, [currentLocale, dateFnsLocale, setDateFns, setDateFnsLocale]) const loadTranslations = useCallback( async (namespace: string, load: LoadTranslationsFn = defaultLoad) => { @@ -259,13 +296,14 @@ const I18nContextProvider = ({ ) const switchLocale = useCallback( - (locale: string) => { + async (locale: string) => { if (supportedLocales.includes(locale)) { localStorage.setItem(localeItemStorage, locale) setCurrentLocale(locale) + await setDateFns(locale) } }, - [localeItemStorage, setCurrentLocale, supportedLocales], + [setDateFns, localeItemStorage, setCurrentLocale, supportedLocales], ) const formatNumber = useCallback( @@ -304,11 +342,10 @@ const I18nContextProvider = ({ const relativeTimeStrict = useCallback( ( date: Date | number, - options: { - addSuffix?: boolean - unit?: 'second' | 'minute' | 'hour' | 'day' | 'month' | 'year' - roundingMethod?: 'floor' | 'ceil' | 'round' - } = { addSuffix: true, unit: 'day' }, + options: FormatDistanceToNowStrictOptions = { + addSuffix: true, + unit: 'day', + }, ) => { const finalDate = new Date(date) @@ -323,10 +360,7 @@ const I18nContextProvider = ({ const relativeTime = useCallback( ( date: Date | number, - options: { - includeSeconds?: boolean - addSuffix?: boolean - } = { addSuffix: true }, + options: FormatDistanceToNowOptions = { addSuffix: true }, ) => { const finalDate = new Date(date)