From 4258fd0108729267ec380c9fa1579824c794bf3c Mon Sep 17 00:00:00 2001 From: Sauli Purhonen Date: Wed, 6 Oct 2021 10:03:38 +0300 Subject: [PATCH] i18n support --- cypress/integration/i18n.spec.js | 38 ++++++ package-lock.json | 88 +++++++++++-- package.json | 2 + src/App.js | 44 +++++-- src/__tests__/Home.test.js | 6 +- src/__tests__/Nav.test.js | 4 +- src/__tests__/PublishedFoldersList.test.js | 8 +- src/__tests__/SubmissionDetailTable.test.js | 6 +- src/__tests__/UnpublishedFoldersList.test.js | 8 +- src/components/Home/SelectedFolderDetails.js | 14 +-- src/components/Home/SubmissionDetailTable.js | 3 +- src/components/Home/SubmissionFolderList.js | 4 +- src/components/Home/SubmissionIndexCard.js | 11 +- src/components/Nav.js | 117 +++++++++++++++--- .../WizardComponents/WizardDraftSelections.js | 4 +- .../WizardComponents/WizardFooter.js | 4 +- .../WizardComponents/WizardStepper.js | 10 +- .../WizardSteps/WizardCreateFolderStep.js | 5 +- src/constants/locale.js | 3 + src/features/localeSlice.js | 24 ++++ src/i18n.js | 29 +++++ src/index.js | 2 + src/rootReducer.js | 2 + src/translations/translation_en.json | 3 + src/translations/translation_fi.json | 3 + src/utils/index.js | 9 ++ src/views/Home.js | 7 +- src/views/NewDraftWizard.js | 4 +- 28 files changed, 391 insertions(+), 71 deletions(-) create mode 100644 cypress/integration/i18n.spec.js create mode 100644 src/constants/locale.js create mode 100644 src/features/localeSlice.js create mode 100644 src/i18n.js create mode 100644 src/translations/translation_en.json create mode 100644 src/translations/translation_fi.json diff --git a/cypress/integration/i18n.spec.js b/cypress/integration/i18n.spec.js new file mode 100644 index 000000000..5e70abef2 --- /dev/null +++ b/cypress/integration/i18n.spec.js @@ -0,0 +1,38 @@ +describe("Internationalization", function () { + it("should login with finnish translation when finnish locale is chosen", () => { + const baseUrl = "http://localhost:" + Cypress.env("port") + "/" + + cy.visit(baseUrl) + + cy.get("#lang-selector").click() + cy.get("li[role=menuitem]").contains("Fi").click() + + cy.get('[alt="CSC Login"]').click() + cy.wait(1000) + + cy.get("[data-testid='logged-in-as'").contains("Kirjautuneena") + }) + + it("should change translation seamlessly", () => { + cy.login() + + cy.get("[data-testid='logged-in-as'").contains("Logged in as") + + cy.get("#lang-selector").click() + cy.get("li[role=menuitem]").contains("Fi").click() + cy.url().should("include", "/fi/") + + cy.get("[data-testid='logged-in-as'").contains("Kirjautuneena") + }) + + it("should navigate with selected locale", () => { + cy.login() + + cy.get("#lang-selector").click() + cy.get("li[role=menuitem]").contains("Fi").click() + + cy.get("button", { timeout: 10000 }).contains("Create Submission").click() + + cy.url().should("include", "/fi/newdraft") + }) +}) diff --git a/package-lock.json b/package-lock.json index ab215f964..e1eff13d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,11 +18,13 @@ "ajv": "^8.6.3", "ajv-formats": "^2.1.1", "apisauce": "^2.1.1", + "i18next": "^21.2.4", "jest": "26.6.0", "lodash": "^4.17.21", "react": "^17.0.2", "react-dom": "^17.0.2", "react-hook-form": "^7.16.2", + "react-i18next": "^11.12.0", "react-redux": "^7.2.5", "react-router-dom": "^5.3.0", "react-scripts": "^4.0.3" @@ -1563,11 +1565,14 @@ } }, "node_modules/@babel/runtime": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.0.tgz", - "integrity": "sha512-JELkvo/DlpNdJ7dlyw/eY7E0suy5i5GQH+Vlxaq1nsNJ+H7f4Vtv3jMeCEgRhZZQFXTjldYfQgv2qmM6M1v5wA==", + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.4.tgz", + "integrity": "sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==", "dependencies": { "regenerator-runtime": "^0.13.4" + }, + "engines": { + "node": ">=6.9.0" } }, "node_modules/@babel/runtime-corejs3": { @@ -10040,6 +10045,14 @@ "node": ">= 6" } }, + "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": "4.5.0", "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-4.5.0.tgz", @@ -10223,6 +10236,14 @@ "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz", "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==" }, + "node_modules/i18next": { + "version": "21.2.4", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-21.2.4.tgz", + "integrity": "sha512-+81XmiwJOLWJFjRZJK5ASFahAo5TXZGz5IrBT4CfLJ3CyXho61A1cj1Kmh8za8TYtGFou0cEkUSjEaqfya7Wfg==", + "dependencies": { + "@babel/runtime": "^7.12.0" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -16324,6 +16345,19 @@ "react": "^16.8.0 || ^17" } }, + "node_modules/react-i18next": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.12.0.tgz", + "integrity": "sha512-M9BT+hqVG03ywrl+L7CK74ugK+4jIo7AeKJ17+g9BoqJz2+/aVbs8SIVXT4KMQ1rjIdcw+GcSRDy1CXjcz6tLQ==", + "dependencies": { + "@babel/runtime": "^7.14.5", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 19.0.0", + "react": ">= 16.8.0" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -20848,6 +20882,14 @@ "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==" }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha1-YU9/v42AHwu18GYfWy9XhXUOTwk=", + "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", @@ -23981,9 +24023,9 @@ } }, "@babel/runtime": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.0.tgz", - "integrity": "sha512-JELkvo/DlpNdJ7dlyw/eY7E0suy5i5GQH+Vlxaq1nsNJ+H7f4Vtv3jMeCEgRhZZQFXTjldYfQgv2qmM6M1v5wA==", + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.4.tgz", + "integrity": "sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==", "requires": { "regenerator-runtime": "^0.13.4" } @@ -25605,9 +25647,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "requires": { - "ajv": "^8.0.0" - } + "requires": {} }, "alphanum-sort": { "version": "1.0.2", @@ -30612,6 +30652,14 @@ } } }, + "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-webpack-plugin": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-4.5.0.tgz", @@ -30761,6 +30809,14 @@ "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz", "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==" }, + "i18next": { + "version": "21.2.4", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-21.2.4.tgz", + "integrity": "sha512-+81XmiwJOLWJFjRZJK5ASFahAo5TXZGz5IrBT4CfLJ3CyXho61A1cj1Kmh8za8TYtGFou0cEkUSjEaqfya7Wfg==", + "requires": { + "@babel/runtime": "^7.12.0" + } + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -35511,6 +35567,15 @@ "integrity": "sha512-99BOspznkVoYppJJSMRsQv9jIw65P0T5g3JhegeosRAeNut/VEEViuBPViDb+mU4sP+uaAcPqqSa7dxZOOgknw==", "requires": {} }, + "react-i18next": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.12.0.tgz", + "integrity": "sha512-M9BT+hqVG03ywrl+L7CK74ugK+4jIo7AeKJ17+g9BoqJz2+/aVbs8SIVXT4KMQ1rjIdcw+GcSRDy1CXjcz6tLQ==", + "requires": { + "@babel/runtime": "^7.14.5", + "html-parse-stringify": "^3.0.1" + } + }, "react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -39062,6 +39127,11 @@ "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==" }, + "void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha1-YU9/v42AHwu18GYfWy9XhXUOTwk=" + }, "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 58d2001b4..103f20752 100644 --- a/package.json +++ b/package.json @@ -13,11 +13,13 @@ "ajv": "^8.6.3", "ajv-formats": "^2.1.1", "apisauce": "^2.1.1", + "i18next": "^21.2.4", "jest": "26.6.0", "lodash": "^4.17.21", "react": "^17.0.2", "react-dom": "^17.0.2", "react-hook-form": "^7.16.2", + "react-i18next": "^11.12.0", "react-redux": "^7.2.5", "react-router-dom": "^5.3.0", "react-scripts": "^4.0.3" diff --git a/src/App.js b/src/App.js index 4f53b489a..bff385faf 100644 --- a/src/App.js +++ b/src/App.js @@ -4,13 +4,15 @@ import React, { useEffect } from "react" import Container from "@material-ui/core/Container" import CssBaseline from "@material-ui/core/CssBaseline" import { makeStyles } from "@material-ui/core/styles" -import { useDispatch } from "react-redux" -import { Switch, Route, useLocation } from "react-router-dom" +import i18n from "i18next" +import { useDispatch, useSelector } from "react-redux" +import { Switch, Route, useLocation, Redirect } from "react-router-dom" import SelectedFolderDetails from "components/Home/SelectedFolderDetails" import SubmissionFolderList from "components/Home/SubmissionFolderList" import Nav from "components/Nav" import { ObjectTypes } from "constants/wizardObject" +import { setLocale } from "features/localeSlice" import { setObjectTypesArray } from "features/objectTypesArraySlice" import schemaAPIService from "services/schemaAPI" import Page400 from "views/ErrorPages/Page400" @@ -67,9 +69,13 @@ const App = (): React$Element => { const classes = useStyles() const dispatch = useDispatch() + const locale = useSelector(state => state.locale) + // Fetch array of schemas from backend and store it in frontend // Fetch only if the initial array is empty // if there is any errors while fetching, it will return a manually created ObjectsArray instead + // && + // Handle initial locale setting useEffect(() => { if (location.pathname === "/" || pathsWithoutNav.indexOf(location.pathname) !== -1) return let isMounted = true @@ -98,18 +104,40 @@ const App = (): React$Element => { } } } + + // Get locale from url and set application wide locale setting + const getLocale = () => { + let locale: string + const locales = ["en", "fi"] + const currentLocale = location.pathname.split("/")[1] + + if (locales.indexOf(currentLocale) > -1) { + locale = currentLocale + } else locale = "en" + + i18n.changeLanguage(locale) + + dispatch(setLocale(locale)) + } + getSchemas() + getLocale() return () => { isMounted = false } }, []) + const setPath = (path: string) => { + return `/:locale(en|fi)/${path}` + } + return ( - + + @@ -119,27 +147,27 @@ const App = (): React$Element => { - + - + - + - + - + diff --git a/src/__tests__/Home.test.js b/src/__tests__/Home.test.js index 2995deefd..f01440df0 100644 --- a/src/__tests__/Home.test.js +++ b/src/__tests__/Home.test.js @@ -12,6 +12,10 @@ import App from "App" const middlewares = [thunk] const mockStore = configureStore(middlewares) +jest.mock("react-i18next", () => ({ + useTranslation: () => ({ t: key => key }), +})) + describe("HomePage", () => { const store = mockStore({ user: { name: "Test User" }, @@ -27,7 +31,7 @@ describe("HomePage", () => { beforeEach(() => { render( - + diff --git a/src/__tests__/Nav.test.js b/src/__tests__/Nav.test.js index e08e08199..12aab78bd 100644 --- a/src/__tests__/Nav.test.js +++ b/src/__tests__/Nav.test.js @@ -18,7 +18,7 @@ describe("NavBar", () => { beforeEach(() => { component = render( - +