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
• 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.
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
•
•
•
4. Target locales set on the project (Projects → Edit → Locales) — they are project state, not config-file state
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 yourself4. 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
6. Paste your API key as the value
7. Click Add secret
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_KEY6. 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 pushWhat this does:
• Triggers when your source locale file changes on
• Pushes source strings, then polls
• Commits updated translation files back to the repo
• The
• 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 loopWhy 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
• No API key needed — this is a local-only check
• 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 --pruneThe CLI checks apiKeyEnv first, then falls back to LOCALINGOS_API_KEY, then apiKey in the config file.