fix(ui): harden i18n report filters

Signed-off-by: samzong <samzong.lu@gmail.com>
This commit is contained in:
samzong
2026-05-13 16:47:50 +08:00
committed by Peter Steinberger
parent ee9d471865
commit 4e76d6e427
2 changed files with 58 additions and 7 deletions

View File

@@ -30,6 +30,7 @@ const LOCALE_LABELS: Record<string, string> = {
"zh-CN": "Simplified Chinese",
"zh-TW": "Traditional Chinese",
};
const REPORT_LOCALES = new Set(Object.keys(LOCALE_LABELS));
const PATH_LABELS: Record<string, string> = {
"ui/src/ui/chat/chat-queue.ts": "Chat queue",
"ui/src/ui/chat/grouped-render.ts": "Chat message groups",
@@ -108,13 +109,16 @@ export function parseArgs(argv: string[]): ReportArgs {
continue;
}
if (arg === "--locale") {
args.locale = readOptionValue(argv, (index += 1), arg);
args.locale = parseLocale(readOptionValue(argv, (index += 1), arg));
continue;
}
if (arg === "--top") {
const raw = readOptionValue(argv, (index += 1), arg);
if (!/^[1-9][0-9]*$/.test(raw)) {
throw new Error(`--top must be a positive integer: ${raw}`);
}
const top = Number.parseInt(raw, 10);
if (!Number.isSafeInteger(top) || top < 1) {
if (!Number.isSafeInteger(top)) {
throw new Error(`--top must be a positive integer: ${raw}`);
}
args.top = top;
@@ -133,6 +137,13 @@ function readOptionValue(argv: string[], index: number, flag: string) {
return value;
}
function parseLocale(locale: string) {
if (!REPORT_LOCALES.has(locale)) {
throw new Error(`unknown locale: ${locale}`);
}
return locale;
}
export function filterRawCopyEntries(entries: RawCopyBaselineEntry[], surface?: string) {
if (!surface) {
return entries;
@@ -167,9 +178,13 @@ function compareCountThenName<T>(nameOf: (value: T) => string) {
}
function pathTokens(repoPath: string) {
return repoPath
.split("/")
.flatMap((part) => part.replace(/\.[^.]+$/, "").split(/[^A-Za-z0-9]+/))
return repoPath.split("/").flatMap((part) => surfaceTokens(part.replace(/\.[^.]+$/, "")));
}
function surfaceTokens(value: string) {
return value
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
.split(/[^A-Za-z0-9]+/)
.filter(Boolean)
.map(normalizeToken);
}
@@ -206,7 +221,9 @@ export function filterTranslationKeysBySurface(keys: string[], surface?: string)
return keys;
}
const normalized = normalizeToken(surface);
return keys.filter((key) => key.split(".").some((part) => normalizeToken(part) === normalized));
return keys.filter((key) =>
key.split(".").some((part) => surfaceTokens(part).some((token) => token === normalized)),
);
}
export function formatReport(input: ReportInput) {

View File

@@ -5,6 +5,7 @@ import {
filterTranslationKeysBySurface,
flattenTranslations,
formatReport,
parseArgs,
summarizeRawCopy,
type RawCopyBaselineEntry,
} from "../../scripts/control-ui-i18n-report.ts";
@@ -35,6 +36,23 @@ const entries: RawCopyBaselineEntry[] = [
];
describe("control-ui-i18n report helpers", () => {
it("rejects invalid numeric limits", () => {
expect(() => parseArgs(["--top", "3abc"])).toThrow("--top must be a positive integer");
expect(() => parseArgs(["--top", "1.5"])).toThrow("--top must be a positive integer");
expect(() => parseArgs(["--top", "0"])).toThrow("--top must be a positive integer");
expect(() => parseArgs(["--top", "999999999999999999999999999"])).toThrow(
"--top must be a positive integer",
);
});
it("rejects locale path traversal before filesystem access", () => {
expect(parseArgs(["--locale", "zh-CN"])).toMatchObject({ locale: "zh-CN" });
expect(() => parseArgs(["--locale", "../zh-CN"])).toThrow("unknown locale");
expect(() => parseArgs(["--locale", "../../../../scripts/control-ui-i18n-report"])).toThrow(
"unknown locale",
);
});
it("filters raw-copy entries by path surface token", () => {
expect(filterRawCopyEntries(entries, "agents")).toEqual([entries[1]]);
expect(filterRawCopyEntries(entries, "config")).toEqual([entries[2]]);
@@ -70,10 +88,26 @@ describe("control-ui-i18n report helpers", () => {
it("filters same-as-English keys by translation key surface token", () => {
expect(
filterTranslationKeysBySurface(
["agents.tabs.cronJobs", "chat.composer.send", "usage.common.emptyValue"],
[
"agents.tabs.cronJobs",
"chat.composer.send",
"sessionsView.thinking",
"usage.common.emptyValue",
],
"chat",
),
).toEqual(["chat.composer.send"]);
expect(
filterTranslationKeysBySurface(
[
"agents.tabs.cronJobs",
"chat.composer.send",
"sessionsView.thinking",
"usage.common.emptyValue",
],
"sessions",
),
).toEqual(["sessionsView.thinking"]);
});
it("formats pasteable report text", () => {