Manual translation workflows don’t scale. As projects grow, manually checking for missing keys, syncing with translation platforms, and validating file formats becomes error-prone and time-consuming.
Automation solves this. Translation validation and sync should be part of your CI/CD pipeline, just like tests and linting.
This guide covers practical approaches to automating i18n in common CI/CD environments.
What to Automate
Translation automation typically includes:
- Validation: Check translation file format and structure
- Completeness checks: Ensure all languages have all keys
- Sync: Pull/push translations from/to external platforms
- Build-time processing: Generate optimized translation bundles
Let’s cover each.
Validation
Catch formatting errors and structural issues before they reach production.
JSON/YAML validation
At minimum, ensure files parse correctly:
# GitHub Actions example
- name: Validate translation files
run: |
for file in locales/*.json; do
jq empty "$file" || exit 1
done
Schema validation
For more structured checks, validate against a schema:
- name: Validate translation schema
run: |
npx ajv validate -s translation-schema.json -d "locales/*.json"
CLI tool validation
langctl doesn’t have a separate validate command, but a dry-run push reads your source-language file exactly as a real push would and reports what would change, without uploading anything:
- name: Preview translation upload
run: npx langctl@0 push --dry-run
env:
LANGCTL_API_KEY: ${{ secrets.LANGCTL_API_KEY }}
Pulls also fail loudly instead of silently dropping data:
- Two keys that map to the same Android or ARB resource name
- A nested-JSON key that is both a string and a parent
- An unknown language or format (exit code 2)
Completeness Checks
Missing translations should fail the build (or at least warn).
Basic check
Compare key counts across language files:
- name: Check translation completeness
run: |
base_count=$(jq 'keys | length' locales/en.json)
for file in locales/*.json; do
count=$(jq 'keys | length' "$file")
if [ "$count" -lt "$base_count" ]; then
echo "Warning: $file has fewer keys than base language"
# Optionally: exit 1
fi
done
CLI tool check
More sophisticated tools check against the translation platform itself:
- name: Check for missing translations
run: npx langctl@0 pull --require-complete
env:
LANGCTL_API_KEY: ${{ secrets.LANGCTL_API_KEY }}
This exits with code 1 if any language is missing translations, instead of comparing file key counts.
Configurable strictness
You might want different behavior for different branches:
- name: Check translations
run: |
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
npx langctl@0 pull --require-complete
else
npx langctl@0 projects stats web-app # report coverage, don't fail
fi
Strict on main, permissive on feature branches. (web-app is your project slug.)
Syncing with Translation Platforms
If you use an external translation management system, sync in CI/CD.
Pull before build
Ensure builds use latest translations:
- name: Sync translations
env:
LANGCTL_API_KEY: ${{ secrets.LANGCTL_API_KEY }}
run: |
npx langctl@0 pull
git diff --exit-code locales/ || echo "Translations updated"
If you commit translation files and want the build to fail when they’re stale instead, use npx langctl@0 pull --check—it writes nothing and exits with code 7 when any file is out of date.
Push after merge
When new keys are merged, push to the platform:
# On push to main
- name: Push new keys to translation platform
if: github.ref == 'refs/heads/main'
env:
LANGCTL_API_KEY: ${{ secrets.LANGCTL_API_KEY }}
run: npx langctl@0 push --publish
push uploads new keys from your source-language file and never overwrites existing translations unless you pass --overwrite. --publish publishes the uploaded keys; without it they stay drafts, which pull excludes until someone publishes them.
Handling credentials
Translation platform credentials should be stored as secrets:
# GitHub Actions
env:
LANGCTL_API_KEY: ${{ secrets.LANGCTL_API_KEY }}
# GitLab CI
variables:
LANGCTL_API_KEY: $LANGCTL_API_KEY # Set in GitLab CI/CD settings
No config file or login step is needed in CI—langctl reads LANGCTL_API_KEY from the environment. Never commit API keys to your repository, and give the CI key only the scopes the job needs (translations:read is enough for pull).
Complete Pipeline Examples
GitHub Actions
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
check-translations:
runs-on: ubuntu-latest
env:
LANGCTL_API_KEY: ${{ secrets.LANGCTL_API_KEY }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22 }
- name: Fail if committed translation files are out of date
run: npx langctl@0 pull --check
- name: Check for missing translations
run: npx langctl@0 pull --require-complete
build:
needs: check-translations
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22 }
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
sync-keys:
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22 }
- name: Push new keys
env:
LANGCTL_API_KEY: ${{ secrets.LANGCTL_API_KEY }}
run: npx langctl@0 push
Because check-translations already verified the committed files match the platform, the build uses them as-is.
GitLab CI
stages:
- validate
- build
- sync
check-translations:
stage: validate
image: node:22
script:
- npx langctl@0 pull --check
- npx langctl@0 pull --require-complete
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == "main"
build:
stage: build
image: node:22
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/
sync-keys:
stage: sync
image: node:22
script:
- npx langctl@0 push
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: on_success
Generic Pipeline (Bash)
For other CI systems:
#!/bin/bash
set -e
# Requires Node.js 20+ and LANGCTL_API_KEY in the environment
# Validate: fail (exit 7) if committed files are out of date
echo "Checking translation files..."
npx langctl@0 pull --check
# Check completeness
echo "Checking for missing translations..."
npx langctl@0 pull --require-complete
# Build
echo "Building..."
npm run build
# Push new keys (if on main branch after successful build)
if [ "$BRANCH" = "main" ]; then
echo "Pushing new keys..."
npx langctl@0 push
fi
Advanced Patterns
Scheduled translation sync
Run translation sync on a schedule, not just on commits:
# GitHub Actions
on:
schedule:
- cron: '0 6 * * *' # Daily at 6 AM
jobs:
sync-translations:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22 }
- name: Pull translations
env:
LANGCTL_API_KEY: ${{ secrets.LANGCTL_API_KEY }}
run: |
npx langctl@0 pull
- name: Commit if changed
run: |
git config user.name "Translation Bot"
git config user.email "bot@example.com"
git add locales/
git diff --staged --quiet || git commit -m "chore: sync translations"
git push
Translation preview in PRs
Add a preview of what the PR would upload to a pull request comment:
- name: Translation preview
env:
LANGCTL_API_KEY: ${{ secrets.LANGCTL_API_KEY }}
run: |
echo "## Translation Preview" > comment.md
echo '~~~' >> comment.md
npx langctl@0 push --dry-run --no-color >> comment.md
echo '~~~' >> comment.md
- name: Post comment
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const body = fs.readFileSync('comment.md', 'utf8');
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body
});
Branch-specific languages
Only require certain languages in feature branches:
- name: Check translations
run: |
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
npx langctl@0 pull --languages en,es,fr,de --require-complete
else
npx langctl@0 pull --languages en --require-complete
fi
Troubleshooting
Build failures from translation issues
If translations block builds unexpectedly:
- Check if new keys were added without translations
- Check the exit code: 7 means committed files are out of date (run
langctl pulllocally and commit), 1 with--require-completemeans missing translations - Review CI logs for specific error messages
- Consider relaxing strictness for feature branches
Sync conflicts
If CI sync creates conflicts:
- Pull translations locally first
- Resolve conflicts
- Push resolved state
- Re-run CI
Credential issues
If authentication fails in CI:
- Verify secrets are configured correctly (exit code 3 means a missing, invalid or revoked key, or a missing scope)
- Check the API key hasn’t been revoked
- Ensure key has appropriate scopes
- Run
langctl whoamiwith the same key to see the org, key source and scopes
Summary
Automating i18n in CI/CD catches issues early and keeps translations in sync. The key steps:
- Validate file format and structure
- Check completeness to catch missing translations
- Sync with translation platforms automatically
- Adjust strictness based on branch and context
Start simple—add validation first, then completeness checks, then sync. Iterate as your needs evolve.
See also: How to Manage i18n in Git Workflows for Git-specific practices that complement CI/CD automation.