From cfd0c44f6965e47a8a1ea707c9b90b5a850e7ff1 Mon Sep 17 00:00:00 2001 From: eliebman-godaddy Date: Thu, 6 Jun 2024 17:50:39 -0500 Subject: [PATCH 1/3] Update useLocalesRequired, withLocaleRequired, and LocaleRequired to accept an array as `localesPath` --- packages/gasket-react-intl/src/index.d.ts | 8 +- .../src/use-locale-required.js | 100 ++++++++++-------- 2 files changed, 59 insertions(+), 49 deletions(-) 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; } From bbc5851b311597bcab984a8e43254805aa943d69 Mon Sep 17 00:00:00 2001 From: eliebman-godaddy Date: Thu, 6 Jun 2024 17:51:02 -0500 Subject: [PATCH 2/3] Update Unit Tests --- .../test/use-locale-required.test.js | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) 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..40ebbc1fb 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,53 @@ describe('useLocaleRequired', function () { expect(console.error).toHaveBeenCalledWith('Bad things man!'); }); + 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 () { From c451b4ed9890019cce4b47a9cd03b6dfa10ce742 Mon Sep 17 00:00:00 2001 From: eliebman-godaddy Date: Thu, 6 Jun 2024 17:51:10 -0500 Subject: [PATCH 3/3] Update Docs --- packages/gasket-react-intl/README.md | 12 +-- .../test/use-locale-required.test.js | 76 ++++++++++--------- 2 files changed, 45 insertions(+), 43 deletions(-) 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/test/use-locale-required.test.js b/packages/gasket-react-intl/test/use-locale-required.test.js index 40ebbc1fb..d19f91536 100644 --- a/packages/gasket-react-intl/test/use-locale-required.test.js +++ b/packages/gasket-react-intl/test/use-locale-required.test.js @@ -116,51 +116,53 @@ describe('useLocaleRequired', function () { expect(console.error).toHaveBeenCalledWith('Bad things man!'); }); - 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'); - }); + 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; + 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); - }); + 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; + 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); - }); + 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; + 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); - }); + 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'); + 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'); + 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 () {