Skip to content
· 8 min read · langctl Team

Automating i18n in CI/CD Pipelines

How to integrate translation validation, sync, and deployment into your CI/CD workflow. Practical examples for GitHub Actions, GitLab CI, and general pipelines.

#i18n#ci-cd#automation#devops

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:

  1. Validation: Check translation file format and structure
  2. Completeness checks: Ensure all languages have all keys
  3. Sync: Pull/push translations from/to external platforms
  4. 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:

  1. Check if new keys were added without translations
  2. Check the exit code: 7 means committed files are out of date (run langctl pull locally and commit), 1 with --require-complete means missing translations
  3. Review CI logs for specific error messages
  4. Consider relaxing strictness for feature branches

Sync conflicts

If CI sync creates conflicts:

  1. Pull translations locally first
  2. Resolve conflicts
  3. Push resolved state
  4. Re-run CI

Credential issues

If authentication fails in CI:

  1. Verify secrets are configured correctly (exit code 3 means a missing, invalid or revoked key, or a missing scope)
  2. Check the API key hasn’t been revoked
  3. Ensure key has appropriate scopes
  4. Run langctl whoami with 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:

  1. Validate file format and structure
  2. Check completeness to catch missing translations
  3. Sync with translation platforms automatically
  4. 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.

All posts