Most engineering teams treat localization as an afterthought, a manual handoff that happens weeks after code freezes, outside version control, and with zero automated quality gates. The result is predictable: stale translations, broken placeholders, release delays, and locale-specific bugs that slip past QA. The fix is equally straightforward: treat localization like any other build artifact. Wire string extraction, translation, and validation directly into your CI/CD pipeline so that every commit triggers a deterministic, auditable localization workflow. This article walks through the architecture of repo-connected localization pipelines, from commit hooks and delta detection through translation API calls, webhook-driven PR creation, and merge gating. By the end, you'll have a concrete blueprint for shipping localized software as reliably as you ship code.
If your team is already struggling with manual handoffs and inconsistent translation quality, see how Ollang can automate your localization pipeline.
Why Localization Belongs in Your CI/CD Pipeline
The Cost of Manual Localization Handoffs
Manual localization workflows create a parallel, unversioned process that runs alongside your codebase but never truly integrates with it. Product managers export strings into spreadsheets, email them to translators, wait days or weeks, then hand the results back to engineers who paste them into resource files. Every step introduces risk: keys get renamed in the interim, context is lost, and nobody notices that a translator removed an {amount} placeholder until a customer sees a broken checkout screen.
The operational cost compounds. According to research from the Localization Industry Standards Association (LISA), rework caused by poor handoff processes historically accounts for a significant share of total localization spend. Beyond direct cost, manual workflows make it impossible to ship all locales simultaneously, some languages lag behind, forcing staggered rollouts or incomplete user experiences.
Continuous Localization as a First-Class Build Step
Continuous localization reframes translation as a pipeline stage, no different from linting, testing, or container image builds. A commit that adds or modifies a source string automatically triggers extraction, sends the delta to a translation API or TMS, and blocks the merge until translations meet completeness and quality thresholds. The feedback loop is tight: developers see localization failures in the same PR checks they already monitor.
This model delivers three concrete benefits:
- Velocity: Translations begin the moment strings land in a branch, not after a release manager remembers to export them.
- Traceability: Every translated string is tied to a commit SHA, a PR, and an API job ID. Auditing is trivial.
- Quality: Placeholder validation, pseudo-localization, and completeness gates catch errors before they reach production.
One practical next step is to have an expert review how these checks map to your CI system; Ollang can review your pipeline and suggest specific automation steps.
Architecting a Repo-Connected Localization Pipeline
String Extraction on Commit
The pipeline starts with deterministic string extraction. When a developer pushes a commit that modifies source locale files, or source code files containing translatable strings, a CI job parses those files and produces a canonical representation of the strings that need translation.
For projects using structured resource files (JSON, XLIFF, .strings, Android XML), extraction is straightforward: diff the source locale file against the previous commit to identify added, modified, and deleted keys. For frameworks that embed strings in code (e.g., gettext calls, React intl wrappers), extraction requires a parser or CLI tool that walks the AST and emits a resource file.
A minimal GitHub Actions step for JSON-based extraction might look like this:
- name: Detect changed source strings
id: delta
run: |
git diff HEAD~1 -- src/locales/en.json > /tmp/diff.patch
python scripts/extract_delta.py /tmp/diff.patch --output /tmp/delta.json
The key principle: only extract the delta. Sending the entire resource file on every commit wastes API calls, inflates translation costs, and makes it harder to track what actually changed.
Delta Detection and Diffing Strategies
Delta detection deserves its own discussion because getting it wrong undermines the entire pipeline. There are two primary strategies:
| Strategy | How It Works | Best For |
|---|---|---|
| Key-level diff | Compare key sets between the current and previous commit of the source locale file. New keys are "added," changed values are "modified." | Flat JSON, .properties, Android XML |
| Content-hash diff | Hash each string's value and compare against a stored manifest of previously sent hashes. Only strings with new hashes are sent. | XLIFF, PO files, any format where key stability isn't guaranteed |
Content-hash diffing is more resilient to key renames and format migrations. Store the hash manifest as a committed file (e.g., .l10n-manifest.json) so it's versioned alongside the resource files.
Avoid diffing against the TMS or translation API's state as the source of truth. Your repository is the source of truth; the translation system is a downstream consumer.
Pseudo-Localization Checks Before API Calls
Before spending money on real translation, run pseudo-localization in CI. Pseudo-localization replaces source strings with accented or expanded variants (e.g., "Save" becomes "[Šåvé______]") to catch:
- Hardcoded strings that bypassed the extraction pipeline
- UI elements that can't accommodate longer text (Romance and Germanic languages commonly expand text length by 20-40%)
- Broken concatenation patterns where translators would need to reorder segments
A pseudo-locale file can be generated entirely in CI without any external API call. If the build or visual regression tests fail with the pseudo-locale active, the pipeline should fail the PR check before any translation work begins.
Pushing Resource Files to a Translation API
Once the delta is extracted and pseudo-localization passes, the pipeline pushes strings to a translation API. The API call typically looks like this:
curl -X POST https://api.example.com/v1/translate/batch \
-H "Authorization: Bearer $TRANSLATION_API_KEY" \
-H "Content-Type: application/json" \
-d @/tmp/delta.json
The request payload should include:
- Source locale and target locales (e.g., "source": "en", "targets": ["de", "fr", "ja"])
- String context or developer notes to guide translators or the MT engine
- Glossary or terminology IDs to enforce brand-specific terms
- Translation memory references so previously approved translations are reused
- Placeholder metadata so the API knows which tokens (e.g., {count}, %d, {{name}}) must be preserved verbatim
For large batches, use an asynchronous pattern: the API returns a job ID immediately, and your pipeline polls or listens for a webhook when results are ready. Synchronous calls work for small deltas (fewer than a hundred strings) but will time out or hit rate limits on larger jobs.
Receiving Results via Webhooks and Opening PRs
The most elegant pipeline architecture uses webhooks rather than polling. When the translation API completes a job, it sends a POST request to a webhook endpoint you control, often a serverless function or a CI trigger URL. The webhook payload contains the translated strings, the job ID, and status metadata.
The webhook handler:
- Validates the webhook signature to prevent spoofing.
- Writes the translated strings into the appropriate locale files in the repository.
- Opens a pull request (or updates an existing one) targeting the original feature branch.
- Adds CI status checks to the PR so reviewers can see completeness and QA results at a glance.
In GitHub Actions, you can trigger a workflow on a repository_dispatch event fired by your webhook handler:
on:
repository_dispatch:
types: [translation_complete]
jobs:
update-translations:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Write translated files
run: python scripts/write_translations.py --payload '${{ toJson(github.event.client_payload) }}'
- name: Create PR
uses: peter-evans/create-pull-request@v6
with:
title: "chore(l10n): update translations for ${{ github.event.client_payload.job_id }}"
branch: l10n/${{ github.event.client_payload.job_id }}
This keeps the localization loop entirely within version control. Every translation change is a PR with a diff, a review trail, and CI checks.
File Formats and Placeholder Handling
XLIFF, JSON, Android XML, and iOS .strings
Choosing the right resource file format, or supporting multiple formats, is a foundational decision. Here's a practical comparison:
| Format | Ecosystem | Strengths | Watch Out For |
|---|---|---|---|
| XLIFF 1.2 / 2.0 | Cross-platform, TMS-native | Rich metadata (notes, state, alt-trans), industry standard per OASIS XLIFF TC | Verbose XML; tooling varies in 2.0 support |
| JSON (flat or nested) | Web (React, Vue, Angular) | Simple to parse, easy to diff | No built-in metadata; plural rules need library support (ICU) |
| Android XML | Android | Native tooling, plural support via <plurals> | XML namespacing quirks; array resources need special handling |
| iOS .strings / .stringsdict | iOS/macOS | Native Xcode integration | .stringsdict plists are painful to hand-edit; encoding must be UTF-16 or UTF-8 depending on toolchain |
If your product spans multiple platforms, consider using a canonical format (JSON or XLIFF) in your pipeline and converting to platform-native formats as a build step. This keeps the translation API integration simple while respecting each platform's expectations.
Validating Placeholders and Markup in CI
Placeholder corruption is the single most common localization bug that reaches production. A translator, or an MT engine, replaces {username} with {nom_utilisateur}, or drops a </b> closing tag, and the app crashes or renders garbage.
Your CI pipeline should include a validation step that runs after translations are written to locale files but before the PR is approved. The validator checks:
- Named placeholders ({count}, {{name}}): every placeholder in the source string must appear exactly in the target string.
- Positional placeholders (%1$s, %2$d): same tokens, same order (or reordered only if the format supports it).
- HTML/XML markup: tags must be balanced and properly nested.
- ICU message syntax: plural, select, and selectordinal patterns must be syntactically valid.
A simple Python validation snippet:
import re
def validate_placeholders(source: str, target: str) -> list[str]:
src_tokens = set(re.findall(r'\{[^}]+\}', source))
tgt_tokens = set(re.findall(r'\{[^}]+\}', target))
missing = src_tokens - tgt_tokens
extra = tgt_tokens - src_tokens
errors = []
if missing:
errors.append(f"Missing placeholders: {missing}")
if extra:
errors.append(f"Unexpected placeholders: {extra}")
return errors
Fail the CI check if any string has placeholder errors. This is a hard gate, not a warning.
