Configuring Weblate Webhooks for Auto-Translation
Configuring Weblate webhooks for auto-translation means wiring a repository push to the /hooks/update/ endpoint so Weblate pulls new source strings and fires the auto-translate addon without manual intervention. The friction it removes is real: manual string extraction and periodic batch syncs leave localization files stale, drift source context between cycles, and stall feature rollouts for international markets. This page walks the precise sequence — repo notify hook, automatic receiving, push-on-commit, and the auto-translate addon trigger — and the failure modes that quietly break each link in that chain.
The hardest part to reason about is ordering: a push must finish, Weblate must receive and match the notification, the VCS pull must succeed, and only then can the addon run. Skip a step and you get silent no-ops — a 200 OK from the hook while nothing translates. The diagram below shows where each signal hands off to the next.
A correctly wired loop builds on the broader Weblate Self-Hosted Setup and feeds the wider Translation Workflows & CI/CD Pipeline Sync strategy by keeping localization in lockstep with every merge.
Root Cause Analysis: Why the Hook Fires But Nothing Translates
Weblate exposes a generic /hooks/update/ HTTP endpoint that triggers a VCS pull for every component whose configured repository URL matches the incoming notification. The endpoint is deliberately thin: it does not parse a files_changed list the way GitHub’s native webhook does. Component file masks live in the Weblate admin UI, not in the payload. That design is the source of most “it returns 200 but nothing happens” reports.
Three independent conditions must all hold:
- Repository URL match. Weblate compares the
repositoryfield against the repo registered on each component, including scheme and.gitsuffix.https://github.com/org/repoandhttps://github.com/org/repo.gitare treated as different strings. A near-miss yields a processed hook with zero matched components — a200, never an error. - Receiving enabled. Weblate must accept anonymous notify hooks. The relevant settings are
ENABLE_HOOKS = Trueplus per-host control throughWEBLATE_GET_HELP/ALLOWED_HOSTS; behind a reverse proxy you also need correctX-Forwarded-*handling or the source IP filter drops the request. - Auto-translate addon present. The notify hook only pulls source strings. It does not machine-translate by itself. You must add the Automatic translation addon (
weblate.autotranslate.autotranslate) to the component and set it to run on new strings, or hook a post-update component step.
Miss any one and the chain looks healthy from the SCM side while the translation queue stays empty. The fix is to verify all three in order, top to bottom.
Minimal Reproducible Example
The smallest payload Weblate’s generic endpoint accepts is a single repository URL:
{
"repository": "https://github.com/org/repo.git"
}
Fire it by hand to isolate the receiver from your CI:
curl -i -X POST \
-H "Content-Type: application/json" \
-d '{"repository":"https://github.com/org/repo.git"}' \
https://weblate.example.com/hooks/update/
If you get 200 OK but the component’s Repository status page shows no new pull, the URL did not match a component — the classic silent failure. An optional branch field narrows which components update; omit it and every component on that repo updates.
The Fix: Push-on-Commit Plus the Auto-Translate Addon
Wire the notify hook from CI after validation, then let the addon translate. Doing it in this order keeps broken source strings out of translation memory.
- Validate before notifying. Run key extraction (for i18next projects, see extracting translation keys with i18next-parser) and your build, then dispatch the hook only on green.
- Send the notify hook from CI on protected branches only.
- Enable the auto-translate addon on the component so new keys are filled on update.
- Push translations back on commit with
WEBLATE_AUTO_COMMIT/commit_pendingso machine output lands in the repo.
Here is the corrected GitHub Actions step, with annotations on the non-obvious lines:
name: Notify Weblate
on:
push:
branches: [main, "release/*"] # protected branches only — never feature pushes
paths-ignore:
- "**/*.md"
- "docs/**"
jobs:
notify-weblate:
runs-on: ubuntu-latest
steps:
- name: POST notify hook
run: |
# Use the exact registered repo URL, including the .git suffix,
# or Weblate matches zero components and silently returns 200.
curl -fsS -X POST \
-H "Content-Type: application/json" \
-d "{\"repository\":\"https://github.com/${{ github.repository }}.git\",\"branch\":\"${{ github.ref_name }}\"}" \
https://weblate.example.com/hooks/update/
# -f makes curl exit non-zero on 4xx/5xx so the job actually fails.
The auto-translate addon is the piece most teams forget. Configure it on the component so it runs when the update pull brings in untranslated keys:
# Component addon: weblate.autotranslate.autotranslate
mode: translate # fill empty strings (not "fuzzy" which only suggests)
filter_type: todo # only act on untranslated entries
auto_source: mt # source machine-translation engines
engines:
- weblate-translation-memory # reuse TM first
- deepl # then MT fallback
threshold: 80 # min MT confidence to accept a suggestion
mode: translate is what actually writes strings; mode: suggest only proposes them and leaves the queue looking idle. filter_type: todo keeps the addon from re-touching reviewed translations on every push.
Verification Snippet
Prove the loop end-to-end from the command line. After firing the hook, poll the component statistics API and assert the untranslated count dropped:
# 1. Trigger
curl -fsS -X POST -H "Content-Type: application/json" \
-d '{"repository":"https://github.com/org/repo.git"}' \
https://weblate.example.com/hooks/update/
# 2. Wait for the Celery worker, then check remaining untranslated strings
sleep 15
curl -fsS -H "Authorization: Token $WEBLATE_TOKEN" \
"https://weblate.example.com/api/components/proj/comp/statistics/" \
| jq '.translated_percent' # expect this to rise after auto-translation
Cross-reference Admin → Audit Log, filtered by Hook and Auto-translation: a healthy run shows the hook completed and the addon producing translated entries. A 400 there means the repository URL matched no component — go back to root cause #1.
When to Escalate
This webhook approach assumes the auto-translate work fits in a synchronous-ish pull. If pulls stall, translations sit in queued forever, or the worker never picks up jobs, the problem is no longer your webhook wiring — it is the background queue. A stuck Celery worker masquerades as a broken hook, so when verification shows the pull succeeded but no strings translate, move to debugging the Weblate Celery queue and stuck translations. Likewise, if you are deciding whether self-hosting this loop is worth the operational cost versus a managed alternative, weigh it in Crowdin vs Weblate for self-hosted teams. For the underlying server, addon, and repository configuration, return to the parent Weblate Self-Hosted Setup.
FAQ
Why does my Weblate webhook return 200 but no translation happens?
A 200 OK from /hooks/update/ only means the notification was received, not that a component matched or that anything translated. The most common cause is a repository URL that does not exactly match the one registered on the component — https://github.com/org/repo versus https://github.com/org/repo.git are different strings. Confirm the match in the component’s Repository status page, and remember the notify hook never machine-translates on its own; you still need the auto-translate addon enabled.
What is the difference between the notify hook and the auto-translate addon?
The notify hook (/hooks/update/) tells Weblate to pull updated source strings from VCS and scan the component for new keys. The Automatic translation addon (weblate.autotranslate.autotranslate) is a separate component step that actually fills those new, untranslated keys from translation memory or a machine-translation engine. The hook is the trigger; the addon does the work. You need both, and the addon must use mode: translate rather than mode: suggest to write strings.
Should I trigger auto-translation on every branch?
No. Dispatch the notify hook only from protected branches such as main, release/*, or dedicated l10n/* branches, and only after linting, key extraction, and the build pass. Triggering on broken or in-progress source strings pollutes translation memory and forces manual cleanup, so use paths-ignore and branch filters in your CI to keep feature-branch noise out of the translation queue.
Related
- Weblate Self-Hosted Setup — the server, addon, and repository configuration this hook depends on.
- Extracting translation keys with i18next-parser — the pre-hook validation step that keeps broken keys out of the queue.
- Weblate Celery queue and stuck translations — when the pull succeeds but jobs never run.
- Crowdin vs Weblate for self-hosted teams — choosing whether to self-host this loop at all.
- Connecting Crowdin API to GitHub pull requests — the equivalent event-driven sync on Crowdin.
Part of Weblate Self-Hosted Setup.