Integrating Weblate's Git Branching into CI/CD: An Architecture Note
Architecture note for integrating Weblate's Git branching and pull-request workflow into CI/CD: minimal design, trust boundaries, operational checks, failure modes, and redesign triggers with a concrete GitHub Actions example.
13 Nov 2025, 22:39 UTC

The Problem: Translation Workflows Break CI/CD Assumptions
Most CI/CD pipelines assume source code flows in one direction: feature branch → pull request → merge → deploy. Weblate inverts this for translations. Translators work on Weblate-hosted branches that must merge back into feature branches before the PR lands, or they create their own PRs that the pipeline must gate. If you treat translation branches like code branches, you get merge conflicts in .po files, missing keys in new branches, and pipeline stalls when the Git host is unreachable.
Takeaway: Map each Weblate translation project to a dedicated Git branch, enforce branch protection on the upstream repo, and drive the pipeline with webhook-validated merge events—not polling.
Requirements
- Source repository hosted on GitHub, GitLab, or Bitbucket with branch protection and required status checks.
- Weblate instance (self-hosted or cloud) with write access to the repository via a dedicated service account.
- Translation files in a format Weblate understands (gettext
.po, XLIFF, JSON, etc.) located at a stable path per project. - CI pipeline that can run on pull-request events and expose a merge gate (GitHub Checks, GitLab Merge Request pipelines, etc.).
- Operational SLA: translation sync latency < 15 minutes, merge conflict resolution < 1 hour during business hours.
Minimal Design
Single Weblate Instance, Per-Project Branch Mapping
The smallest viable deployment uses one Weblate instance. Each translation project points to the same Git remote but a different branch:
# Weblate project configuration (via UI or API)
Repository URL: [contact removed]:org/repo.git
Branch: l10n/feature-xyz # unique per translation project
Push branch: l10n/feature-xyz
File mask: locale/*/LC_MESSAGES/app.po
Weblate commits translator changes directly to l10n/feature-xyz. A GitHub Actions workflow (or GitLab CI job) watches for pushes to l10n/* branches and opens a pull request targeting the corresponding feature branch feature-xyz.
Webhook-Driven Pipeline Trigger
Configure the Git host to send pull_request events to your CI webhook endpoint only when the head ref matches l10n/*. In GitHub Actions:
on:
pull_request:
types: [opened, synchronize, reopened]
branches:
- 'feature/*'
paths:
- 'locale/**/*.po'
The CI job runs translation validation (msgfmt, placeholder checks, glossary compliance) and posts a status check. The branch protection rule on feature-xyz requires this check to pass before merge.
Trust and Data Boundaries
| Boundary | Who Trusts Whom | Data Flow | Risk if Compromised |
|---|---|---|---|
| Weblate → Git host | Weblate trusts Git host for commit authenticity (SSH key / token verification) | Translation commits pushed to l10n/* branches | Malicious commits injected into translation branches |
| CI → Weblate | CI trusts Weblate's webhook payload (validated via HMAC secret) | Webhook triggers CI on PR open/sync | Spoofed webhook runs arbitrary pipeline steps |
| Translator → Weblate | Weblate enforces RBAC; translators never touch Git directly | Translations entered in Weblate UI | Privilege escalation if Weblate auth bypassed |
| Source code → Translations | One-way: source strings extracted to Weblate; translations never modify source | Extraction script runs in CI, pushes .pot to Weblate via API | Source code tampering via translation pipeline |
Weblate does not modify source code beyond translation commits. The extraction step (running xgettext or equivalent) lives in your CI, not Weblate.
Operational Checks
Automated
- Branch sync health: Daily cron job compares
l10n/*branch tip with Weblate's reported last commit. Alert if drift > 30 minutes. - Translation coverage: CI job runs
msgfmt --statisticson each.pofile; fails if any locale drops below 80% translated. - Missing key detection: On new feature branch creation, script extracts keys from source and diffs against
l10n/feature-xyz; opens GitHub issue for missing keys. - Webhook delivery: Git host webhook delivery logs monitored for 5xx responses; alert on > 3 failures/hour.
Manual (Weekly)
- Review Weblate's Repository synchronization log for
git pusherrors. - Spot-check three random translation projects for glossary consistency.
- Verify service account SSH key / PAT hasn't expired (rotate quarterly).
Failure Modes and Responses
| Failure | Detection | Response | Rollback Needed? |
|---|---|---|---|
Merge conflict in .po files | CI status check fails with CONFLICT markers | Weblate shows conflict resolver UI; translator resolves, pushes new commit; CI re-runs | No—branch not merged yet |
| Missing keys in new branch | Coverage job reports 0% for new locale | Extraction script adds keys to .pot; Weblate sync pulls them; translators fill | No |
| Git host network outage | Weblate sync log shows ssh: connect to host github.com port 22: Connection timed out | Weblate retries every 10 min (configurable REPOSITORY_SYNC_INTERVAL); alert after 3 failures | No—local commits queued |
| Weblate server crash | Health endpoint /healthz/ returns 503; translators report UI down | Restore from backup (PostgreSQL + media volume); DNS failover to standby if HA deployed | Only if DB corrupted—restore last clean backup |
| Spoofed webhook triggers malicious pipeline | HMAC validation fails in CI webhook handler | Reject payload; alert security; rotate webhook secret | No state change |
Concrete Example: GitHub Actions Workflow
File: .github/workflows/translation-gate.yml
name: Translation Gate
on:
pull_request:
types: [opened, synchronize, reopened]
branches: [ 'feature/*' ]
paths:
- 'locale/**/*.po'
jobs:
validate-translations:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
checks: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.head_ref }}
- name: Install gettext
run: sudo apt-get update && sudo apt-get install -y gettext
- name: Validate all .po files
run: |
set -euo pipefail
for f in locale/*/LC_MESSAGES/app.po; do
echo "Checking $f"
msgfmt --check --statistics -o /dev/null "$f" || exit 1
done
- name: Check placeholder parity
run: |
python scripts/check_placeholders.py locale/*/LC_MESSAGES/app.po
- name: Post coverage comment
if: always()
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const out = fs.readFileSync('coverage.txt', 'utf8');
github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
body: '📊 Translation coverage\n' + out
});
Where to run: GitHub Actions runner (Ubuntu). Permissions: contents: read to fetch the PR branch; pull-requests: write and checks: write to post status and comments. Placeholders: app.po matches your file mask; adjust locale/*/LC_MESSAGES/ to your structure. Expected check: Workflow shows green check on PR; merge button enabled only when green. Risk: fetch-depth: 0 increases clone time for large repos—consider shallow fetch with git fetch --unshallow if history > 5k commits.
Redesign Triggers
Re-architect when any condition holds:
- > 50 translation projects: Single Weblate instance becomes a bottleneck (DB contention, sync queue latency). Move to multiple Weblate instances behind a load balancer with project sharding.
- Multi-tenant isolation required: Different teams need separate Weblate RBAC, data residency, or custom MT engines. Deploy per-tenant Weblate on Kubernetes with namespace isolation.
- GDPR data-subject requests: Translator PII (email, IP) stored in Weblate DB. Implement automated export/delete via Weblate's
/api/users/{id}/data/endpoint; verify quarterly. - External translation memory (TM) integration: Need to share TM across projects or with vendors. Add TM server (e.g., Weblate's built-in TM or external like TMate) and configure
MT_*settings per project.
Verification Checklist (Run Before Go-Live)
- Create feature branch
feature/test-l10n; push.pofiles with 2 missing keys. - In Weblate, create project mapped to
l10n/feature/test-l10n; translate one key. - Confirm Weblate pushes commit to
l10n/feature/test-l10nwithin 5 minutes. - Verify GitHub Actions workflow triggers, runs validation, posts coverage comment.
- Merge PR; confirm
feature/test-l10nnow contains translated strings. - Simulate GitHub outage: block egress to
github.comon Weblate host for 20 min; confirm queued commits push after restore. - Audit service account:
gh api /user --jq '.login'shows dedicated bot; token scopes limited torepoandworkflow.
Limitations
- This design assumes translation files live in the same repo as source. Monorepos with per-service translation directories work; polyrepos need per-repo Weblate projects.
- Large binary assets in translation repos (screenshots, audio) may exceed Git LFS defaults. Configure
lfs.fetchexcludeor split assets to object storage. - Concurrent edits on the same
l10n/*branch by multiple translators can race. Mitigate: enable Weblate's Lock translations setting and enforce branch protection requiring linear history. - Webhook HMAC validation code is not shown here—implement per your CI platform (GitHub:
X-Hub-Signature-256; GitLab:X-Gitlab-Token).
How to Know It's Working
After deployment, the dashboard you watch is simple: median time from translator save to green CI check on the target feature branch. Target < 10 minutes. If it drifts above 30, check Weblate sync interval, CI queue depth, and Git host rate limits. That single metric catches sync failures, validation regressions, and capacity problems without parsing logs.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.