From cc425f0b311a9651b336a2fe143522cd049921ca Mon Sep 17 00:00:00 2001 From: Sawyer Date: Wed, 29 Nov 2023 17:50:45 -0800 Subject: [PATCH 01/32] Add next-intl --- app/.eslintrc.js | 1 + app/next.config.js | 3 +- app/package.json | 4 ++ app/src/i18n/config.ts | 59 ++++++++++++++++++++++++++++ app/src/i18n/messages/en-US/index.ts | 25 ++++++++++++ app/src/i18n/messages/es-US/index.ts | 10 +++++ app/src/middleware.ts | 21 ++++++++++ app/src/types/i18n.d.ts | 3 ++ 8 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 app/src/i18n/config.ts create mode 100644 app/src/i18n/messages/en-US/index.ts create mode 100644 app/src/i18n/messages/es-US/index.ts create mode 100644 app/src/middleware.ts create mode 100644 app/src/types/i18n.d.ts diff --git a/app/.eslintrc.js b/app/.eslintrc.js index 280651af..f8d1b0aa 100644 --- a/app/.eslintrc.js +++ b/app/.eslintrc.js @@ -3,6 +3,7 @@ module.exports = { extends: [ "eslint:recommended", "plugin:storybook/recommended", + "plugin:you-dont-need-lodash-underscore/compatible", // Disable ESLint code formatting rules which conflict with Prettier "prettier", // `next` should be extended last according to their docs diff --git a/app/next.config.js b/app/next.config.js index d1287386..926e2d46 100644 --- a/app/next.config.js +++ b/app/next.config.js @@ -1,5 +1,6 @@ // @ts-check const { i18n } = require("./next-i18next.config"); +const withNextIntl = require("next-intl/plugin")("./src/i18n/config.ts"); const sassOptions = require("./scripts/sassOptions"); /** @@ -31,4 +32,4 @@ const nextConfig = { ], }; -module.exports = nextConfig; +module.exports = withNextIntl(nextConfig); diff --git a/app/package.json b/app/package.json index 55d3bc90..b47fe4ae 100644 --- a/app/package.json +++ b/app/package.json @@ -25,8 +25,10 @@ "@trussworks/react-uswds": "^6.0.0", "@uswds/uswds": "3.7.0", "i18next": "^23.0.0", + "lodash": "^4.17.21", "next": "^14.0.0", "next-i18next": "^15.0.0", + "next-intl": "^3.2.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-i18next": "^13.0.0" @@ -42,6 +44,7 @@ "@testing-library/user-event": "^14.4.3", "@types/jest": "^29.5.5", "@types/jest-axe": "^3.5.5", + "@types/lodash": "^4.14.202", "@types/node": "^20.0.0", "@types/react": "^18.0.28", "@types/react-dom": "^18.0.11", @@ -57,6 +60,7 @@ "i18next-browser-languagedetector": "^7.0.2", "i18next-http-backend": "^2.2.1", "i18next-resources-for-ts": "^1.3.0", + "eslint-plugin-you-dont-need-lodash-underscore": "^6.13.0", "jest": "^29.5.0", "jest-axe": "^8.0.0", "jest-cli": "^29.5.0", diff --git a/app/src/i18n/config.ts b/app/src/i18n/config.ts new file mode 100644 index 00000000..09ef34bd --- /dev/null +++ b/app/src/i18n/config.ts @@ -0,0 +1,59 @@ +import { merge } from "lodash"; + +import { getRequestConfig } from "next-intl/server"; + +type RequestConfig = Awaited< + ReturnType[0]> +>; + +/** + * List of languages supported by the application. Other tools (Storybook, tests) reference this. + * These must be BCP47 language tags: https://en.wikipedia.org/wiki/IETF_language_tag#List_of_common_primary_language_subtags + */ +export const locales = ["en-US", "es-US"] as const; +export type SupportedLocale = (typeof locales)[number]; +export const defaultLocale: SupportedLocale = "en-US"; + +/** + * Define the default formatting for date, time, and numbers. + * @see https://next-intl-docs.vercel.app/docs/usage/configuration#formats + */ +const formats: RequestConfig["formats"] = { + number: { + currency: { + currency: "USD", + }, + }, +}; + +/** + * Get the entire locale messages object for the given locale. If any + * translations are missing from the current locale, the missing key will + * fallback to the default locale + */ +async function getLocaleMessages(locale: string) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + let { messages } = await import(`./messages/${locale}`); + + if (locale !== defaultLocale) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const { messages: fallbackMessages } = await import( + `./messages/${defaultLocale}` + ); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + messages = merge({}, fallbackMessages, messages); + } + + return messages as RequestConfig["messages"]; +} + +/** + * The next-intl config. This method is used behind the scenes by `next-intl/plugin` + * when its called in next.config.js. + */ +export default getRequestConfig(async ({ locale }) => { + return { + formats, + messages: await getLocaleMessages(locale), + }; +}); diff --git a/app/src/i18n/messages/en-US/index.ts b/app/src/i18n/messages/en-US/index.ts new file mode 100644 index 00000000..9cae72a1 --- /dev/null +++ b/app/src/i18n/messages/en-US/index.ts @@ -0,0 +1,25 @@ +export const messages = { + components: { + Header: { + nav_link_home: "Home", + nav_link_health: "Health", + nav_menu_toggle: "Menu", + title: "Site title", + }, + Footer: { + agency_name: "Agency name", + return_to_top: "Return to top", + }, + Layout: { + skip_to_main: "Skip to main content", + }, + }, + home: { + title: "Home", + intro: + "This is a template for a React web application using the Next.js framework.", + body: "This is template includes:", + formatting: + "The template includes an internationalization library with basic formatters built-in. Such as numbers: { amount, number, currency }, and dates: { isoDate, date, long}.", + }, +}; diff --git a/app/src/i18n/messages/es-US/index.ts b/app/src/i18n/messages/es-US/index.ts new file mode 100644 index 00000000..356c61e4 --- /dev/null +++ b/app/src/i18n/messages/es-US/index.ts @@ -0,0 +1,10 @@ +export const messages = { + components: { + Header: { + title: "Título del sitio", + }, + }, + home: { + title: "Hogar", + }, +}; diff --git a/app/src/middleware.ts b/app/src/middleware.ts new file mode 100644 index 00000000..90049eee --- /dev/null +++ b/app/src/middleware.ts @@ -0,0 +1,21 @@ +import createIntlMiddleware from "next-intl/middleware"; +import { NextRequest } from "next/server"; + +import { defaultLocale, locales } from "./i18n/config"; + +// Don't run middleware on API routes or Next.js build output +export const config = { + matcher: ["/((?!api|_next|.*\\..*).*)"], +}; + +const i18nMiddleware = createIntlMiddleware({ + locales, + defaultLocale, + // Don't prefix the URLs when the locale is the default locale + localePrefix: "as-needed", +}); + +export default function middleware(req: NextRequest) { + const response = i18nMiddleware(req); + return response; +} diff --git a/app/src/types/i18n.d.ts b/app/src/types/i18n.d.ts new file mode 100644 index 00000000..b4e6411e --- /dev/null +++ b/app/src/types/i18n.d.ts @@ -0,0 +1,3 @@ +// Use type safe message keys with `next-intl` +type Messages = typeof import("src/i18n/messages/en-US").default; +type IntlMessages = Messages; From 7f6cd30a375b704934fe32d6fe1e06c990c6393d Mon Sep 17 00:00:00 2001 From: Sawyer Date: Wed, 29 Nov 2023 17:55:19 -0800 Subject: [PATCH 02/32] Remove i18next --- app/.eslintrc.js | 13 ----- app/.prettierrc.js | 1 - app/.storybook/i18next.js | 22 -------- app/.storybook/main.js | 2 +- app/.storybook/preview.js | 5 -- app/next-i18next.config.d.ts | 6 --- app/next-i18next.config.js | 71 -------------------------- app/next.config.js | 10 ---- app/package.json | 12 +---- app/public/locales/en-US/common.json | 15 ------ app/public/locales/en-US/home.json | 6 --- app/public/locales/es-US/common.json | 5 -- app/src/types/generated-i18n-bundle.ts | 12 ----- app/src/types/i18next.d.ts | 16 ------ app/tests/jest-i18n.ts | 19 ------- app/tests/types/i18next.test.ts | 45 ---------------- renovate.json | 5 -- 17 files changed, 2 insertions(+), 263 deletions(-) delete mode 100644 app/.storybook/i18next.js delete mode 100644 app/next-i18next.config.d.ts delete mode 100644 app/next-i18next.config.js delete mode 100644 app/public/locales/en-US/common.json delete mode 100644 app/public/locales/en-US/home.json delete mode 100644 app/public/locales/es-US/common.json delete mode 100644 app/src/types/generated-i18n-bundle.ts delete mode 100644 app/src/types/i18next.d.ts delete mode 100644 app/tests/types/i18next.test.ts diff --git a/app/.eslintrc.js b/app/.eslintrc.js index f8d1b0aa..3b434b40 100644 --- a/app/.eslintrc.js +++ b/app/.eslintrc.js @@ -15,19 +15,6 @@ module.exports = { // dependencies to work in standalone mode. It may be overkill for most projects at // Nava which aren't image heavy. "@next/next/no-img-element": "off", - "no-restricted-imports": [ - "error", - { - paths: [ - { - message: - 'Import from "next-i18next" instead of "react-i18next" so server-side translations work.', - name: "react-i18next", - importNames: ["useTranslation", "Trans"], - }, - ], - }, - ], }, // Additional lint rules. These get layered onto the top-level rules. overrides: [ diff --git a/app/.prettierrc.js b/app/.prettierrc.js index df0f1cd7..d52ac129 100644 --- a/app/.prettierrc.js +++ b/app/.prettierrc.js @@ -15,7 +15,6 @@ module.exports = { "", "", "", // blank line - "i18next", "^next[/-](.*)$", "^react$", "uswds", diff --git a/app/.storybook/i18next.js b/app/.storybook/i18next.js deleted file mode 100644 index a3eeb9d9..00000000 --- a/app/.storybook/i18next.js +++ /dev/null @@ -1,22 +0,0 @@ -// Configure i18next for Storybook -// See https://storybook.js.org/addons/storybook-react-i18next -import i18nConfig from "../next-i18next.config"; -import i18next from "i18next"; -import LanguageDetector from "i18next-browser-languagedetector"; -import Backend from "i18next-http-backend"; -import { initReactI18next } from "react-i18next"; - -i18next - .use(initReactI18next) - .use(LanguageDetector) - .use(Backend) - .init({ - ...i18nConfig, - backend: { - loadPath: `${ - process.env.NEXT_PUBLIC_BASE_PATH ?? "" - }/locales/{{lng}}/{{ns}}.json`, - }, - }); - -export default i18next; diff --git a/app/.storybook/main.js b/app/.storybook/main.js index 8cde926c..89387b90 100644 --- a/app/.storybook/main.js +++ b/app/.storybook/main.js @@ -25,7 +25,7 @@ function blockSearchEnginesInHead(head) { */ const config = { stories: ["../stories/**/*.stories.@(mdx|js|jsx|ts|tsx)"], - addons: ["@storybook/addon-essentials", "storybook-react-i18next"], + addons: ["@storybook/addon-essentials"], framework: { name: "@storybook/nextjs", options: { diff --git a/app/.storybook/preview.js b/app/.storybook/preview.js index a7c243bb..5efa84a9 100644 --- a/app/.storybook/preview.js +++ b/app/.storybook/preview.js @@ -2,9 +2,6 @@ // Apply global styling to our stories import "../src/styles/styles.scss"; -// Import i18next config. -import i18n from "./i18next.js"; - const parameters = { actions: { argTypesRegex: "^on[A-Z].*" }, controls: { @@ -13,8 +10,6 @@ const parameters = { date: /Date$/, }, }, - // Configure i18next and locale/dropdown options. - i18n, options: { storySort: { method: "alphabetical", diff --git a/app/next-i18next.config.d.ts b/app/next-i18next.config.d.ts deleted file mode 100644 index 2a24a62a..00000000 --- a/app/next-i18next.config.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * @file You shouldn't have to worry about this file. Mainly here so imports of the config file - * (in tests or Storybook) have the correct type for the config object. - */ -declare const UserConfig: import("next-i18next").UserConfig; -export default UserConfig; diff --git a/app/next-i18next.config.js b/app/next-i18next.config.js deleted file mode 100644 index a958183c..00000000 --- a/app/next-i18next.config.js +++ /dev/null @@ -1,71 +0,0 @@ -// @ts-check -const fs = require("fs"); -const path = require("path"); - -// Source of truth for the list of languages supported by the application. Other tools (i18next, Storybook, tests) reference this. -// These must be BCP47 language tags: https://en.wikipedia.org/wiki/IETF_language_tag#List_of_common_primary_language_subtags -const locales = [ - "en-US", // English - "es-US", // Spanish -]; -const defaultLocale = locales[0]; - -/** - * Next.js i18n routing options - * https://nextjs.org/docs/advanced-features/i18n-routing - * @type {import('next').NextConfig['i18n']} - */ -const i18n = { - defaultLocale, - locales, -}; - -function getNamespaces() { - if (typeof fs === "undefined" || typeof fs.readdirSync === "undefined") { - console.log( - "No fs module available, which means next-i18next.config is being referenced from a client-side bundle. Returning an empty list of namespaces, which should be fine since this list is only necessary for preloading locales on the server." - ); - return []; - } - - const namespaces = fs - .readdirSync(path.resolve(__dirname, `public/locales/${defaultLocale}`)) - .map((file) => file.replace(/\.json$/, "")); - return namespaces; -} - -/** - * i18next and react-i18next options - * https://www.i18next.com/overview/configuration-options - * https://react.i18next.com/latest/i18next-instance - * @type {import("i18next").InitOptions} - */ -const i18next = { - ns: getNamespaces(), // Namespaces to preload on the server - defaultNS: "common", - fallbackLng: i18n.defaultLocale, - interpolation: { - escapeValue: false, // React already does escaping - }, -}; - -/** - * next-i18next options - * https://github.com/i18next/next-i18next#options - * @type {Partial} - */ -const nextI18next = { - // Locale resources are loaded once when the server is started, which - // is good for production but not ideal for local development. Show - // updates to locale files without having to restart the server: - reloadOnPrerender: process.env.NODE_ENV === "development", -}; - -/** - * @type {import("next-i18next").UserConfig} - */ -module.exports = { - i18n, - ...i18next, - ...nextI18next, -}; diff --git a/app/next.config.js b/app/next.config.js index 926e2d46..a137445e 100644 --- a/app/next.config.js +++ b/app/next.config.js @@ -1,5 +1,4 @@ // @ts-check -const { i18n } = require("./next-i18next.config"); const withNextIntl = require("next-intl/plugin")("./src/i18n/config.ts"); const sassOptions = require("./scripts/sassOptions"); @@ -16,20 +15,11 @@ const appSassOptions = sassOptions(basePath); /** @type {import('next').NextConfig} */ const nextConfig = { basePath, - i18n, reactStrictMode: true, // Output only the necessary files for a deployment, excluding irrelevant node_modules // https://nextjs.org/docs/app/api-reference/next-config-js/output output: "standalone", sassOptions: appSassOptions, - // Continue to support older browsers (ES5) - transpilePackages: [ - // https://github.com/i18next/i18next/issues/1948 - "i18next", - "react-i18next", - // https://github.com/trussworks/react-uswds/issues/2605 - "@trussworks/react-uswds", - ], }; module.exports = withNextIntl(nextConfig); diff --git a/app/package.json b/app/package.json index b47fe4ae..601671a0 100644 --- a/app/package.json +++ b/app/package.json @@ -4,12 +4,9 @@ "private": true, "scripts": { "build": "next build", - "prebuild": "npm run i18n-types", "dev": "next dev", - "predev": "npm run i18n-types", "format": "prettier --write '**/*.{js,json,md,mdx,ts,tsx,scss,yaml,yml}'", "format-check": "prettier --check '**/*.{js,json,md,mdx,ts,tsx,scss,yaml,yml}'", - "i18n-types": "i18next-resources-for-ts toc -i ./public/locales/en-US -o ./src/types/generated-i18n-bundle.ts -c \"Run 'npm run i18n-types' to generate this file\"", "lint": "next lint --dir src --dir stories --dir .storybook --dir tests --dir scripts --dir app --dir lib --dir types", "lint-fix": "npm run lint -- --fix", "postinstall": "node ./scripts/postinstall.js", @@ -24,14 +21,11 @@ "dependencies": { "@trussworks/react-uswds": "^6.0.0", "@uswds/uswds": "3.7.0", - "i18next": "^23.0.0", "lodash": "^4.17.21", "next": "^14.0.0", - "next-i18next": "^15.0.0", "next-intl": "^3.2.0", "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-i18next": "^13.0.0" + "react-dom": "^18.2.0" }, "devDependencies": { "@ianvs/prettier-plugin-sort-imports": "^4.0.2", @@ -57,9 +51,6 @@ "eslint-plugin-jest-dom": "^5.0.1", "eslint-plugin-storybook": "^0.6.12", "eslint-plugin-testing-library": "^6.0.0", - "i18next-browser-languagedetector": "^7.0.2", - "i18next-http-backend": "^2.2.1", - "i18next-resources-for-ts": "^1.3.0", "eslint-plugin-you-dont-need-lodash-underscore": "^6.13.0", "jest": "^29.5.0", "jest-axe": "^8.0.0", @@ -72,7 +63,6 @@ "sass": "^1.59.3", "sass-loader": "^13.2.0", "storybook": "^7.6.0", - "storybook-react-i18next": "^2.0.6", "style-loader": "^3.3.2", "typescript": "^5.0.0" } diff --git a/app/public/locales/en-US/common.json b/app/public/locales/en-US/common.json deleted file mode 100644 index f6a46b0c..00000000 --- a/app/public/locales/en-US/common.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "Header": { - "nav_link_home": "Home", - "nav_link_health": "Health", - "nav_menu_toggle": "Menu", - "title": "Site title" - }, - "Footer": { - "agency_name": "Agency name", - "return_to_top": "Return to top" - }, - "Layout": { - "skip_to_main": "Skip to main content" - } -} diff --git a/app/public/locales/en-US/home.json b/app/public/locales/en-US/home.json deleted file mode 100644 index 56fda3f9..00000000 --- a/app/public/locales/en-US/home.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "title": "Home", - "intro": "This is a template for a React web application using the Next.js framework.", - "body": "This is template includes:
  • Framework for server-side rendered, static, or hybrid React applications
  • TypeScript and React testing tools
  • U.S. Web Design System for themeable styling and a set of common components
  • Type checking, linting, and code formatting tools
  • Storybook for a frontend workshop environment
", - "formatting": "The template includes an internationalization library with basic formatters built-in. Such as numbers: {{amount, currency(USD)}}." -} diff --git a/app/public/locales/es-US/common.json b/app/public/locales/es-US/common.json deleted file mode 100644 index 045f7bd7..00000000 --- a/app/public/locales/es-US/common.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "Header": { - "title": "Título del sitio" - } -} diff --git a/app/src/types/generated-i18n-bundle.ts b/app/src/types/generated-i18n-bundle.ts deleted file mode 100644 index aa7275fd..00000000 --- a/app/src/types/generated-i18n-bundle.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Run 'npm run i18n-types' to generate this file - */ -import common from '../../public/locales/en-US/common.json'; -import home from '../../public/locales/en-US/home.json'; - -const resources = { - common, - home -} as const; - -export default resources; diff --git a/app/src/types/i18next.d.ts b/app/src/types/i18next.d.ts deleted file mode 100644 index 3e5ab449..00000000 --- a/app/src/types/i18next.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @file Type-safe internationalization. See the internationalization.md - * doc file for more information. - */ -import "i18next"; - -import i18nConfig from "next-i18next.config"; - -import resources from "./generated-i18n-bundle"; - -declare module "i18next" { - interface CustomTypeOptions { - resources: typeof resources; - defaultNS: i18nConfig.defaultNS; - } -} diff --git a/app/tests/jest-i18n.ts b/app/tests/jest-i18n.ts index 07a34201..a8df21d8 100644 --- a/app/tests/jest-i18n.ts +++ b/app/tests/jest-i18n.ts @@ -1,22 +1,3 @@ /** * @file Setup internationalization for tests so snapshots and queries reference the correct translations */ -import i18nConfig from "../next-i18next.config"; -import i18n from "i18next"; -import { initReactI18next } from "react-i18next"; - -import resources from "../src/types/generated-i18n-bundle"; - -i18n - .use(initReactI18next) - .init({ - ...i18nConfig, - resources: { en: resources }, - }) - .catch((err) => { - throw err; - }); - -// Export i18n so tests can manually set the language with: -// i18n.changeLanguage('es') -export default i18n; diff --git a/app/tests/types/i18next.test.ts b/app/tests/types/i18next.test.ts deleted file mode 100644 index cb9f3c77..00000000 --- a/app/tests/types/i18next.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Test to help ensure the generated i18n TypeScript file remains up to date - * with the English locale files that are present. Since the generation script - * is potentially a confusing aspect of the i18n approach, this test is intended - * to help bring visibility to its existence and help clarify why an engineering - * might be receiving type errors in their i18n code. - * @jest-environment node - */ -import generatedEnglishResources from "src/types/generated-i18n-bundle"; - -import i18nConfig from "../../next-i18next.config"; - -/** - * Add a custom matcher so we can provide a more helpful test failure message - */ -function toHaveI18nNamespaces(received: string[], expected: string[]) { - const missingNamespaces = expected.filter( - (namespace) => !received.includes(namespace) - ); - - return { - pass: missingNamespaces.length === 0, - message: () => { - const missingNamespacesString = missingNamespaces.join(", "); - let message = `The src/types/generated-i18n-bundle.ts file is missing imports for these English namespaces: ${missingNamespacesString}`; - message += `\n\nYou can fix this by re-running "npm run i18n-types" to regenerate the file.`; - message += `\n\nYou likely added a new namespace to the English locale files but the i18n-types script hasn't ran yet.`; - message += `\n\nIt's important for the generated-i18n-bundle.ts file to be up to date so that you don't get inaccurate TypeScript errors.`; - - return message; - }, - }; -} - -expect.extend({ toHaveI18nNamespaces }); - -describe("types/generated-i18n-bundle.ts", () => { - it("includes all English namespaces", () => { - // @ts-expect-error - Not adding a type declaration for this matcher since it is only used in this test - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - expect(Object.keys(generatedEnglishResources)).toHaveI18nNamespaces( - i18nConfig.ns - ); - }); -}); diff --git a/renovate.json b/renovate.json index fff22c2f..d170f11f 100644 --- a/renovate.json +++ b/renovate.json @@ -22,11 +22,6 @@ "matchPackagePatterns": ["storybook"], "groupName": "Storybook" }, - { - "description": "Group I18next packages together", - "matchPackagePatterns": ["i18next"], - "groupName": "I18next" - }, { "description": "Group test packages together", "matchPackagePatterns": ["jest", "testing-library"], From d391826698be9b7463b9f03b60fbfb319dff7e25 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Wed, 29 Nov 2023 17:55:54 -0800 Subject: [PATCH 03/32] Automated changes due to app router being enabled --- app/next-env.d.ts | 1 + app/tsconfig.json | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/app/next-env.d.ts b/app/next-env.d.ts index 4f11a03d..fd36f949 100644 --- a/app/next-env.d.ts +++ b/app/next-env.d.ts @@ -1,5 +1,6 @@ /// /// +/// // NOTE: This file should not be edited // see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/app/tsconfig.json b/app/tsconfig.json index 0fb8cb02..095a9c63 100644 --- a/app/tsconfig.json +++ b/app/tsconfig.json @@ -18,8 +18,19 @@ "allowJs": false, "checkJs": false, // Help speed up type checking in larger applications - "incremental": true + "incremental": true, + "plugins": [ + { + "name": "next" + } + ] }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "__mocks__/styleMock.js"], + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + "__mocks__/styleMock.js", + ".next/types/**/*.ts" + ], "exclude": ["node_modules"] } From 42c8b02113680f589c051eed132cfb11f8ed784b Mon Sep 17 00:00:00 2001 From: Sawyer Date: Wed, 29 Nov 2023 17:56:44 -0800 Subject: [PATCH 04/32] Migrate pages and layout to app directory --- app/src/app/[locale]/health/page.tsx | 3 ++ app/src/app/[locale]/layout.tsx | 66 ++++++++++++++++++++++++++++ app/src/app/[locale]/page.tsx | 46 +++++++++++++++++++ app/src/components/Layout.tsx | 35 --------------- app/src/pages/_app.tsx | 25 ----------- app/src/pages/health.tsx | 15 ------- app/src/pages/index.tsx | 61 ------------------------- 7 files changed, 115 insertions(+), 136 deletions(-) create mode 100644 app/src/app/[locale]/health/page.tsx create mode 100644 app/src/app/[locale]/layout.tsx create mode 100644 app/src/app/[locale]/page.tsx delete mode 100644 app/src/components/Layout.tsx delete mode 100644 app/src/pages/_app.tsx delete mode 100644 app/src/pages/health.tsx delete mode 100644 app/src/pages/index.tsx diff --git a/app/src/app/[locale]/health/page.tsx b/app/src/app/[locale]/health/page.tsx new file mode 100644 index 00000000..adbd9f30 --- /dev/null +++ b/app/src/app/[locale]/health/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return <>healthy; +} diff --git a/app/src/app/[locale]/layout.tsx b/app/src/app/[locale]/layout.tsx new file mode 100644 index 00000000..d7137452 --- /dev/null +++ b/app/src/app/[locale]/layout.tsx @@ -0,0 +1,66 @@ +/** + * Root layout component, wraps all pages. + * @see https://nextjs.org/docs/app/api-reference/file-conventions/layout + */ +import { Metadata } from "next"; + +import { + NextIntlClientProvider, + useMessages, + useTranslations, +} from "next-intl"; +import { GovBanner, Grid, GridContainer } from "@trussworks/react-uswds"; + +import Footer from "src/components/Footer"; +import Header from "src/components/Header"; + +import "src/styles/styles.scss"; + +import { pick } from "lodash"; + +export const metadata: Metadata = { + title: "Home", + icons: [`${process.env.NEXT_PUBLIC_BASE_PATH ?? ""}/img/logo.svg`], +}; + +interface LayoutProps { + children: React.ReactNode; + params: { + locale: string; + }; +} + +export default function Layout({ children, params }: LayoutProps) { + const t = useTranslations("components.Layout"); + const messages = useMessages(); + + return ( + + + {/* Flex and min height sticks the footer to the bottom of the page */} +
+ + {t("skip_to_main")} + + + +
+ + {/* Flex pushes the footer to the bottom of the page */} +
+ + + {children} + + +
+
+
+ + + ); +} diff --git a/app/src/app/[locale]/page.tsx b/app/src/app/[locale]/page.tsx new file mode 100644 index 00000000..0ce2fa99 --- /dev/null +++ b/app/src/app/[locale]/page.tsx @@ -0,0 +1,46 @@ +import { Metadata } from "next"; + +import { useTranslations } from "next-intl"; +import { getTranslations } from "next-intl/server"; + +export async function generateMetadata({ + params: { locale }, +}: { + params: { locale: string }; +}): Promise { + const t = await getTranslations({ locale, namespace: "home" }); + + return { + title: t("title"), + }; +} + +export default function Page() { + const t = useTranslations("home"); + + return ( + <> +

{t("title")}

+

+ {t.rich("intro", { + LinkToNextJs: (content) => ( + {content} + ), + })} +

+
+ {t.rich("body", { + ul: (content) =>
    {content}
, + li: (content) =>
  • {content}
  • , + })} + +

    + {t("formatting", { + amount: 1234, + isoDate: new Date("2023-11-29T23:30:00.000Z"), + })} +

    +
    + + ); +} diff --git a/app/src/components/Layout.tsx b/app/src/components/Layout.tsx deleted file mode 100644 index 8f01d33b..00000000 --- a/app/src/components/Layout.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { useTranslation } from "next-i18next"; -import { Grid, GridContainer } from "@trussworks/react-uswds"; - -import Footer from "./Footer"; -import Header from "./Header"; - -type Props = { - children: React.ReactNode; -}; - -const Layout = ({ children }: Props) => { - const { t } = useTranslation("common", { - keyPrefix: "Layout", - }); - - return ( - // Stick the footer to the bottom of the page -
    - - {t("skip_to_main")} - -
    -
    - - - {children} - - -
    -
    -
    - ); -}; - -export default Layout; diff --git a/app/src/pages/_app.tsx b/app/src/pages/_app.tsx deleted file mode 100644 index 67a974eb..00000000 --- a/app/src/pages/_app.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { appWithTranslation } from "next-i18next"; -import type { AppProps } from "next/app"; -import Head from "next/head"; - -import Layout from "../components/Layout"; - -import "../styles/styles.scss"; - -function MyApp({ Component, pageProps }: AppProps) { - return ( - <> - - - - - - - - ); -} - -export default appWithTranslation(MyApp); diff --git a/app/src/pages/health.tsx b/app/src/pages/health.tsx deleted file mode 100644 index e4afd4f2..00000000 --- a/app/src/pages/health.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import type { GetServerSideProps, NextPage } from "next"; - -import { serverSideTranslations } from "next-i18next/serverSideTranslations"; -import React from "react"; - -const Health: NextPage = () => { - return <>healthy; -}; - -export const getServerSideProps: GetServerSideProps = async ({ locale }) => { - const translations = await serverSideTranslations(locale ?? "en-US"); - return { props: { ...translations } }; -}; - -export default Health; diff --git a/app/src/pages/index.tsx b/app/src/pages/index.tsx deleted file mode 100644 index 4054147f..00000000 --- a/app/src/pages/index.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import type { GetServerSideProps, NextPage } from "next"; - -import { Trans, useTranslation } from "next-i18next"; -import { serverSideTranslations } from "next-i18next/serverSideTranslations"; -import Head from "next/head"; - -const Home: NextPage = () => { - const { t } = useTranslation("home"); - - return ( - <> - - {t("title")} - - -

    {t("title")}

    - - {/* Demonstration of more complex translated strings, with safe-listed links HTML elements */} -

    - , - }} - /> -

    -
    - , - li:
  • , - }} - /> - -

    - {/* Demonstration of formatters */} - -

    -
  • - - ); -}; - -// Change this to getStaticProps if you're not using server-side rendering -export const getServerSideProps: GetServerSideProps = async ({ locale }) => { - const translations = await serverSideTranslations(locale ?? "en-US"); - return { props: { ...translations } }; -}; - -export default Home; From ccecc897706657f8eda771d16448af66b120401c Mon Sep 17 00:00:00 2001 From: Sawyer Date: Wed, 29 Nov 2023 17:56:58 -0800 Subject: [PATCH 05/32] Use next-intl in components --- app/src/components/Footer.tsx | 6 ++---- app/src/components/Header.tsx | 17 +++++++---------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/app/src/components/Footer.tsx b/app/src/components/Footer.tsx index 24a62f4f..4d7a35d0 100644 --- a/app/src/components/Footer.tsx +++ b/app/src/components/Footer.tsx @@ -1,4 +1,4 @@ -import { useTranslation } from "next-i18next"; +import { useTranslations } from "next-intl"; import { Address, FooterNav, @@ -8,9 +8,7 @@ import { } from "@trussworks/react-uswds"; const Footer = () => { - const { t } = useTranslation("common", { - keyPrefix: "Footer", - }); + const t = useTranslations("components.Footer"); return ( { - const { t, i18n } = useTranslation("common", { - keyPrefix: "Header", - }); + const t = useTranslations("components.Header"); const [isMobileNavExpanded, setIsMobileNavExpanded] = useState(false); const handleMobileNavToggle = () => { @@ -30,9 +30,9 @@ const Header = () => { }; const navItems = primaryLinks.map((link) => ( - + {t(link.i18nKey)} - + )); return ( @@ -40,9 +40,6 @@ const Header = () => {
    -
    From 825292afd5fa46653c2f886906e66c1ea9eed53e Mon Sep 17 00:00:00 2001 From: Sawyer Date: Thu, 30 Nov 2023 11:53:08 -0800 Subject: [PATCH 06/32] Remove unused ts-jest file --- app/tsconfig.ts-jest.json | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 app/tsconfig.ts-jest.json diff --git a/app/tsconfig.ts-jest.json b/app/tsconfig.ts-jest.json deleted file mode 100644 index 4fd5045d..00000000 --- a/app/tsconfig.ts-jest.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "jsx": "react-jsx" - } -} From 4c9de3430e101f7566dac481c834c9709fda6782 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Fri, 1 Dec 2023 16:23:07 -0800 Subject: [PATCH 07/32] Fix i18n type safety --- app/src/types/i18n.d.ts | 2 +- app/tsconfig.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/types/i18n.d.ts b/app/src/types/i18n.d.ts index b4e6411e..733cc674 100644 --- a/app/src/types/i18n.d.ts +++ b/app/src/types/i18n.d.ts @@ -1,3 +1,3 @@ // Use type safe message keys with `next-intl` -type Messages = typeof import("src/i18n/messages/en-US").default; +type Messages = typeof import("src/i18n/messages/en-US").messages; type IntlMessages = Messages; diff --git a/app/tsconfig.json b/app/tsconfig.json index 095a9c63..40b9cba1 100644 --- a/app/tsconfig.json +++ b/app/tsconfig.json @@ -30,7 +30,8 @@ "**/*.ts", "**/*.tsx", "__mocks__/styleMock.js", - ".next/types/**/*.ts" + ".next/types/**/*.ts", + "src/types/**/*.d.ts" ], "exclude": ["node_modules"] } From 1fadabd5dfcc28e90612c5a5d79acb8aa906997c Mon Sep 17 00:00:00 2001 From: Sawyer Date: Fri, 1 Dec 2023 17:43:26 -0800 Subject: [PATCH 08/32] Migrate tests --- app/.eslintrc.js | 16 +- app/README.md | 17 +- app/jest.config.js | 5 +- app/package-lock.json | 1053 ++++++----------- app/package.json | 6 +- app/src/app/[locale]/layout.tsx | 1 + app/src/i18n/config.ts | 14 +- app/src/i18n/formats.ts | 17 + app/tests/app/layout.test.tsx | 50 + .../index.test.tsx => app/page.test.tsx} | 11 +- app/tests/components/Header.test.tsx | 4 +- app/tests/components/Layout.test.tsx | 29 - app/tests/jest-i18n.ts | 3 - app/tests/test-utils.tsx | 38 + 14 files changed, 495 insertions(+), 769 deletions(-) create mode 100644 app/src/i18n/formats.ts create mode 100644 app/tests/app/layout.test.tsx rename app/tests/{pages/index.test.tsx => app/page.test.tsx} (68%) delete mode 100644 app/tests/components/Layout.test.tsx delete mode 100644 app/tests/jest-i18n.ts create mode 100644 app/tests/test-utils.tsx diff --git a/app/.eslintrc.js b/app/.eslintrc.js index 3b434b40..a9bce0ef 100644 --- a/app/.eslintrc.js +++ b/app/.eslintrc.js @@ -20,13 +20,27 @@ module.exports = { overrides: [ // Lint config specific to Test files { - files: ["tests/**"], + files: ["tests/**/?(*.)+(spec|test).[jt]s?(x)"], plugins: ["jest"], extends: [ "plugin:jest/recommended", "plugin:jest-dom/recommended", "plugin:testing-library/react", ], + rules: { + "no-restricted-imports": [ + "error", + { + paths: [ + { + message: + 'Import from "tests/test-utils" instead so that translations work.', + name: "@testing-library/react", + }, + ], + }, + ], + }, }, // Lint config specific to TypeScript files { diff --git a/app/README.md b/app/README.md index 18eb5aa2..c7c268e6 100644 --- a/app/README.md +++ b/app/README.md @@ -103,9 +103,7 @@ From the `app/` directory: ## 🐛 Testing -[Jest](https://jestjs.io/docs/getting-started) is used as the test runner and [React Testing Library](https://testing-library.com/docs/react-testing-library/intro) provides React testing utilities. - -Tests are manged as `.test.ts` (or `.tsx`) files in the the `tests/` directory. +[Jest](https://jestjs.io/docs/getting-started) is used as the test runner. Tests are manged as `.test.ts` (or `.tsx`) files in the the `tests/` directory. To run tests: @@ -119,6 +117,19 @@ A subset of tests can be ran by passing a pattern to the script. For example, to npm run test-watch -- pages ``` +### Testing React components + +[React Testing Library (RTL)](https://testing-library.com/docs/react-testing-library/intro) provides the utilities for rendering and querying, and [`jest-axe`](https://www.npmjs.com/package/jest-axe) is used for accessibility testing. Refer to their docs to learn more about their APIs, or view an existing test for examples. + +It's important that you **don't import from `@testing-library/react`**, instead import from `tests/test-utils`. Using the custom `render` method from `tests/test-utils` sets up internationalization in the component. + +Import React Testing Library utilities from `tests/test-utils`: + +```diff +- import { render, screen } from '@testing-library/react'; ++ import { render, screen } from 'tests/test-utils'; +``` + ## 🤖 Type checking, linting, and formatting - [TypeScript](https://www.typescriptlang.org/) is used for type checking. diff --git a/app/jest.config.js b/app/jest.config.js index 79bbe6e3..af16d0e8 100644 --- a/app/jest.config.js +++ b/app/jest.config.js @@ -9,10 +9,7 @@ const createJestConfig = nextJest({ // Add any custom config to be passed to Jest /** @type {import('jest').Config} */ const customJestConfig = { - setupFilesAfterEnv: [ - "/tests/jest.setup.ts", - "/tests/jest-i18n.ts", - ], + setupFilesAfterEnv: ["/tests/jest.setup.ts"], testEnvironment: "jsdom", // if using TypeScript with a baseUrl set to the root directory then you need the below for alias' to work moduleDirectories: ["node_modules", "/"], diff --git a/app/package-lock.json b/app/package-lock.json index e5507b84..b406192a 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -9,14 +9,13 @@ "version": "0.1.0", "hasInstallScript": true, "dependencies": { - "@trussworks/react-uswds": "^6.0.0", + "@trussworks/react-uswds": "^6.1.0", "@uswds/uswds": "3.7.0", - "i18next": "^23.0.0", - "next": "^14.0.0", - "next-i18next": "^15.0.0", + "lodash": "^4.17.21", + "next": "^14.0.3", + "next-intl": "^3.2.1", "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-i18next": "^13.0.0" + "react-dom": "^18.2.0" }, "devDependencies": { "@ianvs/prettier-plugin-sort-imports": "^4.0.2", @@ -29,6 +28,7 @@ "@testing-library/user-event": "^14.4.3", "@types/jest": "^29.5.5", "@types/jest-axe": "^3.5.5", + "@types/lodash": "^4.14.202", "@types/node": "^20.0.0", "@types/react": "^18.0.28", "@types/react-dom": "^18.0.11", @@ -41,9 +41,7 @@ "eslint-plugin-jest-dom": "^5.0.1", "eslint-plugin-storybook": "^0.6.12", "eslint-plugin-testing-library": "^6.0.0", - "i18next-browser-languagedetector": "^7.0.2", - "i18next-http-backend": "^2.2.1", - "i18next-resources-for-ts": "^1.3.0", + "eslint-plugin-you-dont-need-lodash-underscore": "^6.13.0", "jest": "^29.5.0", "jest-axe": "^8.0.0", "jest-cli": "^29.5.0", @@ -55,7 +53,6 @@ "sass": "^1.59.3", "sass-loader": "^13.2.0", "storybook": "^7.6.0", - "storybook-react-i18next": "^2.0.6", "style-loader": "^3.3.2", "typescript": "^5.0.0" } @@ -2073,6 +2070,7 @@ "version": "7.23.2", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", + "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -3563,6 +3561,92 @@ "integrity": "sha512-m0G6wlnhm/AX0H12IOWtK8gASEMffnX08RtKkCgTdHb9JpHKGloI7icFfLg9ZmQeavcvR0PKmzxClyuFPSjKWw==", "dev": true }, + "node_modules/@formatjs/ecma402-abstract": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.18.0.tgz", + "integrity": "sha512-PEVLoa3zBevWSCZzPIM/lvPCi8P5l4G+NXQMc/CjEiaCWgyHieUoo0nM7Bs0n/NbuQ6JpXEolivQ9pKSBHaDlA==", + "dependencies": { + "@formatjs/intl-localematcher": "0.5.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@formatjs/ecma402-abstract/node_modules/@formatjs/intl-localematcher": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.2.tgz", + "integrity": "sha512-txaaE2fiBMagLrR4jYhxzFO6wEdEG4TPMqrzBAcbr4HFUYzH/YC+lg6OIzKCHm8WgDdyQevxbAAV1OgcXctuGw==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@formatjs/fast-memoize": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-1.2.1.tgz", + "integrity": "sha512-Rg0e76nomkz3vF9IPlKeV+Qynok0r7YZjL6syLz4/urSg0IbjPZCB/iYUMNsYA643gh4mgrX3T7KEIFIxJBQeg==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.1.0.tgz", + "integrity": "sha512-Qxv/lmCN6hKpBSss2uQ8IROVnta2r9jd3ymUEIjm2UyIkUCHVcbUVRGL/KS/wv7876edvsPe+hjHVJ4z8YuVaw==", + "dependencies": { + "@formatjs/ecma402-abstract": "1.11.4", + "@formatjs/icu-skeleton-parser": "1.3.6", + "tslib": "^2.1.0" + } + }, + "node_modules/@formatjs/icu-messageformat-parser/node_modules/@formatjs/ecma402-abstract": { + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz", + "integrity": "sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==", + "dependencies": { + "@formatjs/intl-localematcher": "0.2.25", + "tslib": "^2.1.0" + } + }, + "node_modules/@formatjs/icu-messageformat-parser/node_modules/@formatjs/intl-localematcher": { + "version": "0.2.25", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.25.tgz", + "integrity": "sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.6.tgz", + "integrity": "sha512-I96mOxvml/YLrwU2Txnd4klA7V8fRhb6JG/4hm3VMNmeJo1F03IpV2L3wWt7EweqNLES59SZ4d6hVOPCSf80Bg==", + "dependencies": { + "@formatjs/ecma402-abstract": "1.11.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@formatjs/icu-skeleton-parser/node_modules/@formatjs/ecma402-abstract": { + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz", + "integrity": "sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==", + "dependencies": { + "@formatjs/intl-localematcher": "0.2.25", + "tslib": "^2.1.0" + } + }, + "node_modules/@formatjs/icu-skeleton-parser/node_modules/@formatjs/intl-localematcher": { + "version": "0.2.25", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.25.tgz", + "integrity": "sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.2.32", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.32.tgz", + "integrity": "sha512-k/MEBstff4sttohyEpXxCmC3MqbUn9VvHGlZ8fauLzkbwXmVrEeyzS+4uhrvAk9DWU9/7otYWxyDox4nT/KVLQ==", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.13", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", @@ -6387,25 +6471,6 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, - "node_modules/@storybook/channels": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.5.3.tgz", - "integrity": "sha512-dhWuV2o2lmxH0RKuzND8jxYzvSQTSmpE13P0IT/k8+I1up/rSNYOBQJT6SalakcNWXFAMXguo/8E7ApmnKKcEw==", - "dev": true, - "peer": true, - "dependencies": { - "@storybook/client-logger": "7.5.3", - "@storybook/core-events": "7.5.3", - "@storybook/global": "^5.0.0", - "qs": "^6.10.0", - "telejson": "^7.2.0", - "tiny-invariant": "^1.3.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, "node_modules/@storybook/cli": { "version": "7.6.0", "resolved": "https://registry.npmjs.org/@storybook/cli/-/cli-7.6.0.tgz", @@ -6617,20 +6682,6 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, - "node_modules/@storybook/client-logger": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.5.3.tgz", - "integrity": "sha512-vUFYALypjix5FoJ5M/XUP6KmyTnQJNW1poHdW7WXUVSg+lBM6E5eAtjTm0hdxNNDH8KSrdy24nCLra5h0X0BWg==", - "dev": true, - "peer": true, - "dependencies": { - "@storybook/global": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, "node_modules/@storybook/codemod": { "version": "7.6.0", "resolved": "https://registry.npmjs.org/@storybook/codemod/-/codemod-7.6.0.tgz", @@ -6717,33 +6768,6 @@ "url": "https://opencollective.com/storybook" } }, - "node_modules/@storybook/components": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/@storybook/components/-/components-7.5.3.tgz", - "integrity": "sha512-M3+cjvEsDGLUx8RvK5wyF6/13LNlUnKbMgiDE8Sxk/v/WPpyhOAIh/B8VmrU1psahS61Jd4MTkFmLf1cWau1vw==", - "dev": true, - "peer": true, - "dependencies": { - "@radix-ui/react-select": "^1.2.2", - "@radix-ui/react-toolbar": "^1.0.4", - "@storybook/client-logger": "7.5.3", - "@storybook/csf": "^0.1.0", - "@storybook/global": "^5.0.0", - "@storybook/theming": "7.5.3", - "@storybook/types": "7.5.3", - "memoizerific": "^1.11.3", - "use-resize-observer": "^9.1.0", - "util-deprecate": "^1.0.2" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/@storybook/core-common": { "version": "7.6.0", "resolved": "https://registry.npmjs.org/@storybook/core-common/-/core-common-7.6.0.tgz", @@ -6900,20 +6924,6 @@ "node": ">=8" } }, - "node_modules/@storybook/core-events": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.5.3.tgz", - "integrity": "sha512-DFOpyQ22JD5C1oeOFzL8wlqSWZzrqgDfDbUGP8xdO4wJu+FVTxnnWN6ZYLdTPB1u27DOhd7TzjQMfLDHLu7kbQ==", - "dev": true, - "peer": true, - "dependencies": { - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, "node_modules/@storybook/core-server": { "version": "7.6.0", "resolved": "https://registry.npmjs.org/@storybook/core-server/-/core-server-7.6.0.tgz", @@ -7453,74 +7463,6 @@ "url": "https://opencollective.com/storybook" } }, - "node_modules/@storybook/manager-api": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/@storybook/manager-api/-/manager-api-7.5.3.tgz", - "integrity": "sha512-d8mVLr/5BEG4bAS2ZeqYTy/aX4jPEpZHdcLaWoB4mAM+PAL9wcWsirUyApKtDVYLITJf/hd8bb2Dm2ok6E45gA==", - "dev": true, - "peer": true, - "dependencies": { - "@storybook/channels": "7.5.3", - "@storybook/client-logger": "7.5.3", - "@storybook/core-events": "7.5.3", - "@storybook/csf": "^0.1.0", - "@storybook/global": "^5.0.0", - "@storybook/router": "7.5.3", - "@storybook/theming": "7.5.3", - "@storybook/types": "7.5.3", - "dequal": "^2.0.2", - "lodash": "^4.17.21", - "memoizerific": "^1.11.3", - "semver": "^7.3.7", - "store2": "^2.14.2", - "telejson": "^7.2.0", - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@storybook/manager-api/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "peer": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@storybook/manager-api/node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "dev": true, - "peer": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@storybook/manager-api/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "peer": true - }, "node_modules/@storybook/mdx2-csf": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@storybook/mdx2-csf/-/mdx2-csf-1.1.0.tgz", @@ -8090,26 +8032,6 @@ "undici-types": "~5.26.4" } }, - "node_modules/@storybook/router": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/@storybook/router/-/router-7.5.3.tgz", - "integrity": "sha512-/iNYCFore7R5n6eFHbBYoB0P2/sybTVpA+uXTNUd3UEt7Ro6CEslTaFTEiH2RVQwOkceBp/NpyWon74xZuXhMg==", - "dev": true, - "peer": true, - "dependencies": { - "@storybook/client-logger": "7.5.3", - "memoizerific": "^1.11.3", - "qs": "^6.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/@storybook/telemetry": { "version": "7.6.0", "resolved": "https://registry.npmjs.org/@storybook/telemetry/-/telemetry-7.6.0.tgz", @@ -8195,44 +8117,6 @@ "node": ">=8" } }, - "node_modules/@storybook/theming": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-7.5.3.tgz", - "integrity": "sha512-Cjmthe1MAk0z4RKCZ7m72gAD8YD0zTAH97z5ryM1Qv84QXjiCQ143fGOmYz1xEQdNFpOThPcwW6FEccLHTkVcg==", - "dev": true, - "peer": true, - "dependencies": { - "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0", - "@storybook/client-logger": "7.5.3", - "@storybook/global": "^5.0.0", - "memoizerific": "^1.11.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@storybook/types": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.5.3.tgz", - "integrity": "sha512-iu5W0Kdd6nysN5CPkY4GRl+0BpxRTdSfBIJak7mb6xCIHSB5t1tw4BOuqMQ5EgpikRY3MWJ4gY647QkWBX3MNQ==", - "dev": true, - "peer": true, - "dependencies": { - "@storybook/channels": "7.5.3", - "@types/babel__core": "^7.0.0", - "@types/express": "^4.7.0", - "file-system-cache": "2.3.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - } - }, "node_modules/@swc/core": { "version": "1.3.99", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.99.tgz", @@ -8636,9 +8520,9 @@ } }, "node_modules/@trussworks/react-uswds": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@trussworks/react-uswds/-/react-uswds-6.0.0.tgz", - "integrity": "sha512-kHF6u6uswlexw/1OKZcY6PBmuo3gcNltby2hERqpV9iGYnO2zxMtvfxuUFN4BsyLszTr1difJevvn/8//D2Faw==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@trussworks/react-uswds/-/react-uswds-6.1.0.tgz", + "integrity": "sha512-UyS+quISxEAXY7nrYonSkQYLJv4xR8jPYOaGFm4YzgOo3TaopJ30Laf4hIlfjtazDIZrNbaRTfBu/fArQv6PPQ==", "engines": { "node": ">= 16.20.0" }, @@ -8818,15 +8702,6 @@ "@types/node": "*" } }, - "node_modules/@types/hoist-non-react-statics": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", - "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", - "dependencies": { - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0" - } - }, "node_modules/@types/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", @@ -9002,7 +8877,8 @@ "node_modules/@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", + "dev": true }, "node_modules/@types/qs": { "version": "6.9.7", @@ -9020,6 +8896,7 @@ "version": "18.2.39", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.39.tgz", "integrity": "sha512-Oiw+ppED6IremMInLV4HXGbfbG6GyziY3kqAwJYOR0PNbkYDmLWQA3a95EhdSmamsvbkJN96ZNN+YD+fGjzSBA==", + "dev": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -9044,7 +8921,8 @@ "node_modules/@types/scheduler": { "version": "0.16.3", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", - "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==" + "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", + "dev": true }, "node_modules/@types/semver": { "version": "7.5.0", @@ -11957,16 +11835,6 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "dev": true }, - "node_modules/core-js": { - "version": "3.31.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.31.0.tgz", - "integrity": "sha512-NIp2TQSGfR6ba5aalZD+ZQ1fSxGhDo/s1w0nx3RYzf2pnJxt7YynxFlFScP6eV7+GZsKO95NSjGxyJsU3DZgeQ==", - "hasInstallScript": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, "node_modules/core-js-compat": { "version": "3.33.3", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.33.3.tgz", @@ -12129,15 +11997,6 @@ "node": ">=8" } }, - "node_modules/cross-fetch": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", - "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", - "dev": true, - "dependencies": { - "node-fetch": "^2.6.12" - } - }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -12405,7 +12264,8 @@ "node_modules/csstype": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", + "dev": true }, "node_modules/damerau-levenshtein": { "version": "1.0.8", @@ -13929,6 +13789,18 @@ "eslint": "^7.5.0 || ^8.0.0" } }, + "node_modules/eslint-plugin-you-dont-need-lodash-underscore": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-you-dont-need-lodash-underscore/-/eslint-plugin-you-dont-need-lodash-underscore-6.13.0.tgz", + "integrity": "sha512-6FkFLp/R/QlgfJl5NrxkIXMQ36jMVLczkWDZJvMd7/wr/M3K0DS7mtX7plZ3giTDcbDD7VBfNYUfUVaBCZOXKA==", + "dev": true, + "dependencies": { + "kebab-case": "^1.0.0" + }, + "engines": { + "node": ">=4.0" + } + }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -15464,14 +15336,6 @@ "minimalistic-crypto-utils": "^1.0.1" } }, - "node_modules/hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "dependencies": { - "react-is": "^16.7.0" - } - }, "node_modules/hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", @@ -15533,14 +15397,6 @@ "node": ">=12" } }, - "node_modules/html-parse-stringify": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", - "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", - "dependencies": { - "void-elements": "3.1.0" - } - }, "node_modules/html-tags": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", @@ -15653,63 +15509,6 @@ "node": ">=10.17.0" } }, - "node_modules/i18next": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.6.0.tgz", - "integrity": "sha512-z0Cxr0MGkt+kli306WS4nNNM++9cgt2b2VCMprY92j+AIab/oclgPxdwtTZVLP1zn5t5uo8M6uLsZmYrcjr3HA==", - "funding": [ - { - "type": "individual", - "url": "https://locize.com" - }, - { - "type": "individual", - "url": "https://locize.com/i18next.html" - }, - { - "type": "individual", - "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" - } - ], - "dependencies": { - "@babel/runtime": "^7.22.5" - } - }, - "node_modules/i18next-browser-languagedetector": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.1.0.tgz", - "integrity": "sha512-cr2k7u1XJJ4HTOjM9GyOMtbOA47RtUoWRAtt52z43r3AoMs2StYKyjS3URPhzHaf+mn10hY9dZWamga5WPQjhA==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.19.4" - } - }, - "node_modules/i18next-fs-backend": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/i18next-fs-backend/-/i18next-fs-backend-2.3.0.tgz", - "integrity": "sha512-N0SS2WojoVIh2x/QkajSps8RPKzXqryZsQh12VoFY4cLZgkD+62EPY2fY+ZjkNADu8xA5I5EadQQXa8TXBKN3w==" - }, - "node_modules/i18next-http-backend": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-2.3.1.tgz", - "integrity": "sha512-jnagFs5cnq4ryb+g92Hex4tB5kj3tWmiRWx8gHMCcE/PEgV1fjH5rC7xyJmPSgyb9r2xgcP8rvZxPKgsmvMqTw==", - "dev": true, - "dependencies": { - "cross-fetch": "4.0.0" - } - }, - "node_modules/i18next-resources-for-ts": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/i18next-resources-for-ts/-/i18next-resources-for-ts-1.3.3.tgz", - "integrity": "sha512-fWe56BYUS7MIx0h0uxD+ydvNhELPQeOBSdNA/uaqlHbkgSqiYUvWXEGQ8pMZglIZ695qQn12X6skU/XTYG+mRw==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.22.15" - }, - "bin": { - "i18next-resources-for-ts": "bin/i18next-resources-for-ts.js" - } - }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -15946,6 +15745,34 @@ "node": ">= 0.4" } }, + "node_modules/intl-messageformat": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-9.13.0.tgz", + "integrity": "sha512-7sGC7QnSQGa5LZP7bXLDhVDtQOeKGeBFGHF2Y8LVBwYZoQZCgWeKoPGTa5GMG8g/TzDgeXuYJQis7Ggiw2xTOw==", + "dependencies": { + "@formatjs/ecma402-abstract": "1.11.4", + "@formatjs/fast-memoize": "1.2.1", + "@formatjs/icu-messageformat-parser": "2.1.0", + "tslib": "^2.1.0" + } + }, + "node_modules/intl-messageformat/node_modules/@formatjs/ecma402-abstract": { + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz", + "integrity": "sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==", + "dependencies": { + "@formatjs/intl-localematcher": "0.2.25", + "tslib": "^2.1.0" + } + }, + "node_modules/intl-messageformat/node_modules/@formatjs/intl-localematcher": { + "version": "0.2.25", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.25.tgz", + "integrity": "sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -18834,6 +18661,12 @@ "node": ">=4.0" } }, + "node_modules/kebab-case": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/kebab-case/-/kebab-case-1.0.2.tgz", + "integrity": "sha512-7n6wXq4gNgBELfDCpzKc+mRrZFs7D+wgfF5WRFLNAr4DA/qtr9Js8uOAVAfHhuLMfAcQ0pRKqbpjx+TcJVdE1Q==", + "dev": true + }, "node_modules/keyboardevent-key-polyfill": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/keyboardevent-key-polyfill/-/keyboardevent-key-polyfill-1.1.0.tgz", @@ -18964,8 +18797,7 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lodash.debounce": { "version": "4.0.8", @@ -19472,7 +19304,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -19528,39 +19359,18 @@ } } }, - "node_modules/next-i18next": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/next-i18next/-/next-i18next-15.0.0.tgz", - "integrity": "sha512-9iGEU4dt1YCC5CXh6H8YHmDpmeWKjxES6XfoABxy9mmfaLLJcqS92F56ZKmVuZUPXEOLtgY/JtsnxsHYom9J4g==", - "funding": [ - { - "type": "individual", - "url": "https://locize.com/i18next.html" - }, - { - "type": "individual", - "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" - }, - { - "type": "individual", - "url": "https://locize.com" - } - ], + "node_modules/next-intl": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-3.2.1.tgz", + "integrity": "sha512-Tfin94t/d9tNh+EPiWLPto2WsLC2/AFvBVi+OB8568l3TvflaVBbDWFn1a08sbEsjz7OltFKmS565+NIsXBi1w==", "dependencies": { - "@babel/runtime": "^7.23.2", - "@types/hoist-non-react-statics": "^3.3.4", - "core-js": "^3", - "hoist-non-react-statics": "^3.3.2", - "i18next-fs-backend": "^2.2.0" - }, - "engines": { - "node": ">=14" + "@formatjs/intl-localematcher": "^0.2.32", + "negotiator": "^0.6.3", + "use-intl": "^3.2.1" }, "peerDependencies": { - "i18next": "^23.6.0", - "next": ">= 12.0.0", - "react": ">= 17.0.2", - "react-i18next": "^13.3.1" + "next": "^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "node_modules/no-case": { @@ -21993,31 +21803,11 @@ "integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==", "dev": true }, - "node_modules/react-i18next": { - "version": "13.3.1", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-13.3.1.tgz", - "integrity": "sha512-JAtYREK879JXaN9GdzfBI4yJeo/XyLeXWUsRABvYXiFUakhZJ40l+kaTo+i+A/3cKIED41kS/HAbZ5BzFtq/Og==", - "dependencies": { - "@babel/runtime": "^7.22.5", - "html-parse-stringify": "^3.0.1" - }, - "peerDependencies": { - "i18next": ">= 23.2.3", - "react": ">= 16.8.0" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - } - } - }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true }, "node_modules/react-refresh": { "version": "0.14.0", @@ -22307,7 +22097,8 @@ "node_modules/regenerator-runtime": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", + "dev": true }, "node_modules/regenerator-transform": { "version": "0.15.2", @@ -23313,57 +23104,6 @@ "url": "https://opencollective.com/storybook" } }, - "node_modules/storybook-i18n": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/storybook-i18n/-/storybook-i18n-2.0.13.tgz", - "integrity": "sha512-p0VPL5QiHdeS3W9BvV7UnuTKw7Mj1HWLW67LK0EOoh5fpSQIchu7byfrUUe1RbCF1gT0gOOhdNuTSXMoVVoTDw==", - "dev": true, - "peerDependencies": { - "@storybook/components": "^7.0.0", - "@storybook/manager-api": "^7.0.0", - "@storybook/preview-api": "^7.0.0", - "@storybook/types": "^7.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - } - }, - "node_modules/storybook-react-i18next": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/storybook-react-i18next/-/storybook-react-i18next-2.0.9.tgz", - "integrity": "sha512-GFTOrYwOWShLqWNuTesPNhC79P3OHw1jkZ4gU3R50yTD2MUclF5DHLnuKeVfKZ323iV+I9fxLxuLIVHWVDJgXA==", - "dev": true, - "dependencies": { - "storybook-i18n": "2.0.13" - }, - "peerDependencies": { - "@storybook/components": "^7.0.0", - "@storybook/manager-api": "^7.0.0", - "@storybook/preview-api": "^7.0.0", - "@storybook/types": "^7.0.0", - "i18next": "^22.0.0 || ^23.0.0", - "i18next-browser-languagedetector": "^7.0.0", - "i18next-http-backend": "^2.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-i18next": "^12.0.0 || ^13.0.0" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - } - }, "node_modules/stream-shift": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", @@ -24761,6 +24501,18 @@ } } }, + "node_modules/use-intl": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-3.2.1.tgz", + "integrity": "sha512-QPbeKNBD43ZhSb4eVix2VrsIeF82im6j7izyh2sgQMSRwEYlARwzYHAJcuoD14E6Ko+wjVQBgYXh364vlm/Pzg==", + "dependencies": { + "@formatjs/ecma402-abstract": "^1.11.4", + "intl-messageformat": "^9.3.18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/use-resize-observer": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/use-resize-observer/-/use-resize-observer-9.1.0.tgz", @@ -24884,14 +24636,6 @@ "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", "dev": true }, - "node_modules/void-elements": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", - "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/w3c-xmlserializer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", @@ -26860,6 +26604,7 @@ "version": "7.23.2", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", + "dev": true, "requires": { "regenerator-runtime": "^0.14.0" } @@ -27533,6 +27278,98 @@ "integrity": "sha512-m0G6wlnhm/AX0H12IOWtK8gASEMffnX08RtKkCgTdHb9JpHKGloI7icFfLg9ZmQeavcvR0PKmzxClyuFPSjKWw==", "dev": true }, + "@formatjs/ecma402-abstract": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.18.0.tgz", + "integrity": "sha512-PEVLoa3zBevWSCZzPIM/lvPCi8P5l4G+NXQMc/CjEiaCWgyHieUoo0nM7Bs0n/NbuQ6JpXEolivQ9pKSBHaDlA==", + "requires": { + "@formatjs/intl-localematcher": "0.5.2", + "tslib": "^2.4.0" + }, + "dependencies": { + "@formatjs/intl-localematcher": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.2.tgz", + "integrity": "sha512-txaaE2fiBMagLrR4jYhxzFO6wEdEG4TPMqrzBAcbr4HFUYzH/YC+lg6OIzKCHm8WgDdyQevxbAAV1OgcXctuGw==", + "requires": { + "tslib": "^2.4.0" + } + } + } + }, + "@formatjs/fast-memoize": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-1.2.1.tgz", + "integrity": "sha512-Rg0e76nomkz3vF9IPlKeV+Qynok0r7YZjL6syLz4/urSg0IbjPZCB/iYUMNsYA643gh4mgrX3T7KEIFIxJBQeg==", + "requires": { + "tslib": "^2.1.0" + } + }, + "@formatjs/icu-messageformat-parser": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.1.0.tgz", + "integrity": "sha512-Qxv/lmCN6hKpBSss2uQ8IROVnta2r9jd3ymUEIjm2UyIkUCHVcbUVRGL/KS/wv7876edvsPe+hjHVJ4z8YuVaw==", + "requires": { + "@formatjs/ecma402-abstract": "1.11.4", + "@formatjs/icu-skeleton-parser": "1.3.6", + "tslib": "^2.1.0" + }, + "dependencies": { + "@formatjs/ecma402-abstract": { + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz", + "integrity": "sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==", + "requires": { + "@formatjs/intl-localematcher": "0.2.25", + "tslib": "^2.1.0" + } + }, + "@formatjs/intl-localematcher": { + "version": "0.2.25", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.25.tgz", + "integrity": "sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==", + "requires": { + "tslib": "^2.1.0" + } + } + } + }, + "@formatjs/icu-skeleton-parser": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.6.tgz", + "integrity": "sha512-I96mOxvml/YLrwU2Txnd4klA7V8fRhb6JG/4hm3VMNmeJo1F03IpV2L3wWt7EweqNLES59SZ4d6hVOPCSf80Bg==", + "requires": { + "@formatjs/ecma402-abstract": "1.11.4", + "tslib": "^2.1.0" + }, + "dependencies": { + "@formatjs/ecma402-abstract": { + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz", + "integrity": "sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==", + "requires": { + "@formatjs/intl-localematcher": "0.2.25", + "tslib": "^2.1.0" + } + }, + "@formatjs/intl-localematcher": { + "version": "0.2.25", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.25.tgz", + "integrity": "sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==", + "requires": { + "tslib": "^2.1.0" + } + } + } + }, + "@formatjs/intl-localematcher": { + "version": "0.2.32", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.32.tgz", + "integrity": "sha512-k/MEBstff4sttohyEpXxCmC3MqbUn9VvHGlZ8fauLzkbwXmVrEeyzS+4uhrvAk9DWU9/7otYWxyDox4nT/KVLQ==", + "requires": { + "tslib": "^2.4.0" + } + }, "@humanwhocodes/config-array": { "version": "0.11.13", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", @@ -29448,21 +29285,6 @@ } } }, - "@storybook/channels": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-7.5.3.tgz", - "integrity": "sha512-dhWuV2o2lmxH0RKuzND8jxYzvSQTSmpE13P0IT/k8+I1up/rSNYOBQJT6SalakcNWXFAMXguo/8E7ApmnKKcEw==", - "dev": true, - "peer": true, - "requires": { - "@storybook/client-logger": "7.5.3", - "@storybook/core-events": "7.5.3", - "@storybook/global": "^5.0.0", - "qs": "^6.10.0", - "telejson": "^7.2.0", - "tiny-invariant": "^1.3.1" - } - }, "@storybook/cli": { "version": "7.6.0", "resolved": "https://registry.npmjs.org/@storybook/cli/-/cli-7.6.0.tgz", @@ -29622,16 +29444,6 @@ } } }, - "@storybook/client-logger": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-7.5.3.tgz", - "integrity": "sha512-vUFYALypjix5FoJ5M/XUP6KmyTnQJNW1poHdW7WXUVSg+lBM6E5eAtjTm0hdxNNDH8KSrdy24nCLra5h0X0BWg==", - "dev": true, - "peer": true, - "requires": { - "@storybook/global": "^5.0.0" - } - }, "@storybook/codemod": { "version": "7.6.0", "resolved": "https://registry.npmjs.org/@storybook/codemod/-/codemod-7.6.0.tgz", @@ -29700,25 +29512,6 @@ } } }, - "@storybook/components": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/@storybook/components/-/components-7.5.3.tgz", - "integrity": "sha512-M3+cjvEsDGLUx8RvK5wyF6/13LNlUnKbMgiDE8Sxk/v/WPpyhOAIh/B8VmrU1psahS61Jd4MTkFmLf1cWau1vw==", - "dev": true, - "peer": true, - "requires": { - "@radix-ui/react-select": "^1.2.2", - "@radix-ui/react-toolbar": "^1.0.4", - "@storybook/client-logger": "7.5.3", - "@storybook/csf": "^0.1.0", - "@storybook/global": "^5.0.0", - "@storybook/theming": "7.5.3", - "@storybook/types": "7.5.3", - "memoizerific": "^1.11.3", - "use-resize-observer": "^9.1.0", - "util-deprecate": "^1.0.2" - } - }, "@storybook/core-common": { "version": "7.6.0", "resolved": "https://registry.npmjs.org/@storybook/core-common/-/core-common-7.6.0.tgz", @@ -29839,16 +29632,6 @@ } } }, - "@storybook/core-events": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-7.5.3.tgz", - "integrity": "sha512-DFOpyQ22JD5C1oeOFzL8wlqSWZzrqgDfDbUGP8xdO4wJu+FVTxnnWN6ZYLdTPB1u27DOhd7TzjQMfLDHLu7kbQ==", - "dev": true, - "peer": true, - "requires": { - "ts-dedent": "^2.0.0" - } - }, "@storybook/core-server": { "version": "7.6.0", "resolved": "https://registry.npmjs.org/@storybook/core-server/-/core-server-7.6.0.tgz", @@ -30263,59 +30046,6 @@ "integrity": "sha512-HJ1DCCf3GT+irAFCZg9WsPcGwSZlDyQiJHsaqxFVzuoPnz2lx10eHkXTnKa3t8x6hJeWK9BFHVyOXEFUV78ryg==", "dev": true }, - "@storybook/manager-api": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/@storybook/manager-api/-/manager-api-7.5.3.tgz", - "integrity": "sha512-d8mVLr/5BEG4bAS2ZeqYTy/aX4jPEpZHdcLaWoB4mAM+PAL9wcWsirUyApKtDVYLITJf/hd8bb2Dm2ok6E45gA==", - "dev": true, - "peer": true, - "requires": { - "@storybook/channels": "7.5.3", - "@storybook/client-logger": "7.5.3", - "@storybook/core-events": "7.5.3", - "@storybook/csf": "^0.1.0", - "@storybook/global": "^5.0.0", - "@storybook/router": "7.5.3", - "@storybook/theming": "7.5.3", - "@storybook/types": "7.5.3", - "dequal": "^2.0.2", - "lodash": "^4.17.21", - "memoizerific": "^1.11.3", - "semver": "^7.3.7", - "store2": "^2.14.2", - "telejson": "^7.2.0", - "ts-dedent": "^2.0.0" - }, - "dependencies": { - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "peer": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "dev": true, - "peer": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "peer": true - } - } - }, "@storybook/mdx2-csf": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@storybook/mdx2-csf/-/mdx2-csf-1.1.0.tgz", @@ -30709,18 +30439,6 @@ "dev": true, "requires": {} }, - "@storybook/router": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/@storybook/router/-/router-7.5.3.tgz", - "integrity": "sha512-/iNYCFore7R5n6eFHbBYoB0P2/sybTVpA+uXTNUd3UEt7Ro6CEslTaFTEiH2RVQwOkceBp/NpyWon74xZuXhMg==", - "dev": true, - "peer": true, - "requires": { - "@storybook/client-logger": "7.5.3", - "memoizerific": "^1.11.3", - "qs": "^6.10.0" - } - }, "@storybook/telemetry": { "version": "7.6.0", "resolved": "https://registry.npmjs.org/@storybook/telemetry/-/telemetry-7.6.0.tgz", @@ -30782,32 +30500,6 @@ } } }, - "@storybook/theming": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-7.5.3.tgz", - "integrity": "sha512-Cjmthe1MAk0z4RKCZ7m72gAD8YD0zTAH97z5ryM1Qv84QXjiCQ143fGOmYz1xEQdNFpOThPcwW6FEccLHTkVcg==", - "dev": true, - "peer": true, - "requires": { - "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0", - "@storybook/client-logger": "7.5.3", - "@storybook/global": "^5.0.0", - "memoizerific": "^1.11.3" - } - }, - "@storybook/types": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/@storybook/types/-/types-7.5.3.tgz", - "integrity": "sha512-iu5W0Kdd6nysN5CPkY4GRl+0BpxRTdSfBIJak7mb6xCIHSB5t1tw4BOuqMQ5EgpikRY3MWJ4gY647QkWBX3MNQ==", - "dev": true, - "peer": true, - "requires": { - "@storybook/channels": "7.5.3", - "@types/babel__core": "^7.0.0", - "@types/express": "^4.7.0", - "file-system-cache": "2.3.0" - } - }, "@swc/core": { "version": "1.3.99", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.99.tgz", @@ -31039,9 +30731,9 @@ "dev": true }, "@trussworks/react-uswds": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@trussworks/react-uswds/-/react-uswds-6.0.0.tgz", - "integrity": "sha512-kHF6u6uswlexw/1OKZcY6PBmuo3gcNltby2hERqpV9iGYnO2zxMtvfxuUFN4BsyLszTr1difJevvn/8//D2Faw==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@trussworks/react-uswds/-/react-uswds-6.1.0.tgz", + "integrity": "sha512-UyS+quISxEAXY7nrYonSkQYLJv4xR8jPYOaGFm4YzgOo3TaopJ30Laf4hIlfjtazDIZrNbaRTfBu/fArQv6PPQ==", "requires": {} }, "@types/aria-query": { @@ -31214,15 +30906,6 @@ "@types/node": "*" } }, - "@types/hoist-non-react-statics": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", - "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", - "requires": { - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0" - } - }, "@types/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", @@ -31391,7 +31074,8 @@ "@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", + "dev": true }, "@types/qs": { "version": "6.9.7", @@ -31409,6 +31093,7 @@ "version": "18.2.39", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.39.tgz", "integrity": "sha512-Oiw+ppED6IremMInLV4HXGbfbG6GyziY3kqAwJYOR0PNbkYDmLWQA3a95EhdSmamsvbkJN96ZNN+YD+fGjzSBA==", + "dev": true, "requires": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -31433,7 +31118,8 @@ "@types/scheduler": { "version": "0.16.3", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", - "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==" + "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", + "dev": true }, "@types/semver": { "version": "7.5.0", @@ -33608,11 +33294,6 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "dev": true }, - "core-js": { - "version": "3.31.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.31.0.tgz", - "integrity": "sha512-NIp2TQSGfR6ba5aalZD+ZQ1fSxGhDo/s1w0nx3RYzf2pnJxt7YynxFlFScP6eV7+GZsKO95NSjGxyJsU3DZgeQ==" - }, "core-js-compat": { "version": "3.33.3", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.33.3.tgz", @@ -33743,15 +33424,6 @@ } } }, - "cross-fetch": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", - "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", - "dev": true, - "requires": { - "node-fetch": "^2.6.12" - } - }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -33920,7 +33592,8 @@ "csstype": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", + "dev": true }, "damerau-levenshtein": { "version": "1.0.8", @@ -35163,6 +34836,15 @@ "@typescript-eslint/utils": "^5.58.0" } }, + "eslint-plugin-you-dont-need-lodash-underscore": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-you-dont-need-lodash-underscore/-/eslint-plugin-you-dont-need-lodash-underscore-6.13.0.tgz", + "integrity": "sha512-6FkFLp/R/QlgfJl5NrxkIXMQ36jMVLczkWDZJvMd7/wr/M3K0DS7mtX7plZ3giTDcbDD7VBfNYUfUVaBCZOXKA==", + "dev": true, + "requires": { + "kebab-case": "^1.0.0" + } + }, "eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -36245,14 +35927,6 @@ "minimalistic-crypto-utils": "^1.0.1" } }, - "hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "requires": { - "react-is": "^16.7.0" - } - }, "hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", @@ -36295,14 +35969,6 @@ "terser": "^5.10.0" } }, - "html-parse-stringify": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", - "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", - "requires": { - "void-elements": "3.1.0" - } - }, "html-tags": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", @@ -36380,46 +36046,6 @@ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true }, - "i18next": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.6.0.tgz", - "integrity": "sha512-z0Cxr0MGkt+kli306WS4nNNM++9cgt2b2VCMprY92j+AIab/oclgPxdwtTZVLP1zn5t5uo8M6uLsZmYrcjr3HA==", - "requires": { - "@babel/runtime": "^7.22.5" - } - }, - "i18next-browser-languagedetector": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.1.0.tgz", - "integrity": "sha512-cr2k7u1XJJ4HTOjM9GyOMtbOA47RtUoWRAtt52z43r3AoMs2StYKyjS3URPhzHaf+mn10hY9dZWamga5WPQjhA==", - "dev": true, - "requires": { - "@babel/runtime": "^7.19.4" - } - }, - "i18next-fs-backend": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/i18next-fs-backend/-/i18next-fs-backend-2.3.0.tgz", - "integrity": "sha512-N0SS2WojoVIh2x/QkajSps8RPKzXqryZsQh12VoFY4cLZgkD+62EPY2fY+ZjkNADu8xA5I5EadQQXa8TXBKN3w==" - }, - "i18next-http-backend": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-2.3.1.tgz", - "integrity": "sha512-jnagFs5cnq4ryb+g92Hex4tB5kj3tWmiRWx8gHMCcE/PEgV1fjH5rC7xyJmPSgyb9r2xgcP8rvZxPKgsmvMqTw==", - "dev": true, - "requires": { - "cross-fetch": "4.0.0" - } - }, - "i18next-resources-for-ts": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/i18next-resources-for-ts/-/i18next-resources-for-ts-1.3.3.tgz", - "integrity": "sha512-fWe56BYUS7MIx0h0uxD+ydvNhELPQeOBSdNA/uaqlHbkgSqiYUvWXEGQ8pMZglIZ695qQn12X6skU/XTYG+mRw==", - "dev": true, - "requires": { - "@babel/runtime": "^7.22.15" - } - }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -36584,6 +36210,36 @@ "side-channel": "^1.0.4" } }, + "intl-messageformat": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-9.13.0.tgz", + "integrity": "sha512-7sGC7QnSQGa5LZP7bXLDhVDtQOeKGeBFGHF2Y8LVBwYZoQZCgWeKoPGTa5GMG8g/TzDgeXuYJQis7Ggiw2xTOw==", + "requires": { + "@formatjs/ecma402-abstract": "1.11.4", + "@formatjs/fast-memoize": "1.2.1", + "@formatjs/icu-messageformat-parser": "2.1.0", + "tslib": "^2.1.0" + }, + "dependencies": { + "@formatjs/ecma402-abstract": { + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz", + "integrity": "sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==", + "requires": { + "@formatjs/intl-localematcher": "0.2.25", + "tslib": "^2.1.0" + } + }, + "@formatjs/intl-localematcher": { + "version": "0.2.25", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.25.tgz", + "integrity": "sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==", + "requires": { + "tslib": "^2.1.0" + } + } + } + }, "invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -38658,6 +38314,12 @@ "object.assign": "^4.1.3" } }, + "kebab-case": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/kebab-case/-/kebab-case-1.0.2.tgz", + "integrity": "sha512-7n6wXq4gNgBELfDCpzKc+mRrZFs7D+wgfF5WRFLNAr4DA/qtr9Js8uOAVAfHhuLMfAcQ0pRKqbpjx+TcJVdE1Q==", + "dev": true + }, "keyboardevent-key-polyfill": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/keyboardevent-key-polyfill/-/keyboardevent-key-polyfill-1.1.0.tgz", @@ -38758,8 +38420,7 @@ "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "lodash.debounce": { "version": "4.0.8", @@ -39144,8 +38805,7 @@ "negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" }, "neo-async": { "version": "2.6.2", @@ -39176,16 +38836,14 @@ "watchpack": "2.4.0" } }, - "next-i18next": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/next-i18next/-/next-i18next-15.0.0.tgz", - "integrity": "sha512-9iGEU4dt1YCC5CXh6H8YHmDpmeWKjxES6XfoABxy9mmfaLLJcqS92F56ZKmVuZUPXEOLtgY/JtsnxsHYom9J4g==", + "next-intl": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-3.2.1.tgz", + "integrity": "sha512-Tfin94t/d9tNh+EPiWLPto2WsLC2/AFvBVi+OB8568l3TvflaVBbDWFn1a08sbEsjz7OltFKmS565+NIsXBi1w==", "requires": { - "@babel/runtime": "^7.23.2", - "@types/hoist-non-react-statics": "^3.3.4", - "core-js": "^3", - "hoist-non-react-statics": "^3.3.2", - "i18next-fs-backend": "^2.2.0" + "@formatjs/intl-localematcher": "^0.2.32", + "negotiator": "^0.6.3", + "use-intl": "^3.2.1" } }, "no-case": { @@ -40847,19 +40505,11 @@ } } }, - "react-i18next": { - "version": "13.3.1", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-13.3.1.tgz", - "integrity": "sha512-JAtYREK879JXaN9GdzfBI4yJeo/XyLeXWUsRABvYXiFUakhZJ40l+kaTo+i+A/3cKIED41kS/HAbZ5BzFtq/Og==", - "requires": { - "@babel/runtime": "^7.22.5", - "html-parse-stringify": "^3.0.1" - } - }, "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true }, "react-refresh": { "version": "0.14.0", @@ -41063,7 +40713,8 @@ "regenerator-runtime": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", + "dev": true }, "regenerator-transform": { "version": "0.15.2", @@ -41807,22 +41458,6 @@ "@storybook/cli": "7.6.0" } }, - "storybook-i18n": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/storybook-i18n/-/storybook-i18n-2.0.13.tgz", - "integrity": "sha512-p0VPL5QiHdeS3W9BvV7UnuTKw7Mj1HWLW67LK0EOoh5fpSQIchu7byfrUUe1RbCF1gT0gOOhdNuTSXMoVVoTDw==", - "dev": true, - "requires": {} - }, - "storybook-react-i18next": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/storybook-react-i18next/-/storybook-react-i18next-2.0.9.tgz", - "integrity": "sha512-GFTOrYwOWShLqWNuTesPNhC79P3OHw1jkZ4gU3R50yTD2MUclF5DHLnuKeVfKZ323iV+I9fxLxuLIVHWVDJgXA==", - "dev": true, - "requires": { - "storybook-i18n": "2.0.13" - } - }, "stream-shift": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", @@ -42852,6 +42487,15 @@ "tslib": "^2.0.0" } }, + "use-intl": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-3.2.1.tgz", + "integrity": "sha512-QPbeKNBD43ZhSb4eVix2VrsIeF82im6j7izyh2sgQMSRwEYlARwzYHAJcuoD14E6Ko+wjVQBgYXh364vlm/Pzg==", + "requires": { + "@formatjs/ecma402-abstract": "^1.11.4", + "intl-messageformat": "^9.3.18" + } + }, "use-resize-observer": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/use-resize-observer/-/use-resize-observer-9.1.0.tgz", @@ -42949,11 +42593,6 @@ "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", "dev": true }, - "void-elements": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", - "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==" - }, "w3c-xmlserializer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", diff --git a/app/package.json b/app/package.json index 601671a0..8749c949 100644 --- a/app/package.json +++ b/app/package.json @@ -19,11 +19,11 @@ "ts:check": "tsc --noEmit" }, "dependencies": { - "@trussworks/react-uswds": "^6.0.0", + "@trussworks/react-uswds": "^6.1.0", "@uswds/uswds": "3.7.0", "lodash": "^4.17.21", - "next": "^14.0.0", - "next-intl": "^3.2.0", + "next": "^14.0.3", + "next-intl": "^3.2.1", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/app/src/app/[locale]/layout.tsx b/app/src/app/[locale]/layout.tsx index d7137452..61ae87fb 100644 --- a/app/src/app/[locale]/layout.tsx +++ b/app/src/app/[locale]/layout.tsx @@ -46,6 +46,7 @@ export default function Layout({ children, params }: LayoutProps) { language={params.locale.match(/^es-?/) ? "spanish" : "english"} />
    diff --git a/app/src/i18n/config.ts b/app/src/i18n/config.ts index 09ef34bd..f9df7369 100644 --- a/app/src/i18n/config.ts +++ b/app/src/i18n/config.ts @@ -2,6 +2,8 @@ import { merge } from "lodash"; import { getRequestConfig } from "next-intl/server"; +import { formats } from "./formats"; + type RequestConfig = Awaited< ReturnType[0]> >; @@ -14,18 +16,6 @@ export const locales = ["en-US", "es-US"] as const; export type SupportedLocale = (typeof locales)[number]; export const defaultLocale: SupportedLocale = "en-US"; -/** - * Define the default formatting for date, time, and numbers. - * @see https://next-intl-docs.vercel.app/docs/usage/configuration#formats - */ -const formats: RequestConfig["formats"] = { - number: { - currency: { - currency: "USD", - }, - }, -}; - /** * Get the entire locale messages object for the given locale. If any * translations are missing from the current locale, the missing key will diff --git a/app/src/i18n/formats.ts b/app/src/i18n/formats.ts new file mode 100644 index 00000000..e8ad9a65 --- /dev/null +++ b/app/src/i18n/formats.ts @@ -0,0 +1,17 @@ +import type { getRequestConfig } from "next-intl/server"; + +type RequestConfig = Awaited< + ReturnType[0]> +>; + +/** + * Define the default formatting for date, time, and numbers. + * @see https://next-intl-docs.vercel.app/docs/usage/configuration#formats + */ +export const formats: RequestConfig["formats"] = { + number: { + currency: { + currency: "USD", + }, + }, +}; diff --git a/app/tests/app/layout.test.tsx b/app/tests/app/layout.test.tsx new file mode 100644 index 00000000..55eb228d --- /dev/null +++ b/app/tests/app/layout.test.tsx @@ -0,0 +1,50 @@ +import { axe } from "jest-axe"; +import Layout from "src/app/[locale]/layout"; +import { render, screen } from "tests/test-utils"; + +describe("Layout", () => { + beforeEach(() => { + // Testing Library appends the component contents to a
    + // which is problematic for Layout since that causes an error + // to be logged about being a descendant of
    . + // https://github.com/testing-library/react-testing-library/issues/1250 + // For now we hide the error message. + jest.spyOn(console, "error").mockImplementation((e) => { + if ( + e instanceof Error && + typeof e.message === "string" && + e.message.includes("validateDOMNesting") + ) { + console.warn(e); + } + }); + }); + + it("renders children in main section", () => { + render( + +

    child

    +
    + ); + + const header = screen.getByRole("heading", { name: /child/i, level: 1 }); + + expect(header).toBeInTheDocument(); + }); + + it("passes accessibility scan", async () => { + const { container } = render( + +

    child

    +
    + ); + const results = await axe(container, { + rules: { + // gets rendered by Next.js when using generateMetadata, which happens outside of the component + "document-title": { enabled: false }, + }, + }); + + expect(results).toHaveNoViolations(); + }); +}); diff --git a/app/tests/pages/index.test.tsx b/app/tests/app/page.test.tsx similarity index 68% rename from app/tests/pages/index.test.tsx rename to app/tests/app/page.test.tsx index 2d2ef602..d2868ffa 100644 --- a/app/tests/pages/index.test.tsx +++ b/app/tests/app/page.test.tsx @@ -1,21 +1,22 @@ -import { render, screen } from "@testing-library/react"; import { axe } from "jest-axe"; -import Index from "src/pages/index"; +import Page from "src/app/[locale]/page"; +import { render, screen } from "tests/test-utils"; -describe("Index", () => { +describe("Homepage", () => { // Demonstration of rendering translated text, and asserting the presence of a dynamic value. // You can delete this test for your own project. it("renders link to Next.js docs", () => { - render(<Index />); + render(<Page />); const link = screen.getByRole("link", { name: /next\.js/i }); expect(link).toBeInTheDocument(); expect(link).toHaveAttribute("href", "https://nextjs.org/docs"); + expect(screen.getByText(/\$1,234/)).toBeInTheDocument(); }); it("passes accessibility scan", async () => { - const { container } = render(<Index />); + const { container } = render(<Page />); const results = await axe(container); expect(results).toHaveNoViolations(); diff --git a/app/tests/components/Header.test.tsx b/app/tests/components/Header.test.tsx index 8ca91fc5..b1ddb793 100644 --- a/app/tests/components/Header.test.tsx +++ b/app/tests/components/Header.test.tsx @@ -1,5 +1,5 @@ -import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; +import { render, screen } from "tests/test-utils"; import Header from "src/components/Header"; @@ -7,7 +7,7 @@ describe("Header", () => { it("toggles the mobile nav menu", async () => { render(<Header />); - const menuButton = screen.getByRole("button", { name: /menu/i }); + const menuButton = screen.getByRole("button", { name: "Menu" }); expect(menuButton).toBeInTheDocument(); diff --git a/app/tests/components/Layout.test.tsx b/app/tests/components/Layout.test.tsx deleted file mode 100644 index bae699f8..00000000 --- a/app/tests/components/Layout.test.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { render, screen } from "@testing-library/react"; -import { axe } from "jest-axe"; - -import Layout from "src/components/Layout"; - -describe("Layout", () => { - it("renders children in main section", () => { - render( - <Layout> - <h1>child</h1> - </Layout> - ); - - const header = screen.getByRole("heading", { name: /child/i, level: 1 }); - - expect(header).toBeInTheDocument(); - }); - - it("passes accessibility scan", async () => { - const { container } = render( - <Layout> - <h1>child</h1> - </Layout> - ); - const results = await axe(container); - - expect(results).toHaveNoViolations(); - }); -}); diff --git a/app/tests/jest-i18n.ts b/app/tests/jest-i18n.ts deleted file mode 100644 index a8df21d8..00000000 --- a/app/tests/jest-i18n.ts +++ /dev/null @@ -1,3 +0,0 @@ -/** - * @file Setup internationalization for tests so snapshots and queries reference the correct translations - */ diff --git a/app/tests/test-utils.tsx b/app/tests/test-utils.tsx new file mode 100644 index 00000000..460a8ae4 --- /dev/null +++ b/app/tests/test-utils.tsx @@ -0,0 +1,38 @@ +/** + * @file Exposes all of @testing-library/react, with one exception: + * the exported render function is wrapped in a custom wrapper so + * tests render within a global context that includes i18n content + * @see https://testing-library.com/docs/react-testing-library/setup#custom-render + */ +import { render as _render, RenderOptions } from "@testing-library/react"; +import { formats } from "src/i18n/formats"; +import { messages } from "src/i18n/messages/en-US"; + +import { NextIntlClientProvider } from "next-intl"; + +/** + * Wrapper component that provides global context to all tests. Notably, + * it allows our tests to render content when using i18n translation methods. + */ +const GlobalProviders = ({ children }: { children: React.ReactNode }) => { + return ( + <NextIntlClientProvider + locale="en-US" + messages={messages} + formats={formats} + > + {children} + </NextIntlClientProvider> + ); +}; + +// 1. Export everything in "@testing-library/react" as-is +export * from "@testing-library/react"; + +// 2. Then override the "@testing-library/react" render method +export function render( + ui: React.ReactElement, + options: Omit<RenderOptions, "wrapper"> = {} +) { + return _render(ui, { wrapper: GlobalProviders, ...options }); +} From d6de2271914d8d84edcade48808cc70753ae3ce5 Mon Sep 17 00:00:00 2001 From: Sawyer <git@sawyerh.com> Date: Mon, 4 Dec 2023 15:53:52 -0800 Subject: [PATCH 09/32] Docs progress --- app/README.md | 12 +++++------- app/src/middleware.ts | 12 +++++++++++- docs/internationalization.md | 2 ++ 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/app/README.md b/app/README.md index c7c268e6..d706991c 100644 --- a/app/README.md +++ b/app/README.md @@ -10,23 +10,21 @@ ``` ├── .storybook # Storybook configuration ├── public # Static assets -│ └── locales # Internationalized content ├── src # Source code +│ ├── app # Routes, layouts, and loading screens │ ├── components # Reusable UI components -│ ├── pages # Page routes and data fetching -│   │ ├── api # API routes (optional) -│   │ └── _app.tsx # Global entry point +│ ├── i18n # Internationalization config & content │ ├── styles # Sass & design system settings -│ └── types # TypeScript type declarations +│ └── types # Global type declarations ├── stories # Storybook pages └── tests ``` ## 💻 Development -[Next.js](https://nextjs.org/docs) provides the React framework for building the web application. Pages are defined in the `pages/` directory. Pages are automatically routed based on the file name. For example, `pages/index.tsx` is the home page. +[Next.js](https://nextjs.org/docs) provides the React framework for building the web application. Pages are defined in the `app/` directory. Pages are automatically routed based on the file name. For example, `app/[locale]/about/page.tsx` would render at `/about` (for English) or `/es-US/about` (for Spanish). -Files in the `pages/api` are treated as [API routes](https://nextjs.org/docs/api-routes/introduction). An example can be accessed at [localhost:3000/api/hello](http://localhost:3000/api/hello) when running locally. +Files in the `pages/api` are treated as [API routes](https://nextjs.org/docs/api-routes/introduction). [**Learn more about developing Next.js applications** ↗️](https://nextjs.org/docs) diff --git a/app/src/middleware.ts b/app/src/middleware.ts index 90049eee..18d43c06 100644 --- a/app/src/middleware.ts +++ b/app/src/middleware.ts @@ -1,3 +1,9 @@ +/** + * Middleware allows you to run code before a request is completed. Then, based on the + * incoming request, you can modify the response by rewriting, redirecting, modifying + * the request or response headers, or responding directly. + * @see https://nextjs.org/docs/app/building-your-application/routing/middleware + */ import createIntlMiddleware from "next-intl/middleware"; import { NextRequest } from "next/server"; @@ -8,10 +14,14 @@ export const config = { matcher: ["/((?!api|_next|.*\\..*).*)"], }; +/** + * Detect the user's preferred language and redirect to a localized route + * if the preferred language isn't the current locale. + */ const i18nMiddleware = createIntlMiddleware({ locales, defaultLocale, - // Don't prefix the URLs when the locale is the default locale + // Don't prefix the URL with the locale when the locale is the default locale (i.e. "en-US") localePrefix: "as-needed", }); diff --git a/docs/internationalization.md b/docs/internationalization.md index 80873020..fa3525bb 100644 --- a/docs/internationalization.md +++ b/docs/internationalization.md @@ -1,3 +1,5 @@ +// TODO: Update for next-intl + # Internationalization (i18n) - [I18next](https://www.i18next.com/) is used for internationalization. From fff60ce418e52545b78237e9c46b753dd24dbeb4 Mon Sep 17 00:00:00 2001 From: Sawyer <git@sawyerh.com> Date: Wed, 6 Dec 2023 15:16:54 -0800 Subject: [PATCH 10/32] Setup middleware --- app/next.config.js | 4 ++- app/src/i18n/config.ts | 49 -------------------------------- app/src/i18n/formats.ts | 17 ----------- app/src/i18n/index.ts | 2 +- app/src/i18n/next-intl-config.ts | 14 +++++++++ app/src/middleware.ts | 2 +- 6 files changed, 19 insertions(+), 69 deletions(-) delete mode 100644 app/src/i18n/config.ts delete mode 100644 app/src/i18n/formats.ts create mode 100644 app/src/i18n/next-intl-config.ts diff --git a/app/next.config.js b/app/next.config.js index 2b109157..c98aabc1 100644 --- a/app/next.config.js +++ b/app/next.config.js @@ -1,5 +1,7 @@ // @ts-check -const withNextIntl = require("next-intl/plugin")("./src/i18n/index.ts"); +const withNextIntl = require("next-intl/plugin")( + "./src/i18n/next-intl-config.ts" +); const sassOptions = require("./scripts/sassOptions"); /** diff --git a/app/src/i18n/config.ts b/app/src/i18n/config.ts deleted file mode 100644 index f9df7369..00000000 --- a/app/src/i18n/config.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { merge } from "lodash"; - -import { getRequestConfig } from "next-intl/server"; - -import { formats } from "./formats"; - -type RequestConfig = Awaited< - ReturnType<Parameters<typeof getRequestConfig>[0]> ->; - -/** - * List of languages supported by the application. Other tools (Storybook, tests) reference this. - * These must be BCP47 language tags: https://en.wikipedia.org/wiki/IETF_language_tag#List_of_common_primary_language_subtags - */ -export const locales = ["en-US", "es-US"] as const; -export type SupportedLocale = (typeof locales)[number]; -export const defaultLocale: SupportedLocale = "en-US"; - -/** - * Get the entire locale messages object for the given locale. If any - * translations are missing from the current locale, the missing key will - * fallback to the default locale - */ -async function getLocaleMessages(locale: string) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - let { messages } = await import(`./messages/${locale}`); - - if (locale !== defaultLocale) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const { messages: fallbackMessages } = await import( - `./messages/${defaultLocale}` - ); - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - messages = merge({}, fallbackMessages, messages); - } - - return messages as RequestConfig["messages"]; -} - -/** - * The next-intl config. This method is used behind the scenes by `next-intl/plugin` - * when its called in next.config.js. - */ -export default getRequestConfig(async ({ locale }) => { - return { - formats, - messages: await getLocaleMessages(locale), - }; -}); diff --git a/app/src/i18n/formats.ts b/app/src/i18n/formats.ts deleted file mode 100644 index e8ad9a65..00000000 --- a/app/src/i18n/formats.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { getRequestConfig } from "next-intl/server"; - -type RequestConfig = Awaited< - ReturnType<Parameters<typeof getRequestConfig>[0]> ->; - -/** - * Define the default formatting for date, time, and numbers. - * @see https://next-intl-docs.vercel.app/docs/usage/configuration#formats - */ -export const formats: RequestConfig["formats"] = { - number: { - currency: { - currency: "USD", - }, - }, -}; diff --git a/app/src/i18n/index.ts b/app/src/i18n/index.ts index 70a6f178..abd8e8c5 100644 --- a/app/src/i18n/index.ts +++ b/app/src/i18n/index.ts @@ -1,6 +1,6 @@ import { merge } from "lodash"; -import { getRequestConfig } from "next-intl/server"; +import type { getRequestConfig } from "next-intl/server"; import { messages as enUs } from "./messages/en-US"; import { messages as esUs } from "./messages/es-US"; diff --git a/app/src/i18n/next-intl-config.ts b/app/src/i18n/next-intl-config.ts new file mode 100644 index 00000000..0eabacc6 --- /dev/null +++ b/app/src/i18n/next-intl-config.ts @@ -0,0 +1,14 @@ +import { formats, getLocaleMessages } from "src/i18n"; + +import { getRequestConfig } from "next-intl/server"; + +/** + * The next-intl config. This method is used behind the scenes by `next-intl/plugin` + * when its called in next.config.js. + */ +export default getRequestConfig(({ locale }) => { + return { + formats, + messages: getLocaleMessages(locale), + }; +}); diff --git a/app/src/middleware.ts b/app/src/middleware.ts index 18d43c06..aaeb2161 100644 --- a/app/src/middleware.ts +++ b/app/src/middleware.ts @@ -7,7 +7,7 @@ import createIntlMiddleware from "next-intl/middleware"; import { NextRequest } from "next/server"; -import { defaultLocale, locales } from "./i18n/config"; +import { defaultLocale, locales } from "./i18n"; // Don't run middleware on API routes or Next.js build output export const config = { From b82e14d5cd3058a82d89957b705000f0adefc04c Mon Sep 17 00:00:00 2001 From: Sawyer <git@sawyerh.com> Date: Wed, 6 Dec 2023 15:53:43 -0800 Subject: [PATCH 11/32] Fix layout rendering in Storybook --- app/src/app/[locale]/layout.tsx | 45 +++----------------- app/src/components/Layout.tsx | 47 +++++++++++++++++++++ app/stories/components/Layout.stories.tsx | 3 ++ app/tests/app/layout.test.tsx | 50 ----------------------- app/tests/components/Layout.test.tsx | 29 +++++++++++++ 5 files changed, 84 insertions(+), 90 deletions(-) create mode 100644 app/src/components/Layout.tsx delete mode 100644 app/tests/app/layout.test.tsx create mode 100644 app/tests/components/Layout.test.tsx diff --git a/app/src/app/[locale]/layout.tsx b/app/src/app/[locale]/layout.tsx index 61ae87fb..4a072704 100644 --- a/app/src/app/[locale]/layout.tsx +++ b/app/src/app/[locale]/layout.tsx @@ -4,22 +4,11 @@ */ import { Metadata } from "next"; -import { - NextIntlClientProvider, - useMessages, - useTranslations, -} from "next-intl"; -import { GovBanner, Grid, GridContainer } from "@trussworks/react-uswds"; - -import Footer from "src/components/Footer"; -import Header from "src/components/Header"; +import Layout from "src/components/Layout"; import "src/styles/styles.scss"; -import { pick } from "lodash"; - export const metadata: Metadata = { - title: "Home", icons: [`${process.env.NEXT_PUBLIC_BASE_PATH ?? ""}/img/logo.svg`], }; @@ -30,37 +19,13 @@ interface LayoutProps { }; } -export default function Layout({ children, params }: LayoutProps) { - const t = useTranslations("components.Layout"); - const messages = useMessages(); - +export default function RootLayout({ children, params }: LayoutProps) { return ( <html lang={params.locale}> <body> - {/* Flex and min height sticks the footer to the bottom of the page */} - <div className="display-flex flex-column minh-viewport"> - <a className="usa-skipnav" href="#main-content"> - {t("skip_to_main")} - </a> - <GovBanner - language={params.locale.match(/^es-?/) ? "spanish" : "english"} - /> - <NextIntlClientProvider - locale={params.locale} - messages={pick(messages, "components.Header")} - > - <Header /> - </NextIntlClientProvider> - {/* Flex pushes the footer to the bottom of the page */} - <main id="main-content" className="usa-section flex-fill"> - <GridContainer> - <Grid row> - <Grid col>{children}</Grid> - </Grid> - </GridContainer> - </main> - <Footer /> - </div> + {/* Separate layout component for the inner-body UI elements since Storybook + and tests trip over the fact that this file renders an <html> tag */} + <Layout locale={params.locale}>{children}</Layout> </body> </html> ); diff --git a/app/src/components/Layout.tsx b/app/src/components/Layout.tsx new file mode 100644 index 00000000..60892256 --- /dev/null +++ b/app/src/components/Layout.tsx @@ -0,0 +1,47 @@ +import { pick } from "lodash"; + +import { + NextIntlClientProvider, + useMessages, + useTranslations, +} from "next-intl"; +import { GovBanner, Grid, GridContainer } from "@trussworks/react-uswds"; + +import Footer from "./Footer"; +import Header from "./Header"; + +type Props = { + children: React.ReactNode; + locale?: string; +}; + +const Layout = ({ children, locale }: Props) => { + const t = useTranslations("components.Layout"); + const messages = useMessages(); + + return ( + // Stick the footer to the bottom of the page + <div className="display-flex flex-column minh-viewport"> + <a className="usa-skipnav" href="#main-content"> + {t("skip_to_main")} + </a> + <GovBanner language={locale?.match(/^es-?/) ? "spanish" : "english"} /> + <NextIntlClientProvider + locale={locale} + messages={pick(messages, "components.Header")} + > + <Header /> + </NextIntlClientProvider> + <main id="main-content" className="usa-section"> + <GridContainer> + <Grid row> + <Grid col>{children}</Grid> + </Grid> + </GridContainer> + </main> + <Footer /> + </div> + ); +}; + +export default Layout; diff --git a/app/stories/components/Layout.stories.tsx b/app/stories/components/Layout.stories.tsx index 3b0db916..cbe705bb 100644 --- a/app/stories/components/Layout.stories.tsx +++ b/app/stories/components/Layout.stories.tsx @@ -1,4 +1,5 @@ import { Meta } from "@storybook/react"; +import { defaultLocale } from "src/i18n/config"; import Layout from "src/components/Layout"; @@ -14,12 +15,14 @@ export default meta; */ export const Preview1 = { args: { + locale: defaultLocale, children: <h1>Page contents go here</h1>, }, }; export const Preview2 = { args: { + ...Preview1.args, children: ( <> <h1>Another demo</h1> diff --git a/app/tests/app/layout.test.tsx b/app/tests/app/layout.test.tsx deleted file mode 100644 index 55eb228d..00000000 --- a/app/tests/app/layout.test.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { axe } from "jest-axe"; -import Layout from "src/app/[locale]/layout"; -import { render, screen } from "tests/test-utils"; - -describe("Layout", () => { - beforeEach(() => { - // Testing Library appends the component contents to a <div> - // which is problematic for Layout since that causes an error - // to be logged about <html> being a descendant of <div>. - // https://github.com/testing-library/react-testing-library/issues/1250 - // For now we hide the error message. - jest.spyOn(console, "error").mockImplementation((e) => { - if ( - e instanceof Error && - typeof e.message === "string" && - e.message.includes("validateDOMNesting") - ) { - console.warn(e); - } - }); - }); - - it("renders children in main section", () => { - render( - <Layout params={{ locale: "en-US" }}> - <h1>child</h1> - </Layout> - ); - - const header = screen.getByRole("heading", { name: /child/i, level: 1 }); - - expect(header).toBeInTheDocument(); - }); - - it("passes accessibility scan", async () => { - const { container } = render( - <Layout params={{ locale: "en-US" }}> - <h1>child</h1> - </Layout> - ); - const results = await axe(container, { - rules: { - // <title> gets rendered by Next.js when using generateMetadata, which happens outside of the component - "document-title": { enabled: false }, - }, - }); - - expect(results).toHaveNoViolations(); - }); -}); diff --git a/app/tests/components/Layout.test.tsx b/app/tests/components/Layout.test.tsx new file mode 100644 index 00000000..78f60164 --- /dev/null +++ b/app/tests/components/Layout.test.tsx @@ -0,0 +1,29 @@ +import { axe } from "jest-axe"; +import { render, screen } from "tests/react-utils"; + +import Layout from "src/components/Layout"; + +describe("Layout", () => { + it("renders children in main section", () => { + render( + <Layout locale="en-US"> + <h1>child</h1> + </Layout> + ); + + const header = screen.getByRole("heading", { name: /child/i, level: 1 }); + + expect(header).toBeInTheDocument(); + }); + + it("passes accessibility scan", async () => { + const { container } = render( + <Layout locale="en-US"> + <h1>child</h1> + </Layout> + ); + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); +}); From 022740f34a95357a3c6ba9f0f960ab1007fffb15 Mon Sep 17 00:00:00 2001 From: Sawyer <git@sawyerh.com> Date: Wed, 6 Dec 2023 15:54:03 -0800 Subject: [PATCH 12/32] Remove dupe files --- app/src/i18n/index.ts | 69 ---------------------------------------- app/tests/test-utils.tsx | 38 ---------------------- 2 files changed, 107 deletions(-) delete mode 100644 app/src/i18n/index.ts delete mode 100644 app/tests/test-utils.tsx diff --git a/app/src/i18n/index.ts b/app/src/i18n/index.ts deleted file mode 100644 index abd8e8c5..00000000 --- a/app/src/i18n/index.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { merge } from "lodash"; - -import type { getRequestConfig } from "next-intl/server"; - -import { messages as enUs } from "./messages/en-US"; -import { messages as esUs } from "./messages/es-US"; - -type RequestConfig = Awaited< - ReturnType<Parameters<typeof getRequestConfig>[0]> ->; -export type Messages = RequestConfig["messages"]; - -/** - * List of languages supported by the application. Other tools (Storybook, tests) reference this. - * These must be BCP47 language tags: https://en.wikipedia.org/wiki/IETF_language_tag#List_of_common_primary_language_subtags - */ -export const locales = ["en-US", "es-US"] as const; -export type Locale = (typeof locales)[number]; -export const defaultLocale: Locale = "en-US"; - -/** - * All messages for the application for each locale. - * Don't export this object!! Use `getLocaleMessages` instead, - * which handles fallbacks to the default locale when a locale - * is missing a translation. - */ -const _messages: { [locale in Locale]: Messages } = { - "en-US": enUs, - "es-US": esUs, -}; - -/** - * Define the default formatting for date, time, and numbers. - * @see https://next-intl-docs.vercel.app/docs/usage/configuration#formats - */ -export const formats: RequestConfig["formats"] = { - number: { - currency: { - currency: "USD", - }, - }, -}; - -/** - * Get the entire locale messages object for the given locale. If any - * translations are missing from the current locale, the missing key will - * fallback to the default locale - */ -export function getLocaleMessages( - requestedLocale: string = defaultLocale -): Messages { - if (requestedLocale in _messages === false) { - console.error( - "Unsupported locale was requested. Falling back to the default locale.", - { locale: requestedLocale, defaultLocale } - ); - requestedLocale = defaultLocale; - } - - const targetLocale = requestedLocale as Locale; - let messages = _messages[targetLocale]; - - if (targetLocale !== defaultLocale) { - const fallbackMessages = _messages[defaultLocale]; - messages = merge({}, fallbackMessages, messages); - } - - return messages; -} diff --git a/app/tests/test-utils.tsx b/app/tests/test-utils.tsx deleted file mode 100644 index 460a8ae4..00000000 --- a/app/tests/test-utils.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/** - * @file Exposes all of @testing-library/react, with one exception: - * the exported render function is wrapped in a custom wrapper so - * tests render within a global context that includes i18n content - * @see https://testing-library.com/docs/react-testing-library/setup#custom-render - */ -import { render as _render, RenderOptions } from "@testing-library/react"; -import { formats } from "src/i18n/formats"; -import { messages } from "src/i18n/messages/en-US"; - -import { NextIntlClientProvider } from "next-intl"; - -/** - * Wrapper component that provides global context to all tests. Notably, - * it allows our tests to render content when using i18n translation methods. - */ -const GlobalProviders = ({ children }: { children: React.ReactNode }) => { - return ( - <NextIntlClientProvider - locale="en-US" - messages={messages} - formats={formats} - > - {children} - </NextIntlClientProvider> - ); -}; - -// 1. Export everything in "@testing-library/react" as-is -export * from "@testing-library/react"; - -// 2. Then override the "@testing-library/react" render method -export function render( - ui: React.ReactElement, - options: Omit<RenderOptions, "wrapper"> = {} -) { - return _render(ui, { wrapper: GlobalProviders, ...options }); -} From 447a4c588ec9bc51753623c6e349afe7f422d927 Mon Sep 17 00:00:00 2001 From: Sawyer <git@sawyerh.com> Date: Wed, 6 Dec 2023 15:54:13 -0800 Subject: [PATCH 13/32] Fix story import --- app/stories/pages/Index.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/stories/pages/Index.stories.tsx b/app/stories/pages/Index.stories.tsx index 669c8741..4afb9c96 100644 --- a/app/stories/pages/Index.stories.tsx +++ b/app/stories/pages/Index.stories.tsx @@ -1,5 +1,5 @@ import { Meta } from "@storybook/react"; -import Index from "src/pages/index"; +import Index from "src/app/[locale]/page"; const meta: Meta<typeof Index> = { title: "Pages/Home", From 739900c17c12b93e17a56f44edba99491a2e0aa6 Mon Sep 17 00:00:00 2001 From: Sawyer <git@sawyerh.com> Date: Wed, 6 Dec 2023 15:54:23 -0800 Subject: [PATCH 14/32] Reduce diff size --- app/tests/app/page.test.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/tests/app/page.test.tsx b/app/tests/app/page.test.tsx index e7bd3514..240cd1a3 100644 --- a/app/tests/app/page.test.tsx +++ b/app/tests/app/page.test.tsx @@ -1,12 +1,12 @@ import { axe } from "jest-axe"; -import Page from "src/app/[locale]/page"; +import Index from "src/app/[locale]/page"; import { render, screen } from "tests/react-utils"; -describe("Homepage", () => { +describe("Index", () => { // Demonstration of rendering translated text, and asserting the presence of a dynamic value. // You can delete this test for your own project. it("renders link to Next.js docs", () => { - render(<Page />); + render(<Index />); const link = screen.getByRole("link", { name: /next\.js/i }); @@ -16,7 +16,7 @@ describe("Homepage", () => { }); it("passes accessibility scan", async () => { - const { container } = render(<Page />); + const { container } = render(<Index />); const results = await axe(container); expect(results).toHaveNoViolations(); From 9e20c82a67323863b9af1f23d32a832d3bf6b9c2 Mon Sep 17 00:00:00 2001 From: Sawyer <git@sawyerh.com> Date: Wed, 6 Dec 2023 15:54:42 -0800 Subject: [PATCH 15/32] Restructure i18n directory to support client components --- app/.storybook/I18nStoryWrapper.tsx | 5 +++-- app/.storybook/preview.tsx | 2 +- app/next.config.js | 4 +--- app/src/i18n/config.ts | 28 ++++++++++++++++++++++++ app/src/i18n/getMessages.ts | 34 +++++++++++++++++++++++++++++ app/src/i18n/next-intl-config.ts | 14 ------------ app/src/i18n/server.ts | 16 ++++++++++++++ app/src/middleware.ts | 2 +- app/tests/react-utils.tsx | 5 +++-- 9 files changed, 87 insertions(+), 23 deletions(-) create mode 100644 app/src/i18n/config.ts create mode 100644 app/src/i18n/getMessages.ts delete mode 100644 app/src/i18n/next-intl-config.ts create mode 100644 app/src/i18n/server.ts diff --git a/app/.storybook/I18nStoryWrapper.tsx b/app/.storybook/I18nStoryWrapper.tsx index d158ea19..e871dcd0 100644 --- a/app/.storybook/I18nStoryWrapper.tsx +++ b/app/.storybook/I18nStoryWrapper.tsx @@ -7,7 +7,8 @@ import { StoryContext } from "@storybook/react"; import { NextIntlClientProvider } from "next-intl"; import React from "react"; -import { defaultLocale, formats, getLocaleMessages } from "../src/i18n"; +import { defaultLocale, formats } from "../src/i18n/config"; +import { getMessages } from "../src/i18n/getMessages"; const I18nStoryWrapper = ( Story: React.ComponentType, @@ -19,7 +20,7 @@ const I18nStoryWrapper = ( <NextIntlClientProvider formats={formats} locale={locale} - messages={getLocaleMessages(locale)} + messages={getMessages(locale)} > <Story /> </NextIntlClientProvider> diff --git a/app/.storybook/preview.tsx b/app/.storybook/preview.tsx index c8b6da25..a2ad6c3d 100644 --- a/app/.storybook/preview.tsx +++ b/app/.storybook/preview.tsx @@ -6,7 +6,7 @@ import { Preview } from "@storybook/react"; import "../src/styles/styles.scss"; -import { defaultLocale, locales } from "../src/i18n"; +import { defaultLocale, locales } from "../src/i18n/config"; import I18nStoryWrapper from "./I18nStoryWrapper"; const parameters = { diff --git a/app/next.config.js b/app/next.config.js index c98aabc1..34a77d1f 100644 --- a/app/next.config.js +++ b/app/next.config.js @@ -1,7 +1,5 @@ // @ts-check -const withNextIntl = require("next-intl/plugin")( - "./src/i18n/next-intl-config.ts" -); +const withNextIntl = require("next-intl/plugin")("./src/i18n/setup.ts"); const sassOptions = require("./scripts/sassOptions"); /** diff --git a/app/src/i18n/config.ts b/app/src/i18n/config.ts new file mode 100644 index 00000000..96edb808 --- /dev/null +++ b/app/src/i18n/config.ts @@ -0,0 +1,28 @@ +/** + * @file Shared i18n configuration for use across the server and client + */ +import type { getRequestConfig } from "next-intl/server"; + +type RequestConfig = Awaited< + ReturnType<Parameters<typeof getRequestConfig>[0]> +>; + +/** + * List of languages supported by the application. Other tools (Storybook, tests) reference this. + * These must be BCP47 language tags: https://en.wikipedia.org/wiki/IETF_language_tag#List_of_common_primary_language_subtags + */ +export const locales = ["en-US", "es-US"] as const; +export type Locale = (typeof locales)[number]; +export const defaultLocale: Locale = "en-US"; + +/** + * Define the default formatting for date, time, and numbers. + * @see https://next-intl-docs.vercel.app/docs/usage/configuration#formats + */ +export const formats: RequestConfig["formats"] = { + number: { + currency: { + currency: "USD", + }, + }, +}; diff --git a/app/src/i18n/getMessages.ts b/app/src/i18n/getMessages.ts new file mode 100644 index 00000000..93582505 --- /dev/null +++ b/app/src/i18n/getMessages.ts @@ -0,0 +1,34 @@ +import { merge } from "lodash"; +import { defaultLocale, Locale } from "src/i18n/config"; + +import { messages as enUs } from "./messages/en-US"; +import { messages as esUs } from "./messages/es-US"; + +const localeToMessages = { + "en-US": enUs, + "es-US": esUs, +}; + +/** + * Get all messages for the given locale. If any translations are missing + * from the current locale, the missing key will fallback to the default locale + */ +export function getMessages(requestedLocale: string = defaultLocale) { + if (requestedLocale in localeToMessages === false) { + console.error( + "Unsupported locale was requested. Falling back to the default locale.", + { locale: requestedLocale, defaultLocale } + ); + requestedLocale = defaultLocale; + } + + const targetLocale = requestedLocale as Locale; + let messages = localeToMessages[targetLocale]; + + if (targetLocale !== defaultLocale) { + const fallbackMessages = localeToMessages[defaultLocale]; + messages = merge({}, fallbackMessages, messages); + } + + return messages as Messages; +} diff --git a/app/src/i18n/next-intl-config.ts b/app/src/i18n/next-intl-config.ts deleted file mode 100644 index 0eabacc6..00000000 --- a/app/src/i18n/next-intl-config.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { formats, getLocaleMessages } from "src/i18n"; - -import { getRequestConfig } from "next-intl/server"; - -/** - * The next-intl config. This method is used behind the scenes by `next-intl/plugin` - * when its called in next.config.js. - */ -export default getRequestConfig(({ locale }) => { - return { - formats, - messages: getLocaleMessages(locale), - }; -}); diff --git a/app/src/i18n/server.ts b/app/src/i18n/server.ts new file mode 100644 index 00000000..a68446b1 --- /dev/null +++ b/app/src/i18n/server.ts @@ -0,0 +1,16 @@ +import { getRequestConfig } from "next-intl/server"; + +import { formats } from "./config"; +import { getMessages } from "./getMessages"; + +/** + * I18n config for server components. + * This method is used behind the scenes by `next-intl/plugin`, which is setup in next.config.js. + * @see https://next-intl-docs.vercel.app/docs/usage/configuration#nextconfigjs + */ +export default getRequestConfig(({ locale }) => { + return { + formats, + messages: getMessages(locale), + }; +}); diff --git a/app/src/middleware.ts b/app/src/middleware.ts index aaeb2161..18d43c06 100644 --- a/app/src/middleware.ts +++ b/app/src/middleware.ts @@ -7,7 +7,7 @@ import createIntlMiddleware from "next-intl/middleware"; import { NextRequest } from "next/server"; -import { defaultLocale, locales } from "./i18n"; +import { defaultLocale, locales } from "./i18n/config"; // Don't run middleware on API routes or Next.js build output export const config = { diff --git a/app/tests/react-utils.tsx b/app/tests/react-utils.tsx index a9b39690..b439da15 100644 --- a/app/tests/react-utils.tsx +++ b/app/tests/react-utils.tsx @@ -5,7 +5,8 @@ * @see https://testing-library.com/docs/react-testing-library/setup#custom-render */ import { render as _render, RenderOptions } from "@testing-library/react"; -import { defaultLocale, formats, getLocaleMessages } from "src/i18n"; +import { defaultLocale, formats } from "src/i18n/config"; +import { getMessages } from "src/i18n/getMessages"; import { NextIntlClientProvider } from "next-intl"; @@ -17,7 +18,7 @@ const GlobalProviders = ({ children }: { children: React.ReactNode }) => { return ( <NextIntlClientProvider locale={defaultLocale} - messages={getLocaleMessages(defaultLocale)} + messages={getMessages(defaultLocale)} formats={formats} > {children} From d79bb65cc366d4ae0393a0f2335976af451c8929 Mon Sep 17 00:00:00 2001 From: Sawyer <git@sawyerh.com> Date: Wed, 6 Dec 2023 15:57:10 -0800 Subject: [PATCH 16/32] Revert todo --- docs/internationalization.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/internationalization.md b/docs/internationalization.md index ce78e6d1..fd174fba 100644 --- a/docs/internationalization.md +++ b/docs/internationalization.md @@ -1,5 +1,3 @@ -// TODO: Update for next-intl - # Internationalization (i18n) - [next-intl](https://next-intl-docs.vercel.app) is used for internationalization. Toggling between languages is done by changing the URL's path prefix (e.g. `/about` ➡️ `/es-US/about`). From 6af1c77381413dfce8d75ec06a85645866a634e1 Mon Sep 17 00:00:00 2001 From: Sawyer <git@sawyerh.com> Date: Wed, 6 Dec 2023 15:58:15 -0800 Subject: [PATCH 17/32] Rename to getMessagesWithFallbacks --- app/.storybook/I18nStoryWrapper.tsx | 4 ++-- app/src/i18n/{getMessages.ts => getMessagesWithFallbacks.ts} | 4 +++- app/src/i18n/server.ts | 4 ++-- app/tests/react-utils.tsx | 4 ++-- 4 files changed, 9 insertions(+), 7 deletions(-) rename app/src/i18n/{getMessages.ts => getMessagesWithFallbacks.ts} (91%) diff --git a/app/.storybook/I18nStoryWrapper.tsx b/app/.storybook/I18nStoryWrapper.tsx index e871dcd0..784b9c6f 100644 --- a/app/.storybook/I18nStoryWrapper.tsx +++ b/app/.storybook/I18nStoryWrapper.tsx @@ -8,7 +8,7 @@ import { NextIntlClientProvider } from "next-intl"; import React from "react"; import { defaultLocale, formats } from "../src/i18n/config"; -import { getMessages } from "../src/i18n/getMessages"; +import { getMessagesWithFallbacks } from "../src/i18n/getMessagesWithFallbacks"; const I18nStoryWrapper = ( Story: React.ComponentType, @@ -20,7 +20,7 @@ const I18nStoryWrapper = ( <NextIntlClientProvider formats={formats} locale={locale} - messages={getMessages(locale)} + messages={getMessagesWithFallbacks(locale)} > <Story /> </NextIntlClientProvider> diff --git a/app/src/i18n/getMessages.ts b/app/src/i18n/getMessagesWithFallbacks.ts similarity index 91% rename from app/src/i18n/getMessages.ts rename to app/src/i18n/getMessagesWithFallbacks.ts index 93582505..25e82e7b 100644 --- a/app/src/i18n/getMessages.ts +++ b/app/src/i18n/getMessagesWithFallbacks.ts @@ -13,7 +13,9 @@ const localeToMessages = { * Get all messages for the given locale. If any translations are missing * from the current locale, the missing key will fallback to the default locale */ -export function getMessages(requestedLocale: string = defaultLocale) { +export function getMessagesWithFallbacks( + requestedLocale: string = defaultLocale +) { if (requestedLocale in localeToMessages === false) { console.error( "Unsupported locale was requested. Falling back to the default locale.", diff --git a/app/src/i18n/server.ts b/app/src/i18n/server.ts index a68446b1..b7315847 100644 --- a/app/src/i18n/server.ts +++ b/app/src/i18n/server.ts @@ -1,7 +1,7 @@ import { getRequestConfig } from "next-intl/server"; import { formats } from "./config"; -import { getMessages } from "./getMessages"; +import { getMessagesWithFallbacks } from "./getMessagesWithFallbacks"; /** * I18n config for server components. @@ -11,6 +11,6 @@ import { getMessages } from "./getMessages"; export default getRequestConfig(({ locale }) => { return { formats, - messages: getMessages(locale), + messages: getMessagesWithFallbacks(locale), }; }); diff --git a/app/tests/react-utils.tsx b/app/tests/react-utils.tsx index b439da15..fb03d32a 100644 --- a/app/tests/react-utils.tsx +++ b/app/tests/react-utils.tsx @@ -6,7 +6,7 @@ */ import { render as _render, RenderOptions } from "@testing-library/react"; import { defaultLocale, formats } from "src/i18n/config"; -import { getMessages } from "src/i18n/getMessages"; +import { getMessagesWithFallbacks } from "src/i18n/getMessagesWithFallbacks"; import { NextIntlClientProvider } from "next-intl"; @@ -18,7 +18,7 @@ const GlobalProviders = ({ children }: { children: React.ReactNode }) => { return ( <NextIntlClientProvider locale={defaultLocale} - messages={getMessages(defaultLocale)} + messages={getMessagesWithFallbacks(defaultLocale)} formats={formats} > {children} From 79ca32be0a7a3c0a7e051ca385b27e496ad357ef Mon Sep 17 00:00:00 2001 From: Sawyer <git@sawyerh.com> Date: Wed, 6 Dec 2023 16:42:28 -0800 Subject: [PATCH 18/32] Use dynamic import for getting messages --- app/.storybook/I18nStoryWrapper.tsx | 3 +-- app/.storybook/preview.tsx | 9 +++++++- app/next.config.js | 2 +- app/package-lock.json | 16 +++++++-------- app/package.json | 4 ++-- app/src/i18n/getMessagesWithFallbacks.ts | 26 +++++++++++++----------- app/src/i18n/server.ts | 4 ++-- app/tests/react-utils.tsx | 4 ++-- docs/internationalization.md | 4 ++-- 9 files changed, 40 insertions(+), 32 deletions(-) diff --git a/app/.storybook/I18nStoryWrapper.tsx b/app/.storybook/I18nStoryWrapper.tsx index 784b9c6f..ececc8f1 100644 --- a/app/.storybook/I18nStoryWrapper.tsx +++ b/app/.storybook/I18nStoryWrapper.tsx @@ -8,7 +8,6 @@ import { NextIntlClientProvider } from "next-intl"; import React from "react"; import { defaultLocale, formats } from "../src/i18n/config"; -import { getMessagesWithFallbacks } from "../src/i18n/getMessagesWithFallbacks"; const I18nStoryWrapper = ( Story: React.ComponentType, @@ -20,7 +19,7 @@ const I18nStoryWrapper = ( <NextIntlClientProvider formats={formats} locale={locale} - messages={getMessagesWithFallbacks(locale)} + messages={context.loaded.messages} > <Story /> </NextIntlClientProvider> diff --git a/app/.storybook/preview.tsx b/app/.storybook/preview.tsx index a2ad6c3d..05e1c170 100644 --- a/app/.storybook/preview.tsx +++ b/app/.storybook/preview.tsx @@ -2,11 +2,12 @@ * @file Setup the toolbar, styling, and global context for each Storybook story. * @see https://storybook.js.org/docs/configure#configure-story-rendering */ -import { Preview } from "@storybook/react"; +import { Loader, Preview, StoryContext } from "@storybook/react"; import "../src/styles/styles.scss"; import { defaultLocale, locales } from "../src/i18n/config"; +import { getMessagesWithFallbacks } from "../src/i18n/getMessagesWithFallbacks"; import I18nStoryWrapper from "./I18nStoryWrapper"; const parameters = { @@ -35,7 +36,13 @@ const parameters = { }, }; +const i18nMessagesLoader: Loader = async (context) => { + const messages = await getMessagesWithFallbacks(context.globals.locale); + return { messages }; +}; + const preview: Preview = { + loaders: [i18nMessagesLoader], decorators: [I18nStoryWrapper], parameters, globalTypes: { diff --git a/app/next.config.js b/app/next.config.js index 34a77d1f..0531a0a8 100644 --- a/app/next.config.js +++ b/app/next.config.js @@ -1,5 +1,5 @@ // @ts-check -const withNextIntl = require("next-intl/plugin")("./src/i18n/setup.ts"); +const withNextIntl = require("next-intl/plugin")("./src/i18n/server.ts"); const sassOptions = require("./scripts/sassOptions"); /** diff --git a/app/package-lock.json b/app/package-lock.json index b406192a..ac59ee0e 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -30,8 +30,8 @@ "@types/jest-axe": "^3.5.5", "@types/lodash": "^4.14.202", "@types/node": "^20.0.0", - "@types/react": "^18.0.28", - "@types/react-dom": "^18.0.11", + "@types/react": "^18.2.42", + "@types/react-dom": "^18.2.17", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "eslint": "^8.36.0", @@ -8893,9 +8893,9 @@ "dev": true }, "node_modules/@types/react": { - "version": "18.2.39", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.39.tgz", - "integrity": "sha512-Oiw+ppED6IremMInLV4HXGbfbG6GyziY3kqAwJYOR0PNbkYDmLWQA3a95EhdSmamsvbkJN96ZNN+YD+fGjzSBA==", + "version": "18.2.42", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.42.tgz", + "integrity": "sha512-c1zEr96MjakLYus/wPnuWDo1/zErfdU9rNsIGmE+NV71nx88FG9Ttgo5dqorXTu/LImX2f63WBP986gJkMPNbA==", "dev": true, "dependencies": { "@types/prop-types": "*", @@ -31090,9 +31090,9 @@ "dev": true }, "@types/react": { - "version": "18.2.39", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.39.tgz", - "integrity": "sha512-Oiw+ppED6IremMInLV4HXGbfbG6GyziY3kqAwJYOR0PNbkYDmLWQA3a95EhdSmamsvbkJN96ZNN+YD+fGjzSBA==", + "version": "18.2.42", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.42.tgz", + "integrity": "sha512-c1zEr96MjakLYus/wPnuWDo1/zErfdU9rNsIGmE+NV71nx88FG9Ttgo5dqorXTu/LImX2f63WBP986gJkMPNbA==", "dev": true, "requires": { "@types/prop-types": "*", diff --git a/app/package.json b/app/package.json index 8749c949..3177df5a 100644 --- a/app/package.json +++ b/app/package.json @@ -40,8 +40,8 @@ "@types/jest-axe": "^3.5.5", "@types/lodash": "^4.14.202", "@types/node": "^20.0.0", - "@types/react": "^18.0.28", - "@types/react-dom": "^18.0.11", + "@types/react": "^18.2.42", + "@types/react-dom": "^18.2.17", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "eslint": "^8.36.0", diff --git a/app/src/i18n/getMessagesWithFallbacks.ts b/app/src/i18n/getMessagesWithFallbacks.ts index 25e82e7b..bc2221fe 100644 --- a/app/src/i18n/getMessagesWithFallbacks.ts +++ b/app/src/i18n/getMessagesWithFallbacks.ts @@ -1,22 +1,24 @@ import { merge } from "lodash"; -import { defaultLocale, Locale } from "src/i18n/config"; +import { defaultLocale, Locale, locales } from "src/i18n/config"; -import { messages as enUs } from "./messages/en-US"; -import { messages as esUs } from "./messages/es-US"; +interface LocaleFile { + messages: Messages; +} -const localeToMessages = { - "en-US": enUs, - "es-US": esUs, -}; +async function importMessages(locale: Locale) { + const { messages } = (await import(`./messages/${locale}`)) as LocaleFile; + return messages; +} /** * Get all messages for the given locale. If any translations are missing * from the current locale, the missing key will fallback to the default locale */ -export function getMessagesWithFallbacks( +export async function getMessagesWithFallbacks( requestedLocale: string = defaultLocale ) { - if (requestedLocale in localeToMessages === false) { + const isValidLocale = locales.includes(requestedLocale as Locale); // https://github.com/microsoft/TypeScript/issues/26255 + if (!isValidLocale) { console.error( "Unsupported locale was requested. Falling back to the default locale.", { locale: requestedLocale, defaultLocale } @@ -25,12 +27,12 @@ export function getMessagesWithFallbacks( } const targetLocale = requestedLocale as Locale; - let messages = localeToMessages[targetLocale]; + let messages = await importMessages(targetLocale); if (targetLocale !== defaultLocale) { - const fallbackMessages = localeToMessages[defaultLocale]; + const fallbackMessages = await importMessages(defaultLocale); messages = merge({}, fallbackMessages, messages); } - return messages as Messages; + return Promise.resolve(messages); } diff --git a/app/src/i18n/server.ts b/app/src/i18n/server.ts index b7315847..912c0c47 100644 --- a/app/src/i18n/server.ts +++ b/app/src/i18n/server.ts @@ -8,9 +8,9 @@ import { getMessagesWithFallbacks } from "./getMessagesWithFallbacks"; * This method is used behind the scenes by `next-intl/plugin`, which is setup in next.config.js. * @see https://next-intl-docs.vercel.app/docs/usage/configuration#nextconfigjs */ -export default getRequestConfig(({ locale }) => { +export default getRequestConfig(async ({ locale }) => { return { formats, - messages: getMessagesWithFallbacks(locale), + messages: await getMessagesWithFallbacks(locale), }; }); diff --git a/app/tests/react-utils.tsx b/app/tests/react-utils.tsx index fb03d32a..51a570a4 100644 --- a/app/tests/react-utils.tsx +++ b/app/tests/react-utils.tsx @@ -6,7 +6,7 @@ */ import { render as _render, RenderOptions } from "@testing-library/react"; import { defaultLocale, formats } from "src/i18n/config"; -import { getMessagesWithFallbacks } from "src/i18n/getMessagesWithFallbacks"; +import { messages } from "src/i18n/messages/en-US"; import { NextIntlClientProvider } from "next-intl"; @@ -18,7 +18,7 @@ const GlobalProviders = ({ children }: { children: React.ReactNode }) => { return ( <NextIntlClientProvider locale={defaultLocale} - messages={getMessagesWithFallbacks(defaultLocale)} + messages={messages} formats={formats} > {children} diff --git a/docs/internationalization.md b/docs/internationalization.md index fd174fba..a82342e2 100644 --- a/docs/internationalization.md +++ b/docs/internationalization.md @@ -1,7 +1,7 @@ # Internationalization (i18n) - [next-intl](https://next-intl-docs.vercel.app) is used for internationalization. Toggling between languages is done by changing the URL's path prefix (e.g. `/about` ➡️ `/es-US/about`). -- Configuration and helpers are located in [`i18n/index.ts`](../app/src/i18n/index.ts). For the most part, you shouldn't need to edit this file unless adding a new formatter or new language. +- Configuration is located in [`i18n/config.ts`](../app/src/i18n/config.ts). For the most part, you shouldn't need to edit this file unless adding a new formatter or new language. ## Managing translations @@ -28,4 +28,4 @@ Locale messages should only ever be loaded on the server-side, to avoid bloating 1. Add a language folder, using the same BCP47 language tag: `mkdir -p src/i18n/messages/<lang>` 1. Add a language file: `touch src/i18n/messages/<lang>/index.ts` and add the translated content. The JSON structure should be the same across languages. However, non-default languages can omit keys, in which case the default language will be used as a fallback. -1. Update [`i18n/index.ts`](../app/src/i18n/index.ts) to include the new language in the `_messages` object and `locales` array. +1. Update [`i18n/config.ts`](../app/src/i18n/config.ts) to include the new language in the `locales` array. From 6e759ead3f92bd78f318ec0004d992152863a7ac Mon Sep 17 00:00:00 2001 From: Sawyer <git@sawyerh.com> Date: Thu, 7 Dec 2023 15:38:03 -0800 Subject: [PATCH 19/32] Fix config path --- app/next.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/next.config.js b/app/next.config.js index a137445e..0531a0a8 100644 --- a/app/next.config.js +++ b/app/next.config.js @@ -1,5 +1,5 @@ // @ts-check -const withNextIntl = require("next-intl/plugin")("./src/i18n/config.ts"); +const withNextIntl = require("next-intl/plugin")("./src/i18n/server.ts"); const sassOptions = require("./scripts/sassOptions"); /** From 2672191eefb1fb411c7cbe428b2e2380a71794f4 Mon Sep 17 00:00:00 2001 From: Sawyer <git@sawyerh.com> Date: Thu, 7 Dec 2023 16:02:02 -0800 Subject: [PATCH 20/32] Cleanup --- app/src/app/[locale]/page.tsx | 22 +++++++++++++--------- app/src/types/i18n.d.ts | 5 ++++- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/app/src/app/[locale]/page.tsx b/app/src/app/[locale]/page.tsx index 0ce2fa99..498b2180 100644 --- a/app/src/app/[locale]/page.tsx +++ b/app/src/app/[locale]/page.tsx @@ -3,16 +3,17 @@ import { Metadata } from "next"; import { useTranslations } from "next-intl"; import { getTranslations } from "next-intl/server"; -export async function generateMetadata({ - params: { locale }, -}: { - params: { locale: string }; -}): Promise<Metadata> { - const t = await getTranslations({ locale, namespace: "home" }); - - return { - title: t("title"), +interface RouteParams { + locale: string; +} + +export async function generateMetadata({ params }: { params: RouteParams }) { + const t = await getTranslations({ locale: params.locale }); + const meta: Metadata = { + title: t("home.title"), }; + + return meta; } export default function Page() { @@ -21,6 +22,8 @@ export default function Page() { return ( <> <h1>{t("title")}</h1> + + {/* Demonstration of more complex translated strings, with safe-listed links HTML elements */} <p className="usa-intro"> {t.rich("intro", { LinkToNextJs: (content) => ( @@ -35,6 +38,7 @@ export default function Page() { })} <p> + {/* Demonstration of formatters */} {t("formatting", { amount: 1234, isoDate: new Date("2023-11-29T23:30:00.000Z"), diff --git a/app/src/types/i18n.d.ts b/app/src/types/i18n.d.ts index 733cc674..f92bc9ad 100644 --- a/app/src/types/i18n.d.ts +++ b/app/src/types/i18n.d.ts @@ -1,3 +1,6 @@ -// Use type safe message keys with `next-intl` +/** + * @file Setup type safe message keys with `next-intl` + * @see https://next-intl-docs.vercel.app/docs/workflows/typescript + */ type Messages = typeof import("src/i18n/messages/en-US").messages; type IntlMessages = Messages; From 6fd126adc7c5b058b1d1e28e7690c90b6b1f1a22 Mon Sep 17 00:00:00 2001 From: Sawyer <git@sawyerh.com> Date: Thu, 7 Dec 2023 16:20:35 -0800 Subject: [PATCH 21/32] Migrate API route --- app/README.md | 10 ++++------ app/src/app/api/hello/route.ts | 17 +++++++++++++++++ app/src/pages/api/hello.ts | 13 ------------- 3 files changed, 21 insertions(+), 19 deletions(-) create mode 100644 app/src/app/api/hello/route.ts delete mode 100644 app/src/pages/api/hello.ts diff --git a/app/README.md b/app/README.md index fd4dcc9b..e44f4739 100644 --- a/app/README.md +++ b/app/README.md @@ -12,13 +12,13 @@ ├── public # Static assets ├── src # Source code │ ├── app # Routes, layouts, and loading screens +│ │ ├── api # Custom request handlers +│ │ ├── layout.tsx # Root layout, wraps every page +│ │ └── page.tsx # Homepage │ ├── components # Reusable UI components │ ├── i18n # Internationalization │ │ ├── config.ts # Supported locales, timezone, and formatters │ │ └── messages # Translated strings -│ ├── pages # Page routes and data fetching -│   │ ├── api # API routes (optional) -│   │ └── _app.tsx # Global entry point │ ├── styles # Sass & design system settings │ └── types # Global type declarations ├── stories # Storybook pages @@ -27,9 +27,7 @@ ## 💻 Development -[Next.js](https://nextjs.org/docs) provides the React framework for building the web application. Pages are defined in the `app/` directory. Pages are automatically routed based on the file name. For example, `app/[locale]/about/page.tsx` would render at `/about` (for English) or `/es-US/about` (for Spanish). - -Files in the `pages/api` are treated as [API routes](https://nextjs.org/docs/api-routes/introduction). +[Next.js](https://nextjs.org/docs) provides the React framework for building the web application. Routes are defined in the `app/` directory. Pages are automatically routed based on the directory name. For example, `app/[locale]/about/page.tsx` would render at `/about` (for English) or `/es-US/about` (for Spanish). [**Learn more about developing Next.js applications** ↗️](https://nextjs.org/docs) diff --git a/app/src/app/api/hello/route.ts b/app/src/app/api/hello/route.ts new file mode 100644 index 00000000..8d24f840 --- /dev/null +++ b/app/src/app/api/hello/route.ts @@ -0,0 +1,17 @@ +/** + * @file Next.js route handler + * @see https://nextjs.org/docs/app/building-your-application/routing/route-handlers + */ +import { NextRequest, NextResponse } from "next/server"; + +type Data = { + name: string; + user_id: string | null; +}; + +export function GET(request: NextRequest) { + const query = request.nextUrl.searchParams; + const user_id = query.get("user_id"); + + return NextResponse.json<Data>({ name: "John Doe", user_id }); +} diff --git a/app/src/pages/api/hello.ts b/app/src/pages/api/hello.ts deleted file mode 100644 index 74a3605d..00000000 --- a/app/src/pages/api/hello.ts +++ /dev/null @@ -1,13 +0,0 @@ -// Next.js API route support: https://nextjs.org/docs/api-routes/introduction -import type { NextApiRequest, NextApiResponse } from "next"; - -type Data = { - name: string; -}; - -export default function handler( - req: NextApiRequest, - res: NextApiResponse<Data> -) { - res.status(200).json({ name: "John Doe" }); -} From b4c4c21d179ffe75974e388a2182f2d49593a40b Mon Sep 17 00:00:00 2001 From: Sawyer <git@sawyerh.com> Date: Thu, 7 Dec 2023 17:00:49 -0800 Subject: [PATCH 22/32] Update docs --- app/README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/README.md b/app/README.md index e44f4739..a1cbc235 100644 --- a/app/README.md +++ b/app/README.md @@ -122,13 +122,11 @@ npm run test-watch -- pages [React Testing Library (RTL)](https://testing-library.com/docs/react-testing-library/intro) provides the utilities for rendering and querying, and [`jest-axe`](https://www.npmjs.com/package/jest-axe) is used for accessibility testing. Refer to their docs to learn more about their APIs, or view an existing test for examples. -It's important that you **don't import from `@testing-library/react`**, instead import from `tests/test-utils`. Using the custom `render` method from `tests/test-utils` sets up internationalization in the component. - -Import React Testing Library utilities from `tests/test-utils`: +`@testing-library/react` methods should be imported from `tests/react-utils` in order for internationalization to work within your tests: ```diff - import { render, screen } from '@testing-library/react'; -+ import { render, screen } from 'tests/test-utils'; ++ import { render, screen } from 'tests/react-utils'; ``` ## 🤖 Type checking, linting, and formatting From 02e2d93879370b2a063488734835aacaf0fb6e9f Mon Sep 17 00:00:00 2001 From: Sawyer <git@sawyerh.com> Date: Thu, 7 Dec 2023 17:02:30 -0800 Subject: [PATCH 23/32] Update doc --- app/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/README.md b/app/README.md index a1cbc235..a2d7c55d 100644 --- a/app/README.md +++ b/app/README.md @@ -127,6 +127,14 @@ npm run test-watch -- pages ```diff - import { render, screen } from '@testing-library/react'; + import { render, screen } from 'tests/react-utils'; + +it("renders submit button", () => { + render(<Page />) + + expect( + screen.getByRole("button", { name: "Submit" }) + ).toBeInTheDocument() +}) ``` ## 🤖 Type checking, linting, and formatting From 2acda0837cf0d3ea32f210f0a6c2063c4cc53c6d Mon Sep 17 00:00:00 2001 From: Sawyer <git@sawyerh.com> Date: Thu, 7 Dec 2023 17:03:11 -0800 Subject: [PATCH 24/32] Reduce diff --- app/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/README.md b/app/README.md index a2d7c55d..1f86373f 100644 --- a/app/README.md +++ b/app/README.md @@ -20,7 +20,7 @@ │ │ ├── config.ts # Supported locales, timezone, and formatters │ │ └── messages # Translated strings │ ├── styles # Sass & design system settings -│ └── types # Global type declarations +│ └── types # TypeScript type declarations ├── stories # Storybook pages └── tests ``` From 43e9695397ef9788f6820f7b98954e57f501b3f1 Mon Sep 17 00:00:00 2001 From: Sawyer <git@sawyerh.com> Date: Thu, 7 Dec 2023 17:07:40 -0800 Subject: [PATCH 25/32] Fix sticky footer --- app/src/components/Layout.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/components/Layout.tsx b/app/src/components/Layout.tsx index 60892256..43a3831e 100644 --- a/app/src/components/Layout.tsx +++ b/app/src/components/Layout.tsx @@ -32,7 +32,8 @@ const Layout = ({ children, locale }: Props) => { > <Header /> </NextIntlClientProvider> - <main id="main-content" className="usa-section"> + {/* grid-col-fill so that the footer sticks to the bottom of tall screens */} + <main id="main-content" className="usa-section grid-col-fill"> <GridContainer> <Grid row> <Grid col>{children}</Grid> From 02b0546c72f33652bfb84e21bc26615d03bfe010 Mon Sep 17 00:00:00 2001 From: Sawyer <git@sawyerh.com> Date: Thu, 7 Dec 2023 17:09:06 -0800 Subject: [PATCH 26/32] Use timeZone --- app/src/i18n/server.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/i18n/server.ts b/app/src/i18n/server.ts index 912c0c47..efdf25da 100644 --- a/app/src/i18n/server.ts +++ b/app/src/i18n/server.ts @@ -1,10 +1,10 @@ import { getRequestConfig } from "next-intl/server"; -import { formats } from "./config"; +import { formats, timeZone } from "./config"; import { getMessagesWithFallbacks } from "./getMessagesWithFallbacks"; /** - * I18n config for server components. + * Make locale messages available to all server components. * This method is used behind the scenes by `next-intl/plugin`, which is setup in next.config.js. * @see https://next-intl-docs.vercel.app/docs/usage/configuration#nextconfigjs */ @@ -12,5 +12,6 @@ export default getRequestConfig(async ({ locale }) => { return { formats, messages: await getMessagesWithFallbacks(locale), + timeZone, }; }); From 3bebef7395f2bba77bf5656bdeadd5afeb053e81 Mon Sep 17 00:00:00 2001 From: Sawyer <git@sawyerh.com> Date: Thu, 7 Dec 2023 17:17:35 -0800 Subject: [PATCH 27/32] Restore transpilePackages --- app/next.config.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/next.config.js b/app/next.config.js index 0531a0a8..e9c39d63 100644 --- a/app/next.config.js +++ b/app/next.config.js @@ -20,6 +20,10 @@ const nextConfig = { // https://nextjs.org/docs/app/api-reference/next-config-js/output output: "standalone", sassOptions: appSassOptions, + transpilePackages: [ + // https://github.com/trussworks/react-uswds/issues/2605 + "@trussworks/react-uswds", + ], }; module.exports = withNextIntl(nextConfig); From 1bbc39e8415091544c122499df43fb0fee3f68a1 Mon Sep 17 00:00:00 2001 From: Sawyer <git@sawyerh.com> Date: Thu, 7 Dec 2023 17:24:25 -0800 Subject: [PATCH 28/32] Restore comment --- app/next.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/next.config.js b/app/next.config.js index e9c39d63..4bb8db71 100644 --- a/app/next.config.js +++ b/app/next.config.js @@ -20,6 +20,7 @@ const nextConfig = { // https://nextjs.org/docs/app/api-reference/next-config-js/output output: "standalone", sassOptions: appSassOptions, + // Continue to support older browsers (ES5) transpilePackages: [ // https://github.com/trussworks/react-uswds/issues/2605 "@trussworks/react-uswds", From 6c42256912332ca04a4a49d70ca828b74958223a Mon Sep 17 00:00:00 2001 From: Sawyer <git@sawyerh.com> Date: Tue, 12 Dec 2023 16:01:02 -0800 Subject: [PATCH 29/32] Missed merge changes --- app/next-env.d.ts | 1 - app/src/app/[locale]/page.tsx | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/app/next-env.d.ts b/app/next-env.d.ts index fd36f949..4f11a03d 100644 --- a/app/next-env.d.ts +++ b/app/next-env.d.ts @@ -1,6 +1,5 @@ /// <reference types="next" /> /// <reference types="next/image-types/global" /> -/// <reference types="next/navigation-types/compat/navigation" /> // NOTE: This file should not be edited // see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/app/src/app/[locale]/page.tsx b/app/src/app/[locale]/page.tsx index fa7e1c0c..f4561982 100644 --- a/app/src/app/[locale]/page.tsx +++ b/app/src/app/[locale]/page.tsx @@ -1,7 +1,6 @@ import { Metadata } from "next"; import { isFeatureEnabled } from "src/services/feature-flags"; -import { useTranslations } from "next-intl"; import { getTranslations } from "next-intl/server"; interface RouteParams { @@ -18,7 +17,7 @@ export async function generateMetadata({ params }: { params: RouteParams }) { } export default async function Page() { - const t = useTranslations("home"); + const t = await getTranslations("home"); const isFooEnabled = await isFeatureEnabled("foo", "anonymous"); return ( From bd0c2de2129d8bbf083ab5f6f0954bd3050013fd Mon Sep 17 00:00:00 2001 From: Sawyer <git@sawyerh.com> Date: Wed, 13 Dec 2023 10:08:04 -0800 Subject: [PATCH 30/32] Seperate controller and view logic, fix tests and story --- app/src/app/[locale]/page.tsx | 11 +++++++++-- app/stories/pages/Index.stories.tsx | 9 ++++++--- app/tests/app/page.test.tsx | 8 ++++---- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/app/src/app/[locale]/page.tsx b/app/src/app/[locale]/page.tsx index f4561982..ae4d7e0a 100644 --- a/app/src/app/[locale]/page.tsx +++ b/app/src/app/[locale]/page.tsx @@ -1,6 +1,7 @@ import { Metadata } from "next"; import { isFeatureEnabled } from "src/services/feature-flags"; +import { useTranslations } from "next-intl"; import { getTranslations } from "next-intl/server"; interface RouteParams { @@ -16,10 +17,16 @@ export async function generateMetadata({ params }: { params: RouteParams }) { return meta; } -export default async function Page() { - const t = await getTranslations("home"); +export default async function Controller() { const isFooEnabled = await isFeatureEnabled("foo", "anonymous"); + return <View isFooEnabled={isFooEnabled} />; +} + +export function View(props: { isFooEnabled: boolean }) { + const { isFooEnabled } = props; + const t = useTranslations("home"); + return ( <> <h1>{t("title")}</h1> diff --git a/app/stories/pages/Index.stories.tsx b/app/stories/pages/Index.stories.tsx index 4afb9c96..816d6610 100644 --- a/app/stories/pages/Index.stories.tsx +++ b/app/stories/pages/Index.stories.tsx @@ -1,9 +1,12 @@ import { Meta } from "@storybook/react"; -import Index from "src/app/[locale]/page"; +import { View } from "src/app/[locale]/page"; -const meta: Meta<typeof Index> = { +const meta: Meta<typeof View> = { title: "Pages/Home", - component: Index, + component: View, + args: { + isFooEnabled: false, + }, }; export default meta; diff --git a/app/tests/app/page.test.tsx b/app/tests/app/page.test.tsx index 339a4eda..ba0eac9a 100644 --- a/app/tests/app/page.test.tsx +++ b/app/tests/app/page.test.tsx @@ -1,5 +1,5 @@ import { axe } from "jest-axe"; -import Index from "src/app/[locale]/page"; +import { View } from "src/app/[locale]/page"; import { LocalFeatureFlagManager } from "src/services/feature-flags/LocalFeatureFlagManager"; import { render, screen } from "tests/react-utils"; @@ -7,7 +7,7 @@ describe("Index", () => { // Demonstration of rendering translated text, and asserting the presence of a dynamic value. // You can delete this test for your own project. it("renders link to Next.js docs", () => { - render(<Index />); + render(<View isFooEnabled={false} />); const link = screen.getByRole("link", { name: /next\.js/i }); @@ -17,7 +17,7 @@ describe("Index", () => { }); it("passes accessibility scan", async () => { - const { container } = render(<Index />); + const { container } = render(<View isFooEnabled={false} />); const results = await axe(container); expect(results).toHaveNoViolations(); @@ -28,7 +28,7 @@ describe("Index", () => { .spyOn(LocalFeatureFlagManager.prototype, "isFeatureEnabled") .mockResolvedValue(true); - const { container } = render(<Index />); + const { container } = render(<View isFooEnabled />); expect(container).toHaveTextContent("Flag is enabled"); }); }); From 2d73ff6f025f4d17416539f052ff9d81a47e2680 Mon Sep 17 00:00:00 2001 From: Sawyer <git@sawyerh.com> Date: Wed, 13 Dec 2023 10:16:24 -0800 Subject: [PATCH 31/32] Demonstate test of async server component --- app/tests/app/page.test.tsx | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/app/tests/app/page.test.tsx b/app/tests/app/page.test.tsx index ba0eac9a..f25aca8f 100644 --- a/app/tests/app/page.test.tsx +++ b/app/tests/app/page.test.tsx @@ -1,9 +1,23 @@ import { axe } from "jest-axe"; -import { View } from "src/app/[locale]/page"; +import Controller, { View } from "src/app/[locale]/page"; import { LocalFeatureFlagManager } from "src/services/feature-flags/LocalFeatureFlagManager"; -import { render, screen } from "tests/react-utils"; +import { cleanup, render, screen } from "tests/react-utils"; -describe("Index", () => { +describe("Index - Controller", () => { + it("retrieves feature flags", async () => { + const featureFlagSpy = jest + .spyOn(LocalFeatureFlagManager.prototype, "isFeatureEnabled") + .mockResolvedValue(true); + + const result = await Controller(); + render(result); + + expect(featureFlagSpy).toHaveBeenCalledWith("foo", "anonymous"); + expect(screen.getByText(/Flag is enabled/)).toBeInTheDocument(); + }); +}); + +describe("Index - View", () => { // Demonstration of rendering translated text, and asserting the presence of a dynamic value. // You can delete this test for your own project. it("renders link to Next.js docs", () => { @@ -24,11 +38,14 @@ describe("Index", () => { }); it("conditionally displays content based on feature flag values", () => { - jest - .spyOn(LocalFeatureFlagManager.prototype, "isFeatureEnabled") - .mockResolvedValue(true); + const enabledFlagTextMatcher = /Flag is enabled/; - const { container } = render(<View isFooEnabled />); - expect(container).toHaveTextContent("Flag is enabled"); + render(<View isFooEnabled />); + expect(screen.getByText(enabledFlagTextMatcher)).toBeInTheDocument(); + + cleanup(); + + render(<View isFooEnabled={false} />); + expect(screen.queryByText(enabledFlagTextMatcher)).not.toBeInTheDocument(); }); }); From ac211f220259b0736b948c2c801b04efae5326dd Mon Sep 17 00:00:00 2001 From: Sawyer <git@sawyerh.com> Date: Fri, 15 Dec 2023 13:51:56 -0800 Subject: [PATCH 32/32] Fix named export limitation of page files --- app/src/app/[locale]/page.tsx | 41 ++--------------------------- app/src/app/[locale]/view.tsx | 39 +++++++++++++++++++++++++++ app/stories/pages/Index.stories.tsx | 2 +- app/tests/app/page.test.tsx | 3 ++- 4 files changed, 44 insertions(+), 41 deletions(-) create mode 100644 app/src/app/[locale]/view.tsx diff --git a/app/src/app/[locale]/page.tsx b/app/src/app/[locale]/page.tsx index ae4d7e0a..0b19ff36 100644 --- a/app/src/app/[locale]/page.tsx +++ b/app/src/app/[locale]/page.tsx @@ -1,9 +1,10 @@ import { Metadata } from "next"; import { isFeatureEnabled } from "src/services/feature-flags"; -import { useTranslations } from "next-intl"; import { getTranslations } from "next-intl/server"; +import { View } from "./view"; + interface RouteParams { locale: string; } @@ -22,41 +23,3 @@ export default async function Controller() { return <View isFooEnabled={isFooEnabled} />; } - -export function View(props: { isFooEnabled: boolean }) { - const { isFooEnabled } = props; - const t = useTranslations("home"); - - return ( - <> - <h1>{t("title")}</h1> - - {/* Demonstration of more complex translated strings, with safe-listed links HTML elements */} - <p className="usa-intro"> - {t.rich("intro", { - LinkToNextJs: (content) => ( - <a href="https://nextjs.org/docs">{content}</a> - ), - })} - </p> - <div className="measure-6"> - {t.rich("body", { - ul: (content) => <ul className="usa-list">{content}</ul>, - li: (content) => <li>{content}</li>, - })} - - <p> - {/* Demonstration of formatters */} - {t("formatting", { - amount: 1234, - isoDate: new Date("2023-11-29T23:30:00.000Z"), - })} - </p> - - {/* Demonstration of feature flagging */} - <p>{t("feature_flagging")}</p> - <p>{isFooEnabled ? t("flag_on") : t("flag_off")}</p> - </div> - </> - ); -} diff --git a/app/src/app/[locale]/view.tsx b/app/src/app/[locale]/view.tsx new file mode 100644 index 00000000..b57ae089 --- /dev/null +++ b/app/src/app/[locale]/view.tsx @@ -0,0 +1,39 @@ +import { useTranslations } from "next-intl"; + +export function View(props: { isFooEnabled: boolean }) { + const { isFooEnabled } = props; + const t = useTranslations("home"); + + return ( + <> + <h1>{t("title")}</h1> + + {/* Demonstration of more complex translated strings, with safe-listed links HTML elements */} + <p className="usa-intro"> + {t.rich("intro", { + LinkToNextJs: (content) => ( + <a href="https://nextjs.org/docs">{content}</a> + ), + })} + </p> + <div className="measure-6"> + {t.rich("body", { + ul: (content) => <ul className="usa-list">{content}</ul>, + li: (content) => <li>{content}</li>, + })} + + <p> + {/* Demonstration of formatters */} + {t("formatting", { + amount: 1234, + isoDate: new Date("2023-11-29T23:30:00.000Z"), + })} + </p> + + {/* Demonstration of feature flagging */} + <p>{t("feature_flagging")}</p> + <p>{isFooEnabled ? t("flag_on") : t("flag_off")}</p> + </div> + </> + ); +} diff --git a/app/stories/pages/Index.stories.tsx b/app/stories/pages/Index.stories.tsx index 816d6610..12587a72 100644 --- a/app/stories/pages/Index.stories.tsx +++ b/app/stories/pages/Index.stories.tsx @@ -1,5 +1,5 @@ import { Meta } from "@storybook/react"; -import { View } from "src/app/[locale]/page"; +import { View } from "src/app/[locale]/view"; const meta: Meta<typeof View> = { title: "Pages/Home", diff --git a/app/tests/app/page.test.tsx b/app/tests/app/page.test.tsx index f25aca8f..93cfb5da 100644 --- a/app/tests/app/page.test.tsx +++ b/app/tests/app/page.test.tsx @@ -1,5 +1,6 @@ import { axe } from "jest-axe"; -import Controller, { View } from "src/app/[locale]/page"; +import Controller from "src/app/[locale]/page"; +import { View } from "src/app/[locale]/view"; import { LocalFeatureFlagManager } from "src/services/feature-flags/LocalFeatureFlagManager"; import { cleanup, render, screen } from "tests/react-utils";