Files
pleno-vue/tests/e2e/i18n.smoke.spec.ts

213 lines
7.7 KiB
TypeScript

import { expect, test } from "@playwright/test";
const expectedReferencePaths = [
"admin.pos.reference",
"bookings.reference",
"bookings_table.reference",
"department_dashboard.wash_log.reference",
"global.payment.reference",
"objects.columns.reference",
"pos.bookings.reference",
"pos.order.reference",
"tables.bookings.reference",
"time_booking_flow.reference",
"user_dashboard.bookings.book.reference",
];
const expectedFuldfValues = {
"bookings.statuses.completed": "Fuldført",
"pos.complete": "Fuldfør",
"pos.completed": "Fuldført",
"pos.finish_and_register": "Fuldfør og registrer",
"pos.finish_and_register_new": "Fuldfør og registrer ny",
"tables.bookings.booking_id_required": "Du skal indtaste booking-id for at fuldføre vask uden vaskecertifikat",
"tables.bookings.complete_wash_without_certificate": "Fuldfør vask uden vaskecertifikat",
"tables.bookings.complete_wash_without_certificate_prompt":
"Indtast booking-id for at fuldføre vask uden vaskecertifikat",
"tables.bookings.completed": "Fuldført",
"tables.bookings.wash_completed": "Vasken er fuldført",
"superuser_invoice_distribution.errors.compare_failed": "Kunne ikke fuldføre sammenligningen",
} as const;
const expectedSelfWashValues = {
"self_wash.lane_unavailable": "Vaskebanen er ikke tilgængelig",
"self_wash.vehicle_step_missing_customer_number": "Indtast kundenummer for at fortsætte.",
"self_wash.vehicle_step_missing_department": "Vent på, at nærmeste afdeling er indlæst, før du fortsætter.",
"self_wash.vehicle_step_missing_registration": "Indtast registreringsnummer for at fortsætte.",
"self_wash.vehicle_step_missing_vehicle_type": "Vælg en tilgængelig køretøjstype for at fortsætte.",
} as const;
const removedSelfWashGuidanceText =
"Maskinvask er ikke tilgængelig for den valgte bane. Vælg manuel vask eller en anden ledig bane.";
const LINK_TOKEN_PATTERN = /@(?<modifier>\.[\p{L}]+)?:(?:\{'(?<literal>[^']+)'\}|(?<path>[\p{L}\p{N}_.-]+))/gu;
const EXACT_LINK_PATTERN = /^@(?<modifier>\.[\p{L}]+)?:(?:\{'(?<literal>[^']+)'\}|(?<path>[\p{L}\p{N}_.-]+))$/u;
function getValueAtPath(value: unknown, path: string): unknown {
return path.split(".").reduce<unknown>((current, segment) => {
if (current && typeof current === "object") {
return (current as Record<string, unknown>)[segment];
}
return undefined;
}, value);
}
function collectExactStringPaths(value: unknown, exact: string, prefix = ""): string[] {
if (value === exact) {
return [prefix];
}
if (Array.isArray(value)) {
return value.flatMap((entry, index) => collectExactStringPaths(entry, exact, `${prefix}[${index}]`));
}
if (value && typeof value === "object") {
return Object.entries(value as Record<string, unknown>).flatMap(([key, entry]) =>
collectExactStringPaths(entry, exact, prefix ? `${prefix}.${key}` : key)
);
}
return [];
}
function collectSubstringPaths(value: unknown, substring: string, prefix = ""): string[] {
if (typeof value === "string") {
return value.includes(substring) ? [prefix] : [];
}
if (Array.isArray(value)) {
return value.flatMap((entry, index) => collectSubstringPaths(entry, substring, `${prefix}[${index}]`));
}
if (value && typeof value === "object") {
return Object.entries(value as Record<string, unknown>).flatMap(([key, entry]) =>
collectSubstringPaths(entry, substring, prefix ? `${prefix}.${key}` : key)
);
}
return [];
}
function isPlainObject(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
function mergeMessages(sharedMessages: Record<string, unknown> = {}, localeMessages: Record<string, unknown> = {}) {
const mergedMessages: Record<string, unknown> = { ...sharedMessages };
for (const [key, value] of Object.entries(localeMessages)) {
if (isPlainObject(value) && isPlainObject(mergedMessages[key])) {
mergedMessages[key] = mergeMessages(mergedMessages[key] as Record<string, unknown>, value);
} else {
mergedMessages[key] = value;
}
}
return mergedMessages;
}
function 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;
}
}
function resolveLinkedString(messages: Record<string, unknown>, value: string, seen = new Set<string>()): string {
const exactLink = value.trim().match(EXACT_LINK_PATTERN);
if (exactLink) {
const target = exactLink.groups?.literal ?? exactLink.groups?.path;
if (!target || seen.has(target)) {
return value;
}
const targetValue = getValueAtPath(messages, target);
const resolved =
typeof targetValue === "string"
? resolveLinkedString(messages, targetValue, new Set([...seen, target]))
: targetValue;
return typeof resolved === "string" ? applyLinkedModifier(resolved, exactLink.groups?.modifier) : value;
}
LINK_TOKEN_PATTERN.lastIndex = 0;
return value.replace(LINK_TOKEN_PATTERN, (token: string, ...args: unknown[]) => {
const groups = args.at(-1) as { modifier?: string; literal?: string; path?: string } | undefined;
const target = groups?.literal ?? groups?.path;
if (!target || seen.has(target)) {
return token;
}
const targetValue = getValueAtPath(messages, target);
if (typeof targetValue !== "string") {
return token;
}
return applyLinkedModifier(
resolveLinkedString(messages, targetValue, new Set([...seen, target])),
groups?.modifier
);
});
}
function resolveLinkedMessages(value: unknown, messages: Record<string, unknown>): unknown {
if (typeof value === "string") {
return resolveLinkedString(messages, value);
}
if (Array.isArray(value)) {
return value.map((entry) => resolveLinkedMessages(entry, messages));
}
if (isPlainObject(value)) {
return Object.fromEntries(
Object.entries(value).map(([key, entry]) => [key, resolveLinkedMessages(entry, messages)])
);
}
return value;
}
test.describe("Danish locale smoke", () => {
test("@smoke @pr normalizes targeted Danish locale copy in da v2 runtime locale", async ({ request }) => {
const globalResponse = await request.get("/src/i18n/generated/global-v2.json");
const localeResponse = await request.get("/src/i18n/generated/da-v2.json");
expect(globalResponse.ok()).toBe(true);
expect(localeResponse.ok()).toBe(true);
const globalV2 = (await globalResponse.json()) as Record<string, unknown>;
const localeV2 = (await localeResponse.json()) as Record<string, unknown>;
const mergedLocale = mergeMessages(
mergeMessages(
globalV2.shared as Record<string, unknown>,
(globalV2.locales as Record<string, unknown>)?.da as Record<string, unknown>
),
localeV2
);
const locale = resolveLinkedMessages(mergedLocale, mergedLocale) as Record<string, unknown>;
for (const path of expectedReferencePaths) {
expect(getValueAtPath(locale, path), path).toBe("Reference");
}
for (const [path, expectedValue] of Object.entries(expectedFuldfValues)) {
expect(getValueAtPath(locale, path), path).toBe(expectedValue);
}
for (const [path, expectedValue] of Object.entries(expectedSelfWashValues)) {
expect(getValueAtPath(locale, path), path).toBe(expectedValue);
}
expect(collectExactStringPaths(locale, "Referanse")).toEqual([]);
expect(collectExactStringPaths(locale, removedSelfWashGuidanceText)).toEqual([]);
expect(collectSubstringPaths(locale, "Fullf")).toEqual([]);
expect(collectSubstringPaths(locale, "fullf")).toEqual([]);
});
});