Element Module API v1.0 support (#29934)

This commit is contained in:
Michael Telatynski
2025-05-14 09:21:24 +01:00
committed by GitHub
parent c9548ec1d0
commit 785a12a029
10 changed files with 153 additions and 81 deletions

View File

@@ -454,55 +454,35 @@ type Languages = {
[lang: string]: string;
};
export function setLanguage(preferredLangs: string | string[]): Promise<void> {
if (!Array.isArray(preferredLangs)) {
preferredLangs = [preferredLangs];
export async function setLanguage(...preferredLangs: string[]): Promise<void> {
PlatformPeg.get()?.setLanguage(preferredLangs);
const availableLanguages = await getLangsJson();
let chosenLanguage = preferredLangs.find((lang) => availableLanguages.hasOwnProperty(lang));
if (!chosenLanguage) {
// Fallback to en_EN if none is found
chosenLanguage = "en";
logger.error("Unable to find an appropriate language, preferred: ", preferredLangs);
}
const plaf = PlatformPeg.get();
if (plaf) {
plaf.setLanguage(preferredLangs);
const languageData = await getLanguageRetry(i18nFolder + availableLanguages[chosenLanguage]);
counterpart.registerTranslations(chosenLanguage, languageData);
counterpart.setLocale(chosenLanguage);
await SettingsStore.setValue("language", null, SettingLevel.DEVICE, chosenLanguage);
// Adds a lot of noise to test runs, so disable logging there.
if (process.env.NODE_ENV !== "test") {
logger.log("set language to " + chosenLanguage);
}
let langToUse: string;
let availLangs: Languages;
return getLangsJson()
.then((result) => {
availLangs = result;
// Set 'en' as fallback language:
if (chosenLanguage !== "en") {
const fallbackLanguageData = await getLanguageRetry(i18nFolder + availableLanguages["en"]);
counterpart.registerTranslations("en", fallbackLanguageData);
}
for (let i = 0; i < preferredLangs.length; ++i) {
if (availLangs.hasOwnProperty(preferredLangs[i])) {
langToUse = preferredLangs[i];
break;
}
}
if (!langToUse) {
// Fallback to en_EN if none is found
langToUse = "en";
logger.error("Unable to find an appropriate language");
}
return getLanguageRetry(i18nFolder + availLangs[langToUse]);
})
.then(async (langData): Promise<ICounterpartTranslation | undefined> => {
counterpart.registerTranslations(langToUse, langData);
await registerCustomTranslations();
counterpart.setLocale(langToUse);
await SettingsStore.setValue("language", null, SettingLevel.DEVICE, langToUse);
// Adds a lot of noise to test runs, so disable logging there.
if (process.env.NODE_ENV !== "test") {
logger.log("set language to " + langToUse);
}
// Set 'en' as fallback language:
if (langToUse !== "en") {
return getLanguageRetry(i18nFolder + availLangs["en"]);
}
})
.then(async (langData): Promise<void> => {
if (langData) counterpart.registerTranslations("en", langData);
await registerCustomTranslations();
});
await registerCustomTranslations();
}
type Language = {

View File

@@ -5,7 +5,9 @@ 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 type { Api, RuntimeModuleConstructor, Config } from "@element-hq/element-web-module-api";
import { createRoot, type Root } from "react-dom/client";
import type { Api, RuntimeModuleConstructor } from "@element-hq/element-web-module-api";
import { ModuleRunner } from "./ModuleRunner.ts";
import AliasCustomisations from "../customisations/Alias.ts";
import { RoomListCustomisations } from "../customisations/RoomList.ts";
@@ -17,7 +19,8 @@ import * as MediaCustomisations from "../customisations/Media.ts";
import UserIdentifierCustomisations from "../customisations/UserIdentifier.ts";
import { WidgetPermissionCustomisations } from "../customisations/WidgetPermissions.ts";
import { WidgetVariableCustomisations } from "../customisations/WidgetVariables.ts";
import SdkConfig from "../SdkConfig.ts";
import { ConfigApi } from "./ConfigApi.ts";
import { I18nApi } from "./I18nApi.ts";
const legacyCustomisationsFactory = <T extends object>(baseCustomisations: T) => {
let used = false;
@@ -28,17 +31,6 @@ const legacyCustomisationsFactory = <T extends object>(baseCustomisations: T) =>
};
};
class ConfigApi {
public get(): Config;
public get<K extends keyof Config>(key: K): Config[K];
public get<K extends keyof Config = never>(key?: K): Config | Config[K] {
if (key === undefined) {
return SdkConfig.get() as Config;
}
return SdkConfig.get(key);
}
}
/**
* Implementation of the @element-hq/element-web-module-api runtime module API.
*/
@@ -65,6 +57,12 @@ class ModuleApi implements Api {
/* eslint-enable @typescript-eslint/naming-convention */
public readonly config = new ConfigApi();
public readonly i18n = new I18nApi();
public readonly rootNode = document.getElementById("matrixchat")!;
public createRoot(element: Element): Root {
return createRoot(element);
}
}
export type ModuleApiType = ModuleApi;

20
src/modules/ConfigApi.ts Normal file
View File

@@ -0,0 +1,20 @@
/*
Copyright 2025 New Vector 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 { ConfigApi as IConfigApi, Config } from "@element-hq/element-web-module-api";
import SdkConfig from "../SdkConfig.ts";
export class ConfigApi implements IConfigApi {
public get(): Config;
public get<K extends keyof Config>(key: K): Config[K];
public get<K extends keyof Config = never>(key?: K): Config | Config[K] {
if (key === undefined) {
return SdkConfig.get() as Config;
}
return SdkConfig.get(key);
}
}

47
src/modules/I18nApi.ts Normal file
View File

@@ -0,0 +1,47 @@
/*
Copyright 2025 New Vector 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 I18nApi as II18nApi, type Variables, type Translations } from "@element-hq/element-web-module-api";
import counterpart from "counterpart";
import { _t, getCurrentLanguage, type TranslationKey } from "../languageHandler.tsx";
export class I18nApi implements II18nApi {
/**
* Read the current language of the user in IETF Language Tag format
*/
public get language(): string {
return getCurrentLanguage();
}
/**
* Register translations for the module, may override app's existing translations
*/
public register(translations: Partial<Translations>): void {
const langs: Record<string, Record<string, string>> = {};
for (const key in translations) {
for (const lang in translations[key]) {
langs[lang] = langs[lang] || {};
langs[lang][key] = translations[key][lang];
}
}
// Finally, tell counterpart about our translations
for (const lang in langs) {
counterpart.registerTranslations(lang, langs[lang]);
}
}
/**
* Perform a translation, with optional variables
* @param key - The key to translate
* @param variables - Optional variables to interpolate into the translation
*/
public translate(key: TranslationKey, variables?: Variables): string {
return _t(key, variables);
}
}

View File

@@ -162,21 +162,18 @@ async function start(): Promise<void> {
// now that the config is ready, try to persist logs
const persistLogsPromise = setupLogStorage();
// Load modules & plugins before language to ensure any custom translations are respected, and any app
// startup functionality is run
const loadModulesPromise = loadModules();
await settled(loadModulesPromise);
const loadPluginsPromise = loadPlugins();
await settled(loadPluginsPromise);
// Load language after loading config.json so that settingsDefaults.language can be applied
const loadLanguagePromise = loadLanguage();
// as quickly as we possibly can, set a default theme...
const loadThemePromise = loadTheme();
// await things settling so that any errors we have to render have features like i18n running
await settled(loadThemePromise, loadLanguagePromise);
const loadModulesPromise = loadModules();
await settled(loadModulesPromise);
const loadPluginsPromise = loadPlugins();
await settled(loadPluginsPromise);
let acceptBrowser = supportedBrowser;
if (!acceptBrowser && window.localStorage) {
acceptBrowser = Boolean(window.localStorage.getItem("mx_accepts_unsupported_browser"));

View File

@@ -75,7 +75,7 @@ export async function loadLanguage(): Promise<void> {
langs = [prefLang];
}
try {
await languageHandler.setLanguage(langs);
await languageHandler.setLanguage(...langs);
document.documentElement.setAttribute("lang", languageHandler.getCurrentLanguage());
} catch (e) {
logger.error("Unable to set language", e);