Table of Contents
Table of Contents
- What is GitHub Translation Sync?
- Why PR-Based Translation Delivery?
- Prerequisites
- Supported Directory Structures
- Step 1: Install the GitHub App
- Permissions Explained
- Step 2: Configure the Sync
- Configuration Options Reference
- Step 3: Understand the Sync Workflow
- Adding New Keys
- Updating Existing Keys
- Deleting Keys
- Step 4: CI Integration
- GitHub Actions Workflow
- Required Status Checks
- Step 5: Branch Strategy for Translations
- The Default: Single Translation Branch
- Advanced: Feature Branch Sync
- Handling Merge Conflicts
- Step 6: Monitoring and Troubleshooting
- Sync Status Dashboard
- Common Issues and Solutions
- Advanced Configuration
- Namespace Filtering
- Custom Commit Messages
- Webhook Events
- Real-World Example: E-Commerce App Setup
- Conclusion
Translation files scattered across branches, manual copy-pasting of JSON keys, Slack messages asking "did anyone push the French translations?" — if this sounds familiar, your localization workflow needs automation.
GitHub Translation Sync eliminates the manual overhead of keeping translation files in sync with your codebase. It watches your repository for new translation keys, sends them for translation (AI or human), and delivers completed translations back as pull requests — all without leaving your Git workflow.
This guide walks through setting up GitHub Translation Sync from scratch, configuring PR-based workflows, integrating with CI, and optimizing for team collaboration.
What is GitHub Translation Sync?
GitHub Translation Sync is a bidirectional synchronization mechanism between your Git repository and a translation management platform. Instead of manually pushing and pulling translation files, the sync:
- Detects new or changed keys when you push code
- Sends them to your translation platform for translation
- Delivers completed translations back as pull requests
- Keeps translation files in sync across branches
Why PR-Based Translation Delivery?
Traditional localization workflows use CLI push/pull commands. The PR-based approach is better because:
| CLI Push/Pull | PR-Based Sync |
|---|---|
| Manual process, easy to forget | Automatic, always in sync |
| No review step for translations | PR review catches issues |
| Direct commits to main branch | Branch protection respected |
| No audit trail | Full Git history for translations |
| One person's responsibility | Team-visible via PR notifications |
| Merge conflicts are common | Sync handles rebasing automatically |
Prerequisites
Before setting up GitHub Translation Sync, you need:
- A GitHub repository with translation files (JSON, YAML, PO, or ARB format)
- A Better i18n project (sign up at better-i18n.com)
- Repository admin access (for installing the GitHub App)
- Translation files in a consistent directory structure
Supported Directory Structures
# Pattern 1: Locale directories
locales/
en/
common.json
dashboard.json
es/
common.json
dashboard.json
# Pattern 2: Locale file suffix
locales/
common.en.json
common.es.json
dashboard.en.json
dashboard.es.json
# Pattern 3: Single directory with locale prefix
i18n/
en.json
es.json
fr.json
# Pattern 4: Framework-specific (e.g., Flutter ARB)
lib/l10n/
app_en.arb
app_es.arb
Step 1: Install the GitHub App
The GitHub Sync works through a GitHub App that has read/write access to your repository's translation files.
- Go to your Better i18n project dashboard
- Navigate to Settings then Integrations
- Click Connect GitHub
- Select the repositories you want to sync
- Approve the requested permissions
Permissions Explained
| Permission | Why It Is Needed |
|---|---|
| Read: Code | Detect new/changed translation keys |
| Read/Write: Pull Requests | Create PRs with completed translations |
| Read: Metadata | Access repository information |
| Webhooks | Receive push events for sync triggers |
The GitHub App only accesses translation-related files. It does not read your source code, environment variables, or secrets.
Step 2: Configure the Sync
Create a configuration file in your repository root:
# better-i18n.yml
sync:
# Source language — keys are extracted from this locale's files
sourceLanguage: en
# Target languages — translations will be generated for these
targetLanguages:
- es
- fr
- de
- ja
- ko
- "zh-CN"
# Path pattern for translation files
# Supports glob patterns with {locale} and {namespace} placeholders
paths:
- "locales/{locale}/{namespace}.json"
# Alternative: single file per locale
# paths:
# - "locales/{locale}.json"
# Branch to watch for changes (default: main)
baseBranch: main
# How to deliver translations
delivery:
# "pr" creates pull requests, "commit" pushes directly (not recommended)
method: pr
# PR branch naming pattern
branchPattern: "i18n/sync-{timestamp}"
# Auto-merge PRs when all checks pass (requires branch protection)
autoMerge: false
# Reviewers to assign to translation PRs
reviewers:
- "@i18n-team"
# Labels to add to translation PRs
labels:
- "i18n"
- "automated"
# AI translation settings
ai:
# Auto-translate new keys with AI
autoTranslate: true
# Require human approval before PR is created
requireApproval: false
# Custom instructions for AI translator
instructions: |
- Use formal register for German (Sie, not du)
- Keep brand name "Better i18n" untranslated in all languages
- Use Oxford comma in English translations
Configuration Options Reference
| Option | Type | Default | Description |
|---|---|---|---|
sourceLanguage | string | "en" | Source locale code |
targetLanguages | string[] | [] | Target locale codes |
paths | string[] | auto-detect | Glob patterns for translation files |
baseBranch | string | "main" | Branch to monitor |
delivery.method | string | "pr" | Delivery method: pr or commit |
delivery.autoMerge | boolean | false | Auto-merge when checks pass |
delivery.reviewers | string[] | [] | PR reviewers (users or teams) |
delivery.labels | string[] | ["i18n"] | PR labels |
ai.autoTranslate | boolean | true | AI-translate new keys |
ai.requireApproval | boolean | false | Require approval before PR |
ai.instructions | string | "" | Custom AI instructions |
Step 3: Understand the Sync Workflow
Once configured, here is what happens during a typical development cycle:
Adding New Keys
Developer GitHub Better i18n GitHub
| | | |
|-- push commit -------->| | |
| (adds new key |-- webhook ----------->| |
| to en/common.json) | | |
| | |-- AI translates ---->|
| | | new key for all |
| | | target languages |
| | | |
| |<---------- create PR with translations ------|
| | | |
|<-- PR notification ----| | |
| | | |
|-- review & merge ----->| | |
Updating Existing Keys
When you modify the source text of an existing key:
- Sync detects the change via webhook
- Existing translations are marked as "needs review"
- AI generates updated translations
- A PR is created with updated translations
- Previous translations are preserved in PR description for reviewer comparison
Deleting Keys
When you remove a key from the source locale:
- Sync detects the deletion
- The corresponding key is removed from all target locale files
- A PR is created with the deletions
- PR description lists all removed keys for audit purposes
Step 4: CI Integration
Add translation validation to your CI pipeline to catch issues before translations are merged.
GitHub Actions Workflow
# .github/workflows/i18n-validate.yml
name: Validate Translations
on:
pull_request:
paths:
- "locales/**"
types: [opened, synchronize]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: bun install
- name: Validate translation coverage
run: |
bunx @better-i18n/cli coverage \
--min 95 \
--format github-actions
- name: Validate ICU syntax
run: |
bunx @better-i18n/cli validate \
--format github-actions
- name: Check for unused keys
run: |
bunx @better-i18n/cli lint \
--unused \
--format github-actions
- name: Comment coverage report on PR
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const { execSync } = require('child_process');
const report = execSync('bunx @better-i18n/cli coverage --format markdown').toString();
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `## Translation Coverage Report\n\n${report}`
});
Required Status Checks
Configure branch protection to require translation validation:
- Go to Settings then Branches then Branch protection rules
- Enable Require status checks to pass before merging
- Add these checks:
Validate translation coverageValidate ICU syntax
This ensures that no PR can be merged with broken translations or insufficient coverage.
Step 5: Branch Strategy for Translations
The Default: Single Translation Branch
By default, each sync creates a new branch with completed translations:
main ------------------------------------------------
\ |
\-- i18n/sync-20260315 ---------/
(translations for new keys)
This works well for most teams. Each sync is isolated, and you can review translations before merging.
Advanced: Feature Branch Sync
For teams using feature branches, configure sync to target feature branches:
# better-i18n.yml
sync:
baseBranch: main
featureBranchSync:
enabled: true
# Sync translations for PRs targeting these branches
targetBranches:
- "main"
- "develop"
# Create translation PR targeting the same branch as the feature PR
followBranch: true
With feature branch sync:
main ------------------------------------------------
\
\-- feature/new-checkout -------------------------
\ |
\-- i18n/new-checkout -----/
(translations for this feature)
Handling Merge Conflicts
The sync handles most merge conflicts automatically by:
- Rebasing translation branches before creating PRs
- Using JSON-aware merging (not line-based) for translation files
- Re-triggering sync if a conflict is detected
If an automatic resolution is not possible, the sync will add a comment to the PR explaining the conflict and suggesting manual resolution steps.
Step 6: Monitoring and Troubleshooting
Sync Status Dashboard
Monitor sync health in your Better i18n dashboard:
- Sync History: Every sync event with status (success, partial, failed)
- Pending Translations: Keys waiting for translation
- Coverage Trends: Translation coverage over time per language
- PR Activity: Open, merged, and closed translation PRs
Common Issues and Solutions
Issue: Sync not triggering on push
- Verify the GitHub App is installed on the repository
- Check that the webhook is active in Settings then Webhooks
- Ensure the push is to the configured
baseBranch - Check that changed files match the configured
pathspatterns
Issue: PR has merge conflicts
- The sync auto-rebases, but if your branch diverged significantly, manual resolution may be needed
- Resolve conflicts in the translation PR, then push — the sync will not overwrite your resolution
Issue: AI translations are low quality
- Add detailed
ai.instructionsin your config - Include glossary terms and brand guidelines
- Consider enabling
requireApprovalfor critical languages
Issue: Too many small PRs
- Configure
delivery.batchWindowto group changes:
sync:
delivery:
# Wait 30 minutes to batch multiple pushes into one PR
batchWindow: 30
Advanced Configuration
Namespace Filtering
Only sync specific namespaces:
sync:
paths:
- "locales/{locale}/common.json"
- "locales/{locale}/marketing.json"
# Explicitly exclude internal namespaces
exclude:
- "locales/{locale}/internal.json"
- "locales/{locale}/test-fixtures.json"
Custom Commit Messages
sync:
delivery:
commitMessage: "chore(i18n): sync translations for {languages}"
prTitle: "chore(i18n): translations update ({date})"
prBody: |
## Automated Translation Update
This PR contains translations synced from Better i18n.
**Languages updated:** {languages}
**Keys added:** {keysAdded}
**Keys updated:** {keysUpdated}
**Keys removed:** {keysRemoved}
---
_Generated by Better i18n GitHub Sync_
Webhook Events
For custom integrations, you can subscribe to sync events via webhooks:
sync:
webhooks:
- url: "https://your-server.com/api/i18n-webhook"
events:
- sync.completed
- sync.failed
- pr.created
- pr.merged
secret: "${WEBHOOK_SECRET}" # Set in Better i18n dashboard
Real-World Example: E-Commerce App Setup
Here is a complete configuration for a typical e-commerce application with Next.js:
# better-i18n.yml
sync:
sourceLanguage: en
targetLanguages:
- es
- fr
- de
- ja
- "pt-BR"
- "zh-CN"
paths:
- "messages/{locale}/{namespace}.json"
baseBranch: main
delivery:
method: pr
branchPattern: "i18n/translations-{date}"
autoMerge: true # Auto-merge when CI passes
reviewers:
- "@frontend-team"
labels:
- "i18n"
- "auto-merge"
batchWindow: 15 # Batch changes within 15 minutes
ai:
autoTranslate: true
requireApproval: false
instructions: |
E-commerce context:
- "Cart" should use shopping cart terminology (not vehicle)
- Currency amounts should not be translated (they are formatted by code)
- Product names in {curly braces} are variables — do not translate
- Use formal register for German and Japanese
- Use informal register for Spanish and Portuguese
- Keep "Better i18n" as-is in all languages
featureBranchSync:
enabled: true
targetBranches: ["main", "staging"]
followBranch: true
With this configuration:
- Every push to
mainorstagingtriggers a sync - New keys are AI-translated within minutes
- Translations arrive as auto-mergeable PRs
- Feature branches get their own translation PRs
- CI validates coverage and syntax before merge
Conclusion
GitHub Translation Sync transforms localization from a manual, error-prone process into an automated pipeline that fits naturally into your existing development workflow. The key principles:
- Translations live in Git — full history, branch protection, code review
- Sync is automatic — no manual push/pull commands to forget
- PRs enable review — translations are reviewed like any other code change
- CI enforces quality — coverage thresholds and syntax validation block bad translations
- AI accelerates — new keys are translated in minutes, not days
Set up takes about 30 minutes. The time saved in the first week alone typically exceeds the setup investment.
Ready to automate your translation workflow? Get started with Better i18n and connect your GitHub repository in minutes.