From ed37131a3dba69243e1a70c368c0dcdb336c365f Mon Sep 17 00:00:00 2001 From: Sawyer Hollenshead Date: Wed, 6 Dec 2023 14:58:43 -0800 Subject: [PATCH] Use next-intl for internationalization, replacing i18next (#260) ## Ticket Part of #66 ## Changes - Replace all I18next dependencies with [`next-intl`](https://next-intl-docs.vercel.app/) - Add new `tests/test-utils` file that would be used for rendering and querying a React tree in tests, rather than directly importing from `@testing-library/react`. This is so that the i18n content is provided to the component being tested. ## Context for reviewers This is one of the main pieces of the larger migration effort from the Pages router to the App Router (#66). To reduce the size of that PR, I've broken out the i18n migration work into this PR. Next.js App Router doesn't support internationalized routing or locale detection, like Pages router, and `next-intl` makes it easy to restore that functionality via middleware. --- app/.eslintrc.js | 31 +- app/.prettierrc.js | 1 - app/.storybook/I18nStoryWrapper.tsx | 29 + app/.storybook/i18next.js | 22 - app/.storybook/main.js | 2 +- app/.storybook/{preview.js => preview.tsx} | 33 +- app/next-i18next.config.d.ts | 6 - app/next-i18next.config.js | 71 -- app/next.config.js | 12 +- app/package-lock.json | 1033 +++++++------------- app/package.json | 16 +- 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/components/Footer.tsx | 6 +- app/src/components/Header.tsx | 10 +- app/src/components/Layout.tsx | 12 +- app/src/i18n/index.ts | 69 ++ app/src/i18n/messages/en-US/index.ts | 25 + app/src/i18n/messages/es-US/index.ts | 10 + app/src/pages/_app.tsx | 24 +- app/src/pages/health.tsx | 10 +- app/src/pages/index.tsx | 52 +- app/src/types/generated-i18n-bundle.ts | 12 - app/src/types/i18n.d.ts | 3 + app/src/types/i18next.d.ts | 16 - app/tests/components/Header.test.tsx | 2 +- app/tests/components/Layout.test.tsx | 2 +- app/tests/jest-i18n.ts | 19 - app/tests/pages/index.test.tsx | 2 +- app/tests/react-utils.tsx | 37 + app/tests/types/i18next.test.ts | 45 - app/tsconfig.ts-jest.json | 6 - docs/internationalization.md | 75 +- renovate.json | 5 - 35 files changed, 627 insertions(+), 1097 deletions(-) create mode 100644 app/.storybook/I18nStoryWrapper.tsx delete mode 100644 app/.storybook/i18next.js rename app/.storybook/{preview.js => preview.tsx} (55%) 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 create mode 100644 app/src/i18n/index.ts create mode 100644 app/src/i18n/messages/en-US/index.ts create mode 100644 app/src/i18n/messages/es-US/index.ts delete mode 100644 app/src/types/generated-i18n-bundle.ts create mode 100644 app/src/types/i18n.d.ts delete mode 100644 app/src/types/i18next.d.ts create mode 100644 app/tests/react-utils.tsx delete mode 100644 app/tests/types/i18next.test.ts delete mode 100644 app/tsconfig.ts-jest.json diff --git a/app/.eslintrc.js b/app/.eslintrc.js index 280651af..fc07b0da 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 @@ -14,35 +15,37 @@ 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: [ // 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/react-utils" instead so that translations work.', + name: "@testing-library/react", + }, + ], + }, + ], + }, }, // Lint config specific to TypeScript files { files: "**/*.+(ts|tsx)", + excludedFiles: [".storybook/*.ts?(x)"], parserOptions: { // These paths need defined to support rules that require type information tsconfigRootDir: __dirname, 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/I18nStoryWrapper.tsx b/app/.storybook/I18nStoryWrapper.tsx new file mode 100644 index 00000000..d158ea19 --- /dev/null +++ b/app/.storybook/I18nStoryWrapper.tsx @@ -0,0 +1,29 @@ +/** + * @file Storybook decorator, enabling internationalization for each story. + * @see https://storybook.js.org/docs/writing-stories/decorators + */ +import { StoryContext } from "@storybook/react"; + +import { NextIntlClientProvider } from "next-intl"; +import React from "react"; + +import { defaultLocale, formats, getLocaleMessages } from "../src/i18n"; + +const I18nStoryWrapper = ( + Story: React.ComponentType, + context: StoryContext +) => { + const locale = context.globals.locale ?? defaultLocale; + + return ( + + + + ); +}; + +export default I18nStoryWrapper; 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.tsx similarity index 55% rename from app/.storybook/preview.js rename to app/.storybook/preview.tsx index a7c243bb..c8b6da25 100644 --- a/app/.storybook/preview.js +++ b/app/.storybook/preview.tsx @@ -1,9 +1,13 @@ -// @ts-check -// Apply global styling to our stories +/** + * @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 "../src/styles/styles.scss"; -// Import i18next config. -import i18n from "./i18next.js"; +import { defaultLocale, locales } from "../src/i18n"; +import I18nStoryWrapper from "./I18nStoryWrapper"; const parameters = { actions: { argTypesRegex: "^on[A-Z].*" }, @@ -13,8 +17,6 @@ const parameters = { date: /Date$/, }, }, - // Configure i18next and locale/dropdown options. - i18n, options: { storySort: { method: "alphabetical", @@ -33,16 +35,17 @@ const parameters = { }, }; -/** - * @type {import("@storybook/react").Preview} - */ -const preview = { +const preview: Preview = { + decorators: [I18nStoryWrapper], parameters, - globals: { - locale: "en-US", - locales: { - "en-US": "English", - "es-US": "Español", + globalTypes: { + locale: { + description: "Active language", + defaultValue: defaultLocale, + toolbar: { + icon: "globe", + items: locales, + }, }, }, }; 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 d1287386..d0e0b044 100644 --- a/app/next.config.js +++ b/app/next.config.js @@ -1,5 +1,5 @@ // @ts-check -const { i18n } = require("./next-i18next.config"); +const withNextIntl = require("next-intl/plugin")("./src/i18n/index.ts"); const sassOptions = require("./scripts/sassOptions"); /** @@ -15,7 +15,10 @@ const appSassOptions = sassOptions(basePath); /** @type {import('next').NextConfig} */ const nextConfig = { basePath, - i18n, + i18n: { + locales: ["en-US", "es-US"], + defaultLocale: "en-US", + }, 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 @@ -23,12 +26,9 @@ const nextConfig = { 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 = nextConfig; +module.exports = withNextIntl(nextConfig); diff --git a/app/package-lock.json b/app/package-lock.json index 47e36f5a..86387433 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -11,12 +11,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", @@ -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", @@ -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,24 @@ } } }, - "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==", + "node_modules/next-intl": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-3.2.4.tgz", + "integrity": "sha512-JadPqGzRtk3C4Pf1i4v9//vvpXrmuKLyIBzrDGYRFyDaAkrV1qorVc9NkBkNCFSqwoM5giSgydEi7fkFtKHhog==", "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" + "url": "https://github.com/sponsors/amannn" } ], "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.4" }, "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 +21809,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 +22103,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 +23110,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 +24507,18 @@ } } }, + "node_modules/use-intl": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-3.2.4.tgz", + "integrity": "sha512-55zHIZ0SWnlzvRvBJhx2iR//SBdItzPF4dEKyWYq2JTSJ5qyGDWb7b2UZ8LXTvhdzptgSL46I6qtYnwoDRvt0A==", + "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 +24642,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 +26610,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 +27284,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 +29291,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 +29450,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 +29518,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 +29638,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 +30052,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 +30445,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 +30506,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", @@ -31214,15 +30912,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 +31080,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 +31099,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 +31124,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 +33300,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 +33430,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 +33598,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 +34842,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 +35933,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 +35975,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 +36052,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 +36216,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 +38320,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 +38426,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 +38811,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 +38842,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.4", + "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-3.2.4.tgz", + "integrity": "sha512-JadPqGzRtk3C4Pf1i4v9//vvpXrmuKLyIBzrDGYRFyDaAkrV1qorVc9NkBkNCFSqwoM5giSgydEi7fkFtKHhog==", "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.4" } }, "no-case": { @@ -40847,19 +40511,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 +40719,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 +41464,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 +42493,15 @@ "tslib": "^2.0.0" } }, + "use-intl": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-3.2.4.tgz", + "integrity": "sha512-55zHIZ0SWnlzvRvBJhx2iR//SBdItzPF4dEKyWYq2JTSJ5qyGDWb7b2UZ8LXTvhdzptgSL46I6qtYnwoDRvt0A==", + "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 +42599,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 55d3bc90..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,12 +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", @@ -42,6 +38,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", @@ -54,9 +51,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", @@ -68,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/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 = () => { @@ -40,9 +37,6 @@ const Header = () => {
-
diff --git a/app/src/components/Layout.tsx b/app/src/components/Layout.tsx index 8f01d33b..6f3f9967 100644 --- a/app/src/components/Layout.tsx +++ b/app/src/components/Layout.tsx @@ -1,17 +1,16 @@ -import { useTranslation } from "next-i18next"; -import { Grid, GridContainer } from "@trussworks/react-uswds"; +import { 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 }: Props) => { - const { t } = useTranslation("common", { - keyPrefix: "Layout", - }); +const Layout = ({ children, locale }: Props) => { + const t = useTranslations("components.Layout"); return ( // Stick the footer to the bottom of the page @@ -19,6 +18,7 @@ const Layout = ({ children }: Props) => { {t("skip_to_main")} +
diff --git a/app/src/i18n/index.ts b/app/src/i18n/index.ts new file mode 100644 index 00000000..70a6f178 --- /dev/null +++ b/app/src/i18n/index.ts @@ -0,0 +1,69 @@ +import { merge } from "lodash"; + +import { 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[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/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:
  • 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, 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/pages/_app.tsx b/app/src/pages/_app.tsx index 67a974eb..314ebaff 100644 --- a/app/src/pages/_app.tsx +++ b/app/src/pages/_app.tsx @@ -1,4 +1,3 @@ -import { appWithTranslation } from "next-i18next"; import type { AppProps } from "next/app"; import Head from "next/head"; @@ -6,7 +5,14 @@ import Layout from "../components/Layout"; import "../styles/styles.scss"; -function MyApp({ Component, pageProps }: AppProps) { +import { defaultLocale, formats, Messages } from "src/i18n"; + +import { NextIntlClientProvider } from "next-intl"; +import { useRouter } from "next/router"; + +function MyApp({ Component, pageProps }: AppProps<{ messages: Messages }>) { + const router = useRouter(); + return ( <> @@ -15,11 +21,17 @@ function MyApp({ Component, pageProps }: AppProps) { href={`${process.env.NEXT_PUBLIC_BASE_PATH ?? ""}/img/logo.svg`} /> - - - + + + + + ); } -export default appWithTranslation(MyApp); +export default MyApp; diff --git a/app/src/pages/health.tsx b/app/src/pages/health.tsx index e4afd4f2..9140931c 100644 --- a/app/src/pages/health.tsx +++ b/app/src/pages/health.tsx @@ -1,15 +1,7 @@ -import type { GetServerSideProps, NextPage } from "next"; - -import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import React from "react"; -const Health: NextPage = () => { +const Health = () => { 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 index 4054147f..5d57d105 100644 --- a/app/src/pages/index.tsx +++ b/app/src/pages/index.tsx @@ -1,11 +1,11 @@ import type { GetServerSideProps, NextPage } from "next"; +import { getLocaleMessages } from "src/i18n"; -import { Trans, useTranslation } from "next-i18next"; -import { serverSideTranslations } from "next-i18next/serverSideTranslations"; +import { useTranslations } from "next-intl"; import Head from "next/head"; const Home: NextPage = () => { - const { t } = useTranslation("home"); + const t = useTranslations("home"); return ( <> @@ -17,35 +17,24 @@ const Home: NextPage = () => { {/* Demonstration of more complex translated strings, with safe-listed links HTML elements */}

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

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

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

    @@ -53,9 +42,12 @@ const Home: NextPage = () => { }; // 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 const getServerSideProps: GetServerSideProps = ({ locale }) => { + return Promise.resolve({ + props: { + messages: getLocaleMessages(locale), + }, + }); }; export default Home; 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/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; 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/components/Header.test.tsx b/app/tests/components/Header.test.tsx index 8ca91fc5..c6c4e866 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/react-utils"; import Header from "src/components/Header"; diff --git a/app/tests/components/Layout.test.tsx b/app/tests/components/Layout.test.tsx index bae699f8..e9be9bdd 100644 --- a/app/tests/components/Layout.test.tsx +++ b/app/tests/components/Layout.test.tsx @@ -1,5 +1,5 @@ -import { render, screen } from "@testing-library/react"; import { axe } from "jest-axe"; +import { render, screen } from "tests/react-utils"; import Layout from "src/components/Layout"; 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/pages/index.test.tsx b/app/tests/pages/index.test.tsx index 2d2ef602..70b43b72 100644 --- a/app/tests/pages/index.test.tsx +++ b/app/tests/pages/index.test.tsx @@ -1,6 +1,6 @@ -import { render, screen } from "@testing-library/react"; import { axe } from "jest-axe"; import Index from "src/pages/index"; +import { render, screen } from "tests/react-utils"; describe("Index", () => { // Demonstration of rendering translated text, and asserting the presence of a dynamic value. diff --git a/app/tests/react-utils.tsx b/app/tests/react-utils.tsx new file mode 100644 index 00000000..a9b39690 --- /dev/null +++ b/app/tests/react-utils.tsx @@ -0,0 +1,37 @@ +/** + * @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 { defaultLocale, formats, getLocaleMessages } from "src/i18n"; + +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 ( + + {children} + + ); +}; + +// 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 = {} +) { + return _render(ui, { wrapper: GlobalProviders, ...options }); +} 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/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" - } -} diff --git a/docs/internationalization.md b/docs/internationalization.md index 80873020..fd174fba 100644 --- a/docs/internationalization.md +++ b/docs/internationalization.md @@ -1,78 +1,31 @@ # Internationalization (i18n) -- [I18next](https://www.i18next.com/) is used for internationalization. -- Next.js's [internationalized routing](https://nextjs.org/docs/advanced-features/i18n-routing) feature is enabled. Toggling between languages is done by changing the URL's path prefix (e.g. `/about` ➡️ `/es-US/about`). -- Configuration for the i18n routing and i18next libraries are located in [`next-i18next.config.js`](../app/next-i18next.config.js). For the most part, you shouldn't need to edit this file unless adding a new language. -- [storybook-react-i18next](https://storybook.js.org/addons/storybook-react-i18next) adds a globe icon to Storybook's toolbar for toggling languages. +- [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. ## Managing translations -- Translations are managed as JSON files in the `public/locales` directory, where each language has its own directory (e.g. `en-US` and `es-US`). Each file is also referred to as a "namespace". -- [**Namespaces**](https://www.i18next.com/principles/namespaces) can be used to organize translations into smaller files. For large sites, it's common to create a namespace for each controller, page, or feature (whatever level makes most sense). See "Type-safe translations" below for additional considerations when adding new namespaces. -- There are a number of built-in formatters based on [JS's `Intl` API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl) that can be used in locale strings, and custom formatters can be added as well. [See the i18next formatting docs for details](https://www.i18next.com/translation-function/formatting#built-in-formats). +- Translations are managed as files in the [`i18n/messages`](../app/src/i18n/messages/) directory, where each language has its own directory (e.g. `en-US` and `es-US`). +- How you organize translations is up to you, but here are some suggestions: + - Group your messages. It's recommended to use component/page names as namespaces and embrace them as the primary unit of organization in your app. + - By default, all messages are in a single file, but you can split them into multiple files if you prefer. Continue to export all messages from `i18n/messages/{locale}/index.ts` so that they can be imported from a single location, and so files that depend on the messages don't need to be updated. +- There are a number of built-in formatters based on [JS's `Intl` API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl) that can be used in locale strings, and custom formatters can be added as well. [See the formatting docs for details](https://next-intl-docs.vercel.app/docs/usage/numbers). - If a string's translation is missing in another language, the default language (usually English) will be used as a fallback. ### Type-safe translations -I18next is configured to report errors if you attempt to reference an i18n key path that doesn't exist in a locale file. Type-safe internationalization requires a bit of extra work to maintain, but it can be an extremely helpful tool for catching translation errors early. +The app is configured to report errors if you attempt to reference an i18n key path that doesn't exist in a locale file. -#### How it works - -1. An NPM script (`i18n-types`) transforms the JSON locale files into a generated TypeScript file: [`generated-i18n-bundle.ts`](../app/src/types/generated-i18n-bundle.ts) -1. [`types/i18next.d.ts`](../app/src/types/i18next.d.ts) configures i18next to use the generated TypeScript file as the source of truth for the available keys. If a key isn't in the generated file, TypeScript will report an error for the key. - -[Learn more about using TypeScript with i18next](https://www.i18next.com/overview/typescript). +[Learn more about using TypeScript with next-intl](https://next-intl-docs.vercel.app/docs/workflows/typescript). ## Load translations -1. `serverSideTranslations` must be called in [`getStaticProps`](https://nextjs.org/docs/basic-features/data-fetching/get-static-props) or [`getServerSideProps`](https://nextjs.org/docs/basic-features/data-fetching/get-server-side-props) to load translations for a page. - - ```tsx - import type { GetServerSideProps } from "next"; - import { serverSideTranslations } from "next-i18next/serverSideTranslations"; - - export const getServerSideProps: GetServerSideProps = async ({ locale }) => { - // serverSideTranslations takes an optional second argument to limit - // which namespaces are sent to the client - const translations = await serverSideTranslations(locale ?? "en-US"); - return { props: { ...translations } }; - }; - ``` - - Note that `serverSideTranslations` needs imported in the same file as the `getServerSideProps` / `getStaticProps` function, so that Next.js properly excludes it from the client-side bundle, where Node.js APIs (e.g. `fs`) aren't available. - -1. Then use the `useTranslation` hook's `t()` method, or the `Trans` component to render localized strings. - - ```tsx - import { Trans, useTranslation } from "next-i18next"; - - const Page = () => { - const { t } = useTranslation(); - - return ( - <> -

    {t("About.title")}

    - - - ); - }; - ``` - - By default, `useTranslation` and `Trans` load translations from the `common` namespace. To load translations from a different namespace, you can pass the `ns` prop to `Trans`, or the `ns` option to `useTranslation`. - - ```tsx - const { t } = useTranslation("someOtherNamespace"); - ``` - - ```tsx - - ``` +Locale messages should only ever be loaded on the server-side, to avoid bloating the client-side bundle. If a client component needs to access translations, only the messages required by that component should be passed into it. -Refer to the [i18next](https://www.i18next.com/) and [react-i18next](https://react.i18next.com/) documentation for more usage docs. +[See the Internationalization of Server & Client Components docs](https://next-intl-docs.vercel.app/docs/environments/server-client-components) for more details. ## Add a new language -1. Edit `next-i18next.config.js` and add the language to `locales`, using the [BCP 47 language tag](https://www.w3.org/International/articles/language-tags/) (e.g. `en` or `en-US`). -1. Add a language folder, using the same BCP47 language tag: `mkdir -p public/locales/` -1. Add a language file: `touch public/locales//common.json` 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. Add a label for the language to the `locales` object in [`.storybook/preview.js`](../app/.storybook/preview.js) +1. Add a language folder, using the same BCP47 language tag: `mkdir -p src/i18n/messages/` +1. Add a language file: `touch src/i18n/messages//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. 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"],