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
Most i18n CLI tools include validation:
- name: Validate translations
run: langctl validate --strict
This catches:
- Duplicate keys
- Invalid placeholder syntax
- Malformed files
- Encoding issues
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 compare actual keys:
- name: Check for missing translations
run: langctl status --fail-on-missing
This identifies specific missing keys, not just counts.
Configurable strictness
You might want different behavior for different branches:
- name: Check translations
run: |
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
langctl status --fail-on-missing
else
langctl status --warn-on-missing
fi
Strict on main, permissive on feature branches.
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: |
langctl pull
git diff --exit-code locales/ || echo "Translations updated"
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: langctl push
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
Never commit API keys to your repository.
Complete Pipeline Examples
GitHub Actions
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
validate-translations:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Install langctl
run: npm install -g langctl
- name: Validate translation files
run: langctl validate
- name: Check for missing translations
run: langctl status --fail-on-missing
build:
needs: validate-translations
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Pull latest translations
env:
LANGCTL_API_KEY: ${{ secrets.LANGCTL_API_KEY }}
run: langctl pull
- 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
- name: Install langctl
run: npm install -g langctl
- name: Push new keys
env:
LANGCTL_API_KEY: ${{ secrets.LANGCTL_API_KEY }}
run: langctl push
GitLab CI
stages:
- validate
- build
- sync
validate-translations:
stage: validate
image: node:20
script:
- npm install -g langctl
- langctl validate
- langctl status --fail-on-missing
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == "main"
build:
stage: build
image: node:20
script:
- npm ci
- npm install -g langctl
- langctl pull
- npm run build
artifacts:
paths:
- dist/
sync-keys:
stage: sync
image: node:20
script:
- npm install -g langctl
- langctl push
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: on_success
Generic Pipeline (Bash)
For other CI systems:
#!/bin/bash
set -e
# Validate
echo "Validating translations..."
langctl validate --strict
# Check completeness
echo "Checking for missing translations..."
langctl status --fail-on-missing
# Sync (if on main branch)
if [ "$BRANCH" = "main" ]; then
echo "Syncing translations..."
langctl pull
fi
# Build
echo "Building..."
npm run build
# Push new keys (if on main branch after successful build)
if [ "$BRANCH" = "main" ]; then
echo "Pushing new keys..."
langctl 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
- name: Pull translations
env:
LANGCTL_API_KEY: ${{ secrets.LANGCTL_API_KEY }}
run: |
langctl pull
- name: Commit if changed
run: |
git config user.name "Translation Bot"
git config user.email "[email protected]"
git add locales/
git diff --staged --quiet || git commit -m "chore: sync translations"
git push
Translation preview in PRs
Add translation status to pull request comments:
- name: Translation status
run: |
status=$(langctl status --format markdown)
echo "## Translation Status" > comment.md
echo "$status" >> 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
langctl status --languages en,es,fr,de --fail-on-missing
else
langctl status --languages en --fail-on-missing
fi
Troubleshooting
Build failures from translation issues
If translations block builds unexpectedly:
- Check if new keys were added without translations
- Verify file format is valid (try parsing locally)
- 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
- Check API key hasn’t expired
- Ensure key has appropriate permissions
- Test locally with same credentials
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.