taler-typescript-core

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

commit 43388d39366a657bf42a30e0fa75d9103bd99531
parent 38fe6df9acc5cd88f740591292c8f5189d7f1362
Author: Florian Dold <dold@taler.net>
Date:   Thu,  3 Sep 2026 00:51:31 +0200

typescript-core: add fast and full release checks

Diffstat:
MMakefile | 16++++++++++++++++
MREADME | 23+++++++++++++++++++++++
Mpackage.json | 1+
Mpackages/challenger-webui/package.json | 1+
Mpackages/pogen/README.md | 15+++++++++------
Apackages/pogen/src/check.test.ts | 41+++++++++++++++++++++++++++++++++++++++++
Mpackages/pogen/src/check.ts | 47++++++++++++++++++++++++++++++++++++++---------
Mpackages/pogen/src/po2ts.test.ts | 7+++++++
Mpackages/pogen/src/po2ts.ts | 2+-
Mpackages/taler-auditor-webui/package.json | 1+
Mpackages/taler-exchange-aml-webui/package.json | 4+++-
Mpackages/taler-exchange-kyc-webui/package.json | 1+
Mpackages/taler-wallet-cli/src/exchanges-pretty.test.ts | 1+
Mpackages/web-util/package.json | 1+
14 files changed, 144 insertions(+), 17 deletions(-)

