Documentation

CI/CD

Wire Localingos into GitHub Actions, poll until every locale is complete, and gate pull requests on translation completeness.

Overview

Automate your translation workflow with GitHub Actions. Two workflows are available:
Sync workflow — Automatically pushes source strings and pulls translations when you merge to main. Commits updated translation files back to the repo.
Check workflow — Runs on pull requests to verify that i18n source files are up to date and no orphan messages exist. Fails the PR if something is off.

Prerequisites

Before setting up CI/CD, make sure you have:
1. A Localingos account with at least one project created
2. An API key — create one on the Get started page, or under Developer Tools → API Keys once signed in
3. Your project initialized with localingos init. This creates two files:
    • localingos.config.json: project config (committed to repo)
    • .localingos.json: API key only (in .gitignore, never committed)
    • init is interactive; in a container or provisioning script, write those two files yourself
4. Target locales set on the project (Projects → Edit → Locales) — they are project state, not config-file state

Step 1 — Create a CI-specific API key

Create a new key on the Get started page, or under Developer Tools → API Keys if you are already signed in. Give it a descriptive name like GitHub Actions or CI/CD Pipeline.
You can reveal the key anytime from the API Keys page using the show button. Copy it and paste it into GitHub in the next step.

Step 2 — Add the API key as a GitHub secret

1. Go to your GitHub repository
2. Click Settings (top menu bar)
3. In the left sidebar, expand Secrets and variables → click Actions
4. Click the green New repository secret button
5. Set the name to LOCALINGOS_API_KEY
6. Paste your API key as the value
7. Click Add secret

Step 3 — Verify your config files

When you ran localingos init, it created two files:
localingos.config.json — committed to your repo, shared with your team and CI:
{
  "projectId": "a3b7c9d1-4e5f-6a7b-8c9d-0e1f2a3b4c5d",
  "sourceLocale": "en-US",
  "format": "json-nested",
  "sourceFile": "./src/i18n/en-US.json",
  "outputDir": "./src/i18n",
  "outputPattern": "{locale}.json"
}
.localingos.json — in .gitignore, contains your API key for local development only:
{
  "apiKey": "your-api-key-here"
}
The CLI merges both files automatically. In CI, the GitHub secret provides the API key via environment variable — no key in the repo.

Step 4 — Add the sync workflow

Create the file .github/workflows/localingos-sync.yml in your repository with this content:
name: Localingos Sync

on:
  push:
    branches: [main]
    paths:
      - 'src/i18n/en-US.json'
      - 'localingos.config.json'

jobs:
  sync:
    runs-on: ubuntu-latest
    permissions:
      contents: write

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install Localingos CLI
        run: npm install -g localingos

      # Translation is asynchronous, so push first and then poll until every
      # locale is complete. A single sync would commit a partial set.
      - name: Sync translations
        env:
          LOCALINGOS_API_KEY: ${{ secrets.LOCALINGOS_API_KEY }}
        run: |
          localingos push
          for i in $(seq 1 20); do
            localingos pull
            if node scripts/i18n-complete.cjs; then
              echo "all locales complete"; exit 0
            fi
            echo "waiting for translations (attempt $i)..."; sleep 15
          done
          echo "timed out with incomplete translations" >&2; exit 1

      - name: Commit updated translations
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add src/i18n/
          git diff --staged --quiet || git commit -m "chore: sync translations [skip ci]" && git push
What this does:
• Triggers when your source locale file changes on main
• Pushes source strings, then polls pull until every locale is complete
• Commits updated translation files back to the repo
• The [skip ci] in the commit message prevents an infinite loop
Why the polling loop: translation runs asynchronously, so the first sync after adding new keys reports them as pending and returns no translations. pull writes whatever is ready at that moment and exits 0, so a workflow that syncs once and commits can open a green PR containing partially translated files. Gate on completeness instead.

Step 5 — Add the completeness check

The workflow above calls scripts/i18n-complete.cjs. It exits non-zero while any target locale is missing keys, which is what makes the loop terminate correctly. The .cjs extension matters: Vite and most modern scaffolds set "type": "module" in package.json, which makes a .js helper using require fail outright.
// scripts/i18n-complete.cjs
const fs = require('fs');
const path = require('path');

const dir = 'src/i18n';
const sourceFile = 'en-US.json';

const flatten = (obj, prefix = '') =>
  Object.entries(obj).flatMap(([k, v]) =>
    v && typeof v === 'object' ? flatten(v, `${prefix}${k}.`) : [`${prefix}${k}`]);

const keysOf = file =>
  flatten(JSON.parse(fs.readFileSync(path.join(dir, file), 'utf8')));

const source = keysOf(sourceFile);
let complete = true;

for (const file of fs.readdirSync(dir)) {
  if (!file.endsWith('.json') || file.startsWith(sourceFile.replace('.json', ''))) continue;
  const present = new Set(keysOf(file));
  const missing = source.filter(k => !present.has(k));
  if (missing.length) {
    console.error(`${file}: missing ${missing.length} of ${source.length} keys`);
    complete = false;
  }
}

process.exit(complete ? 0 : 1);
What this checks:
• Every target locale file contains every key present in your source file
• Run it locally too, before committing a hand-run pull
• No API key needed — this is a local-only check

Multiple projects (monorepo)

If you have multiple projects in one repo, each with its own .localingos.json and API key, add an apiKeyEnv field to each config pointing to a different environment variable:
// apps/web/localingos.config.json
{
  "apiKeyEnv": "LOCALINGOS_KEY_WEB",
  "projectId": "...",
  "sourceLocale": "en-US",
  ...
}

// apps/docs/localingos.config.json
{
  "apiKeyEnv": "LOCALINGOS_KEY_DOCS",
  "projectId": "...",
  "sourceLocale": "en-US",
  ...
}
Then add each key as a separate GitHub secret and pass them in the workflow:
- name: Sync translations
  env:
    LOCALINGOS_KEY_WEB: ${{ secrets.LOCALINGOS_KEY_WEB }}
    LOCALINGOS_KEY_DOCS: ${{ secrets.LOCALINGOS_KEY_DOCS }}
  run: |
    cd apps/web && localingos sync --prune
    cd ../docs && localingos sync --prune
The CLI checks apiKeyEnv first, then falls back to LOCALINGOS_API_KEY, then apiKey in the config file.

Troubleshooting

Issue
Fix
Workflow fails with "Missing required config fields: apiKey"
Make sure the LOCALINGOS_API_KEY secret is set in your repo settings and referenced in the workflow env block.
Sync succeeds but no commit is made
No translation files changed. This is normal if translations are already up to date.
Committed locale files are missing keys
Translation is asynchronous. "pull" writes whatever is ready and exits 0, so poll until scripts/i18n-complete.cjs passes before committing.
First sync writes no locale files at all
Expected on new keys — the push succeeded and the keys are queued. Run "localingos sync" again shortly to collect them.
A new locale produces no file
Target locales live on the project, not in localingos.config.json. Add it under Projects -> Edit -> Locales -> Update Project, then sync again.
Unsupported format "..."
Check the spelling against the format table in Configuration. "icu" is not a format — ICU MessageFormat is syntax inside your strings and is preserved automatically whichever format you choose.
Infinite workflow loop
Make sure the commit message includes "[skip ci]" to prevent the sync commit from re-triggering the workflow.