Check gettext .po files in CI
msgfmt leaves fuzzy entries out of the compiled .mo file, so users see the English msgid. A dropped %(name)s raises KeyError or prints the raw specifier. The gate reads each .po file, compares msgstr with msgid and follows the c-format and python-format flags.
What the gate reads
| Format id | Name | Extensions | Placeholders and plurals | Layout |
|---|---|---|---|---|
gettext-po | GNU gettext PO | .po, .pot | printf and Python: %s, %d, %1$s, %(name)s, {name} | Every locale in one file |
Detected layout
For a Django, Python, PHP and C projects using gettext project laid out like this:
locale/de/LC_MESSAGES/django.po
locale/fr/LC_MESSAGES/django.ponpx shipglot check finds the files without a config. npx shipglot init writes what it detected to shipglot.config.json:
{
"sourceLocale": "en",
"files": [
{
"format": "gettext-po",
"path": "**/{locale}/LC_MESSAGES/{namespace}.po"
}
]
}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:
- "**/*.po"
- "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 fuzzy entry
msgfmt skips entries marked fuzzy, so this string ships in English.
locale/fr/LC_MESSAGES/django.po
msgid ""
msgstr ""
"Language: fr\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#, fuzzy, python-brace-format
#| msgid "Hello {name}"
msgid "Hello {name}, welcome back!"
msgstr "Bonjour {name} !"Gate output
Hello {name}, welcome back!marked fuzzy; gettext's msgfmt drops fuzzy entries, so users see the source textlocale/fr/LC_MESSAGES/django.po:10
2. A %(name)s argument dropped
The folder name disappears from the French message.
locale/fr/LC_MESSAGES/django.po
msgid ""
msgstr ""
"Language: fr\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#, python-format
msgid "%(count)d files uploaded to %(folder)s"
msgstr "%(count)d fichiers envoyés"Gate output
%(count)d files uploaded to %(folder)splaceholder mismatch: missing%(folder)slocale/fr/LC_MESSAGES/django.po:9
3. An empty msgstr
gettext returns the msgid, so the string shows in English.
locale/de/LC_MESSAGES/django.po
msgid ""
msgstr ""
"Language: de\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
msgid "Sign in"
msgstr "Anmelden"
msgid "Delete account"
msgstr ""Gate output
Delete accountmissing in delocale/de/LC_MESSAGES/django.po:10
4. A file that does not parse
An unterminated string stops msgfmt and every later translation in the file.
locale/de/LC_MESSAGES/django.po
msgid ""
msgstr ""
"Language: de\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
msgid "Sign in"
msgstr "Anmelden"locale/fr/LC_MESSAGES/django.po
msgid ""
msgstr ""
"Language: fr\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
msgid "Sign in"
msgstr "Se connecterGate output
Sign inmissing in frlocale/de/LC_MESSAGES/django.po:7- Unterminated string: missing closing double quotelocale/fr/LC_MESSAGES/django.po:8
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: unreviewed, placeholder-mismatch, missing-key, parse-error.
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.