Skip to main content

Continuous Localization Without a TMS

Your code deploys continuously. Your translations should too. Here's how to automate localization in your development workflow.

What Is Continuous Localization?

Continuous localization is the practice of translating new strings automatically as part of your development workflow, rather than batching translations into periodic releases. When a developer adds a new feature with UI strings, those strings are translated before the feature ships.

The Traditional Approach

Most teams batch translations: collect new strings, export to a TMS, wait for translators, import results, then ship. This creates a localization bottleneck that delays international releases by days or weeks.

1

Developer adds new strings to the source locale file

2

Strings are exported and uploaded to a TMS platform

3

Translators are notified and begin work

4

Translations are reviewed and approved

5

Translated files are downloaded and merged back

6

Feature ships with translations (days to weeks later)

Days to weeks

The IDE-Native Approach

With IDE-native translation, the developer translates strings as part of their normal coding workflow. No export, no upload, no waiting — translations happen in the same session as development.

1

Developer adds new strings to the source locale file

2

Developer asks their AI assistant to translate the file

3

Translated files are written directly to the project

4

Feature ships with translations in the same commit

Same commit

The Traditional Approach

6 steps

Multiple tools and platforms

Days to weeks per cycle

The IDE-Native Approach

4 steps

Inside your editor

Same commit

3

Set Up Your Translation Pipeline

For teams that want automated translation beyond the IDE, add a CI/CD step that translates new or changed strings on every push to main. This works with GitHub Actions, GitLab CI, or any CI/CD system.

Start with IDE-native translation for small teams. Add CI/CD automation when your team grows past 3-4 developers or when you need translations guaranteed on every merge to main.

Add translation as a step in your existing CI/CD pipeline. The translation runs after your build succeeds and before deployment, ensuring every release includes up-to-date translations.
.github/workflows/translate.yml
# .github/workflows/translate.yml
name: Translate
on:
  push:
    branches: [main]
    paths: ['locales/en/**']

jobs:
  translate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Translate changed strings
        run: |
          npx i18n-agent translate locales/en.json \
            --to de,ja,es,fr,ko,zh-Hans \
            --api-key ${{ secrets.I18N_AGENT_API_KEY }}

      - name: Commit translations
        run: |
          git config user.name "github-actions"
          git config user.email "[email protected]"
          git add locales/
          git diff --cached --quiet || git commit -m "chore: update translations"
          git push
.gitlab-ci.yml
# .gitlab-ci.yml
translate:
  stage: deploy
  only:
    changes:
      - locales/en/**
  script:
    - npx i18n-agent translate locales/en.json
        --to de,ja,es,fr --api-key $I18N_AGENT_API_KEY
    - git add locales/ && git commit -m "chore: translations" && git push
4

Add Translation QA to CI

Automated checks in your CI pipeline catch missing translations, broken placeholders, and invalid ICU message formats before they reach production. These are simple scripts that compare your source locale file against all target locales.

Without CI-level translation validation, missing translations silently reach production. Users see raw keys like "settings.title" or empty strings instead of translated text. A 5-minute CI check prevents this entirely.
check-translations.sh
#!/bin/bash
# check-translations.sh — Run in CI to catch missing translations

SOURCE="locales/en.json"
LANGS=("de" "ja" "es" "fr")
EXIT_CODE=0

# Extract all keys from source
SOURCE_KEYS=$(jq -r '[paths(scalars)] | map(join(".")) | .[]' "$SOURCE" | sort)
SOURCE_COUNT=$(echo "$SOURCE_KEYS" | wc -l)

for lang in "${LANGS[@]}"; do
  TARGET="locales/$lang.json"
  if [ ! -f "$TARGET" ]; then
    echo "❌ Missing: $TARGET"
    EXIT_CODE=1
    continue
  fi

  TARGET_KEYS=$(jq -r '[paths(scalars)] | map(join(".")) | .[]' "$TARGET" | sort)
  MISSING=$(comm -23 <(echo "$SOURCE_KEYS") <(echo "$TARGET_KEYS"))

  if [ -n "$MISSING" ]; then
    COUNT=$(echo "$MISSING" | wc -l)
    echo "❌ $lang: $COUNT missing keys"
    echo "$MISSING" | head -5
    EXIT_CODE=1
  else
    echo "✅ $lang: all $SOURCE_COUNT keys present"
  fi
done

exit $EXIT_CODE
check-placeholders.sh
#!/bin/bash
# check-placeholders.sh — Validate placeholder consistency

SOURCE="locales/en.json"
LANGS=("de" "ja" "es")

for lang in "${LANGS[@]}"; do
  TARGET="locales/$lang.json"

  # Compare placeholders like {{name}}, {count}, %s, %d
  jq -r 'paths(scalars) as $p | [($p | join(".")), (getpath($p))]
    | @tsv' "$SOURCE" | while IFS=$'\t' read -r key value; do
    SOURCE_PH=$(echo "$value" | grep -oE '\{\{[^}]+\}\}|%[sd@]' | sort)
    TARGET_VAL=$(jq -r "getpath($(echo $key | jq -R 'split(".")'))" "$TARGET")
    TARGET_PH=$(echo "$TARGET_VAL" | grep -oE '\{\{[^}]+\}\}|%[sd@]' | sort)

    if [ "$SOURCE_PH" != "$TARGET_PH" ]; then
      echo "❌ $lang/$key: placeholder mismatch"
      echo "   Source: $SOURCE_PH"
      echo "   Target: $TARGET_PH"
    fi
  done
done

Validate Translations in CI

Add i18n-validate to your CI pipeline to catch missing keys, broken placeholders, and plural issues automatically. Use i18n-pseudo to generate fake translations for visual regression testing.
5

Detect and Handle Translation Drift

Translation drift happens when source strings change but translations don't update. The English says "Save changes" but German still says the old text. Drift detection compares source and translation file timestamps or content hashes.

Stale translations are worse than missing translations. A missing translation is obviously wrong — users see a key or fallback language. A stale translation says the wrong thing convincingly, which can mislead users or cause support issues.
detect-drift.sh
#!/bin/bash
# detect-drift.sh — Find stale translations

SOURCE="locales/en.json"
SOURCE_HASH=$(md5sum "$SOURCE" | cut -d' ' -f1)
HASH_FILE=".translation-hashes"

# Compare current source hash with stored hash
if [ -f "$HASH_FILE" ]; then
  STORED_HASH=$(grep "^en:" "$HASH_FILE" | cut -d: -f2)
  if [ "$SOURCE_HASH" != "$STORED_HASH" ]; then
    echo "⚠️  Source strings changed since last translation"
    echo "   Run translations to update all locales"

    # Show which keys changed
    git diff HEAD~1 "$SOURCE" | grep '^[+-]' | grep -v '^[+-][+-]'
  fi
fi

# Update stored hash
echo "en:$SOURCE_HASH" > "$HASH_FILE"
6

Choose Your Translation Approach

Not all content needs the same translation approach. UI strings and labels work great with AI translation. Marketing copy benefits from AI + human review. Legal and compliance text should always use professional human translators.

AI translation handles 80%+ of typical app strings at production quality. Reserve human translators for legal text, brand voice, and culturally sensitive content where nuance matters most.
Cost Comparison
Translation Approach Comparison:

Content Type         | Approach          | Cost/word  | Quality
─────────────────────┼───────────────────┼────────────┼──────────
UI strings, labels   | AI/LLM            | $0.001-01  | Production
Tooltips, help text  | AI/LLM            | $0.001-01  | Production
Marketing copy       | AI + human review | $0.05-0.15 | High
Legal / compliance   | Human translator  | $0.15-0.40 | Certified
Brand voice content  | Human translator  | $0.20-0.50 | Premium
7

Git Workflow for Translations

Translate on your feature branch before merging, not after. This ensures every merge to main includes complete translations. Add a pre-merge check that verifies translation completeness for all supported locales.

Large JSON locale files cause frequent merge conflicts when multiple branches modify them simultaneously. Sort keys alphabetically and use one key per line to minimize diff size and make conflicts easier to resolve.

Translation files live in your repository alongside your code. They're version-controlled, reviewable in pull requests, and deployed through the same pipeline as everything else.
.gitattributes + package.json
# .gitattributes — reduce merge conflicts in locale files
locales/*.json merge=union

# Sort keys alphabetically to minimize diffs:
# package.json script:
"sort-locales": "node -e \"
  const fs = require('fs');
  const f = process.argv[1];
  const d = JSON.parse(fs.readFileSync(f));
  const s = (o) => Object.keys(o).sort().reduce((r,k) =>
    ({...r, [k]: typeof o[k]==='object' ? s(o[k]) : o[k]}), {});
  fs.writeFileSync(f, JSON.stringify(s(d), null, 2)+'\\n');
\""

Common Pitfalls

Treating Localization as Post-Development

The biggest mistake: waiting until after development to translate. This creates bottlenecks, delays releases, and makes translations an afterthought. Translate during development, not after.

No CI-Level Translation Validation

Without automated checks, missing translations, broken placeholders, and invalid formats reach production. Add completeness and format validation to your CI pipeline.

Over-Engineering with a TMS

A TMS was designed for managing human translator workflows. If you use AI translation, you may not need the overhead of a TMS. Start simple — translate in the IDE or CI/CD — and add a TMS only if you need human translator management.

Not Tracking Translation Freshness

Without freshness tracking, you don't know which translations are stale. Source strings change, but old translations persist. Build drift detection into your workflow to catch stale content.

Try i18n Agent Now

Drop your translation file here

JSON, YAML, PO, XML, CSV, Markdown, Properties

or click to browse

Target languages

No signup requiredInstant estimate

Frequently Asked Questions