From fde0ce6321244507f8ece284137ec3fba8a67815 Mon Sep 17 00:00:00 2001 From: Sean Scully Date: Tue, 6 Feb 2024 10:52:15 +0100 Subject: [PATCH] feature: add react-i18next with ICU (#183) --- packages/frontend-design-poc/README.md | 8 ++ packages/frontend-design-poc/package.json | 3 + .../src/components/HelloWorld/HelloWorld.tsx | 8 +- .../frontend-design-poc/src/i18n/config.ts | 13 +++ .../frontend-design-poc/src/i18n/index.ts | 19 ++++ .../src/i18n/resources/nb.json | 3 + packages/frontend-design-poc/src/main.tsx | 4 +- .../frontend-design-poc/utils/test-utils.tsx | 2 + pnpm-lock.yaml | 97 +++++++++++++++++++ 9 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 packages/frontend-design-poc/src/i18n/config.ts create mode 100644 packages/frontend-design-poc/src/i18n/index.ts create mode 100644 packages/frontend-design-poc/src/i18n/resources/nb.json diff --git a/packages/frontend-design-poc/README.md b/packages/frontend-design-poc/README.md index e0420eafb..e32189c4d 100644 --- a/packages/frontend-design-poc/README.md +++ b/packages/frontend-design-poc/README.md @@ -16,3 +16,11 @@ Typesript and CSS modules. ### Mock Uses [msw](https://mswjs.io/) as API mocking library. + +### i18n + +This project uses [react-i18next](https://react.i18next.com/) as internationalization framework, and is configured +with [ICU format](https://react.i18next.com/misc/using-with-icu-format), a widely used standard for message format. +[This page](https://unicode-org.github.io/icu/userguide/format_parse/messages/) describes the format and covers the most common use cases, including more complex examples. + +`react-i18next`is configured in `./src/i18n/index.ts` and initiated as an import in `main.tsx`. diff --git a/packages/frontend-design-poc/package.json b/packages/frontend-design-poc/package.json index 2f8b79b19..968f4ded9 100644 --- a/packages/frontend-design-poc/package.json +++ b/packages/frontend-design-poc/package.json @@ -14,8 +14,11 @@ "dependencies": { "@digdir/design-system-react": "^0.47.0", "@digdir/design-system-tokens": "^0.12.0", + "i18next": "^23.8.2", + "i18next-icu": "^2.3.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-i18next": "^14.0.3", "react-query": "^3.39.3", "react-router-dom": "^6.16.0" }, diff --git a/packages/frontend-design-poc/src/components/HelloWorld/HelloWorld.tsx b/packages/frontend-design-poc/src/components/HelloWorld/HelloWorld.tsx index 0bf59c474..49974b136 100644 --- a/packages/frontend-design-poc/src/components/HelloWorld/HelloWorld.tsx +++ b/packages/frontend-design-poc/src/components/HelloWorld/HelloWorld.tsx @@ -1,12 +1,18 @@ import styles from "./helloWorld.module.css"; import { useQuery } from "react-query"; import { getUser } from "../../api/queries.ts"; +import { useTranslation } from "react-i18next"; export const HelloWorld = () => { const { isLoading, data } = useQuery("user", getUser); + const { t } = useTranslation(); return (
- {isLoading ? Loading ... :

Hello, {data?.name}!

} + {isLoading ? ( + Loading ... + ) : ( +

{t("example.hello", { person: data?.name })}!

+ )}
); }; diff --git a/packages/frontend-design-poc/src/i18n/config.ts b/packages/frontend-design-poc/src/i18n/config.ts new file mode 100644 index 000000000..ebfab3bad --- /dev/null +++ b/packages/frontend-design-poc/src/i18n/config.ts @@ -0,0 +1,13 @@ +import nb from "./resources/nb.json"; + +export const i18nInitConfig = { + resources: { + nb: { translation: nb }, + }, + lng: "nb", + fallbackLng: "nb", + debug: false, + interpolation: { + escapeValue: false, + }, +}; diff --git a/packages/frontend-design-poc/src/i18n/index.ts b/packages/frontend-design-poc/src/i18n/index.ts new file mode 100644 index 000000000..f7e4f01fb --- /dev/null +++ b/packages/frontend-design-poc/src/i18n/index.ts @@ -0,0 +1,19 @@ +import i18n from "i18next"; +import ICU from "i18next-icu"; +import { initReactI18next } from "react-i18next"; + +import nb from "./resources/nb.json"; + +const i18nInitConfig = { + resources: { + nb: { translation: nb }, + }, + lng: "nb", + fallbackLng: "nb", + debug: false, + interpolation: { + escapeValue: false, + }, +}; + +await i18n.use(ICU).use(initReactI18next).init(i18nInitConfig); diff --git a/packages/frontend-design-poc/src/i18n/resources/nb.json b/packages/frontend-design-poc/src/i18n/resources/nb.json new file mode 100644 index 000000000..be60e0435 --- /dev/null +++ b/packages/frontend-design-poc/src/i18n/resources/nb.json @@ -0,0 +1,3 @@ +{ + "example.hello": "Hei, {person}" +} \ No newline at end of file diff --git a/packages/frontend-design-poc/src/main.tsx b/packages/frontend-design-poc/src/main.tsx index 05659cbd6..4a7aa4886 100644 --- a/packages/frontend-design-poc/src/main.tsx +++ b/packages/frontend-design-poc/src/main.tsx @@ -1,8 +1,10 @@ import React from "react"; import ReactDOM from "react-dom/client"; -import App from "./App.tsx"; import { QueryClient, QueryClientProvider } from "react-query"; import { BrowserRouter } from "react-router-dom"; +import "./i18n/"; + +import App from "./App.tsx"; async function enableMocking() { if (import.meta.env.MODE === "development") { diff --git a/packages/frontend-design-poc/utils/test-utils.tsx b/packages/frontend-design-poc/utils/test-utils.tsx index c57fed221..b06f4aa81 100644 --- a/packages/frontend-design-poc/utils/test-utils.tsx +++ b/packages/frontend-design-poc/utils/test-utils.tsx @@ -3,6 +3,8 @@ import { ReactElement } from "react"; import { MemoryRouter } from "react-router-dom"; import { render, RenderOptions } from "@testing-library/react"; +import "../src/i18n/"; + interface IExtendedRenderOptions extends RenderOptions { initialEntries?: string[]; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 19820dd74..763b3505d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -277,12 +277,21 @@ importers: '@digdir/design-system-tokens': specifier: ^0.12.0 version: 0.12.0 + i18next: + specifier: ^23.8.2 + version: 23.8.2 + i18next-icu: + specifier: ^2.3.0 + version: 2.3.0(intl-messageformat@10.5.11) react: specifier: ^18.2.0 version: 18.2.0 react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) + react-i18next: + specifier: ^14.0.3 + version: 14.0.3(i18next@23.8.2)(react-dom@18.2.0)(react@18.2.0) react-query: specifier: ^3.39.3 version: 3.39.3(react-dom@18.2.0)(react@18.2.0) @@ -2826,6 +2835,40 @@ packages: /@floating-ui/utils@0.2.1: resolution: {integrity: sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==} + /@formatjs/ecma402-abstract@1.18.2: + resolution: {integrity: sha512-+QoPW4csYALsQIl8GbN14igZzDbuwzcpWrku9nyMXlaqAlwRBgl5V+p0vWMGFqHOw37czNXaP/lEk4wbLgcmtA==} + dependencies: + '@formatjs/intl-localematcher': 0.5.4 + tslib: 2.6.2 + dev: false + + /@formatjs/fast-memoize@2.2.0: + resolution: {integrity: sha512-hnk/nY8FyrL5YxwP9e4r9dqeM6cAbo8PeU9UjyXojZMNvVad2Z06FAVHyR3Ecw6fza+0GH7vdJgiKIVXTMbSBA==} + dependencies: + tslib: 2.6.2 + dev: false + + /@formatjs/icu-messageformat-parser@2.7.6: + resolution: {integrity: sha512-etVau26po9+eewJKYoiBKP6743I1br0/Ie00Pb/S/PtmYfmjTcOn2YCh2yNkSZI12h6Rg+BOgQYborXk46BvkA==} + dependencies: + '@formatjs/ecma402-abstract': 1.18.2 + '@formatjs/icu-skeleton-parser': 1.8.0 + tslib: 2.6.2 + dev: false + + /@formatjs/icu-skeleton-parser@1.8.0: + resolution: {integrity: sha512-QWLAYvM0n8hv7Nq5BEs4LKIjevpVpbGLAJgOaYzg9wABEoX1j0JO1q2/jVkO6CVlq0dbsxZCngS5aXbysYueqA==} + dependencies: + '@formatjs/ecma402-abstract': 1.18.2 + tslib: 2.6.2 + dev: false + + /@formatjs/intl-localematcher@0.5.4: + resolution: {integrity: sha512-zTwEpWOzZ2CiKcB93BLngUX59hQkuZjT2+SAQEscSm52peDW/getsawMcWF1rGRpMCX6D7nSJA3CzJ8gn13N/g==} + dependencies: + tslib: 2.6.2 + dev: false + /@humanwhocodes/config-array@0.11.14: resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} engines: {node: '>=10.10.0'} @@ -10017,6 +10060,12 @@ packages: relateurl: 0.2.7 terser: 5.27.0 + /html-parse-stringify@3.0.1: + resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} + dependencies: + void-elements: 3.1.0 + dev: false + /html-tags@3.3.1: resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} engines: {node: '>=8'} @@ -10171,6 +10220,20 @@ packages: engines: {node: '>=16.17.0'} dev: true + /i18next-icu@2.3.0(intl-messageformat@10.5.11): + resolution: {integrity: sha512-x+j7kd5nDJCfbU53uwsMfXD7ALPu5uv0bqjAMQ5nVvXRoj1L7gkmswKtM3XDWYo4YUHf1jznlhSdPyy0xEwU+Q==} + peerDependencies: + intl-messageformat: ^10.3.3 + dependencies: + intl-messageformat: 10.5.11 + dev: false + + /i18next@23.8.2: + resolution: {integrity: sha512-Z84zyEangrlERm0ZugVy4bIt485e/H8VecGUZkZWrH7BDePG6jT73QdL9EA1tRTTVVMpry/MgWIP1FjEn0DRXA==} + dependencies: + '@babel/runtime': 7.23.9 + dev: false + /iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -10310,6 +10373,15 @@ packages: resolution: {integrity: sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==} engines: {node: '>=10.13.0'} + /intl-messageformat@10.5.11: + resolution: {integrity: sha512-eYq5fkFBVxc7GIFDzpFQkDOZgNayNTQn4Oufe8jw6YY6OHVw70/4pA3FyCsQ0Gb2DnvEJEMmN2tOaXUGByM+kg==} + dependencies: + '@formatjs/ecma402-abstract': 1.18.2 + '@formatjs/fast-memoize': 2.2.0 + '@formatjs/icu-messageformat-parser': 2.7.6 + tslib: 2.6.2 + dev: false + /invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} dependencies: @@ -13993,6 +14065,26 @@ packages: resolution: {integrity: sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==} dev: false + /react-i18next@14.0.3(i18next@23.8.2)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Zav2EEnrQNmCawnzj0l7xitj7jipC7kBNG3o6Cl75NwGndvdp/wu3LSVwJpyAc3eSWMwRFYZ5uNi43CtFUDf/g==} + peerDependencies: + i18next: '>= 23.2.3' + react: '>= 16.8.0' + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + dependencies: + '@babel/runtime': 7.23.9 + html-parse-stringify: 3.0.1 + i18next: 23.8.2 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -16438,6 +16530,11 @@ packages: - terser dev: true + /void-elements@3.1.0: + resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} + engines: {node: '>=0.10.0'} + dev: false + /w3c-hr-time@1.0.2: resolution: {integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==} deprecated: Use your platform's native performance.now() and performance.timeOrigin.