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.
This commit is contained in:
David Baker 2025-11-20 18:41:39 +00:00
parent 0eff1caab2
commit e5f227531a
36 changed files with 95 additions and 130 deletions

View File

@ -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",

View File

@ -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",

View File

@ -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();
}
}

View File

@ -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";

View File

@ -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<HTMLButtonElement> {
/**

View File

@ -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<HTMLInputElement> {
/**

View File

@ -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"
}
}
}

View File

@ -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<typeof Translations>;
export const _t: translateFn<TranslationKey> = () => "";
export const humanizeTime: humanizeTimeFn = () => "";

View File

@ -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";

View File

@ -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<HTMLAttributes<HTMLDivElement>, "onClick"> {
/**

View File

@ -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<HTMLLIElement> {

View File

@ -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 {

View File

@ -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"]
}

View File

@ -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",
},
},
},

View File

@ -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"

View File

@ -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;

View File

@ -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 */

View File

@ -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;

View File

@ -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;

View File

@ -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.

View File

@ -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";

View File

@ -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<typeof Translations>;
const i18nFolder = "i18n/";

View File

@ -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"

View File

@ -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/";

View File

@ -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<void> {}
}

View File

@ -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();

View File

@ -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 {

View File

@ -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;

View File

@ -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"),

View File

@ -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<void> {
loadPlugins,
showError,
showIncompatibleBrowser,
_t,
extractErrorMessageFromError,
} = await import(
/* webpackChunkName: "init" */

View File

@ -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<void> {
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<void> {
// 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<void> {
await moduleLoader.start();
}
export { _t } from "../languageHandler";
export { extractErrorMessageFromError } from "../components/views/dialogs/ErrorDialog";

View File

@ -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";

View File

@ -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";