Shipping multilingual software should not depend on a manual handoff between engineering and localization teams. Yet in most organizations, that is exactly what happens: developers extract strings, email files to translators, wait days for delivery, and manually commit the results, introducing drift, missed strings, and broken releases. A CI/CD pipeline connected to translation APIs eliminates this gap by treating localization as code: string changes are detected on push, translation jobs are dispatched automatically, quality gates block merges when coverage is incomplete, and compiled resource bundles are generated as build artifacts. This article provides a reference architecture for wiring your GitHub or GitLab repository to translation APIs, with concrete YAML snippets, retry strategies, environment separation, and rollback patterns. If your team ships software, documentation, or video content across languages, this is the infrastructure blueprint you need.
If you're already feeling the pain of manual localization handoffs, see how Ollang automates these workflows end to end: Get a walkthrough.
Why CI/CD Pipelines Need a Localization Stage
Modern CI/CD pipelines handle linting, testing, security scanning, and deployment, but localization is almost always bolted on as an afterthought. This creates three systemic problems.
First, string drift. When translations lag behind source code by even a single sprint, users see untranslated keys, stale copy, or outright broken UI in non-primary locales. According to the 2023 Nimdzi 100 report, the language services industry surpassed $60 billion in revenue, yet the majority of engineering teams still manage translations through file uploads and spreadsheets rather than automated pipelines.
Second, release blocking without visibility. Without a localization quality gate, teams either ship incomplete translations or delay the entire release while someone manually checks coverage. Neither outcome is acceptable.
Third, multi-asset blindness. Software strings are only one content type. Product documentation, subtitle files, and legal text all live in or near the same repositories. A CI/CD localization stage that only handles .json or .xml resource files leaves the rest unmanaged.
A properly integrated localization stage treats translated content the same way a test suite treats code correctness: it runs automatically, reports status, and blocks promotion when the output is not ready.
Reference Architecture: Repo to Translation API
The architecture is straightforward in concept: a commit triggers a pipeline job that detects changed source strings, sends them to a translation API, polls or receives a webhook for completion, validates the result, and commits the translated files back to the repository. In practice, the details matter.
Detecting String Diffs on Push
The pipeline should not re-submit every string on every commit. Instead, it compares the current source file against the last known translated snapshot. A simple approach uses git diff scoped to your resource file paths:
# GitHub Actions step: detect changed resource files
- name: Detect changed strings
id: diff
run: |
CHANGED=$(git diff --name-only ${{ github.event.before }} ${{ github.sha }} -- 'src/locales/en/**')
if [ -z "$CHANGED" ]; then
echo "no_changes=true" >> $GITHUB_OUTPUT
else
echo "no_changes=false" >> $GITHUB_OUTPUT
echo "$CHANGED" > changed_files.txt
fi
A more precise approach parses the resource files and computes a key-level diff. For JSON resource bundles, a small script can compare keys and values between the previous and current versions, emitting only the delta. This keeps API calls minimal and avoids redundant translation charges.
Creating and Updating Keys via TMS or Direct API
Once you have the diff, the next step is to submit it. There are two broad patterns:
- TMS API integration: Upload changed files or key-value pairs to a translation management system (such as Phrase, Lokalise, or Crowdin) that handles job routing, translator assignment, and translation memory internally.
- Direct provider API: Call a machine translation API (DeepL, Google Cloud Translation, or similar) directly, handling glossary application and post-editing workflow yourself.
A typical TMS upload call looks like this:
curl -X POST "https://api.example-tms.com/v2/projects/${PROJECT_ID}/files" \
-H "Authorization: Bearer ${TMS_API_TOKEN}" \
-F "file=@src/locales/en/messages.json" \
-F "update_strategy=merge" \
-F "target_locales=fr,de,ja,es"
The update_strategy=merge parameter is critical: it tells the TMS to add new keys and update changed values without deleting keys that still exist in the source but were not part of this diff. This is an idempotency safeguard, re-running the same pipeline step produces the same result.
Kicking Off Translation Jobs
Submitting files and starting a translation job are often separate API calls. After uploading, you create a job or order:
{
"source_locale": "en",
"target_locales": ["fr", "de", "ja"],
"file_ids": ["file_abc123"],
"workflow": "machine_translate_then_review",
"callback_url": "https://ci.example.com/hooks/translation-complete"
}
The callback_url is a webhook endpoint your pipeline infrastructure exposes. When the job completes, the TMS posts a status payload to this URL, which triggers the next pipeline stage. For systems that do not support webhooks, fall back to polling with exponential backoff.
Blocking Merges on Missing Coverage
The pipeline should post a commit status check (on GitHub) or an external status (on GitLab) that blocks the pull request from merging until all target locales meet a coverage threshold.
- name: Check translation coverage
run: |
COVERAGE=$(python scripts/check_coverage.py --source src/locales/en --targets src/locales)
if [ "$COVERAGE" != "100" ]; then
echo "Translation coverage is ${COVERAGE}%. Blocking merge."
exit 1
fi
On GitHub, this step's exit code determines the status check result. You can also use the GitHub Checks API to post richer annotations showing exactly which keys are missing in which locales.
GitHub Actions Pipeline: Full YAML Example
Below is a complete GitHub Actions workflow that ties together detection, submission, polling, validation, and commit-back.
name: Localization Pipeline
on:
push:
paths:
- 'src/locales/en/**'
permissions:
contents: write
checks: write
jobs:
localize:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Detect changed strings
id: diff
run: |
CHANGED=$(git diff --name-only HEAD~1 HEAD -- 'src/locales/en/')
if [ -z "$CHANGED" ]; then
echo "skip=true" >> $GITHUB_OUTPUT
else
echo "skip=false" >> $GITHUB_OUTPUT
fi
- name: Upload to translation API
if: steps.diff.outputs.skip == 'false'
env:
TMS_TOKEN: ${{ secrets.TMS_API_TOKEN }}
PROJECT_ID: ${{ secrets.TMS_PROJECT_ID }}
run: |
for f in $(git diff --name-only HEAD~1 HEAD -- 'src/locales/en/'); do
curl -sf -X POST "https://api.example-tms.com/v2/projects/${PROJECT_ID}/files" \
-H "Authorization: Bearer ${TMS_TOKEN}" \
-F "file=@${f}" \
-F "update_strategy=merge"
done
- name: Poll for completion
if: steps.diff.outputs.skip == 'false'
env:
TMS_TOKEN: ${{ secrets.TMS_API_TOKEN }}
PROJECT_ID: ${{ secrets.TMS_PROJECT_ID }}
run: |
RETRIES=0
MAX_RETRIES=30
while [ $RETRIES -lt $MAX_RETRIES ]; do
STATUS=$(curl -sf -H "Authorization: Bearer ${TMS_TOKEN}" \
"https://api.example-tms.com/v2/projects/${PROJECT_ID}/jobs/latest" \
| jq -r '.status')
if [ "$STATUS" = "completed" ]; then
echo "Translations ready."
break
fi
RETRIES=$((RETRIES + 1))
sleep $((RETRIES * 10))
done
if [ "$STATUS" != "completed" ]; then
echo "Translation timed out after ${MAX_RETRIES} retries."
exit 1
fi
- name: Download translations
if: steps.diff.outputs.skip == 'false'
env:
TMS_TOKEN: ${{ secrets.TMS_API_TOKEN }}
PROJECT_ID: ${{ secrets.TMS_PROJECT_ID }}
run: |
for locale in fr de ja es; do
curl -sf -H "Authorization: Bearer ${TMS_TOKEN}" \
"https://api.example-tms.com/v2/projects/${PROJECT_ID}/export/${locale}" \
-o "src/locales/${locale}/messages.json"
done
- name: Validate coverage
run: python scripts/check_coverage.py --source src/locales/en --targets src/locales --threshold 100
- name: Commit translations
if: steps.diff.outputs.skip == 'false'
run: |
git config user.name "localization-bot"
git config user.email "bot@example.com"
git add src/locales/
git diff --cached --quiet || git commit -m "chore: update translations [skip ci]"
git push
Key details:
- [skip ci] in the commit message prevents an infinite loop where the bot's commit retriggers the pipeline.
- fetch-depth: 2 ensures HEAD~1 is available for the diff.
- Exponential backoff in the polling step (sleep $((RETRIES * 10))) avoids hammering the API.
GitLab CI Pipeline: Full YAML Example
The GitLab equivalent uses .gitlab-ci.yml stages and GitLab's built-in variables for diff detection.
stages:
- detect
- translate
- validate
- commit
variables:
TMS_PROJECT_ID: $TMS_PROJECT_ID
detect_changes:
stage: detect
script:
- |
CHANGED=$(git diff --name-only $CI_COMMIT_BEFORE_SHA $CI_COMMIT_SHA -- 'src/locales/en/')
if [ -z "$CHANGED" ]; then
echo "NO_CHANGES" > status.txt
else
echo "HAS_CHANGES" > status.txt
fi
artifacts:
paths:
- status.txt
upload_strings:
stage: translate
needs: [detect_changes]
rules:
- exists:
- status.txt
script:
- |
[ "$(cat status.txt)" = "NO_CHANGES" ] && exit 0
for f in $(git diff --name-only $CI_COMMIT_BEFORE_SHA $CI_COMMIT_SHA -- 'src/locales/en/'); do
curl -sf -X POST "https://api.example-tms.com/v2/projects/${TMS_PROJECT_ID}/files" \
-H "Authorization: Bearer ${TMS_API_TOKEN}" \
-F "file=@${f}" \
-F "update_strategy=merge"
done
- python scripts/poll_and_download.py --project ${TMS_PROJECT_ID} --locales fr,de,ja,es
validate_coverage:
stage: validate
needs: [upload_strings]
script:
- python scripts/check_coverage.py --source src/locales/en --targets src/locales --threshold 100
commit_translations:
stage: commit
needs: [validate_coverage]
script:
- git config user.name "localization-bot"
- git config user.email "bot@ci.example.com"
- git add src/locales/
- git diff --cached --quiet || git commit -m "chore: update translations [skip ci]"
- git push "https://oauth2:${GITLAB_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git" HEAD:${CI_COMMIT_REF_NAME}
GitLab requires an explicit push URL with a project access token (GITLAB_TOKEN) stored in CI/CD variables. The [skip ci] commit message convention also works here.
Environment Separation: Dev, Staging, Production
Not every translation belongs in every environment. A common pattern:
| Environment | Translation quality | Source branch | Behavior |
|---|---|---|---|
| Dev | Raw machine translation | Feature branches | Auto-merge, no review gate |
| Staging | MT + automated QA checks | develop / main | Block on coverage; flag QA issues |
| Production | Reviewed / human-verified | release/* or tags | Block on review sign-off |
In the pipeline, branch detection controls which workflow is invoked:
- name: Select translation workflow
run: |
if [[ "${{ github.ref }}" == refs/heads/release/* ]]; then
echo "workflow=human_review" >> $GITHUB_OUTPUT
elif [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
echo "workflow=mt_plus_qa" >> $GITHUB_OUTPUT
else
echo "workflow=mt_only" >> $GITHUB_OUTPUT
fi
This ensures developers get fast feedback on feature branches while production releases go through rigorous quality review.
Branch-to-Locale Mapping Strategies
Some teams use branch naming conventions to control which locales are translated. For example, a feature/checkout-ja branch might only trigger Japanese translation, while main triggers all target locales. This is implemented by maintaining a mapping file:
{
"branch_patterns": {
"feature/*-ja": ["ja"],
"feature/*-latam": ["es-419", "pt-BR"],
"main": ["fr", "de", "ja", "es", "pt-BR", "zh-Hans"],
"release/*": ["fr", "de", "ja", "es", "pt-BR", "zh-Hans", "ko", "ar"]
}
}
The pipeline reads this file and passes the resolved locale list to the translation API call. This avoids burning API quota on locales that are irrelevant to a given branch's scope.
Secrets Management for API Tokens
Translation API tokens are sensitive credentials. Follow these practices:
- Store tokens in your CI platform's encrypted secrets store (GitHub Actions secrets, GitLab CI/CD variables marked as masked and protected).
- Scope tokens to the minimum required permission. A token that can upload and download files should not have project deletion rights.
- Rotate tokens on a schedule. Use short-lived tokens where the API supports them.
- Never log token values. Mask them in CI output by referencing them only through environment variables.
For teams managing multiple translation providers, a secrets manager like HashiCorp Vault or AWS Secrets Manager can centralize credential rotation and audit logging.
