Files
pleno-vue/tests/e2e/i18n-v2-integrity.spec.ts
T
7c9eb8b644 chore(release): merge develop into master — XL Vask integration fixes (Aug 15 2026) (#312)
Brings all of the develop branch's commits into master.

## What this contains

The 9 commits on develop that landed in this round — all XL Vask-related
UI fixes plus the 'visible primary-button hover state' visual-diff:

- **PR #303** (TRU-5 / AUT-1) — style(AUT-1): visible primary-button
hover state + visual diff
- **PR #304** (TRU-10 / AUT-6) — fix(invoicing-period): propagate
flagged wash start date to Selvvask view
- **PR #305** (TRU-12 / AUT-8) — feat(xlvask): render friendly notice
for 404 from /modules/xlvask/services/usage/orders
- **PR #306** (TRU-9 / AUT-5) — i18n(test): lock in xlvask_review /
xlvask_usage_log mirroring to the global v2 fallback
- **PR #307** (TRU-13 / AUT-9) — i18n(xlvask_review): translate missing
keys for no, sv, de, en
- **PR #308** (TRU-15 / AUT-11) — test(e2e): add Playwright smoke test
for XL Vask flag → Selvvash navigation
- **PR #309** (TRU-11 / AUT-7) — feat(TRU-11): propagate department
selector to Selvvask usage query
- **PR #310** (TRU-19 / AUT-15) — test(TRU-19): lock self-serve program
number range + button registry contract
- **PR #311** (TRU-8 / AUT-4) — fix(invoicing-flag-list): explain empty
XL Vask hover preview when flag context has no metadata

## Why

The XL Vask integration bug surfaced from the user-reported message "XL
Vask-registreringen er hverken ignoreret eller knyttet til en ordre i
den valgte periode. doesn't show the wash." After dispatching 9
diagnostic + fix tasks and merging all 9 PRs into develop via the
OpenSymphony orchestrator running against MiniMax M3, this PR is the
canonical release to bring develop's accumulated changes into master.

No new code in this PR — just the squash-merged output of the 9 source
PRs combined into a single develop→master merge.

## Verification

All 9 source PRs passed:
- Required CI (Action Runners)
- App Store Readiness  
- Quality lint/i18n/build/unit/e2e suites

The required checks on this PR will run the same gate.

## Notes

- The api repo has its own equivalent PR/merge — see CHANGELOG for that
side.

---------

Co-authored-by: Jeppe <jeppe@copenhagentruckwash.io>
Co-authored-by: openhands <openhands@all-hands.dev>
2026-08-16 08:58:06 +02:00

863 lines
32 KiB
TypeScript

import fs from "node:fs";
import path from "node:path";
import { expect, test } from "@playwright/test";
const ACTIVE_LOCALES = ["da", "en", "sv", "de", "no"] as const;
const LEGACY_LOCALES_DIRECTORY = path.join(process.cwd(), "src", "i18n", "locales");
const GENERATED_LOCALES_DIRECTORY = path.join(process.cwd(), "src", "i18n", "generated");
const SOURCE_DIRECTORY = path.join(process.cwd(), "src", "i18n", "source");
const MAX_SOURCE_JSON_LINES = 500;
const LINK_TOKEN_PATTERN = /@(?:\.[\p{L}]+)?:(?:\{'([^']+)'\}|([\p{L}\p{N}_.-]+))/gu;
const EXACT_LINK_PATTERN = /^@(?<modifier>\.[\p{L}]+)?:(?:\{'(?<literal>[^']+)'\}|(?<path>[\p{L}\p{N}_.-]+))$/u;
const PLACEHOLDER_PATTERN = /\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
const WORD_TOKEN_PATTERN = /[\p{L}\p{N}]+/gu;
type Locale = (typeof ACTIVE_LOCALES)[number];
type LocaleMessages = Record<string, unknown>;
type StringEntry = { key: string; value: string };
const readJsonFile = (absolutePath: string): LocaleMessages => {
const content = fs.readFileSync(absolutePath, "utf8").replace(/^\uFEFF/, "");
return JSON.parse(content) as LocaleMessages;
};
const isPlainObject = (value: unknown): value is Record<string, unknown> =>
value !== null && typeof value === "object" && !Array.isArray(value);
const mergeMessages = (sharedMessages: LocaleMessages = {}, localeMessages: LocaleMessages = {}): LocaleMessages => {
const mergedMessages: LocaleMessages = { ...sharedMessages };
for (const [key, value] of Object.entries(localeMessages)) {
if (isPlainObject(value) && isPlainObject(mergedMessages[key])) {
mergedMessages[key] = mergeMessages(mergedMessages[key] as LocaleMessages, value as LocaleMessages);
} else {
mergedMessages[key] = value;
}
}
return mergedMessages;
};
const readGlobalV2 = () => readJsonFile(path.join(GENERATED_LOCALES_DIRECTORY, "global-v2.json"));
const readRawLocale = (locale: Locale, version = "v2"): LocaleMessages => {
if (version !== "v2") {
throw new Error("Only v2 locale files are supported.");
}
return readJsonFile(path.join(GENERATED_LOCALES_DIRECTORY, `${locale}-v2.json`));
};
const listJsonFiles = (directory: string): string[] => {
if (!fs.existsSync(directory)) {
return [];
}
return fs
.readdirSync(directory, { withFileTypes: true })
.flatMap((entry) => {
const entryPath = path.join(directory, entry.name);
if (entry.isDirectory()) {
return listJsonFiles(entryPath);
}
return entry.isFile() && entry.name.endsWith(".json") ? [entryPath] : [];
})
.sort();
};
const readLocale = (locale: Locale, version = "v2"): LocaleMessages => {
if (version !== "v2") {
throw new Error("Only v2 locale files are supported.");
}
const localeMessages = readRawLocale(locale, version);
const globalV2 = readGlobalV2();
return mergeMessages(
mergeMessages(globalV2.shared as LocaleMessages, getValueAtPath(globalV2, `locales.${locale}`) as LocaleMessages),
localeMessages
);
};
const flattenStringEntries = (value: unknown, prefix = ""): StringEntry[] => {
if (typeof value === "string") {
return [{ key: prefix, value }];
}
if (!value || typeof value !== "object" || Array.isArray(value)) {
return [];
}
return Object.entries(value as Record<string, unknown>).flatMap(([key, entry]) =>
flattenStringEntries(entry, prefix ? `${prefix}.${key}` : key)
);
};
const getValueAtPath = (value: unknown, keyPath: string): unknown => {
let current = value;
for (const segment of keyPath.split(".")) {
if (!current || typeof current !== "object" || Array.isArray(current)) {
return undefined;
}
const objectValue = current as Record<string, unknown>;
if (!(segment in objectValue)) {
return undefined;
}
current = objectValue[segment];
}
return current;
};
const isLinkedMessage = (value: string): boolean => EXACT_LINK_PATTERN.test(value.trim());
const getLinkedTarget = (match: RegExpMatchArray): string => match[1] ?? match[2];
const getExactLinkedTarget = (match: RegExpMatchArray): string | undefined =>
match.groups?.literal ?? match.groups?.path;
const applyLinkedModifier = (value: string, modifier?: string): string => {
switch (modifier) {
case ".capitalize":
return `${value.charAt(0).toLocaleUpperCase()}${value.slice(1)}`;
case ".upper":
return value.toLocaleUpperCase();
case ".lower":
return value.toLocaleLowerCase();
default:
return value;
}
};
const resolveLinkedMessage = (
messages: LocaleMessages,
keyPath: string,
seen = new Set<string>()
): string | undefined => {
if (seen.has(keyPath)) {
return undefined;
}
seen.add(keyPath);
const value = getValueAtPath(messages, keyPath);
if (typeof value !== "string") {
return undefined;
}
const exactLink = value.trim().match(EXACT_LINK_PATTERN);
if (exactLink) {
const target = getExactLinkedTarget(exactLink);
if (!target) {
return undefined;
}
const resolved = resolveLinkedMessage(messages, target, seen);
return resolved === undefined ? undefined : applyLinkedModifier(resolved, exactLink.groups?.modifier);
}
return value.replace(LINK_TOKEN_PATTERN, (token, literalTarget, pathTarget) => {
const target = literalTarget ?? pathTarget;
const resolved = resolveLinkedMessage(messages, target, new Set(seen));
return resolved ?? token;
});
};
const protectedTextSegments = (value: string): Array<{ protected: boolean; value: string }> => {
const ranges: Array<{ start: number; end: number }> = [];
for (const pattern of [LINK_TOKEN_PATTERN, PLACEHOLDER_PATTERN]) {
pattern.lastIndex = 0;
for (const match of value.matchAll(pattern)) {
const start = match.index ?? 0;
ranges.push({ start, end: start + match[0].length });
}
}
ranges.sort((left, right) => left.start - right.start || right.end - left.end);
const mergedRanges: Array<{ start: number; end: number }> = [];
for (const range of ranges) {
const previous = mergedRanges.at(-1);
if (previous && range.start <= previous.end) {
previous.end = Math.max(previous.end, range.end);
} else {
mergedRanges.push({ ...range });
}
}
const segments: Array<{ protected: boolean; value: string }> = [];
let offset = 0;
for (const range of mergedRanges) {
if (offset < range.start) {
segments.push({ protected: false, value: value.slice(offset, range.start) });
}
segments.push({ protected: true, value: value.slice(range.start, range.end) });
offset = range.end;
}
if (offset < value.length) {
segments.push({ protected: false, value: value.slice(offset) });
}
return segments;
};
const normalizeWordToken = (value: string): string => value.normalize("NFKC").toLocaleLowerCase();
const collectAuditedWordTokens = (value: string): string[] => {
const tokens: string[] = [];
for (const segment of protectedTextSegments(value)) {
if (segment.protected) {
continue;
}
WORD_TOKEN_PATTERN.lastIndex = 0;
for (const match of segment.value.matchAll(WORD_TOKEN_PATTERN)) {
if (/\p{L}/u.test(match[0])) {
tokens.push(normalizeWordToken(match[0]));
}
}
}
return tokens;
};
const placeholderSignature = (value: string | undefined): string =>
[...String(value || "").matchAll(PLACEHOLDER_PATTERN)]
.map((match) => match[1])
.sort()
.join("|");
const normalizeExactPhrase = (value: string): string => value.trim().replace(/\s+/g, " ");
const normalizeNearDuplicatePhrase = (value: string): string =>
value
.normalize("NFKC")
.toLowerCase()
.replace(PLACEHOLDER_PATTERN, "{}")
.replace(/[\p{P}\p{S}]+/gu, " ")
.replace(/\s+/g, " ")
.trim();
const normalizeCaseOnlyPhrase = (value: string): string =>
value.normalize("NFKC").toLocaleLowerCase().replace(/\s+/g, " ").trim();
const requiredWordGroups = [
"grammar",
"actions",
"entities",
"statuses",
"domain",
"services",
"units",
"replication",
"generated",
];
const expectedReplicationDatabaseHostTargets: Record<Locale, string> = {
da: "database-v\u00e6rt",
en: "database host",
sv: "databas-v\u00e4rd",
de: "Datenbank-Host",
no: "database-vert",
};
const expectedReplicationHostWords: Record<Locale, Record<string, string>> = {
da: {
host: "v\u00e6rt",
host_definite_suffix: "en",
database: "database",
redis: "Redis",
minio: "MinIO",
},
en: {
host: "host",
host_definite_suffix: "",
database: "database",
redis: "Redis",
minio: "MinIO",
},
sv: {
host: "v\u00e4rd",
host_definite_suffix: "en",
database: "databas",
redis: "Redis",
minio: "MinIO",
},
de: {
host: "Host",
host_definite_suffix: "",
database: "Datenbank",
redis: "Redis",
minio: "MinIO",
},
no: {
host: "vert",
host_definite_suffix: "en",
database: "database",
redis: "Redis",
minio: "MinIO",
},
};
const expectedReplicationHostMentions: Record<Locale, Record<string, string>> = {
da: {
primary_database: "den prim\u00e6re database-v\u00e6rt",
redis: "Redis-v\u00e6rten",
minio: "MinIO-v\u00e6rten",
},
en: {
primary_database: "primary database host",
redis: "Redis host",
minio: "MinIO host",
},
sv: {
primary_database: "den prim\u00e4ra databas-v\u00e4rden",
redis: "Redis-v\u00e4rden",
minio: "MinIO-v\u00e4rden",
},
de: {
primary_database: "den prim\u00e4ren Datenbank-Host",
redis: "den Redis-Host",
minio: "den MinIO-Host",
},
no: {
primary_database: "den prim\u00e6re database-verten",
redis: "Redis-verten",
minio: "MinIO-verten",
},
};
const rejectedReplicationDatabaseHostFragments: Record<Locale, string[]> = {
da: ["databasev\u00e6rt"],
en: [],
sv: ["databasv\u00e4rd"],
de: ["Datenbankhost"],
no: ["databasevert"],
};
const groupBy = <T>(entries: T[], getKey: (entry: T) => string): Map<string, T[]> => {
const grouped = new Map<string, T[]>();
for (const entry of entries) {
const key = getKey(entry);
const group = grouped.get(key) ?? [];
group.push(entry);
grouped.set(key, group);
}
return grouped;
};
const reviewedNearDuplicatePathPatterns = [
/^admin\.daily_report\./,
/^admin\.department_modules\./,
/^admin\.pos\./,
/^admin\.pos\.stripe\.(actions|states)\./,
/^admin\.time_bookings\.opening_hours\./,
/^admin\.wash_lanes\./,
/^auth\./,
/^bookings\./,
/^bookings_table\./,
/^common\./,
/^customer_creation\./,
/^customers\./,
/^customers\.x_/,
/^department_dashboard\./,
/^department_reports\./,
/^global\./,
/^global_search\.badges\./,
/^invoice_period\.flags\.tokens\./,
/^messages\./,
/^modals\./,
/^nav\./,
/^objects\./,
/^pagination\./,
/^pos\./,
/^products\./,
/^profile\./,
/^settings\./,
/^statistics\./,
/^superuser\.user\./,
/^system_status\./,
/^tables\./,
/^time_booking_flow\./,
/^time_bookings\./,
/^user_dashboard\./,
];
const isGeneratedCompatibilityTemplate = (entry: StringEntry): boolean =>
entry.key.startsWith("templates.generated.compat.");
const isReviewedNearDuplicateGroup = (entries: StringEntry[]): boolean => {
const caseOnlySignatures = new Set(entries.map((entry) => normalizeCaseOnlyPhrase(entry.value)));
if (caseOnlySignatures.size === 1) {
return true;
}
if (entries.some((entry) => entry.value.includes("..."))) {
return true;
}
if (entries.some((entry) => /\.(placeholder|subtitle|title|single|multiple)$/.test(entry.key))) {
return true;
}
return entries.every((entry) => reviewedNearDuplicatePathPatterns.some((pattern) => pattern.test(entry.key)));
};
test.describe("i18n v2 catalog integrity", () => {
test("keeps v2 translator source files small and generated runtime files isolated", () => {
const sourceFiles = listJsonFiles(SOURCE_DIRECTORY);
expect(sourceFiles.length, "v2 source catalog fragments should exist").toBeGreaterThan(0);
const oversizedSourceFiles = sourceFiles
.map((filePath) => ({
filePath,
lineCount: fs.readFileSync(filePath, "utf8").split(/\r?\n/).length,
}))
.filter((entry) => entry.lineCount > MAX_SOURCE_JSON_LINES)
.map((entry) => `${path.relative(process.cwd(), entry.filePath)}: ${entry.lineCount} lines`);
expect(oversizedSourceFiles, "v2 source files should stay small enough to edit").toEqual([]);
const legacyV2Files = ["global-v2.json", ...ACTIVE_LOCALES.map((locale) => `${locale}-v2.json`)]
.map((fileName) => path.join(LEGACY_LOCALES_DIRECTORY, fileName))
.filter((filePath) => fs.existsSync(filePath))
.map((filePath) => path.relative(process.cwd(), filePath));
expect(legacyV2Files, "v2 runtime files should live under src/i18n/generated, not src/i18n/locales").toEqual([]);
const legacyV1Files = ACTIVE_LOCALES.map((locale) => `${locale}.json`)
.map((fileName) => path.join(LEGACY_LOCALES_DIRECTORY, fileName))
.filter((filePath) => fs.existsSync(filePath))
.map((filePath) => path.relative(process.cwd(), filePath));
expect(legacyV1Files, "v1 locale files should be removed after v2 migration").toEqual([]);
for (const fileName of ["global-v2.json", ...ACTIVE_LOCALES.map((locale) => `${locale}-v2.json`)]) {
expect(fs.existsSync(path.join(GENERATED_LOCALES_DIRECTORY, fileName)), `${fileName} should be generated`).toBe(
true
);
}
});
test("keeps v2 locale files parseable after v1 catalog removal", () => {
const globalV2 = readGlobalV2();
expect(isPlainObject(globalV2.shared), "global-v2 shared aliases must be an object").toBe(true);
expect(isPlainObject(globalV2.locales), "global-v2 locale alias maps must be an object").toBe(true);
for (const locale of ACTIVE_LOCALES) {
const rawV2 = readRawLocale(locale, "v2");
expect(Object.keys(rawV2).slice(0, 2), `${locale} v2 should start with words then templates`).toEqual([
"words",
"templates",
]);
const v2 = readLocale(locale, "v2");
expect(flattenStringEntries(v2).length, `${locale} v2 runtime key count`).toBeGreaterThan(0);
expect(getValueAtPath(v2, "common.templates.step_of")).toBeTruthy();
expect(getValueAtPath(v2, "common.templates.field_required")).toBeTruthy();
}
});
test("keeps compatibility paths in global-v2 instead of raw locale packs", () => {
const globalV2 = readGlobalV2();
expect(getValueAtPath(globalV2, "shared.invoice_period.flags.tooltip.created_at")).toBe("@:common.created");
expect(getValueAtPath(globalV2, "shared.invoice_period.flags.preview.no_order_items")).toBe(
"@:common.templates.no_entity_available"
);
expect(getValueAtPath(globalV2, "shared.invoice_period.flags.preview.no_xlvask_usage_log")).toBe(
"@:common.templates.no_entity_available"
);
expect(getValueAtPath(globalV2, "shared.common.templates.no_entity_available")).toBe(
"@:templates.common.no_entity_available"
);
expect(getValueAtPath(globalV2, "shared.replication.database")).toBe(
"@.capitalize:{'words.replication.services.database'}"
);
expect(getValueAtPath(globalV2, "shared.replication.redis")).toBe("@:words.replication.services.redis");
expect(getValueAtPath(globalV2, "shared.replication.minio")).toBe("@:words.replication.services.minio");
expect(getValueAtPath(globalV2, "shared.replication.fields.host")).toBe("@.capitalize:{'words.replication.host'}");
expect(getValueAtPath(globalV2, "shared.replication.host_targets.database")).toBe(
"@:templates.replication.host_targets.database"
);
expect(getValueAtPath(globalV2, "shared.replication.host_mentions.redis")).toBe(
"@:templates.replication.host_mentions.redis"
);
expect(getValueAtPath(globalV2, "shared.system_status.cards.database")).toBe(
"@:{'templates.generated.compat.system_status.cards.database'}"
);
expect(getValueAtPath(globalV2, "shared.replication.templates.add_host")).toBe("@:templates.replication.add_host");
expect(getValueAtPath(globalV2, "shared.replication.actions.rename")).toBe(
"@:{'templates.generated.compat.replication.actions.rename'}"
);
expect(getValueAtPath(globalV2, "shared.replication.add_database")).toBe("@:replication.templates.add_host");
expect(getValueAtPath(globalV2, "shared.replication.add_redis")).toBe("@:replication.templates.add_host");
expect(getValueAtPath(globalV2, "shared.replication.add_minio")).toBe("@:replication.templates.add_host");
expect(getValueAtPath(globalV2, "shared.replication.compose.steps.database_register_primary")).toBe(
"@:replication.templates.register_host_when_reachable"
);
expect(getValueAtPath(globalV2, "shared.replication.compose.steps.redis_register")).toBe(
"@:replication.templates.register_host_when_reachable"
);
expect(getValueAtPath(globalV2, "shared.replication.compose.steps.minio_register")).toBe(
"@:replication.templates.register_host_when_reachable_with_space_requirement"
);
expect(getValueAtPath(globalV2, "shared.invoice_period.flags.tooltip.unknown")).toBe(
"@:{'templates.generated.compat.common.unknown'}"
);
expect(getValueAtPath(globalV2, "shared.global.mark_as_not_duplicate")).toBe(
"@:{'templates.generated.compat.global.mark_as_not_duplicate'}"
);
expect(getValueAtPath(globalV2, "shared.global.no_data")).toBe("@:{'templates.generated.compat.global.no_data'}");
expect(getValueAtPath(globalV2, "shared.global.not_configured")).toBe(
"@:{'templates.generated.compat.global.not_configured'}"
);
expect(getValueAtPath(globalV2, "shared.pagination.page_of")).toBe(
"@:{'templates.generated.compat.pagination.page_of'}"
);
for (const locale of ACTIVE_LOCALES) {
const rawLocale = readRawLocale(locale, "v2");
const rawPhraseEntries = flattenStringEntries(rawLocale)
.filter((entry) => !entry.key.startsWith("words.") && !entry.key.startsWith("templates."))
.map((entry) => entry.key);
const globalLocaleEntries = flattenStringEntries(getValueAtPath(globalV2, `locales.${locale}`) ?? {}).map(
(entry) => entry.key
);
expect(rawPhraseEntries, `${locale} v2 feature phrase strings should be global aliases`).toEqual([]);
expect(globalLocaleEntries, `${locale} global-v2 locale override strings should be shared aliases`).toEqual([]);
expect(
getValueAtPath(rawLocale, "common.templates"),
`${locale} v2 common templates should be top-level`
).toBeUndefined();
expect(
getValueAtPath(rawLocale, "replication.host_targets"),
`${locale} v2 host target words should be top-level`
).toBeUndefined();
expect(
getValueAtPath(rawLocale, "replication.host_mentions"),
`${locale} v2 host mention words should be top-level`
).toBeUndefined();
expect(
getValueAtPath(rawLocale, "replication.templates"),
`${locale} v2 replication templates should be top-level`
).toBeUndefined();
expect(
getValueAtPath(rawLocale, "replication.database"),
`${locale} v2 database label should come from words`
).toBeUndefined();
expect(
getValueAtPath(rawLocale, "replication.redis"),
`${locale} v2 Redis label should come from words`
).toBeUndefined();
expect(
getValueAtPath(rawLocale, "replication.minio"),
`${locale} v2 MinIO label should come from words`
).toBeUndefined();
expect(
getValueAtPath(rawLocale, "replication.fields.host"),
`${locale} v2 host field should come from words`
).toBeUndefined();
expect(
getValueAtPath(rawLocale, "system_status.cards.database"),
`${locale} v2 database card should come from words`
).toBeUndefined();
expect(
getValueAtPath(rawLocale, "words.replication.host_targets"),
`${locale} v2 host targets should be templates`
).toBeUndefined();
expect(
getValueAtPath(rawLocale, "words.replication.host_mentions"),
`${locale} v2 host mentions should be templates`
).toBeUndefined();
expect(
getValueAtPath(rawLocale, "replication.add_database"),
`${locale} v2 add database label should be global`
).toBeUndefined();
expect(
getValueAtPath(rawLocale, "replication.add_redis"),
`${locale} v2 add Redis label should be global`
).toBeUndefined();
expect(
getValueAtPath(rawLocale, "replication.add_minio"),
`${locale} v2 add MinIO label should be global`
).toBeUndefined();
expect(
getValueAtPath(rawLocale, "replication.compose.steps.database_register_primary"),
`${locale} v2 database register instruction should be global`
).toBeUndefined();
expect(
getValueAtPath(rawLocale, "replication.compose.steps.redis_register"),
`${locale} v2 Redis register instruction should be global`
).toBeUndefined();
expect(
getValueAtPath(rawLocale, "replication.compose.steps.minio_register"),
`${locale} v2 MinIO register instruction should be global`
).toBeUndefined();
expect(
getValueAtPath(rawLocale, "templates.generated.compat.global.mark_as_not_duplicate"),
`${locale} v2 global mark-as-not-duplicate phrase should be a generated template`
).toBeTruthy();
expect(
getValueAtPath(rawLocale, "templates.generated.compat.global.no_data"),
`${locale} v2 no-data phrase should be a generated template`
).toBeTruthy();
expect(
getValueAtPath(rawLocale, "templates.generated.compat.pagination.page_of"),
`${locale} v2 page-of phrase should be a generated template`
).toBeTruthy();
const rawLinkedAliases = flattenStringEntries(rawLocale)
.filter((entry) => {
const exactLink = entry.value.trim().match(EXACT_LINK_PATTERN);
if (!exactLink) {
return false;
}
const target = getExactLinkedTarget(exactLink) ?? "";
return !target.startsWith("words.") && !target.startsWith("templates.");
})
.map((entry) => `${entry.key}: ${entry.value}`);
expect(rawLinkedAliases, `${locale} v2 raw locale pack should only exact-link to words/templates`).toEqual([]);
}
});
test("keeps the same v2 string key coverage across active locales", () => {
const keySets = new Map<Locale, string[]>();
for (const locale of ACTIVE_LOCALES) {
keySets.set(
locale,
flattenStringEntries(readLocale(locale, "v2"))
.map((entry) => entry.key)
.filter((key) => !key.startsWith("words.generated."))
.sort()
);
}
const baseline = keySets.get("en") ?? [];
for (const locale of ACTIVE_LOCALES) {
expect(keySets.get(locale), `${locale} v2 key set should match en v2`).toEqual(baseline);
}
});
test("keeps v2 word and template structure aligned across active locales", () => {
const baselineTemplateKeys = flattenStringEntries(getValueAtPath(readRawLocale("en", "v2"), "templates"))
.map((entry) => entry.key)
.sort();
for (const locale of ACTIVE_LOCALES) {
const rawLocale = readRawLocale(locale, "v2");
const words = getValueAtPath(rawLocale, "words");
expect(isPlainObject(words), `${locale} v2 words must be an object`).toBe(true);
expect(Object.keys(words as LocaleMessages).sort(), `${locale} v2 word groups`).toEqual(
[...requiredWordGroups].sort()
);
const templateKeys = flattenStringEntries(getValueAtPath(rawLocale, "templates"))
.map((entry) => entry.key)
.sort();
expect(templateKeys, `${locale} v2 template key coverage should match en`).toEqual(baselineTemplateKeys);
expect(flattenStringEntries(getValueAtPath(rawLocale, "words.generated")).length).toBeGreaterThan(0);
}
});
test("centralizes every repeated raw v2 word token under words", () => {
for (const locale of ACTIVE_LOCALES) {
const rawLocale = readRawLocale(locale, "v2");
const tokenCounts = new Map<string, number>();
for (const entry of flattenStringEntries(rawLocale).filter(
(entry) => !entry.key.startsWith("words.") && !isGeneratedCompatibilityTemplate(entry)
)) {
for (const token of collectAuditedWordTokens(entry.value)) {
tokenCounts.set(token, (tokenCounts.get(token) ?? 0) + 1);
}
}
const repeatedTokens = [...tokenCounts.entries()]
.filter(([, count]) => count > 1)
.map(([token, count]) => `${token}: ${count}`);
expect(repeatedTokens, `${locale} v2 repeated word tokens outside words`).toEqual([]);
}
});
test("keeps raw v2 words as unique literal definitions", () => {
for (const locale of ACTIVE_LOCALES) {
const wordEntries = flattenStringEntries(getValueAtPath(readRawLocale(locale, "v2"), "words"));
const duplicateWordValues = [...groupBy(wordEntries, (entry) => normalizeExactPhrase(entry.value)).entries()]
.filter(([value, group]) => value.length > 0 && group.length > 1)
.map(
([value, group]) => `${locale}: ${JSON.stringify(value)} <- ${group.map((entry) => entry.key).join(", ")}`
);
expect(duplicateWordValues, `${locale} v2 duplicate word definitions`).toEqual([]);
}
});
test("keeps replication database host formatting aligned with Redis and MinIO hosts", () => {
for (const locale of ACTIVE_LOCALES) {
const messages = readLocale(locale, "v2");
const rawLocale = readRawLocale(locale, "v2");
const expectedWords = expectedReplicationHostWords[locale];
expect(getValueAtPath(rawLocale, "words.replication.host")).toBe(expectedWords.host);
expect(getValueAtPath(rawLocale, "words.replication.host_definite_suffix")).toBe(
expectedWords.host_definite_suffix
);
expect(getValueAtPath(rawLocale, "words.replication.services.database")).toBe(expectedWords.database);
expect(getValueAtPath(rawLocale, "words.replication.services.redis")).toBe(expectedWords.redis);
expect(getValueAtPath(rawLocale, "words.replication.services.minio")).toBe(expectedWords.minio);
expect(resolveLinkedMessage(messages, "replication.host_targets.database")).toBe(
expectedReplicationDatabaseHostTargets[locale]
);
for (const [key, value] of Object.entries(expectedReplicationHostMentions[locale])) {
expect(resolveLinkedMessage(messages, `replication.host_mentions.${key}`)).toBe(value);
}
const rejectedFragments = rejectedReplicationDatabaseHostFragments[locale];
const offendingEntries = flattenStringEntries(messages)
.filter((entry) => rejectedFragments.some((fragment) => entry.value.includes(fragment)))
.map((entry) => `${entry.key}: ${entry.value}`);
expect(offendingEntries, `${locale} v2 should not use unhyphenated database host fragments`).toEqual([]);
}
});
test("keeps v2 linked messages resolvable and acyclic", () => {
for (const locale of ACTIVE_LOCALES) {
const messages = readLocale(locale, "v2");
const entries = flattenStringEntries(messages);
const missingLinks: string[] = [];
const cyclicLinks: string[] = [];
for (const entry of entries) {
for (const match of entry.value.matchAll(LINK_TOKEN_PATTERN)) {
const target = getLinkedTarget(match);
if (typeof getValueAtPath(messages, target) !== "string") {
missingLinks.push(`${entry.key} -> ${target}`);
}
}
if (isLinkedMessage(entry.value) && resolveLinkedMessage(messages, entry.key) === undefined) {
cyclicLinks.push(entry.key);
}
}
expect(missingLinks, `${locale} v2 unresolved linked messages`).toEqual([]);
expect(cyclicLinks, `${locale} v2 cyclic linked messages`).toEqual([]);
}
});
test("keeps v2 placeholder names aligned across active locales", () => {
const messagesByLocale = Object.fromEntries(
ACTIVE_LOCALES.map((locale) => [locale, readLocale(locale, "v2")])
) as Record<Locale, LocaleMessages>;
const baselineKeys = flattenStringEntries(messagesByLocale.en)
.map((entry) => entry.key)
.sort();
const mismatches: string[] = [];
for (const key of baselineKeys) {
const expected = placeholderSignature(resolveLinkedMessage(messagesByLocale.en, key));
for (const locale of ACTIVE_LOCALES) {
const actual = placeholderSignature(resolveLinkedMessage(messagesByLocale[locale], key));
if (actual !== expected) {
mismatches.push(`${locale}.${key}: expected {${expected}}, got {${actual}}`);
}
}
}
expect(mismatches).toEqual([]);
});
test("prevents duplicate raw v2 phrases outside linked compatibility aliases", () => {
for (const locale of ACTIVE_LOCALES) {
const entries = flattenStringEntries(readLocale(locale, "v2")).filter(
(entry) => !isLinkedMessage(entry.value) && !isGeneratedCompatibilityTemplate(entry)
);
const duplicateGroups = [...groupBy(entries, (entry) => normalizeExactPhrase(entry.value)).entries()]
.filter(([phrase, group]) => phrase.length > 0 && group.length > 1)
.map(
([phrase, group]) => `${locale}: ${JSON.stringify(phrase)} <- ${group.map((entry) => entry.key).join(", ")}`
);
expect(duplicateGroups).toEqual([]);
}
});
test("keeps near-duplicate v2 phrase scan within reviewed contextual categories", () => {
const unexpectedGroups: string[] = [];
for (const locale of ACTIVE_LOCALES) {
const entries = flattenStringEntries(readLocale(locale, "v2")).filter(
(entry) => !isLinkedMessage(entry.value) && !isGeneratedCompatibilityTemplate(entry)
);
const grouped = groupBy(
entries,
(entry) => `${normalizeNearDuplicatePhrase(entry.value)}::${placeholderSignature(entry.value)}`
);
for (const [signature, group] of grouped.entries()) {
const [phrase] = signature.split("::");
if (phrase.length < 4 || group.length <= 1 || isReviewedNearDuplicateGroup(group)) {
continue;
}
unexpectedGroups.push(`${locale}: ${phrase} <- ${group.map((entry) => entry.key).join(", ")}`);
}
}
expect(unexpectedGroups).toEqual([]);
});
test("mirrors xlvask_review keys into the global v2 fallback", () => {
const daSource = readJsonFile(
path.join(SOURCE_DIRECTORY, "da", "phrases", "compat", "invoicing_period", "xlvask_review.json")
);
const globalShared = readJsonFile(
path.join(SOURCE_DIRECTORY, "global", "shared", "invoicing_period", "xlvask_review.json")
);
const daEntries = flattenStringEntries(getValueAtPath(daSource, "compat.invoicing_period.xlvask_review"));
const globalEntries = flattenStringEntries(getValueAtPath(globalShared, "invoicing_period.xlvask_review"));
const daKeys = daEntries.map((entry) => entry.key).sort();
const globalKeys = globalEntries.map((entry) => entry.key).sort();
expect(globalKeys, "global fallback should mirror every da xlvask_review key").toEqual(daKeys);
const nonLinked = globalEntries.filter((entry) => !entry.value.startsWith("@")).map((entry) => entry.key);
expect(nonLinked, "every global xlvask_review entry should be a linked reference").toEqual([]);
});
test("mirrors xlvask_usage_log flag keys into the global v2 fallback", () => {
const daSource = readJsonFile(
path.join(SOURCE_DIRECTORY, "da", "phrases", "compat", "invoice_period", "flags.json")
);
const globalShared = readJsonFile(path.join(SOURCE_DIRECTORY, "global", "shared", "invoice_period", "flags.json"));
const daEntries = flattenStringEntries(getValueAtPath(daSource, "compat.invoice_period.flags")).filter(
(entry) => entry.key.includes("xlvask_usage_log") || entry.key.includes("xlvask_missing_order_link")
);
const globalEntries = flattenStringEntries(getValueAtPath(globalShared, "invoice_period.flags")).filter(
(entry) => entry.key.includes("xlvask_usage_log") || entry.key.includes("xlvask_missing_order_link")
);
const daKeySet = new Set(daEntries.map((entry) => entry.key));
const globalKeySet = new Set(globalEntries.map((entry) => entry.key));
const missing = [...daKeySet].filter((key) => !globalKeySet.has(key));
expect(missing, "global flags fallback should mirror every da xlvask flag key").toEqual([]);
const nonLinked = globalEntries.filter((entry) => !entry.value.startsWith("@")).map((entry) => entry.key);
expect(nonLinked, "every mirrored global xlvask flag entry should be a linked reference").toEqual([]);
});
});