shipglot

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 idNameExtensionsPlaceholders and pluralsLayout
apple-xcstringsApple String Catalog.xcstringsFoundation format specifiers: %@, %lld, %1$@, %%Every locale in one file

Detected layout

For a iOS and macOS (Xcode) project laid out like this:

Shop/Localizable.xcstrings

npx 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: error

To 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.title
    placeholder 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 files
    plural is missing category one required by de
    Shop/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

  • warningunreviewedfr
    settings.save
    marked as needs review in the file; review it and mark it translated
    Shop/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

  • warninguntranslatedde
    sync.failed
    identical to the en source
    Shop/Localizable.xcstrings:7

Run it on your machine

npx shipglot check
npx shipglot check --reporter json=shipglot.json

Exit 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.