From a36f64efdb70225ac19da59d4e6640912fc8650b Mon Sep 17 00:00:00 2001 From: Tudor Stanciu Date: Wed, 13 May 2020 17:56:51 +0300 Subject: [PATCH] i18n --- package-lock.json | 54 ++++++++- package.json | 5 + private/Notes.txt | 9 ++ public/locales/en/translations.json | 12 ++ public/locales/ro/translations.json | 3 + src/api/api.js | 3 + src/api/axiosApi.js | 4 +- .../session/components/SessionSummary.js | 21 +++- src/index.js | 1 + src/utils/i18n.js | 106 ++++++++++++++++++ 10 files changed, 212 insertions(+), 6 deletions(-) create mode 100644 private/Notes.txt create mode 100644 public/locales/en/translations.json create mode 100644 public/locales/ro/translations.json create mode 100644 src/utils/i18n.js diff --git a/package-lock.json b/package-lock.json index 77a4d1e..19db94d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4930,6 +4930,14 @@ "uglify-js": "3.4.x" } }, + "html-parse-stringify2": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify2/-/html-parse-stringify2-2.0.1.tgz", + "integrity": "sha1-3FZwtyksoVi3vJFsmmc1rIhyg0o=", + "requires": { + "void-elements": "^2.0.1" + } + }, "html-webpack-plugin": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz", @@ -5076,6 +5084,30 @@ "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.3.tgz", "integrity": "sha512-EcuixamT82oplpoJ2XU4pDtKGWQ7b00CD9f1ug9IaQ3p1bkHMiKCZ9ut9QDI6qsa6cpUuB+A/I+zLtdNK4n2DQ==" }, + "i18next": { + "version": "19.4.4", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-19.4.4.tgz", + "integrity": "sha512-ofaHtdsDdX3A5nYur1HWblB7J4hIcjr2ACdnwTAJgc8hTfPbyzZfGX0hVkKpI3vzDIgO6Uzc4v1ffW2W6gG6zw==", + "requires": { + "@babel/runtime": "^7.3.1" + } + }, + "i18next-browser-languagedetector": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-4.1.1.tgz", + "integrity": "sha512-akv0zurR/2KU7s1qaWkirY9FEEOT1TNsQaezEg8+1BLLQre7vylqb7tYoUgYqP/0/BEzXJgnoQnj+sh5xYFMhg==", + "requires": { + "@babel/runtime": "^7.5.5" + } + }, + "i18next-http-backend": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-1.0.10.tgz", + "integrity": "sha512-HQl3N2plhU7WQd2Bcq3UWrpgM01w/Lkee76airTsKsLG4jnWKy6mYH3O7xz1da2ga9R6AN1MTSVVzJTp0uDl7A==", + "requires": { + "node-fetch": "2.6.0" + } + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -6249,6 +6281,11 @@ "minimist": "^1.2.5" } }, + "moment": { + "version": "2.25.3", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.25.3.tgz", + "integrity": "sha512-PuYv0PHxZvzc15Sp8ybUCoQ+xpyPWvjOuK72a5ovzp2LI32rJXOiIfyoFoYvG3s6EwwrdkMyWuRiEHSZRLJNdg==" + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -6352,8 +6389,7 @@ "node-fetch": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", - "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==", - "dev": true + "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" }, "node-forge": { "version": "0.9.0", @@ -7837,6 +7873,15 @@ "scheduler": "^0.13.4" } }, + "react-i18next": { + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.4.0.tgz", + "integrity": "sha512-lyOZSSQkif4H9HnHN3iEKVkryLI+WkdZSEw3VAZzinZLopfYRMHVY5YxCopdkXPLEHs6S5GjKYPh3+j0j336Fg==", + "requires": { + "@babel/runtime": "^7.3.1", + "html-parse-stringify2": "2.0.1" + } + }, "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -9969,6 +10014,11 @@ "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", "dev": true }, + "void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=" + }, "wait-for-expect": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/wait-for-expect/-/wait-for-expect-1.3.0.tgz", diff --git a/package.json b/package.json index a62a12b..857335e 100644 --- a/package.json +++ b/package.json @@ -15,10 +15,15 @@ "@material-ui/icons": "^4.9.1", "axios": "^0.19.2", "bootstrap": "4.3.1", + "i18next": "^19.4.4", + "i18next-browser-languagedetector": "^4.1.1", + "i18next-http-backend": "^1.0.10", "immer": "2.1.3", + "moment": "^2.25.3", "prop-types": "15.7.2", "react": "16.8.4", "react-dom": "16.8.4", + "react-i18next": "^11.4.0", "react-redux": "6.0.1", "react-router-dom": "5.0.0", "react-toastify": "4.5.2", diff --git a/private/Notes.txt b/private/Notes.txt new file mode 100644 index 0000000..8be00ec --- /dev/null +++ b/private/Notes.txt @@ -0,0 +1,9 @@ + +withTranslation()(LegacyComponentClass) +const { t } = this.props; + + +import { useTranslation } from 'react-i18next'; + +function MyComponent() { + const { t, i18n } = useTranslation(); \ No newline at end of file diff --git a/public/locales/en/translations.json b/public/locales/en/translations.json new file mode 100644 index 0000000..b2ab8d7 --- /dev/null +++ b/public/locales/en/translations.json @@ -0,0 +1,12 @@ +{ + "DATE": "{{date,intlDate}}", + "LONG_DATE": "{{date,intlLongDate}}", + "DATE_FORMAT": "{{date, format}}", + "TIME_FROM_X": "{{date,intlTimeFromX}}", + "H_FROM_X": "{{date,intlHoursFromX}}", + "H_FROM_M": "{{number,intlHoursFromMinutes}}", + "NUMBER": "{{number,intlNumber}}", + "DECIMAL": "{{number,intlDecimal}}", + "DECIMAL2": "{{number,intlDecimal2}}", + "Session": "Session" +} diff --git a/public/locales/ro/translations.json b/public/locales/ro/translations.json new file mode 100644 index 0000000..ea92948 --- /dev/null +++ b/public/locales/ro/translations.json @@ -0,0 +1,3 @@ +{ + "Session": "Sesiune" +} diff --git a/src/api/api.js b/src/api/api.js index c4d183a..bb38b80 100644 --- a/src/api/api.js +++ b/src/api/api.js @@ -1,8 +1,11 @@ +import i18next from "i18next"; + function getHeaders() { const headers = new Headers(); headers.append("Accept", "application/json"); headers.append("Content-Type", "application/json"); headers.append("Authorization", "Basic ***REMOVED***"); + headers.append("Accept-Language", `${i18next.language}`); return headers; } diff --git a/src/api/axiosApi.js b/src/api/axiosApi.js index 8321abf..f291193 100644 --- a/src/api/axiosApi.js +++ b/src/api/axiosApi.js @@ -1,9 +1,11 @@ import axios from "axios"; +import i18next from "i18next"; function getHeaders() { return { "Content-Type": "application/json", - Authorization: "Basic ***REMOVED***" + Authorization: "Basic ***REMOVED***", + "Accept-Language": `${i18next.language}` }; } diff --git a/src/features/session/components/SessionSummary.js b/src/features/session/components/SessionSummary.js index 0b8d9f3..094cecf 100644 --- a/src/features/session/components/SessionSummary.js +++ b/src/features/session/components/SessionSummary.js @@ -3,6 +3,7 @@ import PropTypes from "prop-types"; import { Grid } from "@material-ui/core"; import { makeStyles } from "@material-ui/core/styles"; import { CheckCircleOutlineRounded, RemoveRounded } from "@material-ui/icons"; +import { useTranslation } from "react-i18next"; const useStyles = makeStyles((theme) => ({ value: { @@ -15,12 +16,13 @@ const useStyles = makeStyles((theme) => ({ const SessionSummary = ({ session }) => { const classes = useStyles(); + const { t, i18n } = useTranslation(); return ( <> - {"Session: "} + {`${t("Session")}: `} {session.sessionId} @@ -33,12 +35,25 @@ const SessionSummary = ({ session }) => { {"Start date: "} - {session.startDate} + + {t("DATE_FORMAT", { + date: { value: session.startDate, format: "DD-MM-YYYY HH:mm:ss" } + })} + {"Stop date: "} - {session.stopDate || "---"} + + {session.stopDate + ? t("DATE_FORMAT", { + date: { + value: session.stopDate, + format: "DD-MM-YYYY HH:mm:ss" + } + }) + : "---"} + diff --git a/src/index.js b/src/index.js index 0e5ab40..ec12c83 100644 --- a/src/index.js +++ b/src/index.js @@ -6,6 +6,7 @@ import App from "./components/App"; import "./index.css"; import configureStore from "./redux/configureStore"; import { Provider as ReduxProvider } from "react-redux"; +import "./utils/i18n"; const store = configureStore(); diff --git a/src/utils/i18n.js b/src/utils/i18n.js new file mode 100644 index 0000000..acb14f1 --- /dev/null +++ b/src/utils/i18n.js @@ -0,0 +1,106 @@ +import i18n from "i18next"; +import { initReactI18next } from "react-i18next"; +import Backend from "i18next-http-backend"; +import LanguageDetector from "i18next-browser-languagedetector"; +import moment from "moment"; +import "moment/locale/ro.js"; +import "moment/locale/de.js"; + +i18n + .use(Backend) + .use(LanguageDetector) + .use(initReactI18next) // passes i18n down to react-i18next + .init( + { + fallbackLng: "en", + debug: true, + ns: ["translations"], + defaultNS: "translations", + //whitelist: ["en", "ro"], + interpolation: { + escapeValue: false, // not needed for react as it escapes by default + format: function (value, format, lng) { + if (format === "uppercase") return value.toUpperCase(); + if (format === "intlDate") { + if (value && moment(value).isValid()) { + //new Intl.DateTimeFormat(lng).format(new Date(value)); + return moment(value).format("L"); + } + return ""; + } + + if (format === "intlLongDate") { + if (value && moment(value).isValid()) { + //return new Intl.DateTimeFormat(lng, options).format(new Date(value)); + return moment(value).format("LLLL"); + } + return ""; + } + + if (format === "intlTimeFromX") { + if (value && moment(value.start).isValid()) { + let startDate = moment(value.start); + let endDate = moment(value.end); + return moment(endDate).from(startDate, true); + } + return ""; + } + + if (format === "intlHoursFromX") { + if (value && moment(value.start).isValid()) { + let startDate = moment(value.start); + let endDate = moment(value.end); + let span = moment.duration(endDate - startDate); + return `${parseInt(span.asHours(), 10)}h ${parseInt( + span.asMinutes() % 60, + 10 + )}m`; + } + return ""; + } + + if (format === "intlNumber") + return new Intl.NumberFormat(lng).format(value); + if (format === "intlDecimal") + return new Intl.NumberFormat(lng, { + minimumFractionDigits: 2 + }).format(value); + if (format === "intlDecimal2") + return new Intl.NumberFormat(lng, { + minimumFractionDigits: 2, + maximumFractionDigits: 2 + }).format(value); + + //dateformat + if (value && value.format) { + if (value.value && moment(value).isValid()) { + return moment(value.value).format(value.format); + } + return ""; + } + + return value; + } + }, + react: { + useSuspense: false + }, + backend: { + loadPath: "/public/locales/{{lng}}/{{ns}}.json" + } + }, + () => { + const currentLang = i18n.language; + if (!currentLang || !currentLang.startsWith("ro")) { + i18n.changeLanguage("en"); + } else { + i18n.changeLanguage("ro"); + } + } + ); + +i18n.on("languageChanged", function (lng) { + moment.locale(lng); +}); + +export default i18n;