What is a Git-Based Translation Workflow?
A git-based translation workflow treats translation files as first-class code artifacts stored directly in your version control repository. Translation keys, values, and locale files live alongside source code, following the same development lifecycle: branching, pull requests, code review, CI/CD validation, and deployment. This approach creates a single source of truth where translations are versioned, reviewed, and deployed using the same infrastructure and processes as application code.
Unlike external TMS platforms where translations live in separate databases, git-based workflows keep everything in-repo. Developers commit new translation keys, translators submit changes via pull requests, automated tests validate formatting and completeness, and deployments include the latest translations automatically. This tight integration appeals to engineering teams who value consistency, auditability, and developer-friendly tooling.
Git-Based vs TMS-Based Workflows
Git-Based Workflow Characteristics
Single Source of Truth: Translation files reside in the repository. The deployed application always reflects the git state. No synchronization delays or drift between external systems and production code.
Developer-Centric: Engineers work with familiar git commands, text editors, and PR workflows. No context switching to separate translation platforms or learning new UIs.
Version History: Complete translation change history with git blame, file diffs, and commit messages. Easy rollback to previous translations if issues occur.
CI/CD Integration: Translation validation runs alongside unit tests and linting. Automated checks prevent malformed i18n files from merging.
Challenges:
- Translators need git/GitHub knowledge or wrapper tools
- Merge conflicts in translation files
- No built-in translation memory or terminology management
- Manual coordination for professional translation services
- Difficult to track translation status across languages
TMS-Based Workflow Characteristics
Translator-Friendly: Web interfaces designed for translators, not developers. In-context editing, translation memory, glossaries, and collaboration features.
Translation Memory: Automatic reuse of previous translations across keys and projects. Reduces translation time and cost.
Professional Translator Integration: Export/import workflows for professional translation agencies. Supports standard formats (XLIFF, TMX) that translators expect.
Status Tracking: Visual dashboards showing translation progress per language, missing keys, and quality metrics.
Challenges:
- Synchronization overhead between TMS and repository
- Potential for drift if sync fails or manual edits occur
- Additional tool in the stack to learn and maintain
- Data lives outside version control
- Integration complexity with CI/CD pipelines
Hybrid Approaches
Many teams combine both: TMS with git sync. Translators work in a user-friendly TMS interface, while automated sync keeps the repository updated. IntlPull's GitHub integration exemplifies this: translators use the web platform for context and collaboration, while the TMS automatically commits changes to your repository via pull requests or direct commits.
File-in-Repo Pattern Architecture
Directory Structure Best Practices
Option 1: Flat Structure (Simple Projects)
/locales
├── en.json
├── es.json
├── fr.json
└── de.json
Best for: Small applications with few translation keys (< 500 per language).
Option 2: Nested by Language (Recommended)
/locales
├── en/
│ ├── common.json
│ ├── auth.json
│ ├── dashboard.json
│ └── errors.json
├── es/
│ ├── common.json
│ ├── auth.json
│ └── ...
└── fr/
└── ...
Best for: Medium to large applications. Namespaces reduce merge conflicts and improve organization.
Option 3: Nested by Namespace (Alternative)
/locales
├── common/
│ ├── en.json
│ ├── es.json
│ └── fr.json
├── auth/
│ ├── en.json
│ └── ...
└── dashboard/
└── ...
Best for: Teams where developers work on specific features. Easier to locate all translations for a namespace.
Option 4: Platform-Specific (Multi-Platform Projects)
/apps
├── web/locales/
│ ├── en.json
│ └── es.json
├── mobile/android/values/
│ ├── strings.xml
│ └── strings-es.xml
└── mobile/ios/Localizable/
├── en.lproj/Localizable.strings
└── es.lproj/Localizable.strings
Best for: Monorepos with web, iOS, and Android apps. Each platform uses native formats.
File Format Selection
JSON (Recommended for Web)
JSON1{ 2 "common": { 3 "buttons": { 4 "submit": "Submit", 5 "cancel": "Cancel" 6 } 7 }, 8 "auth": { 9 "login": { 10 "title": "Sign in to your account", 11 "email": "Email address", 12 "password": "Password" 13 } 14 } 15}
Pros: Human-readable, nested structure, widely supported, easy git diffs. Cons: No pluralization metadata (requires i18n library support), no comments.
YAML (Alternative for Web)
YAML1common: 2 buttons: 3 submit: Submit 4 cancel: Cancel 5auth: 6 login: 7 title: Sign in to your account 8 email: Email address
Pros: More readable than JSON, supports comments, less syntax noise. Cons: Indentation-sensitive (merge conflict challenges), slower parsing than JSON.
PO/POT Files (Gettext Standard)
PO1msgid "auth.login.title" 2msgstr "Sign in to your account" 3 4msgid "items" 5msgid_plural "items" 6msgstr[0] "item" 7msgstr[1] "items"
Pros: Industry standard, supports pluralization/context, translator-friendly. Cons: Less intuitive for developers, requires build step.
Platform-Native Formats
- iOS:
.stringsor.stringsdictfiles - Android:
strings.xmlwith resource qualifiers - React Native: Typically JSON, sometimes platform-native per platform
Choose based on your i18n library and team preferences. JSON strikes the best balance for most web projects.
PR-Based Translation Review
Translation Change PR Workflow
1. Developer Creates Translation Keys When adding new features, developers create translation keys in the source language (usually English) and include them in the feature branch.
Terminal1# Developer adds new feature with translations 2git checkout -b feature/user-settings 3 4# Add new keys to en.json 5{ 6 "settings": { 7 "title": "Account Settings", 8 "privacy": { 9 "title": "Privacy Settings", 10 "description": "Manage your privacy preferences" 11 } 12 } 13} 14 15git add locales/en.json 16git commit -m "feat: add user settings page with i18n" 17git push origin feature/user-settings
2. PR Review Includes Translation Keys Code reviewers verify that:
- New features have translation keys (no hardcoded strings)
- Key names follow naming conventions
- Source language translations are clear and complete
- Keys are organized in appropriate namespaces
3. Translation Keys Merge to Main After PR approval, feature merges with English translations. Other languages now have missing keys.
4. Translators Submit Translation PRs Translators (internal or external) create PRs adding translations for missing keys.
Terminal1# Translator creates PR for Spanish translations 2git checkout -b translations/es-user-settings 3 4# Add Spanish translations 5{ 6 "settings": { 7 "title": "Configuración de cuenta", 8 "privacy": { 9 "title": "Configuración de privacidad", 10 "description": "Administra tus preferencias de privacidad" 11 } 12 } 13} 14 15git add locales/es.json 16git commit -m "i18n: add Spanish translations for user settings" 17git push origin translations/es-user-settings
5. Translation Review Native speakers or translation leads review PRs for:
- Linguistic accuracy
- Cultural appropriateness
- Consistency with existing terminology
- Proper formatting (placeholders, punctuation)
6. Automated Validation CI checks validate:
- JSON/YAML syntax is valid
- Placeholder variables match source language
- No duplicate keys
- Character limits respected (if configured)
Review Guidelines and Checklist
For Code Reviewers:
- No hardcoded user-facing strings in code
- Translation keys follow naming convention (
namespace.component.element) - Keys include context comments for translators
- Placeholders use consistent format (
{variable}or{{variable}}) - Pluralization handled correctly with library-specific syntax
For Translation Reviewers:
- Translation accurately conveys source meaning
- Terminology matches glossary and existing translations
- Tone appropriate for brand voice
- Formatting preserved (bold, italics, links)
- Placeholder positions make sense in target language
- No truncation or overflow issues with UI constraints
Git Hooks for i18n Validation
Pre-Commit Hook: Validate Translation Files
Prevent committing malformed translation files before they reach the repository.
Using Husky + lint-staged:
JSON1// package.json 2{ 3 "lint-staged": { 4 "locales/**/*.json": [ 5 "npm run i18n:validate", 6 "npm run i18n:format" 7 ] 8 } 9}
Validation Script:
JavaScript1// scripts/validate-translations.js 2const fs = require('fs'); 3const path = require('path'); 4 5const validateTranslationFile = (filePath) => { 6 try { 7 const content = fs.readFileSync(filePath, 'utf8'); 8 const json = JSON.parse(content); 9 10 // Check for duplicate keys 11 const keys = new Set(); 12 const findDuplicates = (obj, prefix = '') => { 13 Object.keys(obj).forEach(key => { 14 const fullKey = prefix ? `${prefix}.${key}` : key; 15 if (typeof obj[key] === 'object') { 16 findDuplicates(obj[key], fullKey); 17 } else { 18 if (keys.has(fullKey)) { 19 throw new Error(`Duplicate key: ${fullKey}`); 20 } 21 keys.add(fullKey); 22 } 23 }); 24 }; 25 26 findDuplicates(json); 27 28 console.log(`✓ ${filePath} is valid`); 29 return true; 30 } catch (error) { 31 console.error(`✗ ${filePath}: ${error.message}`); 32 process.exit(1); 33 } 34}; 35 36// Validate all translation files 37const localesDir = path.join(__dirname, '../locales'); 38// Recursively validate all JSON files
Pre-Push Hook: Check for Missing Translations
Warn developers before pushing if translation keys are missing in other languages.
JavaScript1// scripts/check-missing-keys.js 2const fs = require('fs'); 3const path = require('path'); 4 5const loadKeys = (obj, prefix = '') => { 6 let keys = []; 7 Object.keys(obj).forEach(key => { 8 const fullKey = prefix ? `${prefix}.${key}` : key; 9 if (typeof obj[key] === 'object' && obj[key] !== null) { 10 keys = keys.concat(loadKeys(obj[key], fullKey)); 11 } else { 12 keys.push(fullKey); 13 } 14 }); 15 return keys; 16}; 17 18const sourceFile = './locales/en.json'; 19const sourceKeys = loadKeys(JSON.parse(fs.readFileSync(sourceFile))); 20 21const languages = ['es', 'fr', 'de']; 22languages.forEach(lang => { 23 const targetFile = `./locales/${lang}.json`; 24 const targetKeys = loadKeys(JSON.parse(fs.readFileSync(targetFile))); 25 26 const missing = sourceKeys.filter(key => !targetKeys.includes(key)); 27 28 if (missing.length > 0) { 29 console.warn(`⚠ ${lang}.json is missing ${missing.length} keys:`); 30 missing.slice(0, 10).forEach(key => console.warn(` - ${key}`)); 31 if (missing.length > 10) { 32 console.warn(` ... and ${missing.length - 10} more`); 33 } 34 } 35});
Configure as warning (non-blocking) in pre-push hook to inform but not prevent pushes.
CI/CD Pipeline Integration
GitHub Actions Workflow
Complete i18n Validation Pipeline:
YAML1name: i18n Validation 2 3on: 4 pull_request: 5 paths: 6 - 'locales/**' 7 push: 8 branches: 9 - main 10 paths: 11 - 'locales/**' 12 13jobs: 14 validate: 15 runs-on: ubuntu-latest 16 steps: 17 - uses: actions/checkout@v3 18 19 - name: Setup Node.js 20 uses: actions/setup-node@v3 21 with: 22 node-version: '18' 23 24 - name: Install dependencies 25 run: npm ci 26 27 - name: Validate JSON syntax 28 run: npm run i18n:validate 29 30 - name: Check for missing keys 31 run: npm run i18n:check-missing 32 33 - name: Check placeholder consistency 34 run: npm run i18n:check-placeholders 35 36 - name: Run i18n tests 37 run: npm run test:i18n 38 39 - name: Comment missing keys on PR 40 if: github.event_name == 'pull_request' 41 uses: actions/github-script@v6 42 with: 43 script: | 44 const fs = require('fs'); 45 const report = fs.readFileSync('./i18n-report.json', 'utf8'); 46 const data = JSON.parse(report); 47 48 if (data.missingKeys.length > 0) { 49 github.rest.issues.createComment({ 50 issue_number: context.issue.number, 51 owner: context.repo.owner, 52 repo: context.repo.repo, 53 body: `## Missing Translation Keys\n\n${data.missingKeys.join('\n')}` 54 }); 55 }
Placeholder Consistency Validation
Ensure variables in translations match the source language.
JavaScript1// scripts/check-placeholders.js 2const extractPlaceholders = (text) => { 3 const regex = /{(w+)}/g; 4 const matches = []; 5 let match; 6 while ((match = regex.exec(text)) !== null) { 7 matches.push(match[1]); 8 } 9 return matches.sort(); 10}; 11 12const validatePlaceholders = (sourceText, targetText, key) => { 13 const sourcePlaceholders = extractPlaceholders(sourceText); 14 const targetPlaceholders = extractPlaceholders(targetText); 15 16 const sourceStr = sourcePlaceholders.join(','); 17 const targetStr = targetPlaceholders.join(','); 18 19 if (sourceStr !== targetStr) { 20 throw new Error( 21 `Key "${key}": placeholder mismatch\n` + 22 ` Source: ${sourceStr}\n` + 23 ` Target: ${targetStr}` 24 ); 25 } 26}; 27 28// Iterate all keys and validate
Automated Translation Deployment
Deploy translations automatically on merge to main:
YAML1name: Deploy Translations 2 3on: 4 push: 5 branches: 6 - main 7 paths: 8 - 'locales/**' 9 10jobs: 11 deploy: 12 runs-on: ubuntu-latest 13 steps: 14 - uses: actions/checkout@v3 15 16 - name: Build application 17 run: npm run build 18 19 - name: Deploy to production 20 run: npm run deploy 21 env: 22 DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }} 23 24 - name: Invalidate CDN cache 25 run: | 26 # Clear CDN cache for translation files 27 npm run cdn:invalidate -- /locales/*
Conflict Resolution Strategies
Common Merge Conflict Scenarios
Scenario 1: Two PRs Add Different Keys
PR A adds settings.theme, PR B adds settings.language. Both merge cleanly as they modify different parts of the JSON tree.
Scenario 2: Same Key, Different Values Two translators submit different translations for the same key in the same language.
DIFF1<<<<<<< HEAD 2 "welcome": "Bienvenido a nuestra aplicación" 3======= 4 "welcome": "Bienvenido a nuestro app" 5>>>>>>> translations/es-welcome
Resolution: Translation lead chooses the better version or combines elements from both.
Scenario 3: Reordering Keys One PR alphabetizes keys, another adds new keys in original order.
Prevention: Use automated formatters (prettier) to enforce consistent key ordering. Configure in pre-commit hook.
Conflict Prevention Tactics
1. Namespace Isolation Organize translations by feature/namespace. Different features rarely conflict.
/locales/en/
├── auth.json # Authentication team
├── dashboard.json # Dashboard team
├── settings.json # Settings team
2. Translation Rotation Assign languages to specific translators to avoid simultaneous edits.
3. Automated Formatting Use prettier or custom formatters to ensure consistent key ordering and formatting.
JSON1// .prettierrc 2{ 3 "tabWidth": 2, 4 "useTabs": false, 5 "trailingComma": "none", 6 "overrides": [ 7 { 8 "files": "locales/**/*.json", 9 "options": { 10 "printWidth": 120 11 } 12 } 13 ] 14}
4. Small, Focused PRs Encourage translators to submit smaller PRs for specific namespaces rather than large PRs touching many files.
5. Rebase Before Merge Translators rebase their branches on latest main before creating PR to catch conflicts early.
Terminal1git checkout translations/es-dashboard 2git fetch origin 3git rebase origin/main 4# Resolve conflicts locally before pushing 5git push --force-with-lease
Branching Strategies for Translations
Strategy 1: Feature Branch Includes Translations
Developers add source language translations in the same PR as the feature code.
Workflow:
- Feature branch includes code + English translations
- PR reviewed and merged
- Separate PRs for other languages follow
Pros:
- Features never deploy without at least source language strings
- Clean separation between development and translation
- Easy to track what needs translation
Cons:
- Other languages lag behind
- Need fallback strategy for missing translations
Strategy 2: Translation Branches Per Language
Long-running branches for each language that periodically sync with main.
Workflow:
- Main branch has English only
translations/es,translations/fr, etc. branches maintained by translators- Translators regularly merge main into their branch
- Completed translations merge back to main
Pros:
- Translators have dedicated workspace
- Can batch multiple translations before merging
Cons:
- Merge conflicts more likely with long-running branches
- Complex to manage multiple active branches
- Risk of translation branches drifting from main
Strategy 3: Trunk-Based with Translation Tasks
All work happens on short-lived branches that merge to main quickly.
Workflow:
- Developer merges feature with source language
- Automated system creates GitHub issues for missing translations
- Translators claim issues and submit PRs
- PRs merge to main when complete
Pros:
- Simple branching model
- Clear tracking of translation status via issues
- Fast integration
Cons:
- Requires discipline to avoid incomplete translations in main
- Need good issue tracking system
Recommended: Strategy 1 (feature branch includes source language) combined with Strategy 3 (trunk-based with task tracking) works best for most teams.
IntlPull's GitHub Integration
While git-based workflows offer developer-friendly version control, they burden translators with technical complexity. IntlPull bridges this gap with bidirectional GitHub synchronization.
How It Works
1. Connect Repository: Link your GitHub repository to IntlPull with one click. Select the branch and file path pattern.
2. Automatic Sync: IntlPull monitors your repository for new translation keys. When developers push changes, IntlPull imports new keys automatically.
3. Translator-Friendly Interface: Translators work in IntlPull's web UI with context, screenshots, translation memory, and glossaries—no git knowledge required.
4. Automated Commits: When translators complete translations, IntlPull commits changes back to your repository via pull requests or direct commits (configurable).
5. CI/CD Integration: IntlPull validates translations before committing. Your CI pipeline treats IntlPull commits like any other PR with full validation.
Benefits Over Pure Git Workflow
No Technical Barrier for Translators: Translators use a web interface designed for localization, not a developer terminal.
Translation Memory Across Projects: Reuse translations from previous work automatically—not possible with git-only workflows.
In-Context Editing: Translators see screenshots and context for each key, reducing errors and questions.
Professional Translator Support: Export to XLIFF for external agencies, import results—standard workflow for professional translation.
Conflict-Free Collaboration: Multiple translators work simultaneously without git merge conflicts. IntlPull handles concurrency.
Developer Workflow Unchanged: Developers continue using git as normal. IntlPull operates invisibly in the background.
This hybrid approach gives developers the version control they need while providing translators with proper localization tools.
Frequently Asked Questions
How do I handle translations for multiple platforms in a monorepo?
Use separate locale directories per platform with appropriate formats: JSON for web in apps/web/locales, strings.xml for Android in apps/android/app/src/main/res/values, and .strings for iOS in apps/ios/Resources. Share a source of truth (usually JSON) and use build scripts to convert to platform-native formats during compilation. Tools like i18n-extract can generate Android/iOS files from JSON.
Should translation files be committed directly to main or via PR?
Use PRs for translation changes to enable review and validation. Configure CI to run syntax checks and placeholder validation. Require at least one approval from a native speaker or translation lead. Direct commits to main risk broken translations reaching production. For urgent fixes, create PR and merge after CI passes.
How do I prevent developers from hardcoding strings instead of using i18n keys?
Add ESLint rules to detect hardcoded strings in JSX/templates. For React: use jsx-no-literals rule. For Vue: vue/no-bare-strings-in-template. Configure exceptions for non-translatable content (developer console messages, technical identifiers). Enforce in CI pipeline so PRs with hardcoded strings cannot merge.
What's the best way to handle translation keys that are removed?
Before deleting keys from source language, check usage across codebase with grep/search. Comment out keys for one release cycle rather than deleting immediately in case usage was missed. Use automated tools like i18n-unused to identify truly unused keys. When deleting, create PR that removes key from all language files simultaneously to maintain consistency.
How can I track translation progress across languages in a git-based workflow?
Create a script that compares keys between source language and target languages, outputting a report of missing translations per language. Run this in CI and generate a badge or dashboard. Example: npm run i18n:status outputs en: 100%, es: 87%, fr: 92%. Store results as JSON and visualize with a simple HTML report committed to the repo or published to GitHub Pages.
Should I use git submodules for shared translations across multiple repositories?
Avoid git submodules for translations due to complexity and synchronization challenges. Instead, publish translations as an npm package or use a monorepo structure. If repositories must be separate, use automated scripts to copy translation files between repos during build. IntlPull can sync translations to multiple repositories from a single project, avoiding submodule complexity entirely.
How do I handle emergency translation fixes in production?
Create a hotfix branch from production tag, update translation files, run validation tests, and deploy via accelerated review process. After production fix, cherry-pick changes to main branch to keep history synchronized. For critical issues, consider feature flags to disable broken translations until fix deploys. IntlPull's OTA updates allow pushing translation fixes without app redeployment, bypassing the emergency release process entirely.
