shipglot

Check Rails i18n YAML in CI

Rails raises I18n::MissingInterpolationArgument when a translation asks for a %{variable} the caller does not pass, and picks plural keys by the language's rules. The gate parses the YAML with line numbers, checks %{...} arguments, plural keys per CLDR and tags in _html strings.

What the gate reads

Format idNameExtensionsPlaceholders and pluralsLayout
rails-yamlRails i18n YAML.yml, .yaml%{name}, %<name>s, one/few/many/other plural keysOne file per locale

Detected layout

For a Ruby on Rails project laid out like this:

Gemfile
config/locales/en.yml
config/locales/fr.yml
config/locales/ru.yml

npx shipglot check finds the files without a config. npx shipglot init writes what it detected to shipglot.config.json:

{
  "sourceLocale": "en",
  "files": [
    {
      "format": "rails-yaml",
      "path": "config/locales/**/{locale}.yml"
    }
  ]
}

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:
      - "config/locales/**"
      - "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 %{variable} dropped

The count disappears from the French notice, and a renamed variable would raise MissingInterpolationArgument.

config/locales/en.yml

en:
  inbox:
    unread: "You have %{count} unread messages from %{sender}"

config/locales/fr.yml

fr:
  inbox:
    unread: "Vous avez des messages non lus de %{sender}"

Gate output

  • inbox.unread
    placeholder mismatch: missing %{count}
    config/locales/fr.yml:3

2. Plural keys Russian needs

Rails looks up :few for 3 and :many for 5. With only :one and :other, those counts raise or fall back.

config/locales/en.yml

en:
  messages:
    one: "%{count} message"
    other: "%{count} messages"

config/locales/ru.yml

ru:
  messages:
    one: "%{count} сообщение"
    other: "%{count} сообщений"

Gate output

  • messages
    plural is missing categories few, many required by ru
    config/locales/ru.yml:4

3. YAML that does not load

A bad indent makes the whole locale file fail to load when the app boots.

config/locales/en.yml

en:
  inbox:
    title: "Inbox"

config/locales/fr.yml

fr:
  inbox:
    title: "Boîte de réception"
   empty: "Aucun message"

Gate output

  • All mapping items must start at the same column
    config/locales/fr.yml:4
  • warningorphan-keyfr
    empty
    not in source locale en
    config/locales/fr.yml:4

4. A tag dropped from an _html string

Keys ending in _html are rendered as HTML. The French text lost the <strong> emphasis.

config/locales/en.yml

en:
  billing:
    notice_html: "Your trial ends in <strong>%{days} days</strong>."

config/locales/fr.yml

fr:
  billing:
    notice_html: "Votre essai se termine dans %{days} jours."

Gate output

  • billing.notice_html
    markup mismatch: missing <strong>, </strong>
    config/locales/fr.yml:3

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, parse-error, markup-mismatch.

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.