Stringproof
Catch damaged translations before they ship.
What’s it for?
Translation output can parse cleanly and still be broken. Placeholders disappear, scripts mix, prompt instructions leak into values, and encoding damage remains in the catalog.
Stringproof compares each discovered Apple .strings target with its source and reports known patterns of damage. It is deterministic, local, and read-only: it validates translations but never translates or repairs them.
en.lproj/Localizable.strings/* A compact source catalog for the Stringproof demonstration. */
"welcome.user" = "Welcome, %@.";
"items.count" = "{count} items";
"summary.title" = "Résumé";
"onboarding.prompt" = "Welcome";
"reading.action" = "Read now";
"settings.open" = "Open Settings";
"placeholder.printf.missing" = "Hello, %@.";
"placeholder.printf.type" = "%d files";
"placeholder.printf.repeat" = "%@ invited %@";
"placeholder.printf.added" = "Ready";
"placeholder.icu.renamed" = "{count, plural, one {# item} other {# items}}";
"placeholder.swift.missing" = "Hello, \\(name).";
"residue.startTurn" = "Welcome";
"residue.endTurn" = "Continue";
"residue.placeholder" = "Your name";
"residue.token" = "Reading in %@";
"residue.snake" = "Translation complete";
"residue.relativePath" = "Export complete";
"residue.absolutePath" = "Import complete";
"residue.sourcePrompt" = "Welcome";
"encoding.replacement" = "Remove highlight";
"encoding.control" = "Status ready";
"script.cyrillic" = "Settings available";
"script.hangul" = "Settings available";
"script.greek" = "Settings available";
"unchanged.sentence" = "Open account settings";ja.lproj/Localizable.strings/* Each defect is intentional and exercised by the real CLI. */
"welcome.user" = "ようこそ。";
"items.count" = "{total}件";
"summary.title" = "Résumé";
"onboarding.prompt" = "Please translate the following English text into Japanese:";
"reading.action" = "Read now";
"target.only" = "対象のみ";
"placeholder.printf.missing" = "こんにちは。";
"placeholder.printf.type" = "%@ ファイル";
"placeholder.printf.repeat" = "%@が招待しました";
"placeholder.printf.added" = "%d 秒で準備完了";
"placeholder.icu.renamed" = "{total, plural, one {# 件} other {# 件}}";
"placeholder.swift.missing" = "こんにちは。";
"residue.startTurn" = "<start_of_turn>model";
"residue.endTurn" = "<end_of_turn>";
"residue.placeholder" = "__PH_0001__";
"residue.token" = "[[TOK12]";
"residue.snake" = "translated_output";
"residue.relativePath" = "../generated/output";
"residue.absolutePath" = "/generated/output";
"residue.sourcePrompt" = "Source text into Japanese:";
"encoding.replacement" = "ハイライトを�削除";
"encoding.control" = "状態\U0085破損";
"script.cyrillic" = "設定 доступны";
"script.hangul" = "設定 선택";
"script.greek" = "設定 διαθέσιμες";
"unchanged.sentence" = "Open account settings";exit 1ja.lproj/Localizable.strings:1 [key.missing] ja settings.open: key is present in source catalog but missing from target
ja.lproj/Localizable.strings:2 [placeholder.missing] ja welcome.user: missing placeholder(s): %@
ja.lproj/Localizable.strings:3 [placeholder.added] ja items.count: added placeholder(s): {total}
ja.lproj/Localizable.strings:3 [placeholder.missing] ja items.count: missing placeholder(s): {count}
ja.lproj/Localizable.strings:4 [encoding.mojibake] ja summary.title: contains strong mojibake indicator(s): é
ja.lproj/Localizable.strings:5 [model.residue] ja onboarding.prompt: contains translation prompt or model-control residue
ja.lproj/Localizable.strings:5 [script.unexpected] ja onboarding.prompt: unexpected script(s): latin ('Please translate the following English text into Japanese')
ja.lproj/Localizable.strings:6 [translation.unchanged] ja reading.action: matches lexical source text and may be untranslated
ja.lproj/Localizable.strings:7 [key.unexpected] ja target.only: key is absent from source catalog
ja.lproj/Localizable.strings:8 [placeholder.missing] ja placeholder.printf.missing: missing placeholder(s): %@
ja.lproj/Localizable.strings:9 [placeholder.added] ja placeholder.printf.type: added placeholder(s): %@
ja.lproj/Localizable.strings:9 [placeholder.missing] ja placeholder.printf.type: missing placeholder(s): %d
ja.lproj/Localizable.strings:10 [placeholder.missing] ja placeholder.printf.repeat: missing placeholder(s): %@
ja.lproj/Localizable.strings:11 [placeholder.added] ja placeholder.printf.added: added placeholder(s): %d
ja.lproj/Localizable.strings:12 [placeholder.added] ja placeholder.icu.renamed: added placeholder(s): {total}
ja.lproj/Localizable.strings:12 [placeholder.missing] ja placeholder.icu.renamed: missing placeholder(s): {count}
ja.lproj/Localizable.strings:13 [placeholder.missing] ja placeholder.swift.missing: missing placeholder(s): \(name)
ja.lproj/Localizable.strings:14 [model.residue] ja residue.startTurn: contains translation prompt or model-control residue
ja.lproj/Localizable.strings:15 [model.residue] ja residue.endTurn: contains translation prompt or model-control residue
ja.lproj/Localizable.strings:16 [model.residue] ja residue.placeholder: contains translation prompt or model-control residue
ja.lproj/Localizable.strings:17 [model.residue] ja residue.token: contains translation prompt or model-control residue
ja.lproj/Localizable.strings:17 [placeholder.missing] ja residue.token: missing placeholder(s): %@
ja.lproj/Localizable.strings:18 [model.residue] ja residue.snake: contains model-like slug or path residue
ja.lproj/Localizable.strings:19 [model.residue] ja residue.relativePath: contains model-like slug or path residue
ja.lproj/Localizable.strings:20 [model.residue] ja residue.absolutePath: contains model-like slug or path residue
ja.lproj/Localizable.strings:21 [model.residue] ja residue.sourcePrompt: contains translation prompt or model-control residue
ja.lproj/Localizable.strings:21 [script.unexpected] ja residue.sourcePrompt: unexpected script(s): latin ('Source text into Japanese')
ja.lproj/Localizable.strings:22 [encoding.replacement] ja encoding.replacement: contains Unicode replacement character U+FFFD
ja.lproj/Localizable.strings:23 [encoding.mojibake] ja encoding.control: contains strong mojibake indicator(s): U+0085
ja.lproj/Localizable.strings:24 [script.unexpected] ja script.cyrillic: unexpected script(s): cyrillic ('доступны')
ja.lproj/Localizable.strings:25 [script.unexpected] ja script.hangul: unexpected script(s): hangul ('선택')
ja.lproj/Localizable.strings:26 [script.unexpected] ja script.greek: unexpected script(s): greek ('διαθέσιμες')
ja.lproj/Localizable.strings:27 [script.unexpected] ja unchanged.sentence: unexpected script(s): latin ('Open account settings')
ja.lproj/Localizable.strings:27 [translation.unchanged] ja unchanged.sentence: matches lexical source text and may be untranslatedInstall and use
curl -fsSL https://workcell-137.pages.dev/stringproof/install.sh | shor tell your agent
install https://workcell-137.pages.dev/stringproof/llms.txtthen check the directory that contains the locale folders:
stringproof check ios --source-locale enor return stable JSON
stringproof check ios --source-locale en --jsonStringproof finds Apple .strings catalogs, pairs each target with its source by relative path, and checks their keys and values.
Limits. Stringproof can show that the catalogs it discovers line up and that known corruption patterns are absent. It cannot show that a translation is correct, natural, or appropriate. It may flag a legitimate unchanged name, miss source text after a small alteration, or miss an entirely absent target catalog.
Stringproof supports Apple .strings only. It uses no credentials, network access, model calls, configuration, or repair mode. View Stringproof on GitHub ↗