From 2ea38dbff1816ccec4bf8109479dcdbb58653104 Mon Sep 17 00:00:00 2001 From: lukas Date: Fri, 8 Dec 2023 12:59:44 +0100 Subject: [PATCH 1/4] 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 5b3ebdc04c..94b3b2dbc2 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.11.3", + "i18next-browser-languagedetector": "7.2.1", + "i18next-http-backend": "2.5.1", "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.1.1", "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", @@ -5609,6 +5608,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", @@ -5654,6 +5661,71 @@ "node": ">= 6" } }, + "node_modules/i18next": { + "version": "23.11.3", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.11.3.tgz", + "integrity": "sha512-Pq/aSKowir7JM0rj+Wa23Kb6KKDUGno/HjG+wRQu0PxoTbpQ4N89MAT0rFGvXmLkRLNMb1BbBOKGozl01dabzg==", + "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.1", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.2.1.tgz", + "integrity": "sha512-h/pM34bcH6tbz8WgGXcmWauNpQupCGr25XPp9cZwZInR9XHSjIFDYp1SIok7zSPsTOMxdvuLyu86V+g2Kycnfw==", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/i18next-http-backend": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-2.5.1.tgz", + "integrity": "sha512-+rNX1tghdVxdfjfPt0bI1sNg5ahGW9kA7OboG7b4t03Fp69NdDlRIze6yXhIbN8rbHxJ8IP4dzRm/okZ15lkQg==", + "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", @@ -7176,6 +7248,27 @@ "react": ">=16.4.1" } }, + "node_modules/react-i18next": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-14.1.1.tgz", + "integrity": "sha512-QSiKw+ihzJ/CIeIYWrarCmXJUySHDwQr5y8uaNIkbxoGRm/5DukkxZs+RPla79IKyyDPzC/DRlgQCABHtrQuQQ==", + "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", @@ -7291,6 +7384,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", @@ -8618,6 +8716,14 @@ "@esbuild/win32-x64": "0.19.12" } }, + "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 7660b17449..665afc84b2 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.11.3", + "i18next-browser-languagedetector": "7.2.1", + "i18next-http-backend": "2.5.1", "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.1.1", "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/', + }, ], } From b485118c169076cd0ef9d0f3a53e1d0e8a57fa83 Mon Sep 17 00:00:00 2001 From: lukas Date: Fri, 15 Dec 2023 14:39:51 +0100 Subject: [PATCH 2/4] Translate components --- public/locales/de.json | 34 +++++++++++++++++++ public/locales/en.json | 34 +++++++++++++++++++ .../confirm-dialog/ConfirmDialog.jsx | 4 ++- .../organisms/navigation/DrawerBreadcrumb.jsx | 5 ++- src/app/organisms/navigation/DrawerHeader.jsx | 24 +++++++------ src/app/organisms/navigation/Selector.jsx | 5 ++- src/app/organisms/navigation/SideBar.jsx | 21 ++++++++---- 7 files changed, 107 insertions(+), 20 deletions(-) diff --git a/public/locales/de.json b/public/locales/de.json index 43a37160ef..6007b25204 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -1,7 +1,41 @@ { + "common": { + "options": "Optionen" + }, "Organisms": { "RoomCommon": { "changed_room_name": " hat den Raum Name geändert" + }, + "DrawerBreadcrumb": { + "home": "Home" + }, + "DrawerHeader": { + "add_rooms_or_spaces": "Raum oder Space hinzufügen", + "create_new_space": "Neuen Space erstellen", + "create_new_room": "Neuen Raum erstellen", + "explore_public_room": "Öffentliche Räume erkunden", + "join_with_address": "Mit Adresse beitreten", + "add_existing": "Bestehendem beitreten", + "manage_rooms": "Manage rooms", + "home": "Home", + "direct_messages": "Direktnachrichten", + "start_dm_tooltip": "Direktnachricht schreiben", + "add_rooms_spaces_tooltip": "Räume/Spaces hinzufügen" + }, + "SideBar": { + "settings_tooltip": "Einstellungen", + "unverified_sessions_one": "{{count}} nicht verifizierte Sitzung", + "unverified_sessions_other": "{{count}} nicht verifizierte Sitzungen", + "home_tooltip": "Home", + "direct_messages_tooltip": "Personen", + "pin_spaces_tooltip": "Spaces anheften", + "search_tooltip": "Suche", + "invites_tooltip": "Einladungen" + } + }, + "Molecules": { + "ConfirmDialog": { + "cancel": "Abbrechen" } } } diff --git a/public/locales/en.json b/public/locales/en.json index 7a2534b8f7..8d6b12d12f 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -1,7 +1,41 @@ { + "common": { + "options": "Options" + }, "Organisms": { "RoomCommon": { "changed_room_name": " changed room name" + }, + "DrawerBreadcrumb": { + "home": "Home" + }, + "DrawerHeader": { + "add_rooms_or_spaces": "Add rooms or spaces", + "create_new_space": "Create new space", + "create_new_room": "Create new room", + "explore_public_room": "Explore public room", + "join_with_address": "Join with address", + "add_existing": "Add existing", + "manage_rooms": "Manage rooms", + "home": "Home", + "direct_messages": "Direct messages", + "start_dm_tooltip": "Start DM", + "add_rooms_spaces_tooltip": "Add rooms/spaces" + }, + "SideBar": { + "settings_tooltip": "Settings", + "unverified_sessions_one": "{{count}} unverified session", + "unverified_sessions_other": "{{count}} unverified sessions", + "home_tooltip": "Home", + "direct_messages_tooltip": "People", + "pin_spaces_tooltip": "Pin spaces", + "search_tooltip": "Search", + "invites_tooltip": "Invites" + } + }, + "Molecules": { + "ConfirmDialog": { + "cancel": "Cancel" } } } diff --git a/src/app/molecules/confirm-dialog/ConfirmDialog.jsx b/src/app/molecules/confirm-dialog/ConfirmDialog.jsx index 5771f2c1e2..0a7d6541e6 100644 --- a/src/app/molecules/confirm-dialog/ConfirmDialog.jsx +++ b/src/app/molecules/confirm-dialog/ConfirmDialog.jsx @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import './ConfirmDialog.scss'; +import { useTranslation } from 'react-i18next'; import { openReusableDialog } from '../../../client/action/navigation'; @@ -10,12 +11,13 @@ import Button from '../../atoms/button/Button'; function ConfirmDialog({ desc, actionTitle, actionType, onComplete, }) { + const { t } = useTranslation(); return (
{desc}
- +
); diff --git a/src/app/organisms/navigation/DrawerBreadcrumb.jsx b/src/app/organisms/navigation/DrawerBreadcrumb.jsx index face349d48..7acd1d68b8 100644 --- a/src/app/organisms/navigation/DrawerBreadcrumb.jsx +++ b/src/app/organisms/navigation/DrawerBreadcrumb.jsx @@ -1,6 +1,7 @@ import React, { useState, useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; import './DrawerBreadcrumb.scss'; +import { useTranslation } from 'react-i18next'; import { twemojify } from '../../../util/twemojify'; @@ -66,6 +67,8 @@ function DrawerBreadcrumb({ spaceId }) { return noti; } + const { t } = useTranslation(); + function getNotiExcept(roomId, childId) { if (!notifications.hasNoti(roomId)) return null; @@ -112,7 +115,7 @@ function DrawerBreadcrumb({ spaceId }) { else selectSpace(id); }} > - {id === cons.tabs.HOME ? 'Home' : twemojify(mx.getRoom(id).name)} + {id === cons.tabs.HOME ? t('Organisms.DrawerBreadcrumb.home') : twemojify(mx.getRoom(id).name)} { noti !== null && ( - Add rooms or spaces + {t('Organisms.DrawerHeader.add_rooms_or_spaces')} { afterOptionSelect(); openCreateRoom(true, spaceId); }} disabled={!canManage} > - Create new space + {t('Organisms.DrawerHeader.create_new_space')} { afterOptionSelect(); openCreateRoom(false, spaceId); }} disabled={!canManage} > - Create new room + {t('Organisms.DrawerHeader.create_new_room')} { !spaceId && ( { afterOptionSelect(); openPublicRooms(); }} > - Explore public rooms + {t('Organisms.DrawerHeader.explore_public_room')} )} { !spaceId && ( @@ -65,7 +68,7 @@ export function HomeSpaceOptions({ spaceId, afterOptionSelect }) { iconSrc={PlusIC} onClick={() => { afterOptionSelect(); openJoinAlias(); }} > - Join with address + {t('Organisms.DrawerHeader.join_with_address')} )} { spaceId && ( @@ -74,7 +77,7 @@ export function HomeSpaceOptions({ spaceId, afterOptionSelect }) { onClick={() => { afterOptionSelect(); openSpaceAddExisting(spaceId); }} disabled={!canManage} > - Add existing + {t('Organisms.DrawerHeader.add_existing')} )} { spaceId && ( @@ -82,7 +85,7 @@ export function HomeSpaceOptions({ spaceId, afterOptionSelect }) { onClick={() => { afterOptionSelect(); openSpaceManage(spaceId); }} iconSrc={HashSearchIC} > - Manage rooms + {t('Organisms.DrawerHeader.manage_rooms')} )} @@ -98,7 +101,8 @@ HomeSpaceOptions.propTypes = { function DrawerHeader({ selectedTab, spaceId }) { const mx = initMatrix.matrixClient; - const tabName = selectedTab !== cons.tabs.DIRECTS ? 'Home' : 'Direct messages'; + const { t } = useTranslation(); + const tabName = selectedTab !== cons.tabs.DIRECTS ? t('Organisms.DrawerHeader.home') : t('Organisms.DrawerHeader.direct_messages'); const isDMTab = selectedTab === cons.tabs.DIRECTS; const room = mx.getRoom(spaceId); @@ -142,8 +146,8 @@ function DrawerHeader({ selectedTab, spaceId }) { )} - { isDMTab && openInviteUser()} tooltip="Start DM" src={PlusIC} size="small" /> } - { !isDMTab && } + { isDMTab && openInviteUser()} tooltip={t('Organisms.DrawerHeader.start_dm_tooltip')} src={PlusIC} size="small" /> } + { !isDMTab && } ); } diff --git a/src/app/organisms/navigation/Selector.jsx b/src/app/organisms/navigation/Selector.jsx index cb1086eaa4..7abddd4bc1 100644 --- a/src/app/organisms/navigation/Selector.jsx +++ b/src/app/organisms/navigation/Selector.jsx @@ -1,6 +1,7 @@ /* eslint-disable react/prop-types */ import React, { useEffect } from 'react'; import PropTypes from 'prop-types'; +import { useTranslation } from 'react-i18next'; import initMatrix from '../../../client/initMatrix'; import cons from '../../../client/state/cons'; @@ -30,6 +31,8 @@ function Selector({ const isMuted = noti.getNotiType(roomId) === cons.notifs.MUTE; + const { t } = useTranslation(); + const [, forceUpdate] = useForceUpdate(); useEffect(() => { @@ -69,7 +72,7 @@ function Selector({ options={( isCrossVerified(device.device_id) === false); @@ -99,7 +104,7 @@ function CrossSigninAlert() { return ( openSettings(settingTabText.SECURITY)} avatar={} /> @@ -109,6 +114,7 @@ function CrossSigninAlert() { function FeaturedTab() { const { roomList, accountData, notifications } = initMatrix; const [selectedTab] = useSelectedTab(); + const { t } = useTranslation(); useNotificationUpdate(); function getHomeNoti() { @@ -147,7 +153,7 @@ function FeaturedTab() { return ( <> selectTab(cons.tabs.HOME)} avatar={} @@ -159,7 +165,7 @@ function FeaturedTab() { ) : null} /> selectTab(cons.tabs.DIRECTS)} avatar={} @@ -342,6 +348,7 @@ function useTotalInvites() { function SideBar() { const [totalInvites] = useTotalInvites(); + const { t } = useTranslation(); return (
@@ -355,7 +362,7 @@ function SideBar() {
openShortcutSpaces()} avatar={} /> @@ -367,13 +374,13 @@ function SideBar() {
openSearch()} avatar={} /> { totalInvites !== 0 && ( openInviteList()} avatar={} notificationBadge={} From f3f94af0db3ea921c5269019d2fcd67e335137de Mon Sep 17 00:00:00 2001 From: lukas Date: Fri, 15 Dec 2023 15:11:37 +0100 Subject: [PATCH 3/4] Translate components --- public/locales/de.json | 63 +++++++++++++++++++ public/locales/en.json | 63 +++++++++++++++++++ src/app/atoms/tabs/Tabs.jsx | 5 +- .../molecules/room-options/RoomOptions.jsx | 19 +++--- .../molecules/space-options/SpaceOptions.jsx | 25 ++++---- src/app/organisms/settings/Settings.jsx | 32 +++++----- src/app/organisms/welcome/Welcome.jsx | 7 ++- 7 files changed, 176 insertions(+), 38 deletions(-) diff --git a/public/locales/de.json b/public/locales/de.json index 6007b25204..2fd2bbc84c 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -3,6 +3,48 @@ "options": "Optionen" }, "Organisms": { + "Welcome": { + "heading": "Willkommen in Cinny!", + "subheading": "Noch ein weiterer Matrix Client" + }, + "SpaceSettings": { + "categorize_subspaces": "Unter-Spaces kategorisieren", + "uncategorize_subspaces": "Unter-Spaces nicht kategorisieren", + "pin_sidebar": "An Seitenleiste anheften", + "unpin_sidebar": "Von Seitenleiste lösen" + }, + "Settings": { + "theme": { + "follow_system": { + "title": "System-Theme verwenden", + "description": "Verwende den hellen oder dunklen Modus basierend auf den Systemeinstellungen." + }, + "title": "Theme", + "theme_light": "Hell", + "theme_silver": "Silber", + "theme_dark": "Dunkel", + "theme_butter": "Butter" + }, + "markdown": { + "title": "Markdown-Formatierung", + "description": "Nachrichten vor dem Senden mit Markdown formatieren" + }, + "hide_membership_events": { + "title": "Mitgliedschaftsereignisse ausblenden", + "description": "Nachrichten zu Änderungen von Mitgliedschaften in der Zeitleiste ausblenden (Beitreten, Verlassen, Einladen, Entfernen und Bannen)" + }, + "hide_nickname_avatar_events": { + "title": "Spitzname/Avatar Ereignisse ausblenden", + "description": "Nachrichten zu Änderungen von Spitznamen und Avataren in der Zeitleiste ausblenden." + }, + "tabs": { + "appearance": "Auftretten", + "notifications": "Benachrichtungen", + "emoji": "Emoji", + "security": "Sicherheit", + "about": "Über" + } + }, "RoomCommon": { "changed_room_name": " hat den Raum Name geändert" }, @@ -36,6 +78,27 @@ "Molecules": { "ConfirmDialog": { "cancel": "Abbrechen" + }, + "RoomOptions": { + "title": "Optionen für {{room_name}}", + "leave": { + "title": "Raum verlassen", + "subtitle": "Bist du sicher, dass du den Raum {{room_name}} verlassen möchtest?", + "button_text": "Verlassen" + }, + "mark_as_read": "Als gelesen markieren", + "notifications_heading": "Benachrichtigungen", + "invite": "Einladen" + }, + "SpaceOptions": { + "leave_space": "Space verlassen", + "leave_space_confirmation": "Bist du sicher, dass du den Space {{space}} verlassen möchtest?", + "leave_space_confirm": "Verlassen", + "mark_as_read": "Als gelesen markieren", + "invite": "Einladen", + "manage_rooms": "Räume verwalten", + "settings": "Einstellungen", + "leave": "Verlassen" } } } diff --git a/public/locales/en.json b/public/locales/en.json index 8d6b12d12f..ed44c7d884 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -3,6 +3,48 @@ "options": "Options" }, "Organisms": { + "Welcome": { + "heading": "Welcome to Cinny!", + "subheading": "Yet another Matrix client" + }, + "SpaceSettings": { + "categorize_subspaces": "Categorize subspaces", + "uncategorize_subspaces": "Uncategorize subspaces", + "pin_sidebar": "Pin to sidebar", + "unpin_sidebar": "Unpin from sidebar" + }, + "Settings": { + "theme": { + "follow_system": { + "title": "Follow system theme", + "description": "Use light or dark mode based on the system settings." + }, + "title": "Theme", + "theme_light": "Light", + "theme_silver": "Silver", + "theme_dark": "Dark", + "theme_butter": "Butter" + }, + "markdown": { + "title": "Markdown formatting", + "description": "Format messages with markdown before sending" + }, + "hide_membership_events": { + "title": "Hide membership events", + "description": "Hide membership change messages from room timeline. (Join, Leave, Invite, Kick and Ban)" + }, + "hide_nickname_avatar_events": { + "title": "Hide nick/avatar events", + "description": "Hide nickname and avatar change messages from the room timeline." + }, + "tabs": { + "appearance": "Appearance", + "notifications": "Notifications", + "emoji": "Emoji", + "security": "Security", + "about": "About" + } + }, "RoomCommon": { "changed_room_name": " changed room name" }, @@ -36,6 +78,27 @@ "Molecules": { "ConfirmDialog": { "cancel": "Cancel" + }, + "RoomOptions": { + "title": "Options for {{room_name}}", + "leave": { + "title": "Leave room", + "subtitle": "Are you sure you want to leave the {{room_name}} room?", + "button_text": "Leave" + }, + "mark_as_read": "Mark as read", + "notifications_heading": "Notifications", + "invite": "Invite" + }, + "SpaceOptions": { + "leave_space": "Leave Space", + "leave_space_confirmation": "Are you sure that you want to leave the {{space}} space?", + "leave_space_confirm": "Leave", + "mark_as_read": "Mark as read", + "invite": "Invite", + "manage_rooms": "Manage rooms", + "settings": "Settings", + "leave": "Leave" } } } diff --git a/src/app/atoms/tabs/Tabs.jsx b/src/app/atoms/tabs/Tabs.jsx index 39800ce350..794ce8ddb6 100644 --- a/src/app/atoms/tabs/Tabs.jsx +++ b/src/app/atoms/tabs/Tabs.jsx @@ -1,6 +1,7 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; import './Tabs.scss'; +import { useTranslation } from 'react-i18next'; import Button from '../button/Button'; import ScrollView from '../scroll/ScrollView'; @@ -41,6 +42,8 @@ TabItem.propTypes = { function Tabs({ items, defaultSelected, onSelect }) { const [selectedItem, setSelectedItem] = useState(items[defaultSelected]); + const { t } = useTranslation(); + const handleTabSelection = (item, index) => { if (selectedItem === item) return; setSelectedItem(item); @@ -59,7 +62,7 @@ function Tabs({ items, defaultSelected, onSelect }) { disabled={item.disabled} onClick={() => handleTabSelection(item, index)} > - {item.text} + {t(item.text)} ))}
diff --git a/src/app/molecules/room-options/RoomOptions.jsx b/src/app/molecules/room-options/RoomOptions.jsx index af18d71206..f23820608c 100644 --- a/src/app/molecules/room-options/RoomOptions.jsx +++ b/src/app/molecules/room-options/RoomOptions.jsx @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { useTranslation } from 'react-i18next'; import { twemojify } from '../../../util/twemojify'; import initMatrix from '../../../client/initMatrix'; @@ -22,6 +23,8 @@ function RoomOptions({ roomId, afterOptionSelect }) { const room = mx.getRoom(roomId); const canInvite = room?.canInvite(mx.getUserId()); + const { t } = useTranslation(); + const handleMarkAsRead = () => { markAsRead(roomId); afterOptionSelect(); @@ -34,9 +37,9 @@ function RoomOptions({ roomId, afterOptionSelect }) { const handleLeaveClick = async () => { afterOptionSelect(); const isConfirmed = await confirmDialog( - 'Leave room', - `Are you sure that you want to leave "${room.name}" room?`, - 'Leave', + t('Molecules.RoomOptions.leave.title'), + t('Molecules.RoomOptions.leave.subtitle', { room_name: room.name }), + t('Molecules.RoomOptions.leave.button_text'), 'danger', ); if (!isConfirmed) return; @@ -45,17 +48,17 @@ function RoomOptions({ roomId, afterOptionSelect }) { return (
- {twemojify(`Options for ${initMatrix.matrixClient.getRoom(roomId)?.name}`)} - Mark as read + {twemojify(t('Molecules.RoomOptions.title', { room_name: initMatrix.matrixClient.getRoom(roomId)?.name }))} + {t('Molecules.RoomOptions.mark_as_read')} - Invite + {t('Molecules.RoomOptions.invite')} - Leave - Notification + {t('Molecules.RoomOptions.leave.button_text')} + {t('Molecules.RoomOptions.notifications_heading')}
); diff --git a/src/app/molecules/space-options/SpaceOptions.jsx b/src/app/molecules/space-options/SpaceOptions.jsx index 0c166c6a90..dd0971c728 100644 --- a/src/app/molecules/space-options/SpaceOptions.jsx +++ b/src/app/molecules/space-options/SpaceOptions.jsx @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { useTranslation } from 'react-i18next'; import { twemojify } from '../../../util/twemojify'; import initMatrix from '../../../client/initMatrix'; @@ -36,6 +37,8 @@ function SpaceOptions({ roomId, afterOptionSelect }) { const isPinned = initMatrix.accountData.spaceShortcut.has(roomId); const isCategorized = initMatrix.accountData.categorizedSpaces.has(roomId); + const { t } = useTranslation(); + const handleMarkAsRead = () => { const spaceChildren = roomList.getCategorizedSpaces([roomId]); spaceChildren?.forEach((childIds) => { @@ -71,9 +74,9 @@ function SpaceOptions({ roomId, afterOptionSelect }) { const handleLeaveClick = async () => { afterOptionSelect(); const isConfirmed = await confirmDialog( - 'Leave space', - `Are you sure that you want to leave "${room.name}" space?`, - 'Leave', + t('Molecules.SpaceOptions.leave_space'), + t('Molecules.SpaceOptions.leave_space_confirmation', { space: room.name }), + t('Molecules.SpaceOptions.leave_space_confirmation'), 'danger', ); if (!isConfirmed) return; @@ -82,35 +85,35 @@ function SpaceOptions({ roomId, afterOptionSelect }) { return (
- {twemojify(`Options for ${initMatrix.matrixClient.getRoom(roomId)?.name}`)} - Mark as read + {twemojify(t('Molecules.RoomOptions.title', { room_name: initMatrix.matrixClient.getRoom(roomId)?.name }))} + {t('Molecules.SpaceOptions.mark_as_read')} - {isCategorized ? 'Uncategorize subspaces' : 'Categorize subspaces'} + {isCategorized ? t('Organisms.SpaceSettings.uncategorize_subspaces') : t('Organisms.SpaceSettings.categorize_subspaces')} - {isPinned ? 'Unpin from sidebar' : 'Pin to sidebar'} + {isPinned ? t('Organisms.SpaceSettings.unpin_sidebar') : t('Organisms.SpaceSettings.pin_sidebar')} - Invite + {t('Molecules.SpaceOptions.invite')} - Manage rooms - Settings + {t('Molecules.SpaceOptions.manage_rooms')} + {t('Molecules.SpaceOptions.settings')} - Leave + {t('Molecules.SpaceOptions.leave')}
); diff --git a/src/app/organisms/settings/Settings.jsx b/src/app/organisms/settings/Settings.jsx index 47abb45c01..a81764723a 100644 --- a/src/app/organisms/settings/Settings.jsx +++ b/src/app/organisms/settings/Settings.jsx @@ -79,15 +79,15 @@ function AppearanceSection() { content={Use light or dark mode based on the system settings.} /> { if (settings.useSystemTheme) toggleSystemTheme(); @@ -154,34 +154,34 @@ function AppearanceSection() { content={{`Use ${isMacOS() ? KeySymbol.Command : 'Ctrl'} + ENTER to send message and ENTER for newline.`}} /> setIsMarkdown(!isMarkdown) } /> )} - content={Format messages with markdown syntax before sending.} + content={{t('Organisms.Settings.markdown.description')}} /> setHideMembershipEvents(!hideMembershipEvents)} /> )} - content={Hide membership change messages from room timeline. (Join, Leave, Invite, Kick and Ban)} + content={{t('Organisms.Settings.hide_membership_events.description')}} /> setHideNickAvatarEvents(!hideNickAvatarEvents)} /> )} - content={Hide nick and avatar change messages from room timeline.} + content={{t('Organisms.Settings.hide_nickname_avatar_events.description')}} />
Cinny logo - Welcome to Cinny - Yet another matrix client + {t('Organisms.Welcome.heading')} + {t('Organisms.Welcome.subheading')}
); From 61ec699a44b49cb4ba387d05dd67a5403c031524 Mon Sep 17 00:00:00 2001 From: lukas Date: Mon, 18 Dec 2023 08:52:27 +0100 Subject: [PATCH 4/4] Translate components --- public/locales/de.json | 99 +++++++++++++- public/locales/en.json | 99 +++++++++++++- src/app/components/Pdf-viewer/PdfViewer.tsx | 14 +- src/app/i18n.ts | 3 + .../room-notification/RoomNotification.jsx | 12 +- src/app/organisms/room/message/Message.tsx | 14 +- src/app/templates/auth/Auth.jsx | 127 ++++++++++-------- src/app/templates/client/Client.jsx | 13 +- 8 files changed, 301 insertions(+), 80 deletions(-) diff --git a/public/locales/de.json b/public/locales/de.json index 2fd2bbc84c..c891ba8202 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -1,6 +1,17 @@ { "common": { - "options": "Optionen" + "options": "Optionen", + "or": "oder" + }, + "Components": { + "Files": { + "download": "Herunterladen", + "failure_pdf": "Fehler beim Laden der PDF", + "retry": "Erneut Versuchen", + "previous": "Vorherige", + "jump_to_page": "Spring zu Seite", + "next": "Nächste" + } }, "Organisms": { "Welcome": { @@ -79,6 +90,19 @@ "ConfirmDialog": { "cancel": "Abbrechen" }, + "Message": { + "add_reaction": "Reaktion hinzufügen", + "reply": "Antworten", + "edit": "Bearbeiten", + "read_receipts": "Lesebestätigungen", + "view_source": "Quelle ansehen" + }, + "RoomNotification": { + "default": "Global", + "all_messages": "Alle Nachrichten", + "mentions_and_keywords": "Erwähnungen & Schlüsselwörter", + "mute": "Stummschalten" + }, "RoomOptions": { "title": "Optionen für {{room_name}}", "leave": { @@ -100,5 +124,78 @@ "settings": "Einstellungen", "leave": "Verlassen" } + }, + "Templates": { + "Auth": { + "loading_homeserver_list": "Lade Liste der Homeserver...", + "looking_for_homeserver": "Suche nach Homeserver...", + "connecting_to_homeserver": "Verbinde mit {{homeserver}}...", + "unable_to_connect": "Keine Verbindung möglich. Bitte prüfe deine Eingabe", + "username_taken": "Benutzername ist bereits vergeben", + "registration_in_progress": "Registrierung in Bearbeitung...", + "link_about": "Über", + "link_twitter": "Twitter", + "link_matrix": "Powered by Matrix", + "dont_have_an_account": "Du hast noch keinen Account?", + "already_have_an_account": "Du hast bereits einen Account?", + "register_link": "Registrieren", + "login_link": "Anmelden", + "redirecting": "Leite weiter...", + "login_in_progress": "Anmeldung in Bearbeitung...", + "bad_localpart_error": "Der Benutzername darf nur die Zeichen a-z, 0-9 oder '=_-./' enthalten", + "user_id_too_long_error": "Deine Benutzer-ID, einschließlich des Hostnamens, darf nicht mehr als 255 Zeichen lang sein.", + "bad_password_error": "Das Kennwort muss mindestens 1 Kleinbuchstaben, 1 Großbuchstaben, 1 Ziffer, 1 Sonderzeichen und 8-127 Zeichen (keine Leerzeichen) enthalten.", + "confirm_password_error": "Die Kennwörter stimmen nicht überein", + "bad_email_error": "Ungültige E-Mail-Adresse", + "invalid_password": "Ungültiges Kennwort", + "check_credentials": "Bitte prüfe deine Zugangsdaten", + "login_types": { + "username": "Benutzername", + "email": "E-Mail" + }, + "homeserver_form": { + "title": "Homeserver", + "header": "Liste der Homeserver" + }, + "login_form": { + "title": "Anmeldung", + "prompt_email": "E-Mail", + "prompt_username": "Benutzername", + "prompt_password": "Kennwort", + "login_button": "Anmelden" + }, + "register_form": { + "title": "Registrierung", + "prompt_email_required": "E-Mail (erforderlich)", + "prompt_email_optional": "E-Mail", + "prompt_username": "Benutzername", + "prompt_password": "Kennwort", + "prompt_confirm_password": "Kennwort bestätigen", + "register_button": "Registrieren" + }, + "terms_and_conditions": { + "title": "Den Bedingungen zustimmen", + "description": "Um die Registrierung abzuschließen, musst du den Nutzungsbedingungen zustimmen.", + "accept": "Ich stimme den Nutzungsbedingungen zu", + "submit_button": "Absenden" + }, + "validate_email": { + "title": "E-Mail verifizieren", + "continue_button": "Fortfahren", + "message": "Bitte prüfe dein E-Mail-Postfach {{email_address}} und validiere deine E-Mail-Adresse bevor du fortfährst" + }, + "captcha": { + "message": "Bitte markiere das folgende Feld um fortzufahren" + } + }, + "Client": { + "loading_messages": { + "default": "Aufwärmen", + "message_one": "Beinahe am Ziel...", + "message_two": "Sieht aus, als hättest du eine Menge Zeug aufzuwärmen!" + }, + "logout_prompt": "Abmeldung", + "clear_cache": "Zwischenspeicher löschen & neuladen" + } } } diff --git a/public/locales/en.json b/public/locales/en.json index ed44c7d884..f9a24de7ec 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -1,6 +1,17 @@ { "common": { - "options": "Options" + "options": "Options", + "or": "Or" + }, + "Components": { + "Files": { + "download": "Download", + "failure_pdf": "Failed to load PDF", + "retry": "Retry", + "previous": "Previous", + "jump_to_page": "Jump To Page", + "next": "Next" + } }, "Organisms": { "Welcome": { @@ -79,6 +90,19 @@ "ConfirmDialog": { "cancel": "Cancel" }, + "Message": { + "add_reaction": "Add reaction", + "reply": "Reply", + "edit": "Edit", + "read_receipts": "Read receipts", + "view_source": "View source" + }, + "RoomNotification": { + "default": "Global", + "all_messages": "All messages", + "mentions_and_keywords": "Mentions & Keywords", + "mute": "Mute" + }, "RoomOptions": { "title": "Options for {{room_name}}", "leave": { @@ -100,5 +124,78 @@ "settings": "Settings", "leave": "Leave" } + }, + "Templates": { + "Auth": { + "loading_homeserver_list": "Loading homeserver list...", + "looking_for_homeserver": "Looking for homeserver...", + "connecting_to_homeserver": "Connecting to {{homeserver}}...", + "unable_to_connect": "Unable to connect. Please check your input", + "username_taken": "Username is already taken", + "registration_in_progress": "Registration in progress...", + "link_about": "About", + "link_twitter": "Twitter", + "link_matrix": "Powered by Matrix", + "dont_have_an_account": "Dont have an account?", + "already_have_an_account": "Already have an account?", + "register_link": "Register", + "login_link": "Login", + "redirecting": "Redirecting...", + "login_in_progress": "Login in progress...", + "bad_localpart_error": "Username can only contain characters a-z, 0-9, or '=_-./'", + "user_id_too_long_error": "Your user ID, including the hostname, can't be more than 255 characters long.", + "bad_password_error": "Password must contain at least 1 lowercase, 1 uppercase, 1 number, 1 non-alphanumeric character, 8-127 characters with no space.", + "confirm_password_error": "Passwords don't match.", + "bad_email_error": "Invalid email address", + "invalid_password": "Invalid password", + "check_credentials": "Please check your credentials", + "login_types": { + "username": "Username", + "email": "Email" + }, + "homeserver_form": { + "title": "Homeserver", + "header": "Homeserver list" + }, + "login_form": { + "title": "Login", + "prompt_email": "Email", + "prompt_username": "Username", + "prompt_password": "Password", + "login_button": "Login" + }, + "register_form": { + "title": "Register", + "prompt_email_required": "Email (Required)", + "prompt_email_optional": "Email", + "prompt_username": "Username", + "prompt_password": "Password", + "prompt_confirm_password": "Confirm password", + "register_button": "Register" + }, + "terms_and_conditions": { + "title": "Agree with terms", + "description": "In order to complete registration, you need to agree to the terms and conditions.", + "accept": "I accept Terms and Conditions", + "submit_button": "Submit" + }, + "validate_email": { + "title": "Verify email", + "continue_button": "Continue", + "message": "Please check your email {{email_address}} and validate before continuing further" + }, + "captcha": { + "message": "Pleace check the box below to proceed" + } + }, + "Client": { + "loading_messages": { + "default": "Heating up", + "message_one": "Almost there...", + "message_two": "Looks like you have a lot of stuff to heat up!" + }, + "logout_prompt": "Logout", + "clear_cache": "Clear cache & reload" + } } } diff --git a/src/app/components/Pdf-viewer/PdfViewer.tsx b/src/app/components/Pdf-viewer/PdfViewer.tsx index c440cce9b8..2904413bf6 100644 --- a/src/app/components/Pdf-viewer/PdfViewer.tsx +++ b/src/app/components/Pdf-viewer/PdfViewer.tsx @@ -19,6 +19,7 @@ import { as, config, } from 'folds'; +import { useTranslation } from 'react-i18next'; import FocusTrap from 'focus-trap-react'; import FileSaver from 'file-saver'; import * as css from './PdfViewer.css'; @@ -97,6 +98,7 @@ export const PdfViewer = as<'div', PdfViewerProps>( if (docState.status !== AsyncStatus.Success) return; setPageNo((n) => Math.min(n + 1, docState.data.numPages)); }; + const { t } = useTranslation(); return ( @@ -139,7 +141,7 @@ export const PdfViewer = as<'div', PdfViewerProps>( radii="300" before={} > - Download + {t('Components.Files.download')} @@ -147,7 +149,7 @@ export const PdfViewer = as<'div', PdfViewerProps>( {isLoading && } {isError && ( <> - Failed to load PDF + {t('Components.Files.failure_pdf')} )} @@ -183,7 +185,7 @@ export const PdfViewer = as<'div', PdfViewerProps>( onClick={handlePrevPage} aria-disabled={pageNo <= 1} > - Previous + {t('Components.Files.previous')} ( aria-label="Page Number" /> @@ -247,7 +249,7 @@ export const PdfViewer = as<'div', PdfViewerProps>( onClick={handleNextPage} aria-disabled={pageNo >= docState.data.numPages} > - Next + {t('Components.Files.next')} )} diff --git a/src/app/i18n.ts b/src/app/i18n.ts index 7a28a5e7cb..56df31ecc8 100644 --- a/src/app/i18n.ts +++ b/src/app/i18n.ts @@ -25,6 +25,9 @@ i18n backend: { loadPath: 'public/locales/{{lng}}.json', }, + react: { + useSuspense: false, + }, }); export default i18n; diff --git a/src/app/molecules/room-notification/RoomNotification.jsx b/src/app/molecules/room-notification/RoomNotification.jsx index 4adb1169e5..ee2a351dff 100644 --- a/src/app/molecules/room-notification/RoomNotification.jsx +++ b/src/app/molecules/room-notification/RoomNotification.jsx @@ -1,6 +1,7 @@ import React, { useState, useEffect, useCallback } from 'react'; import PropTypes from 'prop-types'; import './RoomNotification.scss'; +import { useTranslation } from 'react-i18next'; import initMatrix from '../../../client/initMatrix'; import cons from '../../../client/state/cons'; @@ -16,19 +17,19 @@ import BellOffIC from '../../../../public/res/ic/outlined/bell-off.svg'; const items = [{ iconSrc: BellIC, - text: 'Global', + text: 'Molecules.RoomNotification.default', type: cons.notifs.DEFAULT, }, { iconSrc: BellRingIC, - text: 'All messages', + text: 'Molecules.RoomNotification.all_messages', type: cons.notifs.ALL_MESSAGES, }, { iconSrc: BellPingIC, - text: 'Mentions & Keywords', + text: 'Molecules.RoomNotification.mentions_and_keywords', type: cons.notifs.MENTIONS_AND_KEYWORDS, }, { iconSrc: BellOffIC, - text: 'Mute', + text: 'Molecules.RoomNotification.mute', type: cons.notifs.MUTE, }]; @@ -117,6 +118,7 @@ function useNotifications(roomId) { function RoomNotification({ roomId }) { const [activeType, setNotification] = useNotifications(roomId); + const { t } = useTranslation(); return (
@@ -129,7 +131,7 @@ function RoomNotification({ roomId }) { onClick={() => setNotification(item)} > - {item.text} + {t(item.text)} diff --git a/src/app/organisms/room/message/Message.tsx b/src/app/organisms/room/message/Message.tsx index 25d894f312..ed98becbb8 100644 --- a/src/app/organisms/room/message/Message.tsx +++ b/src/app/organisms/room/message/Message.tsx @@ -31,6 +31,7 @@ import React, { useCallback, useState, } from 'react'; +import { useTranslation } from 'react-i18next'; import FocusTrap from 'focus-trap-react'; import { useHover, useFocusWithin } from 'react-aria'; import { MatrixEvent, Room } from 'matrix-js-sdk'; @@ -180,6 +181,7 @@ export const MessageReadReceiptItem = as< setOpen(false); onClose?.(); }; + const { t } = useTranslation(); return ( <> @@ -208,7 +210,7 @@ export const MessageReadReceiptItem = as< aria-pressed={open} > - Read Receipts + {t('Molecules.Message.read_receipts')} @@ -257,6 +259,7 @@ export const MessageSourceCodeItem = as< setOpen(false); onClose?.(); }; + const { t } = useTranslation(); return ( <> @@ -290,7 +293,7 @@ export const MessageSourceCodeItem = as< aria-pressed={open} > - View Source + {t('Molecules.Message.view_source')} @@ -712,6 +715,7 @@ export const Message = as<'div', MessageProps>( const closeMenu = () => { setMenu(false); }; + const { t } = useTranslation(); return ( ( size="T300" truncate > - Add Reaction + {t('Molecules.Message.add_reaction')} )} @@ -858,7 +862,7 @@ export const Message = as<'div', MessageProps>( size="T300" truncate > - Reply + {t('Molecules.Message.reply')} {canEditEvent(mx, mEvent) && onEditId && ( @@ -878,7 +882,7 @@ export const Message = as<'div', MessageProps>( size="T300" truncate > - Edit Message + {t('Molecules.Message.edit')} )} diff --git a/src/app/templates/auth/Auth.jsx b/src/app/templates/auth/Auth.jsx index 7c21173605..40d74160b9 100644 --- a/src/app/templates/auth/Auth.jsx +++ b/src/app/templates/auth/Auth.jsx @@ -5,6 +5,7 @@ import './Auth.scss'; import ReCAPTCHA from 'react-google-recaptcha'; import { Formik } from 'formik'; +import { useTranslation, Trans } from 'react-i18next'; import * as auth from '../../../client/action/auth'; import cons from '../../../client/state/cons'; import { Debounce, getUrlPrams } from '../../../util/common'; @@ -27,15 +28,10 @@ import CinnySvg from '../../../../public/res/svg/cinny.svg'; import SSOButtons from '../../molecules/sso-buttons/SSOButtons'; const LOCALPART_SIGNUP_REGEX = /^[a-z0-9_\-.=/]+$/; -const BAD_LOCALPART_ERROR = 'Username can only contain characters a-z, 0-9, or \'=_-./\''; -const USER_ID_TOO_LONG_ERROR = 'Your user ID, including the hostname, can\'t be more than 255 characters long.'; const PASSWORD_STRENGHT_REGEX = /^(?=.*\d)(?=.*[A-Z])(?=.*[a-z])(?=.*[^\w\d\s:])([^\s]){8,127}$/; -const BAD_PASSWORD_ERROR = 'Password must contain at least 1 lowercase, 1 uppercase, 1 number, 1 non-alphanumeric character, 8-127 characters with no space.'; -const CONFIRM_PASSWORD_ERROR = 'Passwords don\'t match.'; const EMAIL_REGEX = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i; -const BAD_EMAIL_ERROR = 'Invalid email address'; function isValidInput(value, regex) { if (typeof regex === 'string') return regex === value; @@ -50,16 +46,19 @@ let searchingHs = null; function Homeserver({ onChange }) { const [hs, setHs] = useState(null); const [debounce] = useState(new Debounce()); - const [process, setProcess] = useState({ isLoading: true, message: 'Loading homeserver list...' }); + + const { t } = useTranslation(); + + const [process, setProcess] = useState({ isLoading: true, message: t('Templates.Auth.loading_homeserver_list') }); const hsRef = useRef(); const setupHsConfig = async (servername) => { - setProcess({ isLoading: true, message: 'Looking for homeserver...' }); + setProcess({ isLoading: true, message: t('Templates.Auth.looking_for_homeserver') }); let baseUrl = null; baseUrl = await getBaseUrl(servername); if (searchingHs !== servername) return; - setProcess({ isLoading: true, message: `Connecting to ${baseUrl}...` }); + setProcess({ isLoading: true, message: t('Templates.Auth.connecting_to_homeserver', { homeserver: baseUrl }) }); const tempClient = auth.createTemporaryClient(baseUrl); Promise.allSettled([tempClient.loginFlows(), tempClient.register()]) @@ -74,7 +73,7 @@ function Homeserver({ onChange }) { }).catch(() => { if (searchingHs !== servername) return; onChange(null); - setProcess({ isLoading: false, error: 'Unable to connect. Please check your input.' }); + setProcess({ isLoading: false, error: t('Templates.Auth.unable_to_connect') }); }); }; @@ -118,14 +117,14 @@ function Homeserver({ onChange }) { onChange={handleHsInput} value={hs?.selected} forwardRef={hsRef} - label="Homeserver" + label={t('Templates.Auth.homeserver_form.title')} disabled={hs === null || !hs.allowCustom} /> ( <> - Homeserver list + {t('Templates.Auth.homeserver_form.header')} { hs?.list.map((hsName) => ( flow.type === 'm.login.password')[0]; const ssoProviders = loginFlow?.filter((flow) => flow.type === 'm.login.sso')[0]; @@ -173,7 +173,7 @@ function Login({ loginFlow, baseUrl }) { const validator = (values) => { const errors = {}; if (typeIndex === 1 && values.email.length > 0 && !isValidInput(values.email, EMAIL_REGEX)) { - errors.email = BAD_EMAIL_ERROR; + errors.email = t('Templates.Auth.bad_email_error'); } return errors; }; @@ -196,7 +196,7 @@ function Login({ loginFlow, baseUrl }) { window.location.reload(); }).catch((error) => { let msg = error.message; - if (msg === 'Unknown message') msg = 'Please check your credentials'; + if (msg === 'Unknown message') msg = t('Templates.Auth.check_credentials'); actions.setErrors({ password: msg === 'Invalid password' ? msg : undefined, other: msg !== 'Invalid password' ? msg : undefined, @@ -208,7 +208,7 @@ function Login({ loginFlow, baseUrl }) { return ( <>
- Login + {t('Templates.Auth.login_form.title')} {isPassword && ( ( <> - {isSubmitting && } + {isSubmitting && }
- {typeIndex === 0 && } + {typeIndex === 0 && } {errors.username && {errors.username}} - {typeIndex === 1 && } + {typeIndex === 1 && } {errors.email && {errors.email}}
- + setPassVisible(!passVisible)} src={passVisible ? EyeIC : EyeBlindIC} size="extra-small" />
{errors.password && {errors.password}} {errors.other && {errors.other}}
- +
)} )} - {ssoProviders && isPassword && OR} + {ssoProviders && isPassword && {t('common.or')}} {ssoProviders && ( { const errors = {}; - if (values.username.list > 255) errors.username = USER_ID_TOO_LONG_ERROR; + if (values.username.list > 255) errors.username = t('Templates.Auth.user_id_too_long_error'); if (values.username.length > 0 && !isValidInput(values.username, LOCALPART_SIGNUP_REGEX)) { - errors.username = BAD_LOCALPART_ERROR; + errors.username = t('Templates.Auth.bad_localpart_error'); } if (values.password.length > 0 && !isValidInput(values.password, PASSWORD_STRENGHT_REGEX)) { - errors.password = BAD_PASSWORD_ERROR; + errors.password = t('Templates.Auth.bad_password_error'); } if (values.confirmPassword.length > 0 && !isValidInput(values.confirmPassword, values.password)) { - errors.confirmPassword = CONFIRM_PASSWORD_ERROR; + errors.confirmPassword = t('Templates.Auth.confirm_password_error'); } if (values.email.length > 0 && !isValidInput(values.email, EMAIL_REGEX)) { - errors.email = BAD_EMAIL_ERROR; + errors.email = t('Templates.Auth.bad_email_error'); } return errors; }; @@ -335,7 +337,7 @@ function Register({ registerInfo, loginFlow, baseUrl }) { return tempClient.isUsernameAvailable(values.username) .then(async (isAvail) => { if (!isAvail) { - actions.setErrors({ username: 'Username is already taken' }); + actions.setErrors({ username: t('Templates.Auth.username_taken') }); actions.setSubmitting(false); return; } @@ -349,12 +351,12 @@ function Register({ registerInfo, loginFlow, baseUrl }) { } sid = result.sid; } - setProcess({ type: 'processing', message: 'Registration in progress....' }); + setProcess({ type: 'processing', message: t('Templates.Auth.registration_in_progress') }); actions.setSubmitting(false); }).catch((err) => { const msg = err.message || err.error; if (['M_USER_IN_USE', 'M_INVALID_USERNAME', 'M_EXCLUSIVE'].indexOf(err.errcode) > -1) { - actions.setErrors({ username: err.errcode === 'M_USER_IN_USE' ? 'Username is already taken' : msg }); + actions.setErrors({ username: err.errcode === 'M_USER_IN_USE' ? t('Templates.Auth.username_taken') : msg }); } else if (msg) actions.setErrors({ other: msg }); actions.setSubmitting(false); @@ -409,7 +411,7 @@ function Register({ registerInfo, loginFlow, baseUrl }) { session, }); if (d.done) refreshWindow(); - else setProcess({ type: 'processing', message: 'Registration in progress...' }); + else setProcess({ type: 'processing', message: t('Templates.Auth.registration_in_progress') }); }; const handleTerms = async () => { const [username, password] = getInputs(); @@ -418,7 +420,7 @@ function Register({ registerInfo, loginFlow, baseUrl }) { session, }); if (d.done) refreshWindow(); - else setProcess({ type: 'processing', message: 'Registration in progress...' }); + else setProcess({ type: 'processing', message: t('Templates.Auth.registration_in_progress') }); }; const handleEmailVerify = async () => { const [username, password] = getInputs(); @@ -429,17 +431,17 @@ function Register({ registerInfo, loginFlow, baseUrl }) { session, }); if (d.done) refreshWindow(); - else setProcess({ type: 'processing', message: 'Registration in progress...' }); + else setProcess({ type: 'processing', message: t('Templates.Auth.registration_in_progress') }); }; return ( <> {process.type === 'processing' && } - {process.type === 'm.login.recaptcha' && } + {process.type === 'm.login.recaptcha' && } {process.type === 'm.login.terms' && } {process.type === 'm.login.email.identity' && }
- {!isDisabled && Register} + {!isDisabled && {t('Templates.Auth.register_form.title')}} {isDisabled && {registerInfo.error}}
{!isDisabled && ( @@ -452,25 +454,25 @@ function Register({ registerInfo, loginFlow, baseUrl }) { values, errors, handleChange, handleSubmit, isSubmitting, }) => ( <> - {process.type === undefined && isSubmitting && } + {process.type === undefined && isSubmitting && }
- + {errors.username && {errors.username}}
- + setPassVisible(!passVisible)} src={passVisible ? EyeIC : EyeBlindIC} size="extra-small" />
{errors.password && {errors.password}}
- + setCPassVisible(!cPassVisible)} src={cPassVisible ? EyeIC : EyeBlindIC} size="extra-small" />
{errors.confirmPassword && {errors.confirmPassword}} - {isEmail && } + {isEmail && } {errors.email && {errors.email}} {errors.other && {errors.other}}
- +
@@ -498,6 +500,7 @@ Register.propTypes = { function AuthCard() { const [hsConfig, setHsConfig] = useState(null); const [type, setType] = useState('login'); + const { t } = useTranslation(); const handleHsChange = (info) => { console.log(info); @@ -520,13 +523,13 @@ function AuthCard() { )} { hsConfig !== null && ( - {`${(type === 'login' ? 'Don\'t have' : 'Already have')} an account?`} + {t(type === 'login' ? 'Templates.Auth.dont_have_an_account' : 'Templates.Auth.already_have_an_account')} )} @@ -536,6 +539,7 @@ function AuthCard() { function Auth() { const [loginToken, setLoginToken] = useState(getUrlPrams('loginToken')); + const { t } = useTranslation(); useEffect(async () => { if (!loginToken) return; @@ -558,7 +562,7 @@ function Auth() {
- {loginToken && } + {loginToken && } {!loginToken && ( @@ -624,21 +628,26 @@ Recaptcha.propTypes = { }; function Terms({ url, onSubmit }) { + const { t } = useTranslation(); + return (
{ e.preventDefault(); onSubmit(); }}>
- Agree with terms + {t('Templates.Auth.terms_and_conditions.title')}
- In order to complete registration, you need to agree to the terms and conditions. + {t('Templates.Auth.terms_and_conditions.description')}
- {'I accept '} - Terms and Conditions + }} + />
- +
@@ -650,18 +659,22 @@ Terms.propTypes = { }; function EmailVerify({ email, onContinue }) { + const { t } = useTranslation(); return (
- Verify email + {t('Templates.Auth.validate_email.title')}
- {'Please check your email '} - {`(${email})`} - {' and validate before continuing further.'} + }} + />
- +
); @@ -681,4 +694,4 @@ ProcessWrapper.propTypes = { children: PropTypes.node.isRequired, }; -export default Auth; +export default Auth; \ No newline at end of file diff --git a/src/app/templates/client/Client.jsx b/src/app/templates/client/Client.jsx index e9be6b16eb..78df5a29ce 100644 --- a/src/app/templates/client/Client.jsx +++ b/src/app/templates/client/Client.jsx @@ -1,5 +1,6 @@ import React, { useState, useEffect, useRef } from 'react'; import './Client.scss'; +import { useTranslation } from 'react-i18next'; import { initHotkeys } from '../../../client/event/hotkeys'; import { initRoomListListener } from '../../../client/event/roomList'; @@ -37,9 +38,11 @@ function SystemEmojiFeature() { function Client() { const [isLoading, changeLoading] = useState(true); - const [loadingMsg, setLoadingMsg] = useState('Heating up'); + const [loadingMsg, setLoadingMsg] = useState('Templates.Client.loading_messages.default'); const classNameHidden = 'client__item-hidden'; + const { t } = useTranslation(); + const navWrapperRef = useRef(null); const roomWrapperRef = useRef(null); @@ -66,7 +69,7 @@ function Client() { changeLoading(true); let counter = 0; const iId = setInterval(() => { - const msgList = ['Almost there...', 'Looks like you have a lot of stuff to heat up!']; + const msgList = ['Templates.Client.loading_messages.message_one', 'Templates.Client.loading_messages.message_two']; if (counter === msgList.length - 1) { setLoadingMsg(msgList[msgList.length - 1]); clearInterval(iId); @@ -93,9 +96,9 @@ function Client() { content={ <> initMatrix.clearCacheAndReload()}> - Clear cache & reload + {t('Templates.Client.clear_cache')} - initMatrix.logout()}>Logout + initMatrix.logout()}>{t('Templates.Client.logout_prompt')} } render={(toggle) => ( @@ -105,7 +108,7 @@ function Client() {
- {loadingMsg} + {t(loadingMsg)}