Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
229 lines
8.4 KiB
TypeScript
229 lines
8.4 KiB
TypeScript
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||
import path from "node:path";
|
||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||
import { NATIVE_I18N_LOCALES } from "./native-app-i18n.ts";
|
||
|
||
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
||
const ROOT = path.resolve(HERE, "..");
|
||
const REQUIRED_LOCALES = ["en", ...NATIVE_I18N_LOCALES];
|
||
const FORMAT_RE = /%(?:\d+\$)?[@a-z]/giu;
|
||
const APPLE_LOCALE_DIRECTORIES: Record<string, string> = {
|
||
"ja-JP": "ja",
|
||
"zh-CN": "zh-Hans",
|
||
"zh-TW": "zh-Hant",
|
||
};
|
||
const LOCALIZED_WRAPPER_CONTRACTS: Record<string, string[]> = {
|
||
"apps/ios/Sources/Gateway/GatewayQuickSetupSheet.swift": [
|
||
"fullRowToggle(_ title: LocalizedStringKey",
|
||
],
|
||
"apps/ios/WatchApp/Sources/WatchInboxView.swift": [
|
||
"private struct WatchPrimaryLabel: View {\n let title: LocalizedStringKey",
|
||
"private struct WatchSecondaryLabel: View {\n let title: LocalizedStringKey",
|
||
"private struct WatchSecondaryButton: View {\n let title: LocalizedStringKey",
|
||
"private struct WatchDecisionButton: View {\n let title: LocalizedStringKey",
|
||
],
|
||
};
|
||
|
||
const CATALOGS = [
|
||
{
|
||
path: "apps/ios/Resources/Localizable.xcstrings",
|
||
coverage: {
|
||
"apps/ios/ShareExtension/ShareViewController.swift": [
|
||
"Add a message, then tap Send.",
|
||
"Cancel",
|
||
"Edit text, then tap Send.",
|
||
"Invalid saved gateway URL.",
|
||
"Message is empty.",
|
||
"OpenClaw is not connected to a gateway yet.",
|
||
"Preparing share…",
|
||
"Send failed: %@",
|
||
"Send to OpenClaw",
|
||
"Sending to OpenClaw gateway…",
|
||
"Sent to OpenClaw.",
|
||
],
|
||
"apps/ios/Sources/Design/SettingsChannelsDestination.swift": ["Logout"],
|
||
"apps/ios/Sources/Design/ChatProTab.swift": [
|
||
"Check OpenClaw status",
|
||
"Help me start a realtime voice session from this phone.",
|
||
"Help me start voice chat",
|
||
"Show me which phone controls and device capabilities are available right now.",
|
||
"Summarize the current OpenClaw status and tell me what needs attention.",
|
||
"What can I control here?",
|
||
"What would you like to work on?",
|
||
],
|
||
"apps/ios/Sources/Gateway/GatewayProblemView.swift": ["Done"],
|
||
"apps/ios/Sources/Gateway/GatewayQuickSetupSheet.swift": [
|
||
"Close",
|
||
"Connect",
|
||
"Connect to a Gateway?",
|
||
"Connecting…",
|
||
"Don’t show this again",
|
||
"No gateways found yet. Make sure your gateway is running and Bonjour discovery is enabled.",
|
||
"Not now",
|
||
"Quick Setup",
|
||
],
|
||
"apps/ios/Sources/Gateway/GatewayTrustPromptAlert.swift": [
|
||
"Cancel",
|
||
"First-time TLS connection.\n\nVerify this SHA-256 fingerprint out-of-band before trusting:\n%@",
|
||
"Trust and connect",
|
||
"Trust this gateway?",
|
||
],
|
||
"apps/ios/Sources/Onboarding/OnboardingWizardView.swift": ["Save"],
|
||
"apps/ios/Sources/RootTabs.swift": ["Agent", "Chat", "Control", "Settings", "Talk"],
|
||
"apps/ios/WatchApp/Sources/WatchInboxView.swift": [
|
||
"Approve",
|
||
"Chat",
|
||
"Continue on iPhone",
|
||
"Deny",
|
||
"Message OpenClaw",
|
||
"No chat synced",
|
||
"Open all approvals",
|
||
"Refresh",
|
||
"Review again",
|
||
"Talk to Claw",
|
||
"Tap the message pill below to start from your watch.",
|
||
"You",
|
||
],
|
||
"apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift": ["Writing"],
|
||
},
|
||
},
|
||
{
|
||
path: "apps/macos/Sources/OpenClaw/Resources/Localizable.xcstrings",
|
||
coverage: {
|
||
"apps/macos/Sources/OpenClaw/ChannelsSettings+ChannelSections.swift": [
|
||
"Logout",
|
||
"Refresh",
|
||
"Save",
|
||
],
|
||
"apps/macos/Sources/OpenClaw/CronSettings+Rows.swift": ["Run now"],
|
||
},
|
||
},
|
||
] as const;
|
||
|
||
type Catalog = {
|
||
sourceLanguage?: string;
|
||
strings?: Record<
|
||
string,
|
||
{
|
||
localizations?: Record<string, { stringUnit?: { state?: string; value?: string } }>;
|
||
}
|
||
>;
|
||
};
|
||
|
||
function formatTokens(value: string): string[] {
|
||
return [...value.matchAll(FORMAT_RE)].map((match) => match[0]).toSorted();
|
||
}
|
||
|
||
function stringsLiteral(value: string): string {
|
||
return JSON.stringify(value);
|
||
}
|
||
|
||
export async function checkAppleAppI18n() {
|
||
let checked = 0;
|
||
for (const [sourcePath, contracts] of Object.entries(LOCALIZED_WRAPPER_CONTRACTS)) {
|
||
const source = await readFile(path.join(ROOT, sourcePath), "utf8");
|
||
const missing = contracts.filter((contract) => !source.includes(contract));
|
||
if (missing.length) {
|
||
throw new Error(
|
||
`Apple i18n wrapper ${sourcePath} bypasses localized string lookup: ${missing.join(", ")}`,
|
||
);
|
||
}
|
||
}
|
||
for (const spec of CATALOGS) {
|
||
const catalogPath = path.join(ROOT, spec.path);
|
||
const catalog = JSON.parse(await readFile(catalogPath, "utf8")) as Catalog;
|
||
if (catalog.sourceLanguage !== "en" || !catalog.strings) {
|
||
throw new Error(`invalid Apple string catalog: ${spec.path}`);
|
||
}
|
||
|
||
const expectedKeys = new Set(Object.values(spec.coverage).flat());
|
||
const actualKeys = new Set(Object.keys(catalog.strings));
|
||
const missingKeys = [...expectedKeys].filter((key) => !actualKeys.has(key));
|
||
const extraKeys = [...actualKeys].filter((key) => !expectedKeys.has(key));
|
||
if (missingKeys.length || extraKeys.length) {
|
||
throw new Error(
|
||
[
|
||
`Apple catalog ${spec.path} does not match its phased source coverage.`,
|
||
`missing=${missingKeys.join(",") || "none"}`,
|
||
`extra=${extraKeys.join(",") || "none"}`,
|
||
].join("\n"),
|
||
);
|
||
}
|
||
|
||
for (const [sourcePath, keys] of Object.entries(spec.coverage)) {
|
||
const source = await readFile(path.join(ROOT, sourcePath), "utf8");
|
||
const absent = keys.filter((key) => {
|
||
const escapedKey = JSON.stringify(key).slice(1, -1);
|
||
return !source.includes(key) && !source.includes(escapedKey);
|
||
});
|
||
if (absent.length) {
|
||
throw new Error(
|
||
`Apple i18n coverage ${sourcePath} no longer contains: ${absent.join(", ")}`,
|
||
);
|
||
}
|
||
}
|
||
|
||
for (const [key, entry] of Object.entries(catalog.strings)) {
|
||
const sourceTokens = formatTokens(key);
|
||
for (const locale of REQUIRED_LOCALES) {
|
||
const unit = entry.localizations?.[locale]?.stringUnit;
|
||
const value = unit?.value?.trim();
|
||
if (!value || unit?.state !== "translated") {
|
||
throw new Error(
|
||
`Apple catalog ${spec.path} is missing ${locale} for ${JSON.stringify(key)}`,
|
||
);
|
||
}
|
||
if (formatTokens(value).join("\u0000") !== sourceTokens.join("\u0000")) {
|
||
throw new Error(
|
||
`Apple catalog ${spec.path} has placeholder drift in ${locale} for ${JSON.stringify(key)}`,
|
||
);
|
||
}
|
||
}
|
||
checked += 1;
|
||
}
|
||
}
|
||
process.stdout.write(
|
||
`apple-app-i18n: catalogs=${CATALOGS.length} keys=${checked} locales=${NATIVE_I18N_LOCALES.join(",")}\n`,
|
||
);
|
||
}
|
||
|
||
export async function compileMacosLocalizations(outputDir: string) {
|
||
await checkAppleAppI18n();
|
||
const spec = CATALOGS[1];
|
||
const catalog = JSON.parse(await readFile(path.join(ROOT, spec.path), "utf8")) as Catalog;
|
||
if (!catalog.strings) {
|
||
throw new Error(`invalid Apple string catalog: ${spec.path}`);
|
||
}
|
||
|
||
for (const locale of REQUIRED_LOCALES) {
|
||
const localeDir = APPLE_LOCALE_DIRECTORIES[locale] ?? locale;
|
||
const lprojDir = path.join(outputDir, `${localeDir}.lproj`);
|
||
const lines = Object.entries(catalog.strings)
|
||
.toSorted(([left], [right]) => left.localeCompare(right))
|
||
.map(([key, entry]) => {
|
||
const value = entry.localizations?.[locale]?.stringUnit?.value;
|
||
if (!value) {
|
||
throw new Error(
|
||
`Apple catalog ${spec.path} is missing ${locale} for ${JSON.stringify(key)}`,
|
||
);
|
||
}
|
||
return `${stringsLiteral(key)} = ${stringsLiteral(value)};`;
|
||
});
|
||
await mkdir(lprojDir, { recursive: true });
|
||
await writeFile(path.join(lprojDir, "Localizable.strings"), `${lines.join("\n")}\n`, "utf8");
|
||
}
|
||
}
|
||
|
||
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
||
const [command, flag, value] = process.argv.slice(2);
|
||
if (command === "check") {
|
||
await checkAppleAppI18n();
|
||
} else if (command === "compile-macos" && flag === "--output" && value) {
|
||
await compileMacosLocalizations(path.resolve(value));
|
||
} else {
|
||
throw new Error(
|
||
"usage: node --import tsx scripts/apple-app-i18n.ts check|compile-macos --output <dir>",
|
||
);
|
||
}
|
||
}
|