diff --git a/packages/gasket-react-intl/README.md b/packages/gasket-react-intl/README.md index 84fa20891..c75dc4678 100644 --- a/packages/gasket-react-intl/README.md +++ b/packages/gasket-react-intl/README.md @@ -56,8 +56,8 @@ wrapped component will be rendered. **Props** -- `localesPath` - (string|function) Path to endpoint with JSON files or - [thunk] that returns one. See more about [locales path] in the plugin docs. +- `localesPath` - (string|function|Array(string|function)) Path to endpoint with JSON files or + [thunk] that returns one. Also supports an array of either. See more about [locales path] in the plugin docs. - `[options]` - (object) Optional configuration - `loading` - (string|node) Content to render while loading, otherwise null. - `initialProps` - (boolean) Enable `getInitialProps` to load locale files @@ -99,8 +99,8 @@ content until a [split locales] file loads. **Props** -- `localesPath` - (string|function) Path to endpoint with JSON files or - [thunk] that returns one. See more about [locales path] in the plugin docs. +- `localesPath` - (string|function|Array(string|function)) Path to endpoint with JSON files or + [thunk] that returns one. Also supports an array of either. See more about [locales path] in the plugin docs. - `loading` - (string|node) Content to render while loading, otherwise null. ```jsx @@ -134,8 +134,8 @@ hook will return the current loading status of the locale file. **Props** -- `localesPath` - (string|function) Path to endpoint with JSON files or - [thunk] that returns one. See more about [locales path] in the plugin docs. +- `localesPath` - (string|function|Array(string|function)) Path to endpoint with JSON files or + [thunk] that returns one. Also supports an array of either. See more about [locales path] in the plugin docs. ```jsx import { useLocaleRequired, LocaleStatus } from '@gasket/react-intl'; diff --git a/packages/gasket-react-intl/src/index.d.ts b/packages/gasket-react-intl/src/index.d.ts index 8fff6835b..6b3acc5bf 100644 --- a/packages/gasket-react-intl/src/index.d.ts +++ b/packages/gasket-react-intl/src/index.d.ts @@ -38,7 +38,7 @@ export type LocaleRequiredWrapper = (props: { */ export function withLocaleRequired( /** Path containing locale files */ - localePathPart?: LocalePathPartOrThunk, + localePathPart?: LocalePathPartOrThunk | LocalePathPartOrThunk[], options?: { /** Custom component to show while loading */ loading?: React.ReactNode; @@ -50,7 +50,7 @@ export function withLocaleRequired( export interface LocaleRequiredProps { /** Path containing locale files */ - localesPath: LocalePathPartOrThunk; + localesPath: LocalePathPartOrThunk | LocalePathPartOrThunk[]; /** Custom component to show while loading */ loading?: React.ReactNode; } @@ -67,7 +67,7 @@ export function LocaleRequired( */ export function useLocaleRequired( /** Path containing locale files */ - localePathPart: LocalePathPartOrThunk + localePathPart: LocalePathPartOrThunk | LocalePathPartOrThunk[] ): LocaleStatus; interface NextStaticContext extends Record { @@ -148,7 +148,7 @@ export function attachGetInitialProps( }; }, /** Path containing locale files */ - localePathPart: LocalePathPartOrThunk + localePathPart: LocalePathPartOrThunk | LocalePathPartOrThunk[], ): void; export async function attachedGetInitialProps( diff --git a/packages/gasket-react-intl/src/use-locale-required.js b/packages/gasket-react-intl/src/use-locale-required.js index 518712728..c02262e64 100644 --- a/packages/gasket-react-intl/src/use-locale-required.js +++ b/packages/gasket-react-intl/src/use-locale-required.js @@ -8,53 +8,63 @@ import { GasketIntlContext } from './context'; * React that fetches a locale file and returns loading status * @type {import('./index').useLocaleRequired} */ -export default function useLocaleRequired(localePathPart) { +export default function useLocaleRequired(localePathParam) { const { locale, status = {}, dispatch } = useContext(GasketIntlContext); - // thunks are supported but with context will be browser-only (empty object) - const localePath = localeUtils.getLocalePath(localePathPart, locale); - - const fileStatus = status[localePath]; - if (fileStatus) return fileStatus; - - // We cannot use dispatch from useReducer during SSR, so exit early. - // If you want a locale file to be ready, preload it to gasketIntl data - // or load with getStaticProps or getServerSideProps. - if (!isBrowser) return LocaleStatus.LOADING; - - // Mutating status state to avoids an unnecessary render with using dispatch. - status[localePath] = LocaleStatus.LOADING; - - const url = localeUtils.pathToUrl(localePath); - - // Upon fetching, we will dispatch file status and messages to kick off a render. - fetch(url) - .then((r) => - r.ok - ? r.json() - : Promise.reject( - new Error(`Error loading locale file (${r.status}): ${url}`) - ) - ) - .then((messages) => { - dispatch({ - type: LocaleStatus.LOADED, - payload: { - locale, - messages, - file: localePath - } - }); - }) - .catch((e) => { - console.error(e.message || e); // eslint-disable-line no-console - dispatch({ - type: LocaleStatus.ERROR, - payload: { - file: localePath - } + if (!Array.isArray(localePathParam)) { + localePathParam = [localePathParam]; + } + + const loadingStatuses = localePathParam.map((localePathPart) => { + // thunks are supported but with context will be browser-only (empty object) + const localePath = localeUtils.getLocalePath(localePathPart, locale); + + const fileStatus = status[localePath]; + if (fileStatus) return fileStatus; + + // We cannot use dispatch from useReducer during SSR, so exit early. + // If you want a locale file to be ready, preload it to gasketIntl data + // or load with getStaticProps or getServerSideProps. + if (!isBrowser) return LocaleStatus.LOADING; + + // Mutating status state to avoids an unnecessary render with using dispatch. + status[localePath] = LocaleStatus.LOADING; + + const url = localeUtils.pathToUrl(localePath); + + // Upon fetching, we will dispatch file status and messages to kick off a render. + fetch(url) + .then((r) => + r.ok + ? r.json() + : Promise.reject( + new Error(`Error loading locale file (${r.status}): ${url}`) + ) + ) + .then((messages) => { + dispatch({ + type: LocaleStatus.LOADED, + payload: { + locale, + messages, + file: localePath + } + }); + }) + .catch((e) => { + console.error(e.message || e); // eslint-disable-line no-console + dispatch({ + type: LocaleStatus.ERROR, + payload: { + file: localePath + } + }); }); - }); - return LocaleStatus.LOADING; + return LocaleStatus.LOADING; + }); + + if (loadingStatuses.includes(LocaleStatus.ERROR)) return LocaleStatus.ERROR; + if (loadingStatuses.includes(LocaleStatus.LOADING)) return LocaleStatus.LOADING; + return LocaleStatus.LOADED; } diff --git a/packages/gasket-react-intl/test/use-locale-required.test.js b/packages/gasket-react-intl/test/use-locale-required.test.js index ab02c938d..d19f91536 100644 --- a/packages/gasket-react-intl/test/use-locale-required.test.js +++ b/packages/gasket-react-intl/test/use-locale-required.test.js @@ -22,6 +22,7 @@ const { ERROR, LOADED, LOADING } = LocaleStatus; // helper to wait for async actions const pause = ms => new Promise((resolve) => setTimeout(resolve, ms)); +// eslint-disable-next-line max-statements describe('useLocaleRequired', function () { let mockConfig, mockContext, dispatchMock; @@ -115,6 +116,55 @@ describe('useLocaleRequired', function () { expect(console.error).toHaveBeenCalledWith('Bad things man!'); }); + describe('when localesPath is an array', () => { + it('accepts an array of locale paths, and fetches each path provided', () => { + const results = useLocaleRequired(['/locales', '/custom/locales', 'modules/module/locales']); + expect(results).toEqual(LOADING); + expect(fetch).toHaveBeenCalled(); + expect(fetch).toHaveBeenCalledWith('/locales/en.json'); + expect(fetch).toHaveBeenCalledWith('/custom/locales/en.json'); + expect(fetch).toHaveBeenCalledWith('/modules/module/locales/en.json'); + }); + + it('returns ERROR if any of the calls fail', () => { + mockContext.status['/locales/en.json'] = LOADED; + mockContext.status['/custom/locales/en.json'] = ERROR; + mockContext.status['/modules/module/locales/en.json'] = LOADING; + + const result = useLocaleRequired(['/locales', '/custom/locales', 'modules/module/locales']); + expect(result).toEqual(ERROR); + }); + + it('returns LOADING if any of the calls are in progress and none have failed', () => { + mockContext.status['/locales/en.json'] = LOADED; + mockContext.status['/custom/locales/en.json'] = LOADED; + mockContext.status['/modules/module/locales/en.json'] = LOADING; + + const result = useLocaleRequired(['/locales', '/custom/locales', 'modules/module/locales']); + expect(result).toEqual(LOADING); + }); + + it('returns LOADED if all calls succeed', () => { + mockContext.status['/locales/en.json'] = LOADED; + mockContext.status['/custom/locales/en.json'] = LOADED; + mockContext.status['/modules/module/locales/en.json'] = LOADED; + + const result = useLocaleRequired(['/locales', '/custom/locales', 'modules/module/locales']); + expect(result).toEqual(LOADED); + }); + + it('handle array containing thunks', function () { + const mockThunk = jest.fn().mockReturnValue('/custom/locales'); + + const results = useLocaleRequired(['/locales', mockThunk, 'modules/module/locales']); + expect(results).toEqual(LOADING); + expect(fetch).toHaveBeenCalled(); + expect(fetch).toHaveBeenCalledWith('/locales/en.json'); + expect(fetch).toHaveBeenCalledWith('/custom/locales/en.json'); + expect(fetch).toHaveBeenCalledWith('/modules/module/locales/en.json'); + }); + }); + describe('SSR', function () { beforeEach(function () {