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, andvalueon 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-Formsheader, or from the Unicode CLDR plural rules for formats that name formsone,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 --checkafterwards, 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:
- Getting started
- Tutorials: Django, Flask, React with i18next, Vue with vue-i18n, Next.js with next-intl, Angular
- Translating in CI
- Reference: command line, formats, LLM providers, Django, Python API, output and errors, extending
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)
| 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)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| 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 logRelease 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 logSecurity 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.
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 logA 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.