From b18dc2e815209859ed947f780a568de306708450 Mon Sep 17 00:00:00 2001 From: Trish Rempel Date: Sat, 10 Jun 2023 14:19:15 -0500 Subject: [PATCH] Add i18n framework and extract hard coded strings --- package-lock.json | 125 +++++++++++++++++++++++++++ package.json | 4 + public/locales/ar/translation.json | 14 +++ public/locales/en/translation.json | 14 +++ public/locales/fr/translation.json | 14 +++ src/App.jsx | 133 +++++++++++++++++++---------- src/i18n.js | 19 +++++ src/index.js | 13 +-- 8 files changed, 285 insertions(+), 51 deletions(-) create mode 100644 public/locales/ar/translation.json create mode 100644 public/locales/en/translation.json create mode 100644 public/locales/fr/translation.json create mode 100644 src/i18n.js diff --git a/package-lock.json b/package-lock.json index 6343aa8..927fa50 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,8 +11,12 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "i18next": "^22.5.1", + "i18next-browser-languagedetector": "^7.0.2", + "i18next-http-backend": "^2.2.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-i18next": "^12.3.1", "react-scripts": "5.0.1", "web-vitals": "^2.1.4" }, @@ -6174,6 +6178,14 @@ "node": ">=10" } }, + "node_modules/cross-fetch": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.6.tgz", + "integrity": "sha512-riRvo06crlE8HiqOwIpQhxwdOk4fOeR7FVM/wXoxchFEqMNUjvbs3bfo4OTgMEMHzppd4DxFBDbyySj8Cv781g==", + "dependencies": { + "node-fetch": "^2.6.11" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -8950,6 +8962,14 @@ "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-webpack-plugin": { "version": "5.5.1", "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.1.tgz", @@ -9084,6 +9104,44 @@ "node": ">=10.17.0" } }, + "node_modules/i18next": { + "version": "22.5.1", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-22.5.1.tgz", + "integrity": "sha512-8TGPgM3pAD+VRsMtUMNknRz3kzqwp/gPALrWMsDnmC1mKqJwpWyooQRLMcbTwq8z8YwSmuj+ZYvc+xCuEpkssA==", + "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.20.6" + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.0.2.tgz", + "integrity": "sha512-5ViaK+gikxfqZ9M3jJ7gJkUzzu/p3HwiqfLoL1bdiL7CUb0IylcTyVLdPaTU3pH5VFWFCiGFuJDg3VkLUikWgg==", + "dependencies": { + "@babel/runtime": "^7.19.4" + } + }, + "node_modules/i18next-http-backend": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-2.2.1.tgz", + "integrity": "sha512-ZXIdn/8NJIBJ0X4hzXfc3STYxKrCKh1fYjji9HPyIpEJfvTvy8/ZlTl8RuTizzCPj2ZcWrfaecyOMKs6bQ7u5A==", + "dependencies": { + "cross-fetch": "3.1.6" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -12333,6 +12391,44 @@ "tslib": "^2.0.3" } }, + "node_modules/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", @@ -14503,6 +14599,27 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" }, + "node_modules/react-i18next": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-12.3.1.tgz", + "integrity": "sha512-5v8E2XjZDFzK7K87eSwC7AJcAkcLt5xYZ4+yTPDAW1i7C93oOY1dnr4BaQM7un4Hm+GmghuiPvevWwlca5PwDA==", + "dependencies": { + "@babel/runtime": "^7.20.6", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 19.0.0", + "react": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -16520,6 +16637,14 @@ "node": ">= 0.8" } }, + "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-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", diff --git a/package.json b/package.json index a2ed920..f89c6f5 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,12 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "i18next": "^22.5.1", + "i18next-browser-languagedetector": "^7.0.2", + "i18next-http-backend": "^2.2.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-i18next": "^12.3.1", "react-scripts": "5.0.1", "web-vitals": "^2.1.4" }, diff --git a/public/locales/ar/translation.json b/public/locales/ar/translation.json new file mode 100644 index 0000000..3d3a882 --- /dev/null +++ b/public/locales/ar/translation.json @@ -0,0 +1,14 @@ +{ + "label-amount": "أدخل مبلغًا:", + "label-currency": "أدخل عملة:", + "label-date": "أدخل تاريخًا:", + "label-formatted-amount": "المبلغ المنسق:", + "label-formatted-currency": "العملة المنسقة:", + "label-formatted-date": "التاريخ المنسق:", + "label-formatted-quantity": "الكمية المنسقة:", + "label-locale": "أدخل لغة:", + "label-quantity": "أدخل كمية:", + "label-formatted-file-type": "نوع الملف المنسق:", + "label-file-type": "حدد نوع الملف:", + "heading": "التدويل للجميع" +} diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json new file mode 100644 index 0000000..b28cfdb --- /dev/null +++ b/public/locales/en/translation.json @@ -0,0 +1,14 @@ +{ + "heading": "Internationalization is for Everyone", + "label-locale": "Enter a locale:", + "label-date": "Enter a date:", + "label-formatted-date": "Formatted date:", + "label-currency": "Enter a currency:", + "label-quantity": "Enter a quantity:", + "label-formatted-quantity": "Formatted quantity:", + "label-amount": "Enter an amount:", + "label-formatted-amount": "Formatted amount:", + "label-formatted-currency": "Formatted currency:", + "label-file-type": "Select a file type:", + "label-formatted-file-type": "Formatted file type:" +} diff --git a/public/locales/fr/translation.json b/public/locales/fr/translation.json new file mode 100644 index 0000000..0c8fbfc --- /dev/null +++ b/public/locales/fr/translation.json @@ -0,0 +1,14 @@ +{ + "label-amount": "Saisissez un montant :", + "label-currency": "Saisissez une devise :", + "label-date": "Saisissez une date :", + "label-formatted-amount": "Montant formaté :", + "label-formatted-currency": "Devise formatée :", + "label-formatted-date": "Date formatée :", + "label-formatted-quantity": "Quantité formatée :", + "label-locale": "Saisissez un paramètre régional :", + "label-quantity": "Saisissez une quantité :", + "label-formatted-file-type": "Type de fichier formaté :", + "label-file-type": "Sélectionnez un type de fichier :", + "heading": "L'internationalisation est pour tout le monde" +} diff --git a/src/App.jsx b/src/App.jsx index 4f11621..227e345 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,9 +1,18 @@ import "./App.css"; -import React, { useEffect, useState, useCallback, useMemo } from "react"; +import React, { + useEffect, + useState, + useCallback, + useMemo, + Suspense, +} from "react"; +import { useTranslation } from "react-i18next"; +import LoadingSpinner from "./components/LoadingSpinner"; const FILE_TYPES = ["file", "image", "video"]; function App() { + const { t, i18n } = useTranslation(); const [formattedFileType, setFormattedFileType] = useState(""); const [formattedDate, setFormattedDate] = useState(""); const [formattedQuantity, setFormattedQuantity] = useState(""); @@ -34,6 +43,15 @@ function App() { updateFormattedAmount(); }, [amount, currency, updateFormattedAmount]); + const handleLocaleChange = (e) => { + i18n.changeLanguage(e.target.value); + setFormattedFileType(""); + setFormattedDate(""); + setFormattedQuantity(""); + setFormattedAmount(""); + setFormattedCurrency(""); + }; + const handleFileTypeChange = (e) => { const fileType = e.target.value; setFormattedFileType(`The ${fileType} is ready.`); @@ -74,50 +92,75 @@ function App() { }; return ( -
-
-

Internationalization is for Everyone

-
- - - -
{formattedFileType}
- - - -
{formattedDate}
- - - -
{formattedQuantity}
- - - - - -
{formattedAmount}
- -
{formattedCurrency}
-
-

- Slide deck available{" "} - - here - - . -

-
-
+ +
+
+

{t("heading")}

+
+ + + + + +
{formattedFileType}
+ + + +
{formattedDate}
+ + + +
{formattedQuantity}
+ + + + + +
{formattedAmount}
+ +
{formattedCurrency}
+
+

+ Slide deck available{" "} + + here + + . +

+
+
+
); } diff --git a/src/i18n.js b/src/i18n.js new file mode 100644 index 0000000..41140fd --- /dev/null +++ b/src/i18n.js @@ -0,0 +1,19 @@ +import i18n from "i18next"; +import { initReactI18next } from "react-i18next"; + +import Backend from "i18next-http-backend"; +import LanguageDetector from "i18next-browser-languagedetector"; + +i18n + .use(Backend) + .use(LanguageDetector) + .use(initReactI18next) + .init({ + fallbackLng: "en", + debug: true, + interpolation: { + escapeValue: false, + }, + }); + +export default i18n; diff --git a/src/index.js b/src/index.js index d563c0f..f389a9c 100644 --- a/src/index.js +++ b/src/index.js @@ -1,10 +1,11 @@ -import React from 'react'; -import ReactDOM from 'react-dom/client'; -import './index.css'; -import App from './App'; -import reportWebVitals from './reportWebVitals'; +import React from "react"; +import ReactDOM from "react-dom/client"; +import "./index.css"; +import App from "./App"; +import reportWebVitals from "./reportWebVitals"; +import "./i18n"; -const root = ReactDOM.createRoot(document.getElementById('root')); +const root = ReactDOM.createRoot(document.getElementById("root")); root.render(