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:
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))