Files
openclaw-openclaw/test/scripts/native-app-i18n.test.ts
Mason Huang f3eccb0dfd fix: harden native i18n identifier filtering (#99098)
Summary:
- The PR replaces native i18n conditional-branch regex filtering with an exported linear ASCII scanner and adds focused coverage.
- PR surface: Tests +9, Other +36. Total +45 across 2 files.
- Reproducibility: yes. from source inspection: current main routes conditional-branch native literals through the backtracking-prone regex before inventory output. I did not run a timing benchmark against current main.

Automerge notes:
- No ClawSweeper repair was needed after automerge opt-in.

Validation:
- ClawSweeper review passed for head 97464c9635.
- Required merge gates passed before the squash merge.

Prepared head SHA: 97464c9635
Review: https://github.com/openclaw/openclaw/pull/99098#issuecomment-4866733462

Co-authored-by: Mason Huang <masonxhuang@tencent.com>
Approved-by: hxy91819
2026-07-03 15:28:17 +00:00

323 lines
12 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { readFile, stat } from "node:fs/promises";
import path from "node:path";
import { describe, expect, it } from "vitest";
import {
collectNativeI18nEntries,
isConditionalBranchIdentifier,
NATIVE_I18N_LOCALES,
parseNativeI18nCommand,
syncNativeLocale,
type NativeI18nEntry,
} from "../../scripts/native-app-i18n.ts";
import { cleanupTempDirs, makeTempDir } from "../helpers/temp-dir.js";
describe("native app i18n inventory", () => {
it("detects conditional branch identifiers without regex backtracking", () => {
expect(isConditionalBranchIdentifier("isEnabled")).toBe(true);
expect(isConditionalBranchIdentifier("hasFA2Enabled")).toBe(true);
expect(isConditionalBranchIdentifier("abc123A")).toBe(false);
expect(isConditionalBranchIdentifier("already_lowercase")).toBe(false);
expect(isConditionalBranchIdentifier(`a${"A".repeat(4_096)}!`)).toBe(false);
});
it("collects stable Android and Apple UI entries", async () => {
const entries = await collectNativeI18nEntries();
const surfaces = new Set(entries.map((entry) => entry.surface));
expect(entries.length).toBeGreaterThan(100);
expect(surfaces).toEqual(new Set(["android", "apple"]));
expect(entries.every((entry) => entry.id.startsWith(`native.${entry.surface}.`))).toBe(true);
expect(new Set(entries.map((entry) => entry.id)).size).toBe(entries.length);
expect(
entries.every(
(entry) => !/(?:\/|\\)(?:Tests?|UITests?|test|Preview(?:s)?)(?:\/|\\)/u.test(entry.path),
),
).toBe(true);
expect(
entries.every(
(entry) => !/(?:Tests?|UITests?|Previews?|Testing)\.(?:swift|kt|kts)$/u.test(entry.path),
),
).toBe(true);
expect(
entries
.filter((entry) => entry.surface === "apple")
.every((entry) =>
/^(?:apps\/ios|apps\/macos\/Sources|apps\/shared\/OpenClawKit\/Sources)\//u.test(
entry.path,
),
),
).toBe(true);
expect(entries.some((entry) => entry.source === "QR Scanner Unavailable")).toBe(true);
expect(entries.some((entry) => entry.source === "Request ID: \\(value)")).toBe(true);
expect(entries.some((entry) => entry.source === "Open ${row.title}")).toBe(true);
expect(entries.some((entry) => entry.source === "$deviceModel · $appVersion")).toBe(true);
expect(entries.some((entry) => entry.source === "Approval command copied")).toBe(true);
expect(entries.some((entry) => entry.source === "Save Profile")).toBe(true);
expect(entries.some((entry) => entry.source === "Pairing required")).toBe(true);
expect(entries.some((entry) => entry.source === "Mute")).toBe(true);
expect(entries.some((entry) => entry.source === "Creating...")).toBe(true);
expect(entries.some((entry) => entry.source === "Permission required")).toBe(true);
expect(entries.some((entry) => entry.source === "Needs setup")).toBe(true);
expect(
entries.some(
(entry) =>
entry.source === "Choose a supported ${issue.target.title} provider on the Gateway",
),
).toBe(true);
expect(
entries.some(
(entry) => entry.source === "Talk failed: Realtime provider closed unexpectedly.",
),
).toBe(true);
expect(entries.some((entry) => entry.source === "Scan QR code")).toBe(true);
expect(entries.some((entry) => entry.source === "Test connection")).toBe(true);
expect(entries.some((entry) => entry.source === "Searching…")).toBe(true);
expect(entries.some((entry) => entry.source === "Run now")).toBe(true);
expect(entries.some((entry) => entry.source === "Loading chat")).toBe(true);
expect(entries.some((entry) => entry.source === "DIARY")).toBe(true);
expect(entries.some((entry) => entry.source === "ask OpenClaw $prompt")).toBe(true);
expect(entries.some((entry) => entry.source === "OpenClaw is paused")).toBe(true);
expect(
entries.some((entry) => entry.source === "Choose system, light, or dark appearance"),
).toBe(true);
expect(
entries.some(
(entry) =>
entry.path === "apps/ios/Sources/Design/TalkRuntimeIssueBanner.swift" &&
entry.kind === "ui-named-argument" &&
entry.source === "Details",
),
).toBe(true);
expect(
entries.some(
(entry) =>
entry.path === "apps/ios/Sources/Design/TalkRuntimeIssueBanner.swift" &&
entry.kind === "ui-named-argument" &&
entry.source === "Open Settings",
),
).toBe(true);
expect(entries.some((entry) => entry.source === "No sessions yet")).toBe(true);
expect(entries.some((entry) => entry.source === "Dont show this again")).toBe(true);
expect(entries.some((entry) => entry.source === "Use Manual Gateway")).toBe(true);
expect(entries.some((entry) => entry.source === "Session target")).toBe(true);
expect(
entries.some(
(entry) =>
entry.source === 'OpenClaw needs ${labels.joinToString(", ")} permissions to continue.',
),
).toBe(true);
expect(
entries.some((entry) => entry.source === "Some channel status checks did not complete."),
).toBe(true);
expect(
entries.some(
(entry) =>
entry.source === '\\(day.entryCount) \\(day.entryCount == 1 ? "entry" : "entries")',
),
).toBe(false);
expect(
entries.some(
(entry) =>
entry.source === 'Missing binaries: \\(self.missingBins.joined(separator: ", "))',
),
).toBe(true);
expect(
entries.some(
(entry) =>
entry.source ===
"Approve this device on the gateway.\n1) `\\(commandLine)`\n2) `/pair approve` in your OpenClaw chat\n\\(requestLine)\nOpenClaw will also retry automatically when you return to this app.",
),
).toBe(true);
expect(entries.some((entry) => entry.source === "Approve this device on the gateway.\n")).toBe(
false,
);
expect(
entries.some((entry) =>
entry.source.startsWith(
"Exec approvals can only be reviewed while OpenClaw is open and connected.",
),
),
).toBe(true);
expect(entries.some((entry) => entry.source === "$(PRODUCT_BUNDLE_IDENTIFIER)")).toBe(false);
expect(entries.some((entry) => entry.source === "ai.openclaw.screenRecord.writer")).toBe(false);
expect(
entries.some(
(entry) =>
entry.surface === "android" && entry.source === "INVALID_REQUEST: expected JSON object",
),
).toBe(false);
expect(
entries.some(
(entry) =>
entry.surface === "android" && ["off", "talk-orb", "pulse"].includes(entry.source),
),
).toBe(false);
expect(entries.some((entry) => entry.source === "false")).toBe(false);
expect(entries.some((entry) => entry.source === "ws")).toBe(false);
expect(entries.some((entry) => entry.source === '{"includeSecrets":true}')).toBe(false);
expect(entries.some((entry) => entry.source === "builtIn")).toBe(false);
expect(entries.some((entry) => entry.source === "State: \\(stateDir)")).toBe(true);
expect(entries.some((entry) => entry.path.endsWith("Info.plist"))).toBe(true);
expect(NATIVE_I18N_LOCALES).toHaveLength(21);
expect(NATIVE_I18N_LOCALES).toContain("sv");
});
it("creates a first-run locale artifact and leaves a complete artifact unchanged", async () => {
const tempDirs: string[] = [];
const translationsDir = makeTempDir(tempDirs, "openclaw-native-i18n-");
const entries: NativeI18nEntry[] = [
{
id: "native.android.hello",
kind: "ui-call",
line: 1,
path: "apps/android/example.kt",
source: "Hello",
surface: "android",
},
{
id: "native.apple.request",
kind: "ui-call",
line: 2,
path: "apps/ios/example.swift",
source: "Request ID: \\(requestId)",
surface: "apple",
},
{
id: "native.android.count",
kind: "ui-call",
line: 3,
path: "apps/android/example.kt",
source: "Showing ${visibleApps.size} of ${apps.size}",
surface: "android",
},
{
id: "native.apple.permissions",
kind: "ui-call",
line: 4,
path: "apps/ios/example.swift",
source: "\\(granted) of \\(total) permissions granted",
surface: "apple",
},
];
try {
const first = await syncNativeLocale("sv", entries, {
glossary: [],
translationsDir,
translate: async (pending) =>
new Map(
pending.map((entry) => {
const translated = {
"native.android.hello": "Hej",
"native.apple.request": "Begärans-ID: \\(requestId)",
"native.android.count": "${apps.size} totalt, ${visibleApps.size} visas",
"native.apple.permissions": "Av \\(total) behörigheter har \\(granted) beviljats",
}[entry.id];
return [entry.id, translated ?? entry.source];
}),
),
});
expect(first).toEqual({ changed: true, translated: 4 });
const artifactPath = path.join(translationsDir, "sv.json");
const firstContents = await readFile(artifactPath, "utf8");
const firstModifiedAt = (await stat(artifactPath)).mtimeMs;
const second = await syncNativeLocale("sv", entries, {
glossary: [],
translationsDir,
translate: async () => {
throw new Error("no-op refresh must not call the provider");
},
});
expect(second).toEqual({ changed: false, translated: 0 });
expect(await readFile(artifactPath, "utf8")).toBe(firstContents);
expect((await stat(artifactPath)).mtimeMs).toBe(firstModifiedAt);
const refreshed = await syncNativeLocale("sv", entries, {
glossary: [{ source: "Request", target: "Begäran" }],
translationsDir,
translate: async (pending) =>
new Map(pending.map((entry) => [entry.id, `refreshed:${entry.source}`])),
});
expect(refreshed).toEqual({ changed: true, translated: 4 });
const refreshedArtifact = JSON.parse(await readFile(artifactPath, "utf8")) as {
entries: Array<{ translated: string }>;
glossaryHash: string;
};
expect(refreshedArtifact.glossaryHash).toMatch(/^[a-f0-9]{64}$/u);
expect(
refreshedArtifact.entries.every((entry) => entry.translated.startsWith("refreshed:")),
).toBe(true);
} finally {
cleanupTempDirs(tempDirs);
}
});
it("rejects native printf placeholder drift", async () => {
const tempDirs: string[] = [];
const translationsDir = makeTempDir(tempDirs, "openclaw-native-i18n-");
const cases = [
{
entry: {
id: "native.android.certificate",
kind: "ui-call",
line: 1,
path: "apps/android/example.kt",
source: "Old fingerprint: %1$s\nNew fingerprint: %2$s",
surface: "android",
},
translated: "Gammalt fingeravtryck: %1$s",
},
{
entry: {
id: "native.apple.failure",
kind: "ui-call",
line: 1,
path: "apps/ios/example.swift",
source: "Send failed: %@",
surface: "apple",
},
translated: "Sändningen misslyckades",
},
] satisfies Array<{ entry: NativeI18nEntry; translated: string }>;
try {
for (const { entry, translated } of cases) {
await expect(
syncNativeLocale("sv", [entry], {
glossary: [],
translationsDir,
translate: async () => new Map([[entry.id, translated]]),
}),
).rejects.toThrow(
`native translation changed placeholders or line breaks for sv:${entry.id}`,
);
}
} finally {
cleanupTempDirs(tempDirs);
}
});
it("validates locale refresh arguments before write paths run", () => {
expect(parseNativeI18nCommand(["sync", "--write", "--locale", "sv"])).toEqual({
command: "sync",
locale: "sv",
write: true,
});
expect(() => parseNativeI18nCommand(["sync", "--write", "--locale"])).toThrow(
"requires a locale value",
);
expect(() => parseNativeI18nCommand(["sync", "--write", "--locale", "--write"])).toThrow(
"requires a locale value",
);
expect(() => parseNativeI18nCommand(["sync", "--write", "--locale", "xx"])).toThrow(
"unsupported native locale",
);
expect(() => parseNativeI18nCommand(["check", "--locale", "sv"])).toThrow(
"requires `sync --write",
);
});
});