Webhook icon
If icon
Exit icon
Request icon
Log icon
WorkingDirectory icon
Clone icon
Script icon
Loop icon
ChatCompletion icon
Anthropic icon
SlackIncomingWebhook icon
Pause icon
PushExecutionFiles icon
Create icon

i18n Translation Sync with AI-Translated Missing Keys, Placeholder Checks and Approval

Keep i18n JSON files in sync. AI translates missing keys per language in parallel, placeholders are checked, and a PR opens after approval.

Categories
AIBusiness

A developer adds new strings to en.json, and every other language file is immediately out of date. The missing keys ship as English or as raw keys until someone notices. Machine translation helps, but a single dropped {name} or broken <bold> tag crashes the screen or shows garbage. This blueprint keeps every language file in sync on each push and never lets a broken placeholder through. It's for product teams and open-source projects with i18next, vue-i18n, next-intl or any other JSON-based localization.

How it works

  1. on_push (io.kestra.plugin.core.trigger.Webhook) receives GitHub push events. check_event (io.kestra.plugin.core.flow.If) stops for the ping and for pushes to any branch other than base_branch.
  2. open_sync_pr (io.kestra.plugin.core.http.Request) looks for an open i18n-sync/<base_branch> pull request, and guard stops while one is open, so keys are never translated or proposed twice. base_head pins the commit every later step reads.
  3. find_missing (io.kestra.plugin.core.flow.WorkingDirectory) clones that commit (io.kestra.plugin.git.Clone). missing (io.kestra.plugin.scripts.python.Script) flattens nested keys and lists the keys each language file lacks. When nothing is missing, the run ends here without a model call.
  4. per_language (io.kestra.plugin.core.flow.Loop, three languages at a time) sends each language's batch to translate_batch (io.kestra.plugin.ai.completion.ChatCompletion with Anthropic), which keeps keys, placeholders, tags and ICU syntax intact.
  5. merge validates every translation deterministically. The key must have been requested, the value can't be empty, and the placeholders ({name}, {{count}}, %s, %(n)d, tags and ICU variables) must match the English exactly. Accepted keys are written into each file in the source's key order, keeping its indentation and any keys only that language has.
  6. ask_reviewer (io.kestra.plugin.slack.notifications.SlackIncomingWebhook) posts a link, and approval (io.kestra.plugin.core.flow.Pause) waits for a decision: open the PR or discard. Without a decision within three days, the execution is cancelled.
  7. On approval, push_branch (io.kestra.plugin.git.PushExecutionFiles, delete: false) commits only the changed language files to i18n-sync/<base_branch>. open_pr (io.kestra.plugin.github.pulls.Create) opens the PR with a per-language table and the rejected keys for a human to translate.
  8. alert_failure posts any failure to Slack. Nothing is pushed before approval, so a failed run can simply be run again.

The repository is the state: whatever is still missing is recomputed on every run, so no KV store is needed. Strings travel as base64 between tasks, so placeholders like {{count}} are never evaluated as Kestra expressions.

Prerequisites

  • A GitHub repository with JSON language files, flat or nested, and a repository webhook for push events with content type application/json, pointing at https://<your-kestra>/api/v1/main/executions/webhook/company.team/i18n-translation-sync/<I18N_SYNC_WEBHOOK_KEY>.
  • Docker on the worker for the Python script tasks.
  • Secrets:
    • GITHUB_TOKEN: a token for the repository. It lists pull requests, clones, pushes the i18n-sync/<base_branch> branch and opens the PR. A fine-grained token needs Contents: read and write and Pull requests: read and write. It's also used as the HTTPS username, which GitHub accepts.
    • ANTHROPIC_API_KEY: Anthropic API key for translate_batch.
    • SLACK_WEBHOOK_URL: Slack incoming webhook for the review request and failure alerts.
    • I18N_SYNC_WEBHOOK_KEY: a long random string you create yourself, for example with openssl rand -hex 16. It forms the secret part of the webhook URL.

Inputs

  • repository (STRING, default your-org/your-app): owner/name. The webhook fills it in from the event.
  • base_branch (STRING, default main): the branch whose pushes are synced and that the PR targets.
  • source_file (STRING, default locales/en.json): the language every other file follows.
  • locale_glob (STRING, default locales/*.json): the language files to keep in sync. The source file is skipped.
  • glossary (STRING, default empty): terms to keep or translate a fixed way, such as Kestra=Kestra; flow=flow.
  • max_keys_per_language (INT, default 200): the most keys translated per language in one run, which caps the cost.
  • dry_run (BOOL, default false): translate and validate, then stop. No review request, branch or PR.

Quick start

  1. Add the four secrets and save the flow. Adjust source_file and locale_glob to your layout.
  2. Run it manually with repository set and dry_run set to true, then read the summary and the edited files in the merge outputs.
  3. Run it again with dry_run set to false, approve in the paused execution, and review the PR.
  4. Add the repository webhook so every push to base_branch is synced.

Outputs

  • {{ outputs.missing_keys }} / {{ outputs.missing.vars.total_missing }}: keys missing across all languages. {{ outputs.missing.vars.languages }} breaks it down per language.
  • {{ outputs.translations_added }} / {{ outputs.merge.vars.added }}: translations that passed the placeholder check. {{ outputs.merge.vars.rejected }} and rejected_details list the rest.
  • {{ outputs.merge.vars.summary }}: a per-language table of added, rejected and still missing keys.
  • {{ outputs.merge.outputFiles }}: the edited language files.
  • {{ outputs.pull_request_url }} / {{ outputs.open_pr.pullRequestUrl }}: the translation PR.

Common pitfalls

  • Only missing keys are translated. When an English string changes but keeps its key, the other languages keep their old translation. Rename the key, or delete it from the other files, to have it translated again.
  • While an i18n-sync/<base_branch> PR is open, runs stop at guard. Merge or close it, and the next push syncs the rest.
  • Rejected keys are listed in the PR. They usually mean the model rewrote a placeholder or tag, so translate them by hand.
  • Only JSON is supported. YAML, .po or .arb files need a different parser in missing and merge.
  • Set the claude_model variable to change the model.

How to extend

  • Pass a glossary from a file in the repository, or from the KV store, for brand terms.
  • Request reviewers per language with the reviewers property of open_pr.
  • Run it from a nightly Schedule instead of a webhook to batch the day's changes into one PR.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.