Skip to main content

llm-localize

Fill in missing translations in your i18n files with an LLM, without breaking them.

The core has run in production in CommCare HQ for about two years. It has one dependency, polib, and calls OpenAI, Anthropic, Gemini, xAI or a local model (Ollama, vLLM, LM Studio) over plain HTTP, with no provider SDKs.

LLMs translate well but are careless with the parts of a message that aren't prose. llm-localize sends only untranslated messages, then checks each translation before it is written:

  • Placeholders (%s, %(name)s, {name}, Android's %1$s, iOS's %@) must match the source exactly. Named and numbered ones may move; unnamed ones (%s, {}) must stay in order.
  • HTML tags must keep the same names, order and attributes. Only text attributes (title, alt, placeholder, aria-label, aria-description, and value on an <input> button) may change value, and the content of <script> and <style> must stay as it is. Numbered tags, like react-i18next's <1>…</1>, must all be kept, in any order.
  • URLs (http, https, ftp, mailto:, tel:, www.) and email addresses must be unchanged, and none may be added.
  • Control characters that the model invented (e.g. — corrupted into \x14) are rejected.
  • Plurals are translated once per plural form of the target language. The model is told which numbers each form covers ("used when n is 2, 3, 4, 22, …"), worked out from a PO file's Plural-Forms header, or from the Unicode CLDR plural rules for formats that name forms one, few, many, other.
  • ICU messages like {count, plural, one {# file} other {# files}} (in JSON and XLIFF) must keep their arguments and select cases, and get a plural case for each category the target language uses.
  • Format checks. PO files are run through msgfmt --check afterwards, and any entry it rejects is blanked. Errors no translation caused, like a message defined twice, stop with an error instead. A PO file without a real charset, or in another one, is saved as UTF-8, so any translation fits.

A translation that fails a check is dropped, so that message stays untranslated and is retried on the next run. Fuzzy entries are retranslated and lose their fuzzy flag.

In this run the model's French is fine, but three of its five translations would have broken the page: a renamed placeholder, a changed link and a dropped tag. Only the two good ones are written:

$ llm-localize locale/fr/LC_MESSAGES/messages.po -v
Translating locale/fr/LC_MESSAGES/messages.po to fr
Rejected (placeholders): 'Hello, %(name)s!' -> 'Bonjour, %(nom)s !'
Rejected (links): 'Read the <a href="https://example.com/docs">documentation</a> first.' -> 'Lisez d\'abord la <a href="https://example.fr/docs">documentation</a>.'
Rejected (HTML tags): 'Open <b>Settings</b> to change your plan.' -> 'Ouvrez Paramètres pour changer votre forfait.'
locale/fr/LC_MESSAGES/messages.po: 1/1 batches done, 2/5 translated
No msgfmt errors in locale/fr/LC_MESSAGES/messages.po
locale/fr/LC_MESSAGES/messages.po (fr): translated 2 of 5
  rejected 3: placeholders 1, links 1, HTML tags 1

It runs on its own as a command line tool, or as a Django app. The engine is format-agnostic: it ships with gettext PO, JSON, Android, iOS, XLIFF, Markdown and Word formats (see Formats), and others plug in as small classes (see Adding a format).

Install

uvx llm-localize locale/fr/LC_MESSAGES/messages.po   # try it without installing
pip install llm-localize             # command line tool and Python API
pip install "llm-localize[django]"   # plus the Django app
uv tool install llm-localize         # just the command, in its own environment

The only dependency is polib; the django extra adds Django. LLM APIs are called over plain HTTP with the standard library, so no provider SDK is needed. Install gettext if you want the msgfmt check (brew install gettext, apt install gettext).

Quick start

export OPENAI_API_KEY=...          # or ANTHROPIC_API_KEY, GEMINI_API_KEY, XAI_API_KEY with --provider
llm-localize locale/fr/LC_MESSAGES/messages.po
llm-localize locales/fr.json locales/de.json          # from locales/en.json
./manage.py makemessages -a --translate               # Django, with llm_localize.contrib.django installed
llm-localize messages.po --api-base http://localhost:11434/v1 --model qwen3   # a local model with Ollama, no key needed

Documentation

The full documentation is at llm-localize.readthedocs.io:

Privacy and cost

Your source strings and their context comments are sent to the LLM provider. Only untranslated, fuzzy or broken messages are sent, so a run after the first one costs little.

Development

uv sync --group dev
uv run pytest
uv run ruff check .

# The core without Django, as CI runs it
uv pip uninstall django pytest-django && uv run --no-sync pytest -p no:django
uv sync --group dev   # to put them back

The documentation is built with MkDocs: uv run --with-requirements docs/requirements.txt mkdocs serve.

License

MIT, see LICENSE. The table of language names from Django is under the BSD 3-Clause license, in licenses/Django-LICENSE.txt. Data from the Unicode CLDR, like plural rules, is under the Unicode License v3, in licenses/Unicode-LICENSE.txt.

Metadata

Release files for llm-localize 0.1.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for llm-localize 0.1.2
File Size Uploaded Provenance Actions
llm_localize-0.1.2.tar.gz 215.3 kB With provenance Details for llm_localize-0.1.2.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for llm-localize 0.1.2
File Interpreter ABI Platform Actions
llm_localize-0.1.2-py3-none-any.whl Python 3 none any Details for llm_localize-0.1.2-py3-none-any.whl

Total release size: 345.4 kB

Release files / llm_localize-0.1.2.tar.gz

Download URL llm_localize-0.1.2.tar.gz
Size 215.3 kB
Tags Source
SHA-256 checksum
How to use checksums
30f54c2e772870a6147f098fd51dea958db31dd6513d059f22859a76f03fcb40
BLAKE2b-256 checksum
How to use checksums
a8d155f3d1a61e6a1d8b30382920877c870fcd505caf960bdada09d3be0bf789
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 4, 2026.

Transparency log

Release files / llm_localize-0.1.2-py3-none-any.whl

Download URL llm_localize-0.1.2-py3-none-any.whl
Size 130.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d30b573dc06c458ee94d2981fce1f8cb8466f40f0b9de43dfe721f8ac06b5646
BLAKE2b-256 checksum
How to use checksums
5a738247dd8494fd7dca9d6769ad31cfb55c0aa06e4fc4db1d193b93094c4b53
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 4, 2026.

Transparency log

Security for llm-localize 0.1.2

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

Both files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for llm_localize-0.1.2.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 4, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page