fix(webui): localize Campaign delivery reporting

This commit is contained in:
2026-07-22 09:04:42 +02:00
parent b0282ebff2
commit 79b576b4bc
11 changed files with 380 additions and 94 deletions

View File

@@ -1,8 +1,14 @@
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
const api = readFileSync("src/api/campaigns.ts", "utf8");
const page = readFileSync("src/features/reports/AggregateReportsPage.tsx", "utf8");
const reportPage = readFileSync("src/features/campaigns/CampaignReportPage.tsx", "utf8");
const reviewPage = readFileSync("src/features/campaigns/ReviewSendPage.tsx", "utf8");
const operatorPage = readFileSync("src/features/operator/OperatorQueuePage.tsx", "utf8");
const deliveryMode = readFileSync("src/features/campaigns/utils/deliveryMode.ts", "utf8");
const translationCatalog = readFileSync("src/i18n/generatedTranslations.ts", "utf8");
assert(api.includes('"/api/v1/campaigns/aggregate-reports"'), "the report list uses the safe aggregate collection");
assert(api.includes("/api/v1/campaigns/aggregate-reports/${encodeURIComponent(campaignId)}"), "the selected report uses the safe aggregate projection");
@@ -17,5 +23,32 @@ assert(!page.includes("localStorage"), "the aggregate page does not persist repo
assert(!page.includes("sessionStorage"), "the aggregate page does not persist report data in session storage");
assert(page.includes("TableActionGroup"), "campaign selection uses the central icon-only table action group");
assert(page.includes("disabled: campaign.id === selectedFromUrl"), "the selected row action stays visible and disabled");
assert(!page.includes("{report.population.denominator_definition}"), "the known denominator contract uses a localized UI explanation");
assert(!page.includes("{report.privacy.rule}"), "the known privacy contract uses a localized UI explanation");
assert(page.includes("all_persisted_recipient_delivery_jobs_for_the_se.208e90cc"), "the denominator explanation is bilingual");
assert(page.includes("positive_counts_below_the_threshold_are_hidden_a.1dc97093"), "the suppression rule is bilingual");
const touchedSources = [page, reportPage, reviewPage, operatorPage, deliveryMode].join("\n");
const touchedKeys = new Set(touchedSources.match(/i18n:govoplan-campaign\.[a-z0-9_]+\.[a-f0-9]{8}/g) ?? []);
for (const key of touchedKeys) {
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const catalogEntries = [...translationCatalog.matchAll(new RegExp(`"${escapedKey}":\\s*("(?:[^"\\\\]|\\\\.)*")`, "g"))];
assert.equal(catalogEntries.length, 2, `${key} is present exactly once in both language catalogs`);
const english = JSON.parse(catalogEntries[0][1]);
const german = JSON.parse(catalogEntries[1][1]);
assert.notEqual(english.trim(), "", `${key} has a non-empty English translation`);
assert.notEqual(german.trim(), "", `${key} has a non-empty German translation`);
}
const sliceSections = [...translationCatalog.matchAll(/ "i18n:govoplan-campaign\.campaign_reports_available_to_you\.f14fa403":[\s\S]*? "i18n:govoplan-campaign\.worker_queue_committed_value0_message_s_and_publ\.24400d3c":.*\n/g)];
assert.equal(sliceSections.length, 2, "the current slice has one contiguous English and German catalog section");
const newEnglishEntries = [...sliceSections[0][0].matchAll(/"(i18n:govoplan-campaign\.[a-z0-9_]+\.[a-f0-9]{8})":\s*("(?:[^"\\]|\\.)*")/g)];
for (const [, key, encodedEnglish] of newEnglishEntries) {
const english = JSON.parse(encodedEnglish);
const digest = createHash("sha1").update(english).digest("hex").slice(0, 8);
const slug = english.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+/, "").slice(0, 48);
assert.equal(key, `i18n:govoplan-campaign.${slug}.${digest}`, `${key} uses the normalized source slug and exact SHA-1 suffix`);
assert(touchedKeys.has(key), `${key} is referenced by the current Campaign UI slices`);
}
console.log("aggregate report UI structure checks passed");

View File

@@ -5,12 +5,22 @@ import { dirname, resolve } from "node:path";
const here = dirname(fileURLToPath(import.meta.url));
const source = readFileSync(resolve(here, "../src/features/campaigns/ReviewSendPage.tsx"), "utf8");
const modeSource = readFileSync(resolve(here, "../src/features/campaigns/utils/deliveryMode.ts"), "utf8");
assert.match(source, /getCampaignDeliveryOptions/);
assert.match(source, /Queue for workers/);
assert.match(source, /Send now keeps this request open/);
assert.match(source, /queue_for_workers\.dae9a3e4/);
assert.match(source, /queue_for_workers_commits_durable_jobs_and_retur\.d0dbe81a/);
assert.match(source, /loading_the_effective_delivery_policy_\.d6893011/);
assert.match(source, /deliveryControlProgressMessage/);
assert.match(source, /synchronousSendAllowed/);
assert.match(source, /commandInProgress[\s\S]*refreshQueueStatus/);
assert.match(source, /<ConfirmDialog[\s\S]*open=\{queueConfirmOpen\}/);
assert.match(source, /<ConfirmDialog[\s\S]*open=\{cancelDeliveryConfirmOpen\}/);
assert.match(source, /deliveryModeLabel\(persistedDeliveryMode\)/);
assert.match(source, /this_version_last_entered_value0_mode_at_value1_\.c087d2f3/);
assert.match(source, /this_version_last_entered_value0_mode_this_recor\.ea2f201f/);
assert.doesNotMatch(source, /This version last entered/);
assert.match(modeSource, /synchronous\.77c61919/);
assert.match(modeSource, /worker_queue\.c911e32c/);
assert.match(modeSource, /database_queue\.8bf98437/);
assert.doesNotMatch(source, /window\.alert\s*\(/);

View File

@@ -66,6 +66,6 @@ test("permission explanations take precedence over queue state", () => {
const blocks = operatorQueueActionBlocks(everything, noPermissions);
assert.deepEqual(
Object.values(blocks),
Array.from({ length: 7 }, () => OPERATOR_QUEUE_REASON.permission)
Array.from({ length: 7 }, () => OPERATOR_QUEUE_REASON.permissionDenied)
);
});

View File

@@ -41,7 +41,8 @@ assert(reportSource.includes("onQueryChange={handleJobGridQuery}"), "header sort
assert(reportSource.includes("initialReportGridFilters()"), "status deep links initialize the DataGrid filters");
assert(reportSource.includes("initialReportQuery()"), "q deep links initialize the report search");
assert(reportSource.includes('"skipped",\n"queued"'), "SMTP skipped is a first-class report filter option");
assert(reportSource.includes("Excluded rows are intentionally omitted from delivery"), "the report explains excluded transport semantics");
assert(reportSource.includes("i18n:govoplan-campaign.excluded_rows_are_intentionally_omitted_from_del.421a1f00"), "the report explains excluded transport semantics through the bilingual catalog");
assert(reportSource.includes('return status === "skipped" ? "i18n:govoplan-campaign.skipped.5a000ad7"'), "the new skipped filter and status badges use the localized label");
assert(reportSource.includes("cards?.skipped ?? jobs.counts.send?.skipped"), "SMTP skipped has a separate report count");
assert(reportSource.includes("cards?.imap_skipped ?? jobs.counts.imap?.skipped"), "IMAP skipped has a separate report count");
assert(!reportSource.includes("setPage((value) => Math.max(1, value - 1))"), "the one-off report pager is removed in favor of the central DataGrid pager");