shipglot

Check Flutter ARB files in GitHub Actions

flutter gen-l10n generates Dart from the template ARB and reads translations as ICU messages. A renamed placeholder or a broken plural fails code generation or shows the wrong text. The gate reads l10n.yaml for the ARB folder and template, then checks each translation against the template.

What the gate reads

Format idNameExtensionsPlaceholders and pluralsLayout
flutter-arbFlutter ARB.arbICU MessageFormat: {name}, {count, plural, …}, {x, select, …}, <tag>…</tag>One file per locale

Detected layout

For a Flutter (gen-l10n) project laid out like this:

l10n.yaml
lib/l10n/app_de.arb
lib/l10n/app_en.arb
lib/l10n/app_pl.arb
pubspec.yaml

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

{
  "sourceLocale": "en",
  "files": [
    {
      "format": "flutter-arb",
      "path": "lib/l10n/app_{locale}.arb"
    }
  ]
}

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:
      - "lib/l10n/**"
      - "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 placeholder renamed in translation

gen-l10n builds the method from the template's {userName}. The German {nutzerName} is never filled in.

lib/l10n/app_en.arb

{
  "@@locale": "en",
  "helloUser": "Hello {userName}",
  "@helloUser": {
    "placeholders": {
      "userName": {
        "type": "String"
      }
    }
  }
}

lib/l10n/app_de.arb

{
  "@@locale": "de",
  "helloUser": "Hallo flagged by placeholder-mismatch: {nutzerName}"
}

Gate output

  • helloUser
    placeholder mismatch: missing {userName}; unexpected {nutzerName}
    lib/l10n/app_de.arb:3

2. A plural message that does not parse

The German plural is missing its closing brace, so gen-l10n stops with a syntax error.

lib/l10n/app_en.arb

{
  "@@locale": "en",
  "cartCount": "{count, plural, =1{One item} other{{count} items}}",
  "@cartCount": {
    "placeholders": {
      "count": {
        "type": "int"
      }
    }
  }
}

lib/l10n/app_de.arb

{
  "@@locale": "de",
  "cartCount": "{count, plural, =1{Ein Artikel} other{{count} Artikel}"
}

Gate output

  • erroricu-syntaxde
    cartCount
    invalid ICU message: unclosed argument, expected } at offset 0
    lib/l10n/app_de.arb:3

3. Plural categories Polish needs

Polish uses few and many for 2-4 and 5+. With only one and other, "2 elementów" is wrong.

lib/l10n/app_en.arb

{
  "@@locale": "en",
  "itemCount": "{count, plural, one{{count} item} other{{count} items}}",
  "@itemCount": {
    "placeholders": {
      "count": {
        "type": "int"
      }
    }
  }
}

lib/l10n/app_pl.arb

{
  "@@locale": "pl",
  "itemCount": "{count, plural, one{{count} element} other{{count} elementów}}"
}

Gate output

  • itemCount
    plural {count} is missing categories few, many required by pl
    lib/l10n/app_pl.arb:3

4. An @@locale that disagrees with the file name

gen-l10n uses @@locale when it is present, so app_de.arb would be generated as French.

lib/l10n/app_en.arb

{
  "@@locale": "en",
  "title": "Shop"
}

lib/l10n/app_de.arb

{
  "@@locale": "fr",
  "title": "Laden"
}

Gate output

  • @@locale is "fr" but the file path says "de"
    lib/l10n/app_de.arb:2
  • title
    missing in de
    lib/l10n/app_en.arb: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, icu-syntax, plural-forms, format-validity.

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.