taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

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:
Mpackages/anastasis-webui/package.json | 1+
Mpackages/challenger-webui/package.json | 1+
Mpackages/libeufin-bank-webui/package.json | 1+
Mpackages/taler-auditor-webui/package.json | 1+
Mpackages/taler-exchange-aml-webui/package.json | 1+
Mpackages/taler-exchange-kyc-webui/package.json | 1+
Mpackages/taler-merchant-webui/package.json | 1+
Mpackages/taler-merchant-webui/src/i18n/catalog.test.ts | 9+++++++++
Mpackages/wallet-webui/package.json | 1+
Mpackages/web-util/package.json | 4++++
Mpackages/web-util/src/context/translation.ts | 66+++++++++---------------------------------------------------------
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),