Why Automate Localization?#
Manual localization workflows are the bottleneck in most teams' release cycles:
Developer: "I pushed the update."
PM: "Did you send the new strings to the translator?"
Developer: "...I'll do it now."
PM: "When will translations be back?"
Developer: "Maybe next week?"
By integrating localization into your CI/CD pipeline, new strings are translated automatically, and your localized app is always ready to ship.
The Goal: Continuous Localization#
Before automation:
Code → Manual export → Email translator → Wait days → Manual import → Test → Deploy
After automation:
Code → Push → CI detects new strings → Auto-translate → PR with translations → Merge → Deploy
The difference: days vs. minutes.
Architecture Overview#
A typical automated localization pipeline has three components:
- String Detection: Identify new or changed strings in your commit
- Translation: Send strings to your translation service (shipglobal.dev)
- Integration: Commit translated files back to your repository
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Git Push │ → │ CI Pipeline │ → │ Translate │
│ (new strings)│ │ (detect │ │ (API call) │
└─────────────┘ │ changes) │ └──────┬──────┘
└──────────────┘ │
▼
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Merge │ ← │ PR with │ ← │ Commit │
│ │ │ translations│ │ translations│
└─────────────┘ └──────────────┘ └─────────────┘
Implementation: GitHub Actions#
Here's a practical GitHub Actions workflow that translates new strings on every push:
Basic Workflow#
# .github/workflows/localize.yml
name: Localize
on:
push:
branches: [main, develop]
paths:
- 'src/locales/en/**' # Watch English source files
- 'src/strings/en.json' # Or your specific file
jobs:
translate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check for string changes
id: changes
run: |
# Detect if source language files changed
CHANGED=$(git diff --name-only HEAD~1 HEAD -- src/locales/en/)
echo "changed_files=$CHANGED" >> $GITHUB_OUTPUT
if [ -z "$CHANGED" ]; then
echo "has_changes=false" >> $GITHUB_OUTPUT
else
echo "has_changes=true" >> $GITHUB_OUTPUT
fi
- name: Translate via shipglobal.dev API
if: steps.changes.outputs.has_changes == 'true'
env:
SHIPGLOBAL_API_KEY: ${{ secrets.SHIPGLOBAL_API_KEY }}
run: |
# Upload source file and get translations
for lang in de es fr ja pt; do
curl -X POST https://api.shipglobal.dev/v1/translate \
-H "Authorization: Bearer $SHIPGLOBAL_API_KEY" \
-F "file=@src/locales/en/strings.json" \
-F "target_language=$lang" \
-o "src/locales/$lang/strings.json"
done
- name: Create Pull Request
if: steps.changes.outputs.has_changes == 'true'
uses: peter-evans/create-pull-request@v5
with:
title: 'chore: update translations'
body: 'Automated translation update triggered by changes to English source strings.'
branch: translations/auto-update
commit-message: 'chore: update translations for latest string changes'
Advanced Workflow with Validation#
# .github/workflows/localize-advanced.yml
name: Localize (Advanced)
on:
push:
branches: [main]
paths:
- 'src/locales/en/**'
jobs:
translate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Detect new strings
id: detect
run: |
# Compare current and previous English strings
git show HEAD~1:src/locales/en/strings.json > /tmp/old_strings.json 2>/dev/null || echo '{}' > /tmp/old_strings.json
NEW_KEYS=$(node -e "
const old = require('/tmp/old_strings.json');
const curr = require('./src/locales/en/strings.json');
const newKeys = Object.keys(curr).filter(k => !old[k] || old[k] !== curr[k]);
console.log(newKeys.length);
")
echo "new_key_count=$NEW_KEYS" >> $GITHUB_OUTPUT
- name: Translate new strings
if: steps.detect.outputs.new_key_count > 0
env:
SHIPGLOBAL_API_KEY: ${{ secrets.SHIPGLOBAL_API_KEY }}
run: |
TARGET_LANGUAGES="de es fr ja pt ko it"
for lang in $TARGET_LANGUAGES; do
curl -X POST https://api.shipglobal.dev/v1/translate \
-H "Authorization: Bearer $SHIPGLOBAL_API_KEY" \
-F "file=@src/locales/en/strings.json" \
-F "target_language=$lang" \
-o "src/locales/$lang/strings.json"
done
- name: Validate translations
run: |
# Check all translation files are valid JSON
for file in src/locales/*/strings.json; do
python3 -m json.tool "$file" > /dev/null || {
echo "Invalid JSON: $file"
exit 1
}
done
# Check all files have the same keys
node -e "
const fs = require('fs');
const en = Object.keys(require('./src/locales/en/strings.json')).sort();
const dirs = fs.readdirSync('./src/locales').filter(d => d !== 'en');
for (const dir of dirs) {
const keys = Object.keys(require('./src/locales/' + dir + '/strings.json')).sort();
const missing = en.filter(k => !keys.includes(k));
if (missing.length) {
console.error(dir + ' missing keys:', missing);
process.exit(1);
}
}
console.log('All translations have matching keys');
"
- name: Create Pull Request
uses: peter-evans/create-pull-request@v5
with:
title: 'chore: update translations (${{ steps.detect.outputs.new_key_count }} new strings)'
body: |
Automated translation update.
- **New/changed strings**: ${{ steps.detect.outputs.new_key_count }}
- **Languages updated**: de, es, fr, ja, pt, ko, it
- **Validation**: Passed
branch: translations/auto-update
Implementation: GitLab CI#
# .gitlab-ci.yml
localize:
stage: post-build
rules:
- changes:
- src/locales/en/**
script:
- |
for lang in de es fr ja pt; do
curl -X POST https://api.shipglobal.dev/v1/translate \
-H "Authorization: Bearer $SHIPGLOBAL_API_KEY" \
-F "file=@src/locales/en/strings.json" \
-F "target_language=$lang" \
-o "src/locales/$lang/strings.json"
done
- git config user.email "ci@yourapp.com"
- git config user.name "CI Bot"
- git add src/locales/
- git commit -m "chore: update translations" || true
- git push origin HEAD:translations/auto-update
Best Practices#
1. Only Translate What Changed#
Don't re-translate your entire file on every push. Use Translation Memory to avoid re-translating unchanged strings and to reduce costs.
2. Create PRs, Don't Auto-Merge#
Always create a pull request for translation changes. This gives your team visibility and a chance to review.
3. Validate Translations#
Add validation steps:
- JSON/XML syntax validation
- Key completeness check (all keys present in all languages)
- Placeholder consistency (e.g.,
{{name}}exists in translations) - String length warnings (translations significantly longer than source)
4. Use Branch Protection#
Don't let translation PRs bypass your normal review process. Treat them like code changes.
5. Cache Translation Memory#
If you're using shipglobal.dev, Translation Memory handles this automatically — you only pay for new or changed strings.
6. Handle Merge Conflicts#
Translation files are JSON/XML, and merge conflicts are common. Use a strategy:
- Always regenerate from source English file
- Or use a merge tool that understands your file format
Platform-Specific Tips#
iOS (Localizable.strings)#
# Convert .strings to JSON for translation, then back
# Or upload .strings directly to shipglobal.dev
curl -X POST https://api.shipglobal.dev/v1/translate \
-F "file=@en.lproj/Localizable.strings" \
-F "target_language=de" \
-o "de.lproj/Localizable.strings"
Android (strings.xml)#
# Upload strings.xml directly
curl -X POST https://api.shipglobal.dev/v1/translate \
-F "file=@app/src/main/res/values/strings.xml" \
-F "target_language=de" \
-o "app/src/main/res/values-de/strings.xml"
React / Next.js (JSON)#
# Upload your i18n JSON file
curl -X POST https://api.shipglobal.dev/v1/translate \
-F "file=@public/locales/en/common.json" \
-F "target_language=de" \
-o "public/locales/de/common.json"
Monitoring and Alerts#
Set up alerts for your localization pipeline:
- Translation failures: Notify the team if the API is unavailable
- Key mismatches: Alert if translations are missing keys
- Budget alerts: Track translation costs per month
- PR age: Alert if translation PRs sit unmerged for more than 24 hours
Conclusion#
Automating localization in your CI/CD pipeline removes the biggest friction from going global. Instead of localization being a manual, error-prone process that delays releases, it becomes an automated step that happens in the background.
Start simple — a basic workflow that translates on push and creates a PR. Then iterate: add validation, monitoring, and optimization as your needs grow.
The result: your app is always ready to ship in every language, with zero manual translation management.