From e5f227531ac95f59b0a8e368cbf2c85ecbac2f26 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 20 Nov 2025 18:41:39 +0000 Subject: [PATCH] WIP refactor of i18n To put traslation logic back into element web but split the keys up so everything is not type dependenct on everything else for the translation keys. --- package.json | 2 +- packages/shared-components/package.json | 6 +- .../scripts/gatherTranslationKeys.ts | 67 ------------------- .../audio/AudioPlayerView/AudioPlayerView.tsx | 2 +- .../audio/PlayPauseButton/PlayPauseButton.tsx | 2 +- .../src/audio/SeekBar/SeekBar.tsx | 2 +- .../src/i18n/strings/en_EN.json | 17 +++++ packages/shared-components/src/i18nStub.ts | 14 ++++ packages/shared-components/src/index.ts | 6 -- .../src/pill-input/Pill/Pill.tsx | 2 +- .../src/rich-list/RichItem/RichItem.tsx | 2 +- .../shared-components/src/test/setupTests.ts | 2 +- packages/shared-components/tsconfig.json | 1 + packages/shared-components/vite.config.js | 3 +- packages/shared-components/yarn.lock | 5 ++ src/@types/global.d.ts | 3 +- src/accessibility/KeyboardShortcuts.ts | 5 +- .../views/beacon/BeaconListItem.tsx | 2 +- .../views/elements/SettingsDropdown.tsx | 2 +- .../views/settings/Notifications.tsx | 3 +- .../verification/VerificationShowSas.tsx | 3 +- .../src/utils => src/i18n}/humanize.test.ts | 0 .../src/utils => src/i18n}/humanize.ts | 0 .../src/utils => src/i18n}/i18n.test.ts | 0 .../src/utils => src/i18n}/i18n.tsx | 5 +- src/i18n/strings/en_EN.json | 4 +- src/languageHandler.tsx | 24 ++----- src/modules/I18nApi.ts | 12 +++- src/settings/Settings.tsx | 4 +- .../room-list/previews/MessageEventPreview.ts | 3 +- .../previews/PollStartEventPreview.ts | 3 +- src/utils/ErrorUtils.tsx | 3 +- src/vector/index.ts | 2 +- src/vector/init.tsx | 6 +- src/widgets/CapabilityText.tsx | 3 +- test/unit-tests/languageHandler-test.tsx | 5 +- 36 files changed, 95 insertions(+), 130 deletions(-) delete mode 100644 packages/shared-components/scripts/gatherTranslationKeys.ts create mode 100644 packages/shared-components/src/i18n/strings/en_EN.json create mode 100644 packages/shared-components/src/i18nStub.ts rename {packages/shared-components/src/utils => src/i18n}/humanize.test.ts (100%) rename {packages/shared-components/src/utils => src/i18n}/humanize.ts (100%) rename {packages/shared-components/src/utils => src/i18n}/i18n.test.ts (100%) rename {packages/shared-components/src/utils => src/i18n}/i18n.tsx (98%) diff --git a/package.json b/package.json index 0e4f633755..87dd44256c 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "UserFriendlyError" ], "scripts": { - "i18n": "matrix-gen-i18n src res packages/shared-components/src && yarn i18n:sort && yarn i18n:lint", + "i18n": "matrix-gen-i18n src res && yarn i18n:sort && yarn i18n:lint", "i18n:sort": "jq --sort-keys '.' src/i18n/strings/en_EN.json > src/i18n/strings/en_EN.json.tmp && mv src/i18n/strings/en_EN.json.tmp src/i18n/strings/en_EN.json", "i18n:lint": "matrix-i18n-lint && prettier --log-level=silent --write src/i18n/strings/ --ignore-path /dev/null", "i18n:diff": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && yarn i18n && matrix-compare-i18n-files src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json", diff --git a/packages/shared-components/package.json b/packages/shared-components/package.json index 3e24c1351d..a72847dd72 100644 --- a/packages/shared-components/package.json +++ b/packages/shared-components/package.json @@ -34,8 +34,11 @@ "package.json" ], "scripts": { + "i18n": "matrix-gen-i18n src && yarn i18n:sort && yarn i18n:lint", + "i18n:sort": "jq --sort-keys '.' src/i18n/strings/en_EN.json > src/i18n/strings/en_EN.json.tmp && mv src/i18n/strings/en_EN.json.tmp src/i18n/strings/en_EN.json", + "i18n:lint": "matrix-i18n-lint && prettier --log-level=silent --write src/i18n/strings/ --ignore-path /dev/null", "test": "jest", - "prepare": "patch-package && yarn --cwd ../.. build:res && node scripts/gatherTranslationKeys.ts && vite build", + "prepare": "patch-package && vite build", "storybook": "storybook dev -p 6007", "build-storybook": "storybook build", "lint": "yarn lint:types && yarn lint:js", @@ -46,6 +49,7 @@ "test:storybook:update": "playwright-screenshots --entrypoint /work/node_modules/.bin/test-storybook --with-node-modules --url http://host.docker.internal:6007/ --updateSnapshot" }, "dependencies": { + "@element-hq/element-web-module-api": "^1.5.0", "classnames": "^2.5.1", "counterpart": "^0.18.6", "lodash": "^4.17.21", diff --git a/packages/shared-components/scripts/gatherTranslationKeys.ts b/packages/shared-components/scripts/gatherTranslationKeys.ts deleted file mode 100644 index 37812df33b..0000000000 --- a/packages/shared-components/scripts/gatherTranslationKeys.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* -Copyright 2025 Element Creations Ltd. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial -Please see LICENSE files in the repository root for full details. -*/ - -// Gathers all the translation keys from element-web's en_EN.json into a TypeScript type definition file -// that exports a type `TranslationKey` which is a union of all supported translation keys. -// This prevents having to import the json file and make typescript do the work as this results in vite-dts -// generating an import to the json file in the .d.ts which doesn't work at runtime: this way, the type -// gets put into the bundle. -// XXX: It should *not* be in the 'src' directory, being a generated file, but if it isn't then the type -// bundler won't bundle the types and will leave the file as a relative import, which will break. - -import * as fs from "fs"; -import * as path from "path"; -import { dirname } from "node:path"; -import { fileURLToPath } from "node:url"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const i18nStringsPath = path.resolve(__dirname, "../../../src/i18n/strings/en_EN.json"); -const outPath = path.resolve(__dirname, "../src/i18nKeys.d.ts"); - -function gatherKeys(obj: any, prefix: string[] = []): string[] { - if (typeof obj !== "object" || obj === null) return []; - let keys: string[] = []; - for (const key of Object.keys(obj)) { - const value = obj[key]; - - // add the path (for both leaves and intermediates as then we include plurals) - keys.push([...prefix, key].join("|")); - if (typeof value === "object" && value !== null) { - // If the value is an object, recurse - keys = keys.concat(gatherKeys(value, [...prefix, key])); - } - } - return keys; -} - -function main() { - const json = JSON.parse(fs.readFileSync(i18nStringsPath, "utf8")); - const keys = gatherKeys(json); - const typeDef = - "/*\n" + - " * Copyright 2025 Element Creations Ltd.\n" + - " *\n" + - " * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial\n" + - " * Please see LICENSE files in the repository root for full details.\n" + - " */\n" + - "\n" + - "// This file is auto-generated by gatherTranslationKeys.ts\n" + - "// Do not edit manually.\n\n" + - "export type TranslationKey =\n" + - keys.map((k) => ` | \"${k}\"`).join("\n") + - ";\n"; - fs.mkdirSync(path.dirname(outPath), { recursive: true }); - fs.writeFileSync(outPath, typeDef, "utf8"); - console.log(`Wrote ${keys.length} keys to ${outPath}`); -} - -if (import.meta.url.startsWith("file:")) { - const modulePath = fileURLToPath(import.meta.url); - if (process.argv[1] === modulePath) { - main(); - } -} diff --git a/packages/shared-components/src/audio/AudioPlayerView/AudioPlayerView.tsx b/packages/shared-components/src/audio/AudioPlayerView/AudioPlayerView.tsx index 29fb02ba34..511064967e 100644 --- a/packages/shared-components/src/audio/AudioPlayerView/AudioPlayerView.tsx +++ b/packages/shared-components/src/audio/AudioPlayerView/AudioPlayerView.tsx @@ -14,7 +14,7 @@ import { Flex } from "../../utils/Flex"; import styles from "./AudioPlayerView.module.css"; import { PlayPauseButton } from "../PlayPauseButton"; import { type PlaybackState } from "../playback"; -import { _t } from "../../utils/i18n"; +import { _t } from "__i18nAPI"; import { formatBytes } from "../../utils/FormattingUtils"; import { Clock } from "../Clock"; import { SeekBar } from "../SeekBar"; diff --git a/packages/shared-components/src/audio/PlayPauseButton/PlayPauseButton.tsx b/packages/shared-components/src/audio/PlayPauseButton/PlayPauseButton.tsx index 400357a3f5..927d41f13a 100644 --- a/packages/shared-components/src/audio/PlayPauseButton/PlayPauseButton.tsx +++ b/packages/shared-components/src/audio/PlayPauseButton/PlayPauseButton.tsx @@ -11,7 +11,7 @@ import Play from "@vector-im/compound-design-tokens/assets/web/icons/play-solid" import Pause from "@vector-im/compound-design-tokens/assets/web/icons/pause-solid"; import styles from "./PlayPauseButton.module.css"; -import { _t } from "../../utils/i18n"; +import { _t } from "__i18nAPI"; export interface PlayPauseButtonProps extends HTMLAttributes { /** diff --git a/packages/shared-components/src/audio/SeekBar/SeekBar.tsx b/packages/shared-components/src/audio/SeekBar/SeekBar.tsx index 3063e2442d..fc14262001 100644 --- a/packages/shared-components/src/audio/SeekBar/SeekBar.tsx +++ b/packages/shared-components/src/audio/SeekBar/SeekBar.tsx @@ -10,7 +10,7 @@ import { throttle } from "lodash"; import classNames from "classnames"; import style from "./SeekBar.module.css"; -import { _t } from "../../utils/i18n"; +import { _t } from "__i18nAPI"; export interface SeekBarProps extends React.InputHTMLAttributes { /** diff --git a/packages/shared-components/src/i18n/strings/en_EN.json b/packages/shared-components/src/i18n/strings/en_EN.json new file mode 100644 index 0000000000..cbf2c8c0be --- /dev/null +++ b/packages/shared-components/src/i18n/strings/en_EN.json @@ -0,0 +1,17 @@ +{ + "a11y": { + "seek_bar_label": "a11y|seek_bar_label" + }, + "action": { + "delete": "action|delete", + "pause": "action|pause", + "play": "action|play" + }, + "timeline": { + "m.audio": { + "audio_player": "timeline|m.audio|audio_player", + "error_downloading_audio": "timeline|m.audio|error_downloading_audio", + "unnamed_audio": "timeline|m.audio|unnamed_audio" + } + } +} diff --git a/packages/shared-components/src/i18nStub.ts b/packages/shared-components/src/i18nStub.ts new file mode 100644 index 0000000000..6e7ce59620 --- /dev/null +++ b/packages/shared-components/src/i18nStub.ts @@ -0,0 +1,14 @@ +/* +Copyright 2025 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import type Translations from "./i18n/strings/en_EN.json"; +import { type TranslationKey as TranslationKeyType } from "matrix-web-i18n"; +import { translateFn, humanizeTimeFn } from "matrix-web-i18n"; +type TranslationKey = TranslationKeyType; + +export const _t: translateFn = () => ""; +export const humanizeTime: humanizeTimeFn = () => ""; diff --git a/packages/shared-components/src/index.ts b/packages/shared-components/src/index.ts index 68935afd3f..b1e423086b 100644 --- a/packages/shared-components/src/index.ts +++ b/packages/shared-components/src/index.ts @@ -21,8 +21,6 @@ export * from "./utils/Box"; export * from "./utils/Flex"; // Utils -export * from "./utils/i18n"; -export * from "./utils/humanize"; export * from "./utils/DateUtils"; export * from "./utils/numbers"; export * from "./utils/FormattingUtils"; @@ -31,7 +29,3 @@ export * from "./utils/FormattingUtils"; export * from "./viewmodel"; export * from "./useMockedViewModel"; export * from "./useViewModel"; - -// i18n (we must export this directly in order to not confuse the type bundler, it seems, -// otherwise it will leave it as a relative import rather than bundling it) -export type * from "./i18nKeys.d.ts"; diff --git a/packages/shared-components/src/pill-input/Pill/Pill.tsx b/packages/shared-components/src/pill-input/Pill/Pill.tsx index b2ac2e28b2..d2667988de 100644 --- a/packages/shared-components/src/pill-input/Pill/Pill.tsx +++ b/packages/shared-components/src/pill-input/Pill/Pill.tsx @@ -12,7 +12,7 @@ import CloseIcon from "@vector-im/compound-design-tokens/assets/web/icons/close" import { Flex } from "../../utils/Flex"; import styles from "./Pill.module.css"; -import { _t } from "../../utils/i18n"; +import { _t } from "__i18nAPI"; export interface PillProps extends Omit, "onClick"> { /** diff --git a/packages/shared-components/src/rich-list/RichItem/RichItem.tsx b/packages/shared-components/src/rich-list/RichItem/RichItem.tsx index 3cef2690fc..28ab692fc2 100644 --- a/packages/shared-components/src/rich-list/RichItem/RichItem.tsx +++ b/packages/shared-components/src/rich-list/RichItem/RichItem.tsx @@ -9,7 +9,7 @@ import React, { type HTMLAttributes, type JSX, memo } from "react"; import CheckIcon from "@vector-im/compound-design-tokens/assets/web/icons/check"; import styles from "./RichItem.module.css"; -import { humanizeTime } from "../../utils/humanize"; +import { humanizeTime } from "__i18nAPI"; import { Flex } from "../../utils/Flex"; export interface RichItemProps extends HTMLAttributes { diff --git a/packages/shared-components/src/test/setupTests.ts b/packages/shared-components/src/test/setupTests.ts index 43ffc0c071..303c332121 100644 --- a/packages/shared-components/src/test/setupTests.ts +++ b/packages/shared-components/src/test/setupTests.ts @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. import fetchMock from "fetch-mock-jest"; -import { setLanguage } from "../../src/utils/i18n"; +import { setLanguage } from "__i18nAPI"; import en from "../../../../src/i18n/strings/en_EN.json"; export function setupLanguageMock(): void { diff --git a/packages/shared-components/tsconfig.json b/packages/shared-components/tsconfig.json index 025901c97d..0c4074b6b6 100644 --- a/packages/shared-components/tsconfig.json +++ b/packages/shared-components/tsconfig.json @@ -17,6 +17,7 @@ "lib": ["es2022", "es2024.promise", "dom", "dom.iterable"], "strict": true, "paths": { + "__i18nAPI": ["./src/i18nStub.ts"], "jest-matrix-react": ["./src/test/utils/jest-matrix-react"], "rollup/parseAst": ["./node_modules/rollup/dist/parseAst"] } diff --git a/packages/shared-components/vite.config.js b/packages/shared-components/vite.config.js index 83c999d87f..999a55bd7d 100644 --- a/packages/shared-components/vite.config.js +++ b/packages/shared-components/vite.config.js @@ -26,13 +26,14 @@ export default defineConfig({ rollupOptions: { // make sure to externalize deps that shouldn't be bundled // into your library - external: ["react", "react-dom", "@vector-im/compound-design-tokens", "@vector-im/compound-web"], + external: ["react", "react-dom", "@vector-im/compound-design-tokens", "@vector-im/compound-web", "__i18nAPI"], output: { // Provide global variables to use in the UMD build // for externalized deps globals: { "react": "react", "react-dom": "ReactDom", + "__i18nAPI": "mxI18nAPI", }, }, }, diff --git a/packages/shared-components/yarn.lock b/packages/shared-components/yarn.lock index 46c2c8d0e4..0cd0a3edfc 100644 --- a/packages/shared-components/yarn.lock +++ b/packages/shared-components/yarn.lock @@ -352,6 +352,11 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== +"@element-hq/element-web-module-api@^1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@element-hq/element-web-module-api/-/element-web-module-api-1.5.0.tgz#077a528917f4eb558059a2a5286b9bb6a2fb1690" + integrity sha512-WI/iMADRouXp9WhQy5jov6Z4eKKlHEPh20DKoCsKZ9dWaYcW/MiBhzi09PZxay+o0RLZXA6aDPxpxaIX3lZXag== + "@element-hq/element-web-playwright-common@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@element-hq/element-web-playwright-common/-/element-web-playwright-common-2.0.0.tgz#30cf741a33c69540b4bc434f5349d0fe900bc611" diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 9d43a13ca4..fb5a892ad6 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details. import "matrix-js-sdk/src/@types/global"; // load matrix-js-sdk's type extensions first import "@types/modernizr"; -import type { ModuleLoader } from "@element-hq/element-web-module-api"; +import type { I18nApi, ModuleLoader } from "@element-hq/element-web-module-api"; import type { logger } from "matrix-js-sdk/src/logger"; import type ContentMessages from "../ContentMessages"; import { type IMatrixClientPeg } from "../MatrixClientPeg"; @@ -125,6 +125,7 @@ declare global { mxOnRecaptchaLoaded?: () => void; mxModuleLoader: ModuleLoader; mxModuleApi: ModuleApiType; + mxI18nAPI: I18nApi; // electron-only electron?: Electron; diff --git a/src/accessibility/KeyboardShortcuts.ts b/src/accessibility/KeyboardShortcuts.ts index 2872e1a1a2..d3db1281c8 100644 --- a/src/accessibility/KeyboardShortcuts.ts +++ b/src/accessibility/KeyboardShortcuts.ts @@ -8,12 +8,11 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -// Import i18n.tsx instead of languageHandler to avoid circular deps -import { _td, type TranslationKey } from "@element-hq/web-shared-components"; - import { IS_MAC, IS_ELECTRON, Key } from "../Keyboard"; import { type IBaseSetting } from "../settings/Settings"; import { type KeyCombo } from "../KeyBindingsManager"; +import { type TranslationKey } from "../i18n/i18n"; +import { _td } from "../i18n/i18n"; export enum KeyBindingAction { /** Send a message */ diff --git a/src/components/views/beacon/BeaconListItem.tsx b/src/components/views/beacon/BeaconListItem.tsx index fb2dc2e17d..3f116eb531 100644 --- a/src/components/views/beacon/BeaconListItem.tsx +++ b/src/components/views/beacon/BeaconListItem.tsx @@ -8,7 +8,6 @@ Please see LICENSE files in the repository root for full details. import React, { type HTMLProps, useContext } from "react"; import { type Beacon, BeaconEvent, LocationAssetType } from "matrix-js-sdk/src/matrix"; -import { humanizeTime } from "@element-hq/web-shared-components"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { useEventEmitterState } from "../../../hooks/useEventEmitter"; @@ -19,6 +18,7 @@ import BeaconStatus from "./BeaconStatus"; import { BeaconDisplayStatus } from "./displayStatus"; import StyledLiveBeaconIcon from "./StyledLiveBeaconIcon"; import ShareLatestLocation from "./ShareLatestLocation"; +import { humanizeTime } from "../../../i18n/humanize"; interface Props { beacon: Beacon; diff --git a/src/components/views/elements/SettingsDropdown.tsx b/src/components/views/elements/SettingsDropdown.tsx index db78e7cc36..bb51145a93 100644 --- a/src/components/views/elements/SettingsDropdown.tsx +++ b/src/components/views/elements/SettingsDropdown.tsx @@ -6,13 +6,13 @@ Please see LICENSE files in the repository root for full details. */ import React, { type JSX, useCallback, useId, useState } from "react"; -import { _t } from "@element-hq/web-shared-components"; import SettingsStore from "../../../settings/SettingsStore"; import { type SettingLevel } from "../../../settings/SettingLevel"; import { SETTINGS, type StringSettingKey } from "../../../settings/Settings"; import { useSettingValueAt } from "../../../hooks/useSettings.ts"; import Dropdown, { type DropdownProps } from "./Dropdown.tsx"; +import { _t } from "../../../languageHandler.tsx"; interface Props { settingKey: StringSettingKey; diff --git a/src/components/views/settings/Notifications.tsx b/src/components/views/settings/Notifications.tsx index b1b0955181..96b9b98777 100644 --- a/src/components/views/settings/Notifications.tsx +++ b/src/components/views/settings/Notifications.tsx @@ -30,7 +30,7 @@ import { VectorState, type VectorPushRuleDefinition, } from "../../../notifications"; -import { _t, type TranslatedString } from "../../../languageHandler"; +import { _t } from "../../../languageHandler"; import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; import SettingsStore from "../../../settings/SettingsStore"; import StyledRadioButton from "../elements/StyledRadioButton"; @@ -52,6 +52,7 @@ import { SettingsSubsectionHeading } from "./shared/SettingsSubsectionHeading"; import { SettingsSubsection } from "./shared/SettingsSubsection"; import { doesRoomHaveUnreadMessages } from "../../../Unread"; import SettingsFlag from "../elements/SettingsFlag"; +import { TranslatedString } from "../../../i18n/i18n"; // TODO: this "view" component still has far too much application logic in it, // which should be factored out to other files. diff --git a/src/components/views/verification/VerificationShowSas.tsx b/src/components/views/verification/VerificationShowSas.tsx index 0f6272a885..14384e62d9 100644 --- a/src/components/views/verification/VerificationShowSas.tsx +++ b/src/components/views/verification/VerificationShowSas.tsx @@ -10,8 +10,9 @@ import React from "react"; import { type Device } from "matrix-js-sdk/src/matrix"; import { type GeneratedSas, type EmojiMapping } from "matrix-js-sdk/src/crypto-api"; import SasEmoji from "@matrix-org/spec/sas-emoji.json"; +import { getNormalizedLanguageKeys } from "matrix-web-i18n"; -import { _t, getNormalizedLanguageKeys, getUserLanguage } from "../../../languageHandler"; +import { _t, getUserLanguage } from "../../../languageHandler"; import { PendingActionSpinner } from "../right_panel/EncryptionInfo"; import AccessibleButton from "../elements/AccessibleButton"; diff --git a/packages/shared-components/src/utils/humanize.test.ts b/src/i18n/humanize.test.ts similarity index 100% rename from packages/shared-components/src/utils/humanize.test.ts rename to src/i18n/humanize.test.ts diff --git a/packages/shared-components/src/utils/humanize.ts b/src/i18n/humanize.ts similarity index 100% rename from packages/shared-components/src/utils/humanize.ts rename to src/i18n/humanize.ts diff --git a/packages/shared-components/src/utils/i18n.test.ts b/src/i18n/i18n.test.ts similarity index 100% rename from packages/shared-components/src/utils/i18n.test.ts rename to src/i18n/i18n.test.ts diff --git a/packages/shared-components/src/utils/i18n.tsx b/src/i18n/i18n.tsx similarity index 98% rename from packages/shared-components/src/utils/i18n.tsx rename to src/i18n/i18n.tsx index 2ce1f78005..555f2cae31 100644 --- a/packages/shared-components/src/utils/i18n.tsx +++ b/src/i18n/i18n.tsx @@ -24,13 +24,14 @@ import React from "react"; import { KEY_SEPARATOR } from "matrix-web-i18n"; import counterpart from "counterpart"; +import { type TranslationKey as TranslationKeyType } from "matrix-web-i18n"; -import type { TranslationKey } from "../index"; +import type Translations from "../i18n/strings/en_EN.json"; // @ts-ignore - $webapp is a webpack resolve alias pointing to the output directory, see webpack config import webpackLangJsonUrl from "$webapp/i18n/languages.json"; -export { KEY_SEPARATOR, normalizeLanguageKey, getNormalizedLanguageKeys } from "matrix-web-i18n"; +export type TranslationKey = TranslationKeyType; const i18nFolder = "i18n/"; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 847c9b3f20..38c6e77f83 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -3463,11 +3463,9 @@ "unable_to_find": "Tried to load a specific point in this room's timeline, but was unable to find it." }, "m.audio": { - "audio_player": "Audio player", "error_downloading_audio": "Error downloading audio", "error_processing_audio": "Error processing audio message", - "error_processing_voice_message": "Error processing voice message", - "unnamed_audio": "Unnamed audio" + "error_processing_voice_message": "Error processing voice message" }, "m.beacon_info": { "view_live_location": "View live location" diff --git a/src/languageHandler.tsx b/src/languageHandler.tsx index da31ab1811..a63e0b1cf9 100644 --- a/src/languageHandler.tsx +++ b/src/languageHandler.tsx @@ -8,21 +8,20 @@ import { logger } from "matrix-js-sdk/src/logger"; import { type Optional } from "matrix-events-sdk"; import { MapWithDefault } from "matrix-js-sdk/src/utils"; +import { KEY_SEPARATOR, normalizeLanguageKey } from "matrix-web-i18n"; import { type TranslationStringsObject } from "@matrix-org/react-sdk-module-api"; import _ from "lodash"; + import { _t, - normalizeLanguageKey, - type TranslationKey, type IVariables, - KEY_SEPARATOR, getLangsJson, registerTranslations, setLocale, getLocale, setMissingEntryGenerator as setMissingEntryGeneratorSharedComponents, -} from "@element-hq/web-shared-components"; - + type TranslationKey, +} from "./i18n/i18n"; import SettingsStore from "./settings/SettingsStore"; import PlatformPeg from "./PlatformPeg"; import { SettingLevel } from "./settings/SettingLevel"; @@ -30,20 +29,7 @@ import { retry } from "./utils/promise"; import SdkConfig from "./SdkConfig"; import { ModuleRunner } from "./modules/ModuleRunner"; -export { - _t, - type IVariables, - type Tags, - type TranslationKey, - type TranslatedString, - _td, - _tDom, - lookupString, - sanitizeForTranslation, - normalizeLanguageKey, - getNormalizedLanguageKeys, - substitute, -} from "@element-hq/web-shared-components"; +export { _t, _td, _tDom, type TranslationKey } from "./i18n/i18n"; const i18nFolder = "i18n/"; diff --git a/src/modules/I18nApi.ts b/src/modules/I18nApi.ts index 9a195a47ec..49dd140c1e 100644 --- a/src/modules/I18nApi.ts +++ b/src/modules/I18nApi.ts @@ -6,9 +6,11 @@ Please see LICENSE files in the repository root for full details. */ import { type I18nApi as II18nApi, type Variables, type Translations } from "@element-hq/element-web-module-api"; -import { registerTranslations } from "@element-hq/web-shared-components"; -import { _t, getCurrentLanguage, type TranslationKey } from "../languageHandler.tsx"; +import { registerTranslations, _t } from "../i18n/i18n.ts"; +import { getCurrentLanguage } from "../languageHandler.tsx"; +import { humanizeTime } from "../i18n/humanize.ts"; +import { type TranslationKey } from "../i18n/i18n.tsx"; export class I18nApi implements II18nApi { /** @@ -44,4 +46,10 @@ export class I18nApi implements II18nApi { public translate(key: TranslationKey, variables?: Variables): string { return _t(key, variables); } + + public humanizeTime(timeMillis: number): string { + return humanizeTime(timeMillis); + } + + public async setLanguage(language: string): Promise {} } diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 2b2a57c2c5..658e37ad00 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -9,8 +9,6 @@ Please see LICENSE files in the repository root for full details. import React, { type ReactNode } from "react"; import { STABLE_MSC4133_EXTENDED_PROFILES, UNSTABLE_MSC4133_EXTENDED_PROFILES } from "matrix-js-sdk/src/matrix"; -// Import these directly from shared-components to avoid circular deps -import { _t, _td, type TranslationKey } from "@element-hq/web-shared-components"; import { type MediaPreviewConfig } from "../@types/media_preview.ts"; import DeviceIsolationModeController from "./controllers/DeviceIsolationModeController.ts"; @@ -50,6 +48,8 @@ import { SortingAlgorithm } from "../stores/room-list-v3/skip-list/sorters/index import MediaPreviewConfigController from "./controllers/MediaPreviewConfigController.ts"; import InviteRulesConfigController from "./controllers/InviteRulesConfigController.ts"; import { type ComputedInviteConfig } from "../@types/invite-rules.ts"; +import { type TranslationKey } from "../i18n/i18n.ts"; +import { _t, _td } from "../i18n/i18n.ts"; export const defaultWatchManager = new WatchManager(); diff --git a/src/stores/room-list/previews/MessageEventPreview.ts b/src/stores/room-list/previews/MessageEventPreview.ts index cf69e6903d..d8698a737a 100644 --- a/src/stores/room-list/previews/MessageEventPreview.ts +++ b/src/stores/room-list/previews/MessageEventPreview.ts @@ -10,10 +10,11 @@ import { type MatrixEvent, MsgType, RelationType } from "matrix-js-sdk/src/matri import { type IPreview } from "./IPreview"; import { type TagID } from "../models"; -import { _t, sanitizeForTranslation } from "../../../languageHandler"; +import { _t } from "../../../languageHandler"; import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils"; import { getHtmlText } from "../../../HtmlUtils"; import { stripHTMLReply, stripPlainReply } from "../../../utils/Reply"; +import { sanitizeForTranslation } from "../../../i18n/i18n"; export class MessageEventPreview implements IPreview { public getTextFor(event: MatrixEvent, tagId?: TagID, isThread?: boolean): string | null { diff --git a/src/stores/room-list/previews/PollStartEventPreview.ts b/src/stores/room-list/previews/PollStartEventPreview.ts index d614e736a3..33b558f9d8 100644 --- a/src/stores/room-list/previews/PollStartEventPreview.ts +++ b/src/stores/room-list/previews/PollStartEventPreview.ts @@ -12,9 +12,10 @@ import { PollStartEvent } from "matrix-js-sdk/src/extensible_events_v1/PollStart import { type IPreview } from "./IPreview"; import { type TagID } from "../models"; -import { _t, sanitizeForTranslation } from "../../../languageHandler"; +import { _t } from "../../../languageHandler"; import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import { sanitizeForTranslation } from "../../../i18n/i18n"; export class PollStartEventPreview implements IPreview { public static contextType = MatrixClientContext; diff --git a/src/utils/ErrorUtils.tsx b/src/utils/ErrorUtils.tsx index 2772350a0c..6cd23d8593 100644 --- a/src/utils/ErrorUtils.tsx +++ b/src/utils/ErrorUtils.tsx @@ -10,12 +10,13 @@ import React, { type ReactNode } from "react"; import { MatrixError, ConnectionError } from "matrix-js-sdk/src/matrix"; import { logger } from "matrix-js-sdk/src/logger"; -import { _t, _td, lookupString, type Tags, type TranslatedString, type TranslationKey } from "../languageHandler"; +import { _t, _td, type TranslationKey } from "../languageHandler"; import SdkConfig from "../SdkConfig"; import { type ValidatedServerConfig } from "./ValidatedServerConfig"; import ExternalLink from "../components/views/elements/ExternalLink"; import Modal from "../Modal.tsx"; import ErrorDialog from "../components/views/dialogs/ErrorDialog.tsx"; +import { lookupString, type Tags, type TranslatedString } from "../i18n/i18n.tsx"; export const resourceLimitStrings = { "monthly_active_user": _td("error|mau"), diff --git a/src/vector/index.ts b/src/vector/index.ts index e705014add..d58b319553 100644 --- a/src/vector/index.ts +++ b/src/vector/index.ts @@ -16,6 +16,7 @@ import { shouldPolyfill as shouldPolyFillIntlSegmenter } from "@formatjs/intl-se // These are things that can run before the skin loads - be careful not to reference the react-sdk though. import { parseQsFromFragment } from "./url_utils"; import "./modernizr"; +import { _t } from "../languageHandler"; // Import shared components CSS import "@element-hq/web-shared-components/dist/element-web-shared-components.css"; @@ -122,7 +123,6 @@ async function start(): Promise { loadPlugins, showError, showIncompatibleBrowser, - _t, extractErrorMessageFromError, } = await import( /* webpackChunkName: "init" */ diff --git a/src/vector/init.tsx b/src/vector/init.tsx index 6706b75672..4262589a18 100644 --- a/src/vector/init.tsx +++ b/src/vector/init.tsx @@ -12,6 +12,7 @@ import { createRoot } from "react-dom/client"; import React, { StrictMode } from "react"; import { logger } from "matrix-js-sdk/src/logger"; import { ModuleLoader } from "@element-hq/element-web-module-api"; +import { getNormalizedLanguageKeys } from "matrix-web-i18n"; import type { QueryDict } from "matrix-js-sdk/src/utils"; import * as languageHandler from "../languageHandler"; @@ -69,7 +70,7 @@ export async function loadLanguage(): Promise { if (!prefLang) { languageHandler.getLanguagesFromBrowser().forEach((l) => { - langs.push(...languageHandler.getNormalizedLanguageKeys(l)); + langs.push(...getNormalizedLanguageKeys(l)); }); } else { langs = [prefLang]; @@ -142,6 +143,7 @@ export async function loadPlugins(): Promise { // every single module to ship its own copy of React. This also makes it easier to access via the console // and incidentally means we can forget our React imports in JSX files without penalty. window.React = React; + window.mxI18nAPI = ModuleApi.instance.i18n; const modules = SdkConfig.get("modules"); if (!modules?.length) return; @@ -155,6 +157,4 @@ export async function loadPlugins(): Promise { await moduleLoader.start(); } -export { _t } from "../languageHandler"; - export { extractErrorMessageFromError } from "../components/views/dialogs/ErrorDialog"; diff --git a/src/widgets/CapabilityText.tsx b/src/widgets/CapabilityText.tsx index effaa0975b..5e241d8168 100644 --- a/src/widgets/CapabilityText.tsx +++ b/src/widgets/CapabilityText.tsx @@ -21,10 +21,11 @@ import { import { EventType, MsgType } from "matrix-js-sdk/src/matrix"; import React from "react"; -import { _t, _td, type TranslatedString, type TranslationKey } from "../languageHandler"; +import { _t, _td, type TranslationKey } from "../languageHandler"; import { ElementWidgetCapabilities } from "../stores/widgets/ElementWidgetCapabilities"; import { MatrixClientPeg } from "../MatrixClientPeg"; import TextWithTooltip from "../components/views/elements/TextWithTooltip"; +import { type TranslatedString } from "../i18n/i18n"; type GENERIC_WIDGET_KIND = "generic"; // eslint-disable-line @typescript-eslint/naming-convention const GENERIC_WIDGET_KIND: GENERIC_WIDGET_KIND = "generic"; diff --git a/test/unit-tests/languageHandler-test.tsx b/test/unit-tests/languageHandler-test.tsx index de1a629608..983a087ed5 100644 --- a/test/unit-tests/languageHandler-test.tsx +++ b/test/unit-tests/languageHandler-test.tsx @@ -20,16 +20,13 @@ import { registerCustomTranslations, setLanguage, setMissingEntryGenerator, - substitute, - type TranslatedString, UserFriendlyError, type TranslationKey, - type IVariables, - type Tags, getLanguagesFromBrowser, } from "../../src/languageHandler"; import { stubClient } from "../test-utils"; import { setupLanguageMock } from "../setup/setupLanguage"; +import { substitute, type IVariables, type Tags, type TranslatedString } from "../../src/i18n/i18n"; async function setupTranslationOverridesForTests(overrides: TranslationStringsObject) { const lookupUrl = "/translations.json";