173 lines
5.7 KiB
JavaScript
173 lines
5.7 KiB
JavaScript
import { readFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { describe, expect, it } from "vitest";
|
|
import { CUSTOMER_RULE_DEFINITIONS } from "@/features/customer/customerRuleRegistry.js";
|
|
import { readI18nRuntimeMessages } from "./helpers/readI18nRuntimeMessages";
|
|
|
|
const root = process.cwd();
|
|
const componentPath = join(root, "src/components/displays/buttons/ActionSettingsWheelButton.vue");
|
|
const activeLocales = ["da", "en", "sv", "de", "no"];
|
|
const translationCallPattern = /(?:\$t|\bt)\(\s*(["'])([^"'\\]*(?:\\.[^"'\\]*)*)\1/g;
|
|
|
|
const readLocale = (locale) => {
|
|
return readI18nRuntimeMessages(locale);
|
|
};
|
|
|
|
const extractLiteralTranslationKeys = (source) => {
|
|
const keys = new Set();
|
|
|
|
for (const match of source.matchAll(translationCallPattern)) {
|
|
const key = match[2];
|
|
if (!key || !key.includes(".")) {
|
|
continue;
|
|
}
|
|
keys.add(key);
|
|
}
|
|
|
|
return [...keys].sort();
|
|
};
|
|
|
|
const hasKeyPath = (messages, keyPath) => {
|
|
let current = messages;
|
|
|
|
for (const segment of keyPath.split(".")) {
|
|
if (!current || typeof current !== "object" || Array.isArray(current)) {
|
|
return false;
|
|
}
|
|
|
|
if (!(segment in current)) {
|
|
return false;
|
|
}
|
|
|
|
current = current[segment];
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
const getKeyPathValue = (messages, keyPath) => {
|
|
let current = messages;
|
|
|
|
for (const segment of keyPath.split(".")) {
|
|
if (!current || typeof current !== "object" || Array.isArray(current)) {
|
|
return undefined;
|
|
}
|
|
|
|
current = current[segment];
|
|
}
|
|
|
|
return current;
|
|
};
|
|
|
|
describe("ActionSettingsWheelButton i18n coverage", () => {
|
|
it("contains all literal translation keys in all active locales", () => {
|
|
const componentSource = readFileSync(componentPath, "utf8");
|
|
const usedKeys = extractLiteralTranslationKeys(componentSource);
|
|
|
|
expect(usedKeys.length).toBeGreaterThan(0);
|
|
|
|
const missingByLocale = {};
|
|
|
|
for (const locale of activeLocales) {
|
|
const messages = readLocale(locale);
|
|
const missingKeys = usedKeys.filter((key) => !hasKeyPath(messages, key));
|
|
|
|
if (missingKeys.length > 0) {
|
|
missingByLocale[locale] = missingKeys;
|
|
}
|
|
}
|
|
|
|
if (Object.keys(missingByLocale).length > 0) {
|
|
const details = Object.entries(missingByLocale)
|
|
.map(([locale, keys]) => `${locale} (${keys.length}):\n- ${keys.join("\n- ")}`)
|
|
.join("\n\n");
|
|
|
|
throw new Error(
|
|
[
|
|
"Missing translation keys detected for ActionSettingsWheelButton literal usages.",
|
|
"Each key below is used in src/components/displays/buttons/ActionSettingsWheelButton.vue",
|
|
"but absent from the locale JSON.",
|
|
"",
|
|
details,
|
|
].join("\n")
|
|
);
|
|
}
|
|
});
|
|
|
|
it("keeps the department hardware menu labels free of encoding placeholders", () => {
|
|
const departmentHardwareKeys = [
|
|
"admin.pos.settings_wheel.self_serve_studio_section",
|
|
"admin.pos.settings_wheel.gates_section",
|
|
"admin.pos.settings_wheel.relays_section",
|
|
"admin.pos.settings_wheel.gateways_section",
|
|
"admin.pos.settings_wheel.open_studio",
|
|
"admin.pos.settings_wheel.open_legacy_self_serve",
|
|
"admin.pos.settings_wheel.open_hardware_workspace_lanes",
|
|
"admin.pos.settings_wheel.open_gates_tab",
|
|
"admin.pos.settings_wheel.open_legacy_gates",
|
|
"admin.pos.settings_wheel.add_gate",
|
|
"admin.pos.settings_wheel.open_relays_tab",
|
|
"admin.pos.settings_wheel.open_legacy_relays",
|
|
"admin.pos.settings_wheel.add_relay",
|
|
"admin.pos.settings_wheel.open_gateways_tab",
|
|
"admin.pos.settings_wheel.open_fleet_landing",
|
|
"admin.pos.settings_wheel.open_primary_gateway",
|
|
];
|
|
|
|
for (const locale of activeLocales) {
|
|
const messages = readLocale(locale);
|
|
|
|
for (const key of departmentHardwareKeys) {
|
|
const value = getKeyPathValue(messages, key);
|
|
|
|
expect(typeof value).toBe("string");
|
|
expect(value).not.toMatch(/[?\uFFFD]/u);
|
|
expect(value).not.toContain("\u00ef\u00bf\u00bd");
|
|
}
|
|
}
|
|
});
|
|
|
|
it("renders the Danish vehicle action labels with the expected copy", () => {
|
|
const messages = readLocale("da");
|
|
const manageValue = getKeyPathValue(messages, "admin.pos.settings_wheel.manage_vehicle");
|
|
const newTabValue = getKeyPathValue(messages, "admin.pos.settings_wheel.view_vehicle_new_tab");
|
|
const deleteValue = getKeyPathValue(messages, "admin.pos.settings_wheel.delete_vehicle");
|
|
|
|
expect(manageValue).toBe("Administrer køretøj");
|
|
expect(newTabValue).toBe("Åbn køretøj i ny fane");
|
|
expect(newTabValue).not.toContain("{reg}");
|
|
expect(deleteValue).toBe("Slet køretøj");
|
|
expect([manageValue, newTabValue, deleteValue].join(" ")).not.toMatch(/[?\uFFFD]/u);
|
|
});
|
|
|
|
it("contains all shared customer rule translation keys in all active locales", () => {
|
|
const sharedCustomerRuleKeys = CUSTOMER_RULE_DEFINITIONS.flatMap((rule) => [rule.labelKey, rule.descriptionKey]);
|
|
const missingByLocale = {};
|
|
|
|
for (const locale of activeLocales) {
|
|
const messages = readLocale(locale);
|
|
const missingKeys = sharedCustomerRuleKeys.filter((key) => !hasKeyPath(messages, key));
|
|
|
|
if (missingKeys.length > 0) {
|
|
missingByLocale[locale] = missingKeys;
|
|
}
|
|
}
|
|
|
|
if (Object.keys(missingByLocale).length > 0) {
|
|
const details = Object.entries(missingByLocale)
|
|
.map(([locale, keys]) => `${locale} (${keys.length}):\n- ${keys.join("\n- ")}`)
|
|
.join("\n\n");
|
|
|
|
throw new Error(
|
|
[
|
|
"Missing translation keys detected for the shared customer rule registry.",
|
|
"Each key below is used by ActionSettingsWheelButton and PosSelectedCustomer",
|
|
"but absent from the locale JSON.",
|
|
"",
|
|
details,
|
|
].join("\n")
|
|
);
|
|
}
|
|
});
|
|
});
|