taler-typescript-core

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

commit 8cc7e0a1a01f6d5515ade6fc5748ab3b42320244
parent b5e17b75d8326a259e3134641c77a39d5a934648
Author: Florian Dold <dold@taler.net>
Date:   Mon, 31 Aug 2026 19:21:48 +0200

web UIs: initialize translations before startup loaders

Diffstat:
Mpackages/taler-exchange-kyc-webui/package.json | 1+
Apackages/taler-exchange-kyc-webui/src/app.test.tsx | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-exchange-kyc-webui/src/app.tsx | 100++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Mpackages/taler-exchange-kyc-webui/tsconfig.json | 1+
Apackages/taler-util/src/i18n.test.ts | 24++++++++++++++++++++++++
Mpackages/taler-util/src/i18n.ts | 5++++-
Mpackages/web-util/src/components/Accessibility.test.tsx | 6++----
Mpnpm-lock.yaml | 3+++
8 files changed, 139 insertions(+), 52 deletions(-)

diff --git a/packages/taler-exchange-kyc-webui/package.json b/packages/taler-exchange-kyc-webui/package.json @@ -26,6 +26,7 @@ "autoprefixer": "^10.4.14", "chai": "^6.2.2", "esbuild": "^0.28.0", + "preact-render-to-string": "^5.2.6", "tailwindcss": "3.4.17", "typescript": "7.0.2" }, diff --git a/packages/taler-exchange-kyc-webui/src/app.test.tsx b/packages/taler-exchange-kyc-webui/src/app.test.tsx @@ -0,0 +1,51 @@ +/* + This file is part of GNU Taler + (C) 2026 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import assert from "node:assert/strict"; +import test from "node:test"; +import { h } from "preact"; +import { render as renderToString } from "preact-render-to-string"; + +test("initial loading state is translated before settings resolve", async () => { + Object.defineProperty(globalThis, "window", { + configurable: true, + writable: true, + value: { + location: { + hash: "", + href: "https://exchange.example/kyc-spa/", + origin: "https://exchange.example", + pathname: "/kyc-spa/", + search: "", + }, + localStorage: { + getItem(key: string): string | null { + return key === "lang-preference" ? JSON.stringify("de") : null; + }, + }, + navigator: { + language: "de-DE", + languages: ["de-DE", "de", "en"], + }, + }, + }); + + const { App } = await import("./app.js"); + const html = renderToString(<App />); + + assert.match(html, /role="status"/); + assert.match(html, /Wird geladen/); +}); diff --git a/packages/taler-exchange-kyc-webui/src/app.tsx b/packages/taler-exchange-kyc-webui/src/app.tsx @@ -45,6 +45,14 @@ import { KycUiSettings, fetchSettings } from "./settings.js"; const WITH_LOCAL_STORAGE_CACHE = false; export function App(): VNode { + return ( + <TranslationProvider source={strings}> + <ConfiguredApp /> + </TranslationProvider> + ); +} + +function ConfiguredApp(): VNode { const [settings, setSettings] = useState<KycUiSettings>(); useEffect(() => { fetchSettings(setSettings); @@ -54,55 +62,53 @@ export function App(): VNode { const baseUrl = getInitialBackendBaseURL(settings.backendBaseURL); return ( <SettingsProvider value={settings}> - <TranslationProvider source={strings}> - <NotificationProvider> - <ExchangeApiProvider - baseUrl={new URL("/", baseUrl)} - frameOnError={Frame} - evictors={{ - exchange: evictExchangeSwrCache, + <NotificationProvider> + <ExchangeApiProvider + baseUrl={new URL("/", baseUrl)} + frameOnError={Frame} + evictors={{ + exchange: evictExchangeSwrCache, + }} + > + <SWRConfig + value={{ + provider: WITH_LOCAL_STORAGE_CACHE + ? localStorageProvider + : undefined, + // normally, do not revalidate + revalidateOnFocus: false, + revalidateOnReconnect: false, + revalidateIfStale: false, + revalidateOnMount: undefined, + focusThrottleInterval: undefined, + + // normally, do not refresh + refreshInterval: undefined, + dedupingInterval: 2000, + refreshWhenHidden: false, + refreshWhenOffline: false, + + // ignore errors + shouldRetryOnError: false, + errorRetryCount: 0, + errorRetryInterval: undefined, + + // do not go to loading again if already has data + keepPreviousData: true, }} > - <SWRConfig - value={{ - provider: WITH_LOCAL_STORAGE_CACHE - ? localStorageProvider - : undefined, - // normally, do not revalidate - revalidateOnFocus: false, - revalidateOnReconnect: false, - revalidateIfStale: false, - revalidateOnMount: undefined, - focusThrottleInterval: undefined, - - // normally, do not refresh - refreshInterval: undefined, - dedupingInterval: 2000, - refreshWhenHidden: false, - refreshWhenOffline: false, - - // ignore errors - shouldRetryOnError: false, - errorRetryCount: 0, - errorRetryInterval: undefined, - - // do not go to loading again if already has data - keepPreviousData: true, - }} - > - <TalerWalletIntegrationBrowserProvider> - <BrowserHashNavigationProvider> - <UiFormsProvider> - <NotifierProvider> - <Routing /> - </NotifierProvider> - </UiFormsProvider> - </BrowserHashNavigationProvider> - </TalerWalletIntegrationBrowserProvider> - </SWRConfig> - </ExchangeApiProvider> - </NotificationProvider> - </TranslationProvider> + <TalerWalletIntegrationBrowserProvider> + <BrowserHashNavigationProvider> + <UiFormsProvider> + <NotifierProvider> + <Routing /> + </NotifierProvider> + </UiFormsProvider> + </BrowserHashNavigationProvider> + </TalerWalletIntegrationBrowserProvider> + </SWRConfig> + </ExchangeApiProvider> + </NotificationProvider> </SettingsProvider> ); } diff --git a/packages/taler-exchange-kyc-webui/tsconfig.json b/packages/taler-exchange-kyc-webui/tsconfig.json @@ -5,6 +5,7 @@ "target": "ES2023", "module": "Node16", "lib": ["DOM", "ES2023"], + "types": ["node"], "allowJs": true /* Allow javascript files to be compiled. */, // "checkJs": true, /* Report errors in .js files. */ "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */, diff --git a/packages/taler-util/src/i18n.test.ts b/packages/taler-util/src/i18n.test.ts @@ -0,0 +1,24 @@ +/* + This file is part of GNU Taler + (C) 2026 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import assert from "node:assert/strict"; +import test from "node:test"; +import { i18n } from "./i18n.js"; + +test("translations fall back to source strings before setup", () => { + assert.equal(i18n.str`Loading`, "Loading"); + assert.deepEqual(i18n.Translate({ children: "Loading" }), ["Loading"]); +}); diff --git a/packages/taler-util/src/i18n.ts b/packages/taler-util/src/i18n.ts @@ -4,7 +4,10 @@ import { Logger } from "./logging.js"; const logger = new Logger("i18n/index.ts"); -export let jed: any = undefined; +// Keep translations usable before an application installs its catalog. This +// matters for shared components that can render while providers are still +// waiting for configuration data. +export let jed: any = new jedLib.Jed({}); /** * Set up jed library for internationalization, diff --git a/packages/web-util/src/components/Accessibility.test.tsx b/packages/web-util/src/components/Accessibility.test.tsx @@ -1,14 +1,12 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { i18n, setupI18n } from "@gnu-taler/taler-util"; +import { i18n } from "@gnu-taler/taler-util"; import { Window } from "happy-dom"; import { h } from "preact"; import { act } from "preact/test-utils"; import { InputSelectOne } from "../forms/fields/InputSelectOne.js"; import { Loading } from "./Loading.js"; -setupI18n("en", {}); - function installDom(): Window { const window = new Window({ url: "https://example.com/" }); for (const [key, value] of Object.entries({ @@ -32,7 +30,7 @@ function installDom(): Window { return window; } -test("loading indicator exposes an accessible status", async () => { +test("loading indicator works before translations are initialized", async () => { const window = installDom(); const { cleanup, render } = await import("@testing-library/preact"); const view = render(<Loading />); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml @@ -436,6 +436,9 @@ importers: esbuild: specifier: ^0.28.0 version: 0.28.0 + preact-render-to-string: + specifier: ^5.2.6 + version: 5.2.6(preact@10.11.3) tailwindcss: specifier: 3.4.17 version: 3.4.17(ts-node@10.9.1(@types/node@20.19.41)(typescript@7.0.2))