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:
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",