Update custom translations to support nested fields in structured JSON (#11685)

* Update matrix-web-i18n

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix custom translations for structured JSON nested fields

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix import

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix export

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update @matrix-org/react-sdk-module-api

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Delint

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update matrix-web-i18n

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update matrix-web-i18n

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski
2023-10-02 11:44:25 +01:00
committed by GitHub
parent 1897962086
commit 632d8f4bc7
5 changed files with 74 additions and 108 deletions

View File

@@ -23,40 +23,7 @@ export type Writeable<T> = { -readonly [P in keyof T]: T[P] };
export type ComponentClass = keyof JSX.IntrinsicElements | JSXElementConstructor<any>;
/**
* Utility type for string dot notation for accessing nested object properties.
* Based on https://stackoverflow.com/a/58436959
* @example
* {
* "a": {
* "b": {
* "c": "value"
* },
* "d": "foobar"
* }
* }
* will yield a type of `"a.b.c" | "a.d"` with Separator="."
* @typeParam Target the target type to generate leaf keys for
* @typeParam Separator the separator to use between key segments when accessing nested objects
* @typeParam LeafType the type which leaves of this object extend, used to determine when to stop recursion
* @typeParam MaxDepth the maximum depth to recurse to
* @returns a union type representing all dot (Separator) string notation keys which can access a Leaf (of LeafType)
*/
export type Leaves<Target, Separator extends string = ".", LeafType = string, MaxDepth extends number = 3> = [
MaxDepth,
] extends [never]
? never
: Target extends LeafType
? ""
: {
[K in keyof Target]-?: Join<K, Leaves<Target[K], Separator, LeafType, Prev[MaxDepth]>, Separator>;
}[keyof Target];
type Prev = [never, 0, 1, 2, 3, ...0[]];
type Join<K, P, S extends string = "."> = K extends string | number
? P extends string | number
? `${K}${"" extends P ? "" : S}${P}`
: never
: never;
export type { Leaves } from "matrix-web-i18n";
export type RecursivePartial<T> = {
[P in keyof T]?: T[P] extends (infer U)[]

View File

@@ -21,7 +21,10 @@ import counterpart from "counterpart";
import React from "react";
import { logger } from "matrix-js-sdk/src/logger";
import { Optional } from "matrix-events-sdk";
import { MapWithDefault, safeSet } from "matrix-js-sdk/src/utils";
import { MapWithDefault } from "matrix-js-sdk/src/utils";
import { normalizeLanguageKey, TranslationKey as _TranslationKey, KEY_SEPARATOR } from "matrix-web-i18n";
import { TranslationStringsObject } from "@matrix-org/react-sdk-module-api";
import _ from "lodash";
import type Translations from "./i18n/strings/en_EN.json";
import SettingsStore from "./settings/SettingsStore";
@@ -30,11 +33,12 @@ import { SettingLevel } from "./settings/SettingLevel";
import { retry } from "./utils/promise";
import SdkConfig from "./SdkConfig";
import { ModuleRunner } from "./modules/ModuleRunner";
import { Leaves } from "./@types/common";
// @ts-ignore - $webapp is a webpack resolve alias pointing to the output directory, see webpack config
import webpackLangJsonUrl from "$webapp/i18n/languages.json";
export { normalizeLanguageKey, getNormalizedLanguageKeys } from "matrix-web-i18n";
const i18nFolder = "i18n/";
// Control whether to also return original, untranslated strings
@@ -42,7 +46,7 @@ const i18nFolder = "i18n/";
const ANNOTATE_STRINGS = false;
// We use english strings as keys, some of which contain full stops
counterpart.setSeparator("|");
counterpart.setSeparator(KEY_SEPARATOR);
// see `translateWithFallback` for an explanation of fallback handling
const FALLBACK_LOCALE = "en";
@@ -110,7 +114,7 @@ export function getUserLanguage(): string {
* }
* }
*/
export type TranslationKey = Leaves<typeof Translations, "|", string | { other: string }, 4>;
export type TranslationKey = _TranslationKey<typeof Translations>;
// Function which only purpose is to mark that a string is translatable
// Does not actually do anything. It's helpful for automatic extraction of translatable strings
@@ -541,41 +545,6 @@ export function getLanguageFromBrowser(): string {
return getLanguagesFromBrowser()[0];
}
/**
* Turns a language string, normalises it,
* (see normalizeLanguageKey) into an array of language strings
* with fallback to generic languages
* (eg. 'pt-BR' => ['pt-br', 'pt'])
*
* @param {string} language The input language string
* @return {string[]} List of normalised languages
*/
export function getNormalizedLanguageKeys(language: string): string[] {
const languageKeys: string[] = [];
const normalizedLanguage = normalizeLanguageKey(language);
const languageParts = normalizedLanguage.split("-");
if (languageParts.length === 2 && languageParts[0] === languageParts[1]) {
languageKeys.push(languageParts[0]);
} else {
languageKeys.push(normalizedLanguage);
if (languageParts.length === 2) {
languageKeys.push(languageParts[0]);
}
}
return languageKeys;
}
/**
* Returns a language string with underscores replaced with
* hyphens, and lowercased.
*
* @param {string} language The language string to be normalized
* @returns {string} The normalized language string
*/
export function normalizeLanguageKey(language: string): string {
return language.toLowerCase().replace("_", "-");
}
export function getCurrentLanguage(): string {
return counterpart.getLocale();
}
@@ -662,34 +631,26 @@ async function getLanguage(langPath: string): Promise<ICounterpartTranslation> {
return res.json();
}
export interface ICustomTranslations {
// Format is a map of english string to language to override
[str: string]: {
[lang: string]: string;
};
}
let cachedCustomTranslations: Optional<ICustomTranslations> = null;
let cachedCustomTranslations: Optional<TranslationStringsObject> = null;
let cachedCustomTranslationsExpire = 0; // zero to trigger expiration right away
// This awkward class exists so the test runner can get at the function. It is
// not intended for practical or realistic usage.
export class CustomTranslationOptions {
public static lookupFn?: (url: string) => ICustomTranslations;
public static lookupFn?: (url: string) => TranslationStringsObject;
private constructor() {
// static access for tests only
}
}
function doRegisterTranslations(customTranslations: ICustomTranslations): void {
// We convert the operator-friendly version into something counterpart can
// consume.
function doRegisterTranslations(customTranslations: TranslationStringsObject): void {
// We convert the operator-friendly version into something counterpart can consume.
// Map: lang → Record: string → translation
const langs: MapWithDefault<string, Record<string, string>> = new MapWithDefault(() => ({}));
for (const [str, translations] of Object.entries(customTranslations)) {
for (const [lang, newStr] of Object.entries(translations)) {
safeSet(langs.getOrCreate(lang), str, newStr);
for (const [translationKey, translations] of Object.entries(customTranslations)) {
for (const [lang, translation] of Object.entries(translations)) {
_.set(langs.getOrCreate(lang), translationKey.split(KEY_SEPARATOR), translation);
}
}
@@ -719,11 +680,11 @@ export async function registerCustomTranslations({
if (!lookupUrl) return; // easy - nothing to do
try {
let json: Optional<ICustomTranslations>;
let json: Optional<TranslationStringsObject>;
if (testOnlyIgnoreCustomTranslationsCache || Date.now() >= cachedCustomTranslationsExpire) {
json = CustomTranslationOptions.lookupFn
? CustomTranslationOptions.lookupFn(lookupUrl)
: ((await (await fetch(lookupUrl)).json()) as ICustomTranslations);
: ((await (await fetch(lookupUrl)).json()) as TranslationStringsObject);
cachedCustomTranslations = json;
// Set expiration to the future, but not too far. Just trying to avoid