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.

The three gates a Weblate notify hook must clear to auto-translate A repository push enters the /hooks/update/ pipeline. Three independent conditions must all hold, checked in order: the incoming repository URL must exactly match a component (scheme and .git suffix included), notify-hook receiving must be enabled, and the Automatic translation addon must be present. Only when all three pass do new strings get translated and committed back to VCS. If any single gate fails, Weblate still returns 200 OK but nothing translates and the queue stays empty. All three conditions must hold — verify top to bottom Repo push main / release/* 1 · URL match org/repo.git 2 · Receiving on ENABLE_HOOKS 3 · Addon on autotranslate Translated + committed Hook still returns 200 OK nothing translates · queue stays empty
The notify hook clears three independent gates in order — repository URL match, receiving enabled, then the auto-translate addon. Fail any one and Weblate returns 200 OK while the translation queue stays empty.

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.

The three gates a notify hook must clear If the repository notify hook does not arrive, Weblate never re-scans the repository and nothing appears to happen. If the component update does not run, new strings are never parsed and the string count stays put. If the auto-translate addon is not installed on the component, the new units appear but stay empty. Three gates between a push and a translation If it fails You see Repository notify hook the repo is never re-scanned nothing at all Component update new strings are never parsed an unchanged string count Auto-translate addon units stay empty new strings, no suggestions
The three failures look different — that is what lets you tell them apart without reading logs.

Three independent conditions must all hold:

  1. Repository URL match. Weblate compares the repository field against the repo registered on each component, including scheme and .git suffix. https://github.com/org/repo and https://github.com/org/repo.git are treated as different strings. A near-miss yields a processed hook with zero matched components — a 200, never an error.
  2. Receiving enabled. Weblate must accept anonymous notify hooks. The relevant settings are ENABLE_HOOKS = True plus per-host control through WEBLATE_GET_HELP / ALLOWED_HOSTS; behind a reverse proxy you also need correct X-Forwarded-* handling or the source IP filter drops the request.
  3. 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.

From git push to auto-translated unit The git host posts to the Weblate hook endpoint, Weblate enqueues a component update task, a Celery worker pulls the repository and re-parses the component, newly detected units are handed to the auto-translate addon, and the addon fills them from machine translation or the existing memory. A push that ends in a filled unit Git host Weblate Celery Addon POST /hooks/github enqueue update task pull, re-parse new units detected units filled from MT
Everything after the first arrow happens in Celery — with no worker, the hook still returns 200.
  1. 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.
  2. Send the notify hook from CI on protected branches only.
  3. Enable the auto-translate addon on the component so new keys are filled on update.
  4. Push translations back on commit with WEBLATE_AUTO_COMMIT / commit_pending so 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.

Part of Weblate Self-Hosted Setup.