commit ebe3be4b83b5f12ceff5478ae18de2e6254b49a2
parent 8e66c44119f8dd45d6a0b78e528a3b9dd84e7a1b
Author: Florian Dold <dold@taler.net>
Date: Fri, 4 Sep 2026 14:16:54 +0200
web UIs: consume the web-util translation catalogue
Export the shared catalogue as a package subpath and list it as a
pogen catalogue dependency in every web UI that uses web-util. Replace
the provider-specific merger with the generic catalogue composer.
Cover generated dependency wiring with a merchant UI regression test.
Diffstat:
11 files changed, 30 insertions(+), 57 deletions(-)
diff --git a/packages/anastasis-webui/package.json b/packages/anastasis-webui/package.json
@@ -42,6 +42,7 @@
},
"pogen": {
"domain": "taler-anastasis",
+ "catalogDependencies": ["@gnu-taler/web-util/i18n"],
"requiredLanguages": ["de", "de-CH", "fr", "it"]
}
}
diff --git a/packages/challenger-webui/package.json b/packages/challenger-webui/package.json
@@ -32,6 +32,7 @@
},
"pogen": {
"domain": "challenger-ui",
+ "catalogDependencies": ["@gnu-taler/web-util/i18n"],
"requiredLanguages": ["de", "de-CH", "fr", "it"]
},
"dependencies": {
diff --git a/packages/libeufin-bank-webui/package.json b/packages/libeufin-bank-webui/package.json
@@ -39,6 +39,7 @@
},
"pogen": {
"domain": "bank",
+ "catalogDependencies": ["@gnu-taler/web-util/i18n"],
"requiredLanguages": ["de", "de-CH", "fr", "it"]
}
}
diff --git a/packages/taler-auditor-webui/package.json b/packages/taler-auditor-webui/package.json
@@ -35,6 +35,7 @@
},
"pogen": {
"domain": "taler-auditor-backoffice",
+ "catalogDependencies": ["@gnu-taler/web-util/i18n"],
"requiredLanguages": ["de", "de-CH", "fr", "it"]
}
}
diff --git a/packages/taler-exchange-aml-webui/package.json b/packages/taler-exchange-aml-webui/package.json
@@ -46,6 +46,7 @@
},
"pogen": {
"domain": "aml-backoffice",
+ "catalogDependencies": ["@gnu-taler/web-util/i18n"],
"minimumCoverage": 55,
"requiredLanguages": ["de", "de-CH", "fr", "it"]
}
diff --git a/packages/taler-exchange-kyc-webui/package.json b/packages/taler-exchange-kyc-webui/package.json
@@ -33,6 +33,7 @@
},
"pogen": {
"domain": "kyc-ui",
+ "catalogDependencies": ["@gnu-taler/web-util/i18n"],
"requiredLanguages": ["de", "de-CH", "fr", "it"]
},
"dependencies": {
diff --git a/packages/taler-merchant-webui/package.json b/packages/taler-merchant-webui/package.json
@@ -45,6 +45,7 @@
},
"pogen": {
"domain": "taler-merchant-webui",
+ "catalogDependencies": ["@gnu-taler/web-util/i18n"],
"requiredLanguages": ["de", "de-CH", "fr", "it"]
}
}
diff --git a/packages/taler-merchant-webui/src/i18n/catalog.test.ts b/packages/taler-merchant-webui/src/i18n/catalog.test.ts
@@ -32,6 +32,7 @@ import { test } from "node:test";
import assert from "node:assert";
import { readFileSync } from "node:fs";
import { MENU_GROUPS } from "../ui/menuStructure.js";
+import { strings } from "./strings.js";
const LANGS = ["de", "de-CH", "fr", "it"] as const;
@@ -110,6 +111,14 @@ function loadPotIds(): Set<string> {
return new Set(parsePo(readFileSync(path, "utf-8")).map((e) => e.msgid));
}
+test("the generated catalogue includes web-util translations", () => {
+ const message = "The operation failed.";
+ assert.equal(loadPotIds().has(message), false);
+ assert.deepEqual(strings.de?.locale_data.messages[message], [
+ "Der Vorgang ist fehlgeschlagen.",
+ ]);
+});
+
/** Live means it will be emitted: translated, and not marked fuzzy. */
function live(entries: Entry[]): Entry[] {
return entries.filter((e) => e.msgstr !== "" && !e.fuzzy);
diff --git a/packages/wallet-webui/package.json b/packages/wallet-webui/package.json
@@ -64,6 +64,7 @@
},
"pogen": {
"domain": "taler-wallet-webui",
+ "catalogDependencies": ["@gnu-taler/web-util/i18n"],
"requiredLanguages": ["de", "de-CH", "fr", "it"]
}
}
diff --git a/packages/web-util/package.json b/packages/web-util/package.json
@@ -7,6 +7,10 @@
"license": "AGPL-3.0-or-later",
"private": false,
"exports": {
+ "./i18n": {
+ "types": "./lib/i18n/strings.d.ts",
+ "default": "./lib/i18n/strings.js"
+ },
"./browser": {
"types": "./lib/index.browser.js",
"default": "./lib/index.browser.mjs"
diff --git a/packages/web-util/src/context/translation.ts b/packages/web-util/src/context/translation.ts
@@ -14,7 +14,11 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { i18n, setupI18n } from "@gnu-taler/taler-util";
+import {
+ composeTranslationCatalogues,
+ i18n,
+ setupI18n,
+} from "@gnu-taler/taler-util";
import { ComponentChildren, createContext, h, VNode } from "preact";
import { useContext, useEffect, useMemo, useRef } from "preact/hooks";
import { strings as webUtilStrings, StringsType } from "../i18n/strings.js";
@@ -80,61 +84,6 @@ interface Props {
source: Record<string, StringsType>;
}
-function mergeTranslationSources(
- application: Record<string, StringsType>,
-): Record<string, StringsType> {
- const sharedByLanguage = Object.fromEntries(
- Object.entries(webUtilStrings).map(([language, catalog]) => [
- canonicalLanguageCode(language),
- catalog,
- ]),
- );
- const applicationByLanguage = Object.fromEntries(
- Object.entries(application).map(([language, catalog]) => [
- canonicalLanguageCode(language),
- catalog,
- ]),
- );
- const result: Record<string, StringsType> = {};
- for (const lang of new Set([
- ...Object.keys(sharedByLanguage),
- ...Object.keys(applicationByLanguage),
- ])) {
- const shared = sharedByLanguage[lang];
- const host = applicationByLanguage[lang];
- if (!shared) {
- result[lang] = host;
- continue;
- }
- if (!host) {
- result[lang] = shared;
- continue;
- }
- const messages = {
- ...shared.locale_data.messages,
- ...host.locale_data.messages,
- };
- const translated = Object.entries(messages).filter(
- ([key, value]) =>
- key !== "" &&
- Array.isArray(value) &&
- value.some((entry) => typeof entry === "string" && entry.length > 0),
- ).length;
- const total = Math.max(1, Object.keys(messages).length - 1);
- result[lang] = {
- ...shared,
- ...host,
- completeness: Math.round((translated * 100) / total),
- locale_data: {
- ...shared.locale_data,
- ...host.locale_data,
- messages,
- },
- };
- }
- return result;
-}
-
// Outmost UI wrapper.
export const TranslationProvider = ({
initial,
@@ -142,7 +91,10 @@ export const TranslationProvider = ({
forceLang__testing: forceLang,
source,
}: Props): VNode => {
- const mergedSource = useMemo(() => mergeTranslationSources(source), [source]);
+ const mergedSource = useMemo(
+ () => composeTranslationCatalogues(webUtilStrings, source),
+ [source],
+ );
const availableLanguages = new Set([
"en",
...Object.keys(source).map(canonicalLanguageCode),