localeproof
v0.1.0
Published
Check that the locales your iOS app ships match the locales your App Store listing promises: String Catalogs and knownRegions versus fastlane metadata and screenshots folders. Zero dependencies, fully offline.
Maintainers
Readme
localeproof
Check that the locales your iOS app ships match the locales your App Store listing promises.
localeproof compares what Xcode actually localizes — String Catalogs (*.xcstrings), project.pbxproj knownRegions, *.lproj folders, and InfoPlist.xcstrings — against the locale folders in a fastlane deliver metadata/ and screenshots/ tree. It maps Xcode identifiers onto App Store Connect codes (de ↔ de-DE), runs fully offline, and returns a non-zero exit code so it can gate CI.
Apple's WWDC26 Localization Q&A states that a missing String Catalog localization may not fall back to the source language. A French catalog that omitted one key can show 1 instead of the English 1 video. Per-surface locale parity is how you catch that before review, not after a one-star screenshot.
It is the fourth check in a submission-preflight set: metaproof lints metadata text, screenproof lints screenshot files, claimproof reads what those screenshots actually say, and localeproof checks that the locales on each of those surfaces agree.
- App locales from
*.xcstrings,knownRegions, and*.lproj(neverBase). - Optional fastlane
metadata/<locale>/andscreenshots/<locale>/trees; absent surfaces are skipped and listed underskippedin JSON. - Xcode → App Store mapping against Apple's 50-code App Store Connect locale table.
- InfoPlist.xcstrings coverage so purpose strings are not left in English.
- Zero runtime dependencies. Fully offline. No network, no credentials, no telemetry.
Requirements
Node.js 24 or newer, and zero runtime dependencies. The published package ships compiled JavaScript, so npx localeproof and npm install just work with no build step on your side. (The GitHub Action and local development run the TypeScript sources directly on Node's native type stripping.)
Install
npm install -g localeproofOr run it without installing:
npx localeproofOr add it to a project:
npm install --save-dev localeproofUsage
localeproof [options]| Option | Description |
| --- | --- |
| --app <dir> | App source root (default .). Scans *.xcstrings, *.pbxproj knownRegions, *.lproj, InfoPlist.xcstrings; skips .git, .build, DerivedData, Pods, Carthage, node_modules, .worktrees. |
| --metadata <dir> | fastlane deliver metadata/ folder (default ./fastlane/metadata if it exists). |
| --screenshots <dir> | fastlane deliver screenshots/ folder (default ./fastlane/screenshots if it exists). |
| --config <file> | localeproof.json (rule severities, locales.extra, locales.ignore). |
| --strict | Exit non-zero on warnings as well as errors. |
| --json | Print the report as JSON. |
| --quiet | Hide clean locales and info findings. |
| --no-color | Disable ANSI color (also respects NO_COLOR). |
| -h, --help | Show help. |
| -v, --version | Show the version. |
Exit codes: 0 clean, 1 lint errors (or warnings under --strict), 2 usage or config error.
A missing --app scan (no catalogs, no pbxproj, no lproj) exits 2 with No app localizations found under <dir>. Metadata and screenshots directories are optional; rules that need an absent surface are skipped.
Example
$ localeproof --app . --metadata fastlane/metadata --screenshots fastlane/screenshots
localeproof /path/to/MyApp
✖ es
error infoplist-locale-missing InfoPlist.xcstrings is missing locale "es" that the app ships.
Add "es" to InfoPlist.xcstrings so purpose strings are not left in the source language.
error app-locale-without-metadata App ships "es" (→ es-ES) but metadata/es-ES/ is missing.
Create metadata/es-ES/.
✖ zh
warning app-locale-unmappable App locale "zh" does not map to an App Store Connect code.
Use a scripted Chinese code (zh-Hans / zh-Hant) or drop the unmappable locale.
✖ en_us
warning unknown-locale-code metadata/en_us/ is not a known App Store locale.
Rename metadata/en_us/ to a code from Apple's locale table, or add it to locales.extra / locales.ignore.
✖ (project)
warning source-language-mismatch String Catalogs disagree on sourceLanguage (en, fr).
Set every catalog's sourceLanguage to the same value.
Summary: 2 errors, 3 warnings — FAILWhat it checks
| Rule | Default | Fires when |
| --- | --- | --- |
| app-locale-without-metadata | error | An app locale maps to App Store code X and metadata/X/ is absent (metadata dir given). |
| app-locale-without-screenshots | error | Same for screenshots/X/ (screenshots dir given). |
| metadata-locale-without-app | warning | metadata/X/ exists but no app locale maps to X. |
| screenshots-locale-without-app | warning | Same for screenshots. |
| metadata-screenshots-mismatch | warning | X is in one of metadata/screenshots but not the other (both dirs given). |
| unknown-locale-code | warning | A metadata/screenshots folder is not in APP_STORE_LOCALES ∪ locales.extra and not in locales.ignore. Warning (not error) so one typo is not reported three times as an error; metaproof and screenproof already error on the same folder. |
| app-locale-unmappable | warning | An app locale with xcodeToAppStore = null (for example eu or zh with no script). |
| infoplist-locale-missing | error | InfoPlist.xcstrings exists and an app (Localizable) locale is absent from it — the purpose-string-in-English bug. |
| infoplist-catalog-missing | info | The app has at least two locales and no InfoPlist.xcstrings. |
| catalog-locale-partial | warning | A locale is present in some .xcstrings catalogs but not all. |
| source-language-mismatch | warning | Catalogs disagree on sourceLanguage. |
| catalog-unreadable | error | A *.xcstrings file is unreadable or not valid JSON. Names the file; does not crash. |
Each finding is { rule, severity, locale, surface, message, hint }. Hints name the exact folder to create or the catalog to add the locale to.
Xcode → App Store mapping
xcodeToAppStore(id):
- Exact match if
idis already one of Apple's 50 App Store Connect codes. - Language-only remap (below) when Xcode ships a language without a region.
- Regional variants that are not themselves App Store codes (
de-AT,fr-CH,es-419),zhwithout a script, and anything else map tonull(unmappable).Baseis never a locale.
| Xcode | App Store |
| --- | --- |
| en | en-US |
| de | de-DE |
| es | es-ES |
| fr | fr-FR |
| nl | nl-NL |
| ar | ar-SA |
| pt | pt-BR |
| nb | no |
| bn | bn-BD |
| gu | gu-IN |
| kn | kn-IN |
| ml | ml-IN |
| mr | mr-IN |
| or | or-IN |
| pa | pa-IN |
| sl | sl-SI |
| ta | ta-IN |
| te | te-IN |
| ur | ur-PK |
Identity (the Xcode id is already the App Store code): ca, cs, da, el, fi, he, hi, hr, hu, id, it, ja, ko, ms, no, pl, ro, ru, sk, sv, th, tr, uk, vi, plus every hyphenated code in Apple's table (en-GB, zh-Hans, pt-PT, …).
The 50-code table is Apple's App Store Connect API locale-shortcode list as of 2026-09-02, matching the App Store localizations reference and fastlane FastlaneCore::Languages::ALL_LANGUAGES.
Non-locale folders skipped on the deliver surfaces: review_information, trade_representative_contact_information, default.
Configuration
Pass --config <file>, or drop a localeproof.json in your working directory and localeproof loads it automatically. Every key is optional and merges over the defaults.
{
"rules": {
"infoplist-catalog-missing": "warning",
"metadata-locale-without-app": "off"
},
"locales": {
"extra": ["en-IN"],
"ignore": ["it"]
}
}rules: set a rule toerror,warning,info, oroff. An unknown id is a config error rather than a setting that is accepted and never applied; an obvious near miss is named in the message.locales.extra: additional valid locale codes.locales.ignore: locale folders to skip entirely.
See examples/localeproof.json.
GitHub Action
- uses: vsolano9/[email protected]
with:
app: .
metadata: fastlane/metadata
screenshots: fastlane/screenshots
strict: "false"The exact tag keeps CI reproducible. The moving @v0 tag points to the
same v0.1.0 release for users who prefer automatic compatible updates. The
action sets up Node 24 and runs localeproof, so a failed check blocks the workflow.
See action.yml.
Programmatic API
import { lint, renderHuman, exitCode } from "localeproof";
const report = await lint({
app: ".",
metadata: "fastlane/metadata",
screenshots: "fastlane/screenshots",
});
console.log(renderHuman(report));
process.exit(exitCode(report, false));lint(paths, config?) returns a LintReport with per-locale findings, skipped rule ids, and severity counts. defaultConfig(), mergeConfig(), and loadConfig() build the config; renderHuman(), renderJson(), and exitCode() format and gate it. xcodeToAppStore() is the mapping used by the linter.
Limitations
- localeproof does not read
.stringsfile content, keyword copy, screenshot pixels, or screenshot claims. Use metaproof for metadata text, screenproof for screenshot files, and claimproof for what those screenshots say. - A
zhXcode locale with no script is reported as unmappable rather than guessed aszh-Hans. - Screenshot locale folders count only if they contain at least one
.png/.jpg/.jpeg.
Validation
npm run lint # typecheck + tests
npm test # node --test
npm run build # compile the published packageChangelog
Release history is in CHANGELOG.md.
Roadmap
- [ ]
--fixto scaffold missingmetadata/<locale>/folders. - [ ] App Store Connect API cross-check of live listing locales.
Contributing
Issues and pull requests are welcome. Keep the tool dependency-free, add a failing test before a change, and run npm run lint (typecheck plus tests) before opening a PR.
Maintainer
localeproof and its siblings metaproof, screenproof, and claimproof are built and maintained by Victor Solano, who runs them against twelve App Store apps before every submission.
License
MIT. See LICENSE. This is an independent tool. It is not affiliated with or endorsed by Apple or fastlane; "App Store" and "iOS" are trademarks of Apple Inc., used here only to describe compatibility.