diff --git a/Makefile b/Makefile @@ -50,6 +50,22 @@ check: pnpm run build pnpm run check +.PHONY: check-release-fast +check-release-fast: check + $(MAKE) lint + pnpm run i18n:source2po + pnpm run i18n:po2strings + pnpm run i18n:check + @if test -n "$$(git status --porcelain --untracked-files=all -- packages/*/src/i18n)"; then \ + echo "generated i18n files are not up to date:"; \ + git status --short --untracked-files=all -- packages/*/src/i18n; \ + exit 1; \ + fi + +.PHONY: check-release +check-release: check-release-fast + ./packages/taler-harness/bin/taler-harness.mjs run-integrationtests + .PHONY: config-lib config-lib: pnpm install --frozen-lockfile --filter '!@gnu-taler/qa-tooling' --filter @gnu-taler/taler-config-lib... diff --git a/README b/README @@ -160,6 +160,29 @@ output, so direct package runs cannot pick up JavaScript left behind by an older source tree. See the [test runner design](doc/testrunner.md) for the output, isolation, and generated-file conventions. +## Release checks + +Before making a release, run the build, unit tests, linter and internationalized +catalogue checks with: + +```shell +make check-release-fast +``` + +This requires GNU gettext and a clean set of checked-in files under +`packages/*/src/i18n/`. The target regenerates the gettext templates, merges the +PO files and emits each `strings.ts`; it fails and leaves the regenerated files +in place when the checked-in output was stale. + +The full release check additionally runs all non-experimental integration tests: + +```shell +make check-release +``` + +It needs the same installed Taler services and test dependencies described +below for running the integration tests directly. + # Integration Tests This repository comes with integration tests for GNU Taler. To run them, diff --git a/package.json b/package.json @@ -7,6 +7,7 @@ "pretty": "prettier --write \"**/*.{ts,tsx,js,mjs,mts}\"", "pretty:check": "prettier --check \"**/*.{ts,tsx,js,mjs,mts}\"", "lint": "pnpm --filter @gnu-taler/qa-tooling install && ./packages/qa-tooling/bin/eslint.mjs .", + "i18n:check": "pnpm run --filter '@gnu-taler/*' i18n:check", "i18n:source2po": "pnpm run --filter '@gnu-taler/*' i18n:source2po", "i18n:po2strings": "pnpm run --filter '@gnu-taler/*' i18n:po2strings", "check": "node ./packages/qa-tooling/bin/test-all.mjs", diff --git a/packages/challenger-webui/package.json b/packages/challenger-webui/package.json @@ -13,6 +13,7 @@ "test": "./test.mjs && node --test 'dist/test/**/*.test.js' 'dist/test/**/test.js'", "lint": "../qa-tooling/bin/eslint.mjs .", "clean": "rm -rf dist lib tsconfig.tsbuildinfo", + "i18n:check": "pogen check", "i18n:source2po": "pogen extract && pogen merge", "i18n:po2strings": "pogen emit", "pretty": "prettier --write src" diff --git a/packages/pogen/README.md b/packages/pogen/README.md @@ -21,17 +21,20 @@ pogen check # validate the catalogues; exits non-zero on any problem ## Configuration -One key, in the package's `package.json`: +Configuration lives under `pogen` in the package's `package.json`: ```json { "pogen": { - "domain": "taler-merchant-webui" + "domain": "taler-merchant-webui", + "minimumCoverage": 85 } } ``` -`domain` names the template: `src/i18n/<domain>.pot`. Paths are otherwise fixed — +`domain` names the template: `src/i18n/<domain>.pot`. `minimumCoverage` is an +optional integer percentage used by `pogen check`; it defaults to 85, matching +the browser language auto-selection threshold. Paths are otherwise fixed — `src/i18n/*.po` in, `src/i18n/strings.ts` out. Two optional files are read if present: `src/i18n/poheader` (replaces the default `.pot` header) and `src/i18n/strings-prelude` (replaces the preamble of the emitted `strings.ts`). @@ -143,9 +146,9 @@ translator comment does survive. Exits non-zero on any of: `msgfmt --check-format` failures; a translation whose `%N$s` placeholders do not match its msgid; or a non-`en` catalogue whose -completeness falls below the threshold the language picker uses to auto-select a -language. Run it in CI — the other three subcommands are deliberately permissive -and will not tell you a catalogue is broken. +completeness falls below the package's configured minimum. Run it in CI — the +other three subcommands are deliberately permissive and will not tell you a +catalogue is broken. ## Notes and known limitations diff --git a/packages/pogen/src/check.test.ts b/packages/pogen/src/check.test.ts @@ -0,0 +1,41 @@ +/* + 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 { + MIN_LANG_COVERAGE_THRESHOLD, + resolveMinimumCoverage, +} from "./check.js"; + +test("coverage defaults to the browser language threshold", () => { + assert.equal(resolveMinimumCoverage(undefined), MIN_LANG_COVERAGE_THRESHOLD); +}); + +test("coverage accepts package-specific integer percentages", () => { + assert.equal(resolveMinimumCoverage(0), 0); + assert.equal(resolveMinimumCoverage(55), 55); + assert.equal(resolveMinimumCoverage(100), 100); +}); + +test("coverage rejects invalid package configuration", () => { + for (const value of [-1, 101, 55.5, "55", null]) { + assert.throws( + () => resolveMinimumCoverage(value), + /pogen\.minimumCoverage.*integer from 0 to 100/, + ); + } +}); diff --git a/packages/pogen/src/check.ts b/packages/pogen/src/check.ts @@ -22,8 +22,7 @@ * - `msgfmt --check-format`, if GNU gettext is installed; * - a placeholder comparison between msgid and msgstr, which needs nothing * but this package and so always runs; - * - the coverage threshold that decides whether a browser will auto-select - * the language at all. + * - the package's minimum catalogue coverage. */ import * as child_process from "node:child_process"; @@ -33,14 +32,33 @@ import * as glob from "glob"; import { poToStrings } from "./po2ts.js"; /** - * Below this, `web-util`'s `useLang` will not auto-select the language and the - * browser silently falls back to English — see - * `packages/web-util/src/hooks/useLang.ts` (`MIN_LANG_COVERAGE_THRESHOLD`). - * Failing here means the regeneration commit that would cause that is loud. + * The default matches `web-util`'s language auto-selection threshold. Packages + * with incomplete legacy catalogues can configure a lower baseline without + * changing the browser's behavior. */ export const MIN_LANG_COVERAGE_THRESHOLD = 85; /** + * Read the package-specific coverage floor. Most packages use the browser's + * auto-selection threshold; packages with older, incomplete catalogues can + * pin their current coverage and still prevent regressions. + */ +export function resolveMinimumCoverage(value: unknown): number { + if (value === undefined) { + return MIN_LANG_COVERAGE_THRESHOLD; + } + if ( + typeof value !== "number" || + !Number.isInteger(value) || + value < 0 || + value > 100 + ) { + throw Error("'pogen.minimumCoverage' must be an integer from 0 to 100"); + } + return value; +} + +/** * The placeholders a message uses, sorted so two messages can be compared * regardless of the word order the target language needs. * @@ -115,6 +133,18 @@ function haveGettext(): boolean { */ export function check(): void { const files = glob.sync("src/i18n/*.po"); + const packageJson = JSON.parse( + fs.readFileSync("./package.json", { encoding: "utf-8" }), + ); + let minimumCoverage: number; + try { + minimumCoverage = resolveMinimumCoverage( + packageJson.pogen?.minimumCoverage, + ); + } catch (e) { + console.error(e instanceof Error ? e.message : e); + process.exit(1); + } if (files.length === 0) { console.error("no .po files found in src/i18n/"); @@ -171,11 +201,10 @@ export function check(): void { failed = true; continue; } - if (lang !== "en" && completeness < MIN_LANG_COVERAGE_THRESHOLD) { + if (lang !== "en" && completeness < minimumCoverage) { console.error( `${f}: only ${completeness}% translated, below the ` + - `${MIN_LANG_COVERAGE_THRESHOLD}% threshold at which a browser stops ` + - `auto-selecting '${lang}' and silently falls back to English`, + `configured minimum of ${minimumCoverage}%`, ); failed = true; } else { diff --git a/packages/pogen/src/po2ts.test.ts b/packages/pogen/src/po2ts.test.ts @@ -225,6 +225,12 @@ test("po2ts formats generated catalogues with the repository's Prettier rules", const i18nDir = path.join(projectDir, "src", "i18n"); const outputPath = path.join(i18nDir, "strings.ts"); fs.mkdirSync(i18nDir, { recursive: true }); + // Emitted catalogues must be formatted even in older packages that still + // list the generated file in a local ignore file. + fs.writeFileSync( + path.join(projectDir, ".prettierignore"), + "src/i18n/strings.ts\n", + ); fs.writeFileSync( path.join(i18nDir, "de.po"), header("de") + @@ -247,6 +253,7 @@ msgstr "erforderlich" assert.match(generated, /Record<string, StringsType>/); assert.match(generated, /strings\["de"\] =/); assert.match(generated, /"required": \["erforderlich"\]/); + assert.doesNotMatch(generated, /\n\n$/); } finally { process.chdir(originalCwd); fs.rmSync(projectDir, { recursive: true, force: true }); diff --git a/packages/pogen/src/po2ts.ts b/packages/pogen/src/po2ts.ts @@ -242,7 +242,7 @@ export function po2ts(): void { ); const tsContents = execFileSync( process.execPath, - [prettierCli, "--stdin-filepath", outputPath], + [prettierCli, "--ignore-path=/dev/null", "--stdin-filepath", outputPath], { encoding: "utf-8", input: chunks.join(""), diff --git a/packages/taler-auditor-webui/package.json b/packages/taler-auditor-webui/package.json @@ -12,6 +12,7 @@ "dev": "./dev.mjs", "test": "./test.mjs && node --test 'dist/test/**/*.test.js'", "lint": "../qa-tooling/bin/eslint.mjs .", + "i18n:check": "pogen check", "i18n:source2po": "pogen extract && pogen merge", "i18n:po2strings": "pogen emit", "i18n": "pnpm i18n:extract && pnpm i18n:merge && pnpm i18n:emit", diff --git a/packages/taler-exchange-aml-webui/package.json b/packages/taler-exchange-aml-webui/package.json @@ -14,6 +14,7 @@ "build:with-deps": "pnpm --filter \"{.}...\" run build", "test": "./test.mjs && node --test 'dist/test/**/*.test.js' 'dist/test/**/test.js'", "lint": "../qa-tooling/bin/eslint.mjs .", + "i18n:check": "pogen check", "i18n:source2po": "pogen extract && pogen merge", "i18n:po2strings": "pogen emit", "i18n": "pnpm i18n:extract && pnpm i18n:merge && pnpm i18n:emit", @@ -44,6 +45,7 @@ "typescript": "7.0.2" }, "pogen": { - "domain": "aml-backoffice" + "domain": "aml-backoffice", + "minimumCoverage": 55 } } diff --git a/packages/taler-exchange-kyc-webui/package.json b/packages/taler-exchange-kyc-webui/package.json @@ -13,6 +13,7 @@ "test": "./test.mjs && node --test 'dist/test/**/*.test.js' 'dist/test/**/test.js'", "lint": "../qa-tooling/bin/eslint.mjs .", "clean": "rm -rf dist lib tsconfig.tsbuildinfo", + "i18n:check": "pogen check", "i18n:source2po": "pogen extract && pogen merge", "i18n:po2strings": "pogen emit", "pretty": "prettier --write src" diff --git a/packages/taler-wallet-cli/src/exchanges-pretty.test.ts b/packages/taler-wallet-cli/src/exchanges-pretty.test.ts @@ -31,6 +31,7 @@ function makeExchange(extra: Record<string, unknown> = {}): ExchangeListItem { exchangeBaseUrl: "https://exchange.example/", currency: "KUDOS", masterPub: "MASTER_PUB", + legacyMasterPubs: [], paytoUris: [], tosStatus: ExchangeTosStatus.Accepted, exchangeEntryStatus: ExchangeEntryStatus.Used, diff --git a/packages/web-util/package.json b/packages/web-util/package.json @@ -29,6 +29,7 @@ "build:with-deps": "pnpm --filter \"{.}...\" run build", "test:clean": "rm -rf lib tsconfig.tsbuildinfo", "test": "pnpm run test:clean && tsc && ./build.mjs && node --test 'lib/**/*.test.js'", + "i18n:check": "pogen check", "i18n:source2po": "pogen extract && pogen merge", "i18n:po2strings": "pogen emit", "clean": "rm -rf dist lib tsconfig.tsbuildinfo",