Files
OmniRoute/tests/unit/i18n-vi-completeness.test.ts
nguyenha935 4012bac41d fix(i18n): preserve remaining Vietnamese localization (#7935)
* fix(i18n): preserve remaining Vietnamese localization

* chore(quality): rebaseline file-size cap for 9 dashboard components (i18n wiring)

Restoring the Vietnamese localization on 9 dashboard components (useTranslations
wiring + t()/tc() call-site swaps for previously hardcoded strings) grows each
file by a small, irreducible amount. Bumps the frozen file-size-baseline.json
caps to match, with a justification entry per the project's own ratchet policy.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(i18n): wire weekday localization + add missing qwen CLI description

Two gaps left by this PR's own new contract tests, caught while
reconciling the branch against the release tip:

- CostOverviewTab.tsx added formatWeekdayLabel() but never called it;
  the Weekly Usage Pattern chart still showed raw English day
  abbreviations regardless of locale. Now maps weeklyPattern rows
  through it before handing them to WeeklyPatternCard.
- cliTools.toolDescriptions was missing an entry for "qwen" (a
  baseUrlSupport:"full" tool) in both en.json and vi.json, failing
  the PR's own cli-catalog-display-contract.test.ts.

Covered by the PR's existing tests/unit/dashboard-localization-contract.test.ts
and tests/unit/cli-catalog-display-contract.test.ts (both now pass).

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* i18n(vi): backfill the 4 proxySubscription keys #7299 added to en.json

#7299 (proxy subscriptions) merged while this branch was rebasing, adding
settings.proxySubscriptionsTab and settings.proxySubscription.error.{LOCAL_CORE_ENDPOINT_INVALID,
NEEDS_CORE_NOT_CONFIGURED,NO_USABLE_NODES} to en.json. This PR's own
i18n-vi-completeness contract asserts full en↔vi key parity, so the merge of the
current release tip surfaced them as missing. Adds the Vietnamese translations,
keeping the SS/VMess/Trojan/VLESS/SOCKS5 technical terms verbatim.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: nguyenha935 <208228297+nguyenha935@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: nguyenha935 <nguyenha935@users.noreply.github.com>
2026-07-21 13:41:02 -03:00

99 lines
3.3 KiB
TypeScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { parse } from "@formatjs/icu-messageformat-parser";
import en from "../../src/i18n/messages/en.json" with { type: "json" };
import vi from "../../src/i18n/messages/vi.json" with { type: "json" };
type MessageEntry = {
key: string;
value: string;
};
function flattenMessages(value: unknown, segments: Array<string | number> = []): MessageEntry[] {
if (typeof value === "string") {
return [{ key: segments.map(String).join("."), value }];
}
if (Array.isArray(value)) {
return value.flatMap((child, index) => flattenMessages(child, [...segments, index]));
}
if (value && typeof value === "object") {
return Object.entries(value).flatMap(([key, child]) =>
flattenMessages(child, [...segments, key])
);
}
return [];
}
function placeholderNames(value: string): string[] {
return [...value.matchAll(/\{\s*([A-Za-z][A-Za-z0-9_]*)\s*(?=[,}])/g)]
.map((match) => match[1])
.sort();
}
const englishMessages = flattenMessages(en);
const vietnameseMessages = flattenMessages(vi);
const vietnameseByKey = new Map(vietnameseMessages.map((entry) => [entry.key, entry.value]));
test("Vietnamese locale has complete key parity with English", () => {
assert.deepEqual(
vietnameseMessages.map((entry) => entry.key).sort(),
englishMessages.map((entry) => entry.key).sort()
);
});
test("Vietnamese locale has no internal missing markers or empty fallbacks", () => {
const invalid = vietnameseMessages.filter(
({ value }) => !value.trim() || /__(?:MISSING|TODO)__:?/i.test(value)
);
assert.deepEqual(invalid, []);
});
test("Vietnamese locale preserves every ICU placeholder name", () => {
const mismatches = englishMessages.flatMap(({ key, value }) => {
const translated = vietnameseByKey.get(key);
if (translated === undefined) return [{ key, reason: "missing" }];
const sourceNames = placeholderNames(value);
const targetNames = placeholderNames(translated);
return JSON.stringify(sourceNames) === JSON.stringify(targetNames)
? []
: [{ key, sourceNames, targetNames }];
});
assert.deepEqual(mismatches, []);
});
test("Vietnamese locale introduces no ICU parse regression", () => {
const regressions = englishMessages.flatMap(({ key, value }) => {
try {
parse(value, { captureLocation: false, shouldParseSkeletons: true });
} catch {
return [];
}
const translated = vietnameseByKey.get(key);
if (translated === undefined) return [{ key, reason: "missing" }];
try {
parse(translated, { captureLocation: false, shouldParseSkeletons: true });
return [];
} catch (error) {
return [{ key, reason: error instanceof Error ? error.message : String(error) }];
}
});
assert.deepEqual(regressions, []);
});
test("no-auth provider controls keep locale translators unambiguous", () => {
const source = readFileSync(
new URL(
"../../src/app/(dashboard)/dashboard/providers/[id]/components/NoAuthProviderControls.tsx",
import.meta.url
),
"utf8"
);
assert.equal(source.match(/import \{ useTranslations \} from "next-intl";/g)?.length, 1);
assert.match(source, /const noAuthT = useTranslations\("noAuthProvider"\);/);
assert.match(source, /const t = useTranslations\("providers"\);/);
});