Check Xcode String Catalogs in CI
An Xcode String Catalog keeps every language in one JSON file. Xcode shows its own warnings only in the editor, and a translation that drops %@ builds fine and prints a sentence without the name. The gate reads every localization, plural variation and substitution in the catalog.
What the gate reads
| Format id | Name | Extensions | Placeholders and plurals | Layout |
|---|---|---|---|---|
apple-xcstrings | Apple String Catalog | .xcstrings | Foundation format specifiers: %@, %lld, %1$@, %% | Every locale in one file |
Detected layout
For a iOS and macOS (Xcode) project laid out like this:
Shop/Localizable.xcstringsnpx shipglot check finds the files without a config. npx shipglot init writes what it detected to shipglot.config.json:
{
"sourceLocale": "en",
"files": [
{
"format": "apple-xcstrings",
"path": "**/*.xcstrings"
}
]
}Add "locales" to restrict target languages, "glossary" for approved terms, and "checks" to change a check's severity. The gate's README lists every option.
GitHub Actions workflow
Save as .github/workflows/translations.yml. The step fails on errors and annotates the pull request; warnings show in the job summary.
name: Translations
on:
pull_request:
paths:
- "**/*.xcstrings"
- "shipglot.config.json"
jobs:
shipglot:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v5
- uses: aurelien64/shipglot@v0
with:
fail-on: errorTo see findings in GitHub code scanning as well, add sarif-file: shipglot.sarif under with: and upload it with github/codeql-action/upload-sarif@v3 (the job then needs security-events: write).
Defects the gate catches
Each example below was run through the gate when this page was built, with the config above. The findings under it are the gate's output.
1. A format specifier dropped
String(localized:) fills %@ with the user's name. The French string has nowhere to put it.
Shop/Localizable.xcstrings
{
"sourceLanguage" : "en",
"strings" : {
"welcome.title" : {
"localizations" : {
"en" : { "stringUnit" : { "state" : "translated", "value" : "Welcome back, %@!" } },
"fr" : { "stringUnit" : { "state" : "translated", "value" : "Bon retour parmi nous !" } }
}
}
},
"version" : "1.0"
}Gate output
welcome.titleplaceholder mismatch: missing%@Shop/Localizable.xcstrings:7
2. A plural variation German needs
German uses one and other. With only other, one file shows as "1 Dateien".
Shop/Localizable.xcstrings
{
"sourceLanguage" : "en",
"strings" : {
"%lld files" : {
"localizations" : {
"en" : { "variations" : { "plural" : { "one" : { "stringUnit" : { "state" : "translated", "value" : "%lld file" } }, "other" : { "stringUnit" : { "state" : "translated", "value" : "%lld files" } } } } },
"de" : { "variations" : { "plural" : { "other" : { "stringUnit" : { "state" : "translated", "value" : "%lld Dateien" } } } } }
}
}
},
"version" : "1.0"
}Gate output
%lld filesplural is missing category one required by deShop/Localizable.xcstrings:7
3. A string still marked needs_review
Xcode ships it, but someone flagged it as not final. The gate lists these so they get reviewed before release.
Shop/Localizable.xcstrings
{
"sourceLanguage" : "en",
"strings" : {
"settings.save" : {
"localizations" : {
"en" : { "stringUnit" : { "state" : "translated", "value" : "Save changes" } },
"fr" : { "stringUnit" : { "state" : "needs_review", "value" : "Enregistrer les modifications" } }
}
}
},
"version" : "1.0"
}Gate output
settings.savemarked as needs review in the file; review it and mark it translatedShop/Localizable.xcstrings:7
4. A translation identical to English
A sentence copied into another language is usually a string nobody translated.
Shop/Localizable.xcstrings
{
"sourceLanguage" : "en",
"strings" : {
"sync.failed" : {
"localizations" : {
"en" : { "stringUnit" : { "state" : "translated", "value" : "Sync failed. Check your connection." } },
"de" : { "stringUnit" : { "state" : "translated", "value" : "Sync failed. Check your connection." } }
}
}
},
"version" : "1.0"
}Gate output
sync.failedidentical to the en sourceShop/Localizable.xcstrings:7
Run it on your machine
npx shipglot check
npx shipglot check --reporter json=shipglot.jsonExit code 0 means clean, 1 means findings at or above --fail-on, 2 means a usage or config error. Checks used on this page: placeholder-mismatch, plural-forms, unreviewed, untranslated.
To check a single file without installing anything, use the in-browser checker.
Translation in the same workflow
With a Shipglot account, npx shipglot translate translates new and changed keys and writes them into these files before the check runs. Hosted translation is not open yet; see the signup page.