From d8372ed78a0b848d3193bceaf2ec81788513b346 Mon Sep 17 00:00:00 2001 From: lukas Date: Fri, 8 Dec 2023 12:59:44 +0100 Subject: [PATCH] Add translation support using i18next --- package-lock.json | 124 ++++++++++++++++++++++-- package.json | 4 + public/locales/de.json | 7 ++ public/locales/en.json | 7 ++ src/app/i18n.ts | 30 ++++++ src/app/organisms/room/RoomTimeline.tsx | 5 +- src/index.tsx | 3 + vite.config.js | 4 + 8 files changed, 174 insertions(+), 10 deletions(-) create mode 100644 public/locales/de.json create mode 100644 public/locales/en.json create mode 100644 src/app/i18n.ts diff --git a/package-lock.json b/package-lock.json index fba4072e64..a2c827b313 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,9 @@ "formik": "2.2.9", "html-dom-parser": "4.0.0", "html-react-parser": "4.2.0", + "i18next": "23.9.0", + "i18next-browser-languagedetector": "7.2.0", + "i18next-http-backend": "2.4.3", "immer": "9.0.16", "is-hotkey": "0.2.0", "jotai": "2.6.0", @@ -54,6 +57,7 @@ "react-dom": "18.2.0", "react-error-boundary": "4.0.10", "react-google-recaptcha": "2.1.0", + "react-i18next": "14.0.5", "react-modal": "3.16.1", "react-range": "1.8.14", "react-router-dom": "6.20.0", @@ -411,11 +415,11 @@ } }, "node_modules/@babel/runtime": { - "version": "7.20.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.6.tgz", - "integrity": "sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", + "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", "dependencies": { - "regenerator-runtime": "^0.13.11" + "regenerator-runtime": "^0.14.0" }, "engines": { "node": ">=6.9.0" @@ -440,11 +444,6 @@ "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", "dev": true }, - "node_modules/@babel/runtime/node_modules/regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" - }, "node_modules/@babel/template": { "version": "7.22.15", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", @@ -5585,6 +5584,14 @@ "entities": "^4.5.0" } }, + "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-react-parser": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/html-react-parser/-/html-react-parser-4.2.0.tgz", @@ -5630,6 +5637,71 @@ "node": ">= 6" } }, + "node_modules/i18next": { + "version": "23.9.0", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.9.0.tgz", + "integrity": "sha512-f3MUciKqwzNV//mHG6EtdSlC65+nqH/3zK8sOSWqNV6FVu2tmHhF/rFOp9UF8S4m1odojtuipKaKJrP0Loh60g==", + "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.23.2" + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.2.0.tgz", + "integrity": "sha512-U00DbDtFIYD3wkWsr2aVGfXGAj2TgnELzOX9qv8bT0aJtvPV9CRO77h+vgmHFBMe7LAxdwvT/7VkCWGya6L3tA==", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/i18next-http-backend": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-2.4.3.tgz", + "integrity": "sha512-jo2M03O6n1/DNb51WSQ8PsQ0xEELzLZRdYUTbf17mLw3rVwnJF9hwNgMXvEFSxxb+N8dT+o0vtigA6s5mGWyPA==", + "dependencies": { + "cross-fetch": "4.0.0" + } + }, + "node_modules/i18next-http-backend/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==", + "dependencies": { + "node-fetch": "^2.6.12" + } + }, + "node_modules/i18next-http-backend/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -7160,6 +7232,27 @@ "react": ">=16.4.1" } }, + "node_modules/react-i18next": { + "version": "14.0.5", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-14.0.5.tgz", + "integrity": "sha512-5+bQSeEtgJrMBABBL5lO7jPdSNAbeAZ+MlFWDw//7FnVacuVu3l9EeWFzBQvZsKy+cihkbThWOAThEdH8YjGEw==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "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", @@ -7275,6 +7368,11 @@ "@babel/runtime": "^7.9.2" } }, + "node_modules/regenerator-runtime": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" + }, "node_modules/regexp.prototype.flags": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", @@ -8601,6 +8699,14 @@ "@esbuild/win32-x64": "0.19.9" } }, + "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/warning": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", diff --git a/package.json b/package.json index 56e7b8c01e..1139d9d4bf 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,9 @@ "formik": "2.2.9", "html-dom-parser": "4.0.0", "html-react-parser": "4.2.0", + "i18next": "23.9.0", + "i18next-browser-languagedetector": "7.2.0", + "i18next-http-backend": "2.4.3", "immer": "9.0.16", "is-hotkey": "0.2.0", "jotai": "2.6.0", @@ -64,6 +67,7 @@ "react-dom": "18.2.0", "react-error-boundary": "4.0.10", "react-google-recaptcha": "2.1.0", + "react-i18next": "14.0.5", "react-modal": "3.16.1", "react-range": "1.8.14", "react-router-dom": "6.20.0", diff --git a/public/locales/de.json b/public/locales/de.json new file mode 100644 index 0000000000..43a37160ef --- /dev/null +++ b/public/locales/de.json @@ -0,0 +1,7 @@ +{ + "Organisms": { + "RoomCommon": { + "changed_room_name": " hat den Raum Name geƤndert" + } + } +} diff --git a/public/locales/en.json b/public/locales/en.json new file mode 100644 index 0000000000..7a2534b8f7 --- /dev/null +++ b/public/locales/en.json @@ -0,0 +1,7 @@ +{ + "Organisms": { + "RoomCommon": { + "changed_room_name": " changed room name" + } + } +} diff --git a/src/app/i18n.ts b/src/app/i18n.ts new file mode 100644 index 0000000000..7a28a5e7cb --- /dev/null +++ b/src/app/i18n.ts @@ -0,0 +1,30 @@ +import i18n from 'i18next'; +import LanguageDetector from 'i18next-browser-languagedetector'; +import Backend, { HttpBackendOptions } from 'i18next-http-backend'; +import { initReactI18next } from 'react-i18next'; + +i18n + // i18next-http-backend + // loads translations from your server + // https://github.com/i18next/i18next-http-backend + .use(Backend) + // detect user language + // learn more: https://github.com/i18next/i18next-browser-languageDetector + .use(LanguageDetector) + // pass the i18n instance to react-i18next. + .use(initReactI18next) + // init i18next + // for all options read: https://www.i18next.com/overview/configuration-options + .init({ + debug: false, + fallbackLng: 'en', + interpolation: { + escapeValue: false, // not needed for react as it escapes by default + }, + load: 'languageOnly', + backend: { + loadPath: 'public/locales/{{lng}}.json', + }, + }); + +export default i18n; diff --git a/src/app/organisms/room/RoomTimeline.tsx b/src/app/organisms/room/RoomTimeline.tsx index 0c74de520a..d1b338f4cb 100644 --- a/src/app/organisms/room/RoomTimeline.tsx +++ b/src/app/organisms/room/RoomTimeline.tsx @@ -44,6 +44,7 @@ import { toRem, } from 'folds'; import { isKeyHotkey } from 'is-hotkey'; +import { useTranslation } from 'react-i18next'; import { decryptFile, eventWithShortcode, @@ -1254,6 +1255,8 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli }, }); + const { t } = useTranslation(); + const renderMatrixEvent = useMatrixEventRenderer<[number, EventTimelineSet, boolean]>({ renderRoomMessage: (mEventId, mEvent, item, timelineSet, collapse) => { const reactionRelations = getEventReactions(timelineSet, mEventId); @@ -1494,7 +1497,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli {senderName} - {' changed room name'} + {t('Organisms.RoomCommon.changed_room_name')} } diff --git a/src/index.tsx b/src/index.tsx index 1d86420371..a289ed1cc0 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -14,6 +14,9 @@ import settings from './client/state/settings'; import App from './app/pages/App'; +// import i18n (needs to be bundled ;)) +import './app/i18n'; + document.body.classList.add(configClass, varsClass); settings.applyTheme(); diff --git a/vite.config.js b/vite.config.js index 20c7765c58..a4663334c2 100644 --- a/vite.config.js +++ b/vite.config.js @@ -34,6 +34,10 @@ const copyFiles = { src: 'public/res/android', dest: 'public/', }, + { + src: 'public/locales', + dest: 'public/', + }, ], }