Skip to content
yf_
← Writing

Localebot: localization without touching your codebase

3 min read
  • Localization
  • GitHub
  • Ruby on Rails
  • Architecture

A pull request adds checkout.promo_code to en.json. fr.json and es.json don’t get it. The PR merges, and depending on the i18n setup, production shows English text or a raw key.

Localebot catches that on the pull request itself. It’s a GitHub App I built. When a PR changes a source locale file, it comments with a link to a review session listing every string that’s missing or changed in the target languages. Translators fill them in, or ask an AI model for a suggestion and review it. Then Localebot opens a second PR with the translations, aimed at the author’s branch.

What it fixes

  • Missing translations show up on the PR that introduced them, not after release.
  • Translators work in a web editor. They don’t need git, and developers don’t paste strings into a spreadsheet.
  • AI models can mangle placeholders. Localebot extracts tokens like %{count} and {{name}} from the source string, tells the model to keep them, and checks the output. The editor flags any translation that drops or renames one.
  • Brand terms stay consistent. A glossary of approved translations goes into the AI prompt as a rule: “Workspace” must be translated as “Espace de travail” in French.

Mostly stateless

There’s no clone and no sync job. Each webhook runs the same sequence:

  1. Fetch three files from the GitHub API: the source locale at the PR’s base commit, the source at its head commit, and each target at head.
  2. Flatten them into key paths like auth.login.title, in memory.
  3. Mark a key missing if the target lacks it or it’s blank. Mark it modified if the target has a value but the source text changed since the base.
  4. Update the session and the PR comment.

There’s no translation memory and no ledger of what’s done. Whether a string is translated is whatever the target file says at the head commit, recomputed on every push.

It isn’t fully stateless, though. Two things persist:

  • A session per PR. It holds source text, drafts and AI suggestions while the PR is open. A push rebuilds it from the diff: keys still in the diff keep their drafts, the rest are dropped. Merging or closing the PR archives it.
  • Settings: the repo, the locale file paths, the glossary, and the AI provider key (encrypted at rest).

Files are parsed in RAM and discarded. Apart from the PR comment, submitting is the only write to the repo. A job deep-merges the translations into the target files, commits them to localebot/pr-<n>, and opens the PR. Localebot ignores PRs from its own branches, so that second PR doesn’t start another session.

Nothing to change in the repo

  • No SDK, gem or npm package. i18next, Rails I18n, gettext and the rest keep working as they are.
  • No config file either. The source and target paths go into Localebot’s UI, and each one is checked against the default branch through the GitHub API as it’s entered.
  • The parsers handle JSON, YAML, PO, Android XML, Apple strings, Flutter ARB, Java properties and CSV. Translations are written back into the existing file without restructuring it.
  • Leaving means revoking the GitHub App. The only trace in the repo is the merged [Localebot] Translations for PR #42 commits.

The access it needs is read and write on contents and pull requests, through GitHub App installation tokens that expire after an hour.