Changelog¶
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[1.6.0] - 2026-10-02¶
Changed¶
TRANSLATEBOT_MODELdefaults togpt-6-lunainstead ofgpt-4o-mini, which dates from July 2024.gpt-6-lunais newer and cheaper per token ($0.10 / $0.50 per million input / output tokens, against $0.15 / $0.60). This only affects projects that don't setTRANSLATEBOT_MODEL. To keep the old model, setTRANSLATEBOT_MODEL = "gpt-4o-mini".gpt-6-lunais a reasoning model, so requests to it are sent withreasoning_effort="low"and withouttemperature.- Python 3.10: set
TRANSLATEBOT_MODELexplicitly (for exampleTRANSLATEBOT_MODEL = "gpt-4o-mini"). litellm is capped below 1.98 there, and those releases don't recognisegpt-6-lunaas a reasoning model: they dropreasoning_effortand still sendtemperature=0.2, which reasoning models don't accept, so translation requests with the default model are expected to fail. - litellm 1.100.3 or newer is required on Python 3.11+. Older releases drop
reasoning_effortforgpt-6-luna. - No litellm release includes
gpt-6-lunain its bundled model list yet; litellm downloads the current list when it's imported. Offline, or withLITELLM_LOCAL_MODEL_COST_MAP=True, the token limits ofgpt-6-lunacan't be looked up and batches fall back to 8192 input / 4096 output tokens, which means more API requests per run. - README: the "under $0.01 per language" cost estimate is removed. It was measured with
gpt-4o-mini. - README: leads with database content translation (django-modeltranslation), describes the 1.5.0 placeholder validation, gives
translate --modelsits own setup example, and corrects which settings can come from environment variables (only the API key).
[1.5.0] - 2026-10-02¶
Added¶
- PO translations are validated before they are written, using the rules
msgfmt --check-format(run bycompilemessages) applies topython-formatandpython-brace-formatentries: named placeholders and{fields}must match the source and keep their type (%(n)dcan't become%(n)s), positional%s/%dmust match in number and order, and a literal percent sign must stay%%. A{field}'s format spec must stay as in the source ({a:.2f}can't become{a:,.2f}or{a}), which gettext 0.21 (Debian/Ubuntu) requires. Plural forms are checked againstmsgid_pluraland may leave named placeholders out ("one file"), except in languages with a single form (ja, zh, …). Translations that are index markers (#1,#2, …) or empty are rejected too. - Rejected translations are retried once, on their own. If they fail again, they never break
compilemessages: an untranslated entry gets the translation as a draft marked#, fuzzy, whichcompilemessagesskips (unless run with--use-fuzzy), PO editors flag for review, and the next run translates again. A draft never replaces any existing translation, not even with--overwrite, and empty translations or index markers aren't written. The run ends with a list of every rejected translation, per PO file, andtranslate()reports their number inTranslateResult.strings_rejected. - A response that fails validation as a whole (wrong number of translations, invalid JSON) is retried once before the run stops.
Fixed¶
- Plural messages in single-form languages (ja, zh, …) get the plural translation in
msgstr[0]when the singular and plural are translated as two strings (always with DeepL; with LLM providers when the PO file has no usablePlural-Formsheader). It used to get the singular, which lacked the count placeholder and brokecompilemessages.
[1.4.1] - 2026-10-01¶
Fixed¶
- A blank
TRANSLATEBOT_API_KEY(an empty or whitespace-only string, e.g. fromenv("TRANSLATEBOT_API_KEY", default="")) is treated as not configured: it falls back to the environment variable, or stops with "API key not configured". It used to be sent as an emptyBearertoken, which failed as a misleading server error after two retries. Surrounding whitespace is stripped from the key. If you set an empty key for a provider that needs none (e.g. a local Ollama server), use any placeholder value instead, such as"unused".
[1.4.0] - 2026-10-01¶
Added¶
TRANSLATEBOT_TIMEOUTsetting: seconds to wait for each LLM API request (default 300). Previously no timeout was set, so a stalled provider could hangtranslateindefinitely.- LLM requests that time out, fail to connect, or get a server error (5xx) are retried twice, after 5 and 15 seconds, with a logged warning. The HTTP client's own silent retries are disabled so the timeout applies as configured; rate-limit retries now honour the provider's
Retry-Afterinstead of always waiting at least 60 seconds. - A warning is logged for PO files with plural entries but no usable
Plural-Formsheader (missing, or makemessages'nplurals=INTEGERplaceholder); their plural forms can't be translated individually.
Fixed¶
- PO files: messages that share a msgid but differ in
msgctxt(pgettext("month", "May")vspgettext("permission", "May")) are translated separately. They used to receive one shared translation, and the msgctxt is now sent to the LLM as a disambiguation hint. - PO files: languages with more than two plural forms (Polish, Russian, Czech, Arabic, …) get a correct translation for every form. LLM providers are given the target's plural forms from the
Plural-Formsheader, with example counts per form; previously the English plural was reused for every form past the first (5 plikiinstead of5 plików). DeepL keeps the previous behaviour. - PO files: a translation made with one app's
TRANSLATING.mdcould be written into another context group's file when that group saved an earlier batch first, after which the group's own translation was skipped. - PO files:
strings_translatedand the "✓ Translated" lines counted entries that were already translated in their own file but shared a msgid with an entry translated elsewhere. - Failed LLM requests (connection errors, timeouts, 5xx responses, unknown models), unparseable responses and exhausted rate-limit retries now stop with a clear error instead of a traceback.
Changed¶
- PO files: a message shared by several files with the same translation context (e.g.
django.poanddjangojs.po, or several apps) is sent for translation once, and written to each file that needs it. Counts are still per file. - The "Found N untranslated entries" and "Translating with …" lines are printed before translation starts instead of after all batches finished.
- README: Quick Start covers
LANGUAGES,TRANSLATEBOT_MODELand DeepL setup; lists the CI check, Python API and install extras. - Updated ruff from 0.16.8 to 0.16.9, pytest-mock from 3.15.1 to 3.16.0, and urllib3 from 2.7.0 to 2.8.0
[1.3.4] - 2026-09-21¶
Fixed¶
- modeltranslation backend: a translated
SlugFieldis stored as a slug again. Providers translate a slug as prose (moja-lijepa-kuca→My beautiful house); the result is now slugified (honouringallow_unicode) and cut to the field'smax_lengthat a word boundary. - DeepL: the source language of modeltranslation fields is now sent along instead of being auto-detected per text. Short strings were often misdetected (Croatian as Serbian or Slovenian), giving inconsistent results within one run. Rows sourced from different language columns are translated in separate requests.
- DeepL: all-caps text keeps its casing. DeepL's newer models (Croatian, Serbian, …) rewrite
STAN NA PRODAJUasApartment for sale; the casing is now restored without touching HTML tags, entities or format placeholders. A single short all-caps word (FAQ,OK) is treated as an acronym and left as translated.
Changed¶
- Updated ruff from 0.16.6 to 0.16.8
[1.3.3] - 2026-09-10¶
Fixed¶
- modeltranslation backend: rows created before modeltranslation was installed (or written via
queryset.update()/ imports) were invisible to translation discovery, because only the language-suffixed columns were queried. The original column is now included as a default-language source, with explicit language columns still taking priority over a stale original mirror. - modeltranslation backend: translations no longer clobber each other when a row has multiple translated fields.
bulk_updatereceived one instance copy per translated field with duplicate pks, so only the first field per row per batch survived; instances are now consolidated per row and updated grouped by their exact set of translated fields. - A
FileField's original column is never used as a translation source. - Capped litellm below 1.98 on Python 3.10 to fix an import error.
Changed¶
- Updated Django from 5.2.14 to 5.2.17
- Updated aiohttp from 3.13.4 to 3.14.3
- Updated sqlparse from 0.5.5 to 0.6.0
- Updated litellm, ruff, pytest, and pytest-django
- Refreshed
uv.lockwith latest compatible versions
[1.3.2] - 2026-05-24¶
Changed¶
- Updated Django from 5.2.13 to 5.2.14
- Updated litellm
- Updated urllib3 from 2.6.3 to 2.7.0
- Updated idna from 3.13 to 3.15
- Updated python-dotenv from 1.0.1 to 1.2.2
- Updated aiohttp from 3.13.3 to 3.13.4
- Updated ruff
- Refreshed
uv.lockwith latest compatible versions
[1.3.1] - 2026-04-17¶
Fixed¶
- Batching token budget: input and output token limits are now tracked separately via
litellm.get_model_info(), and the effective output budget is capped at 8K. Previously, models with large advertised output caps (e.g. gpt-5-mini at 128K) let hundreds of strings pack into one batch, triggering degraded structured output — returned-as-source strings, malformed JSON, and missing translations.
Changed¶
- Updated aiohttp from 3.13.3 to 3.13.4
- Updated litellm
- Refreshed
uv.lockwith latest compatible versions (charset-normalizer, regex, requests, rich, ruff, tzdata, yarl, and others)
[1.3.0] - 2026-04-13¶
Added¶
- Runtime LLM model override: pass
model="gpt-4o"totranslate()or--llm-model gpt-4oto thetranslatemanagement command to overridesettings.TRANSLATEBOT_MODELfor a single call. Useful for A/B testing models or running higher-quality translations on specific languages without mutating Django settings. - Raises
CommandErrorwhen themodelparameter is passed together with the DeepL provider, since DeepL has no model concept.
[1.2.0] - 2026-04-12¶
Added¶
translate()now returns aTranslateResultdataclass with statistics:strings_found,strings_translated,po_files,model_fields_found,model_fields_translated,target_langs, anddry_run
Changed¶
- Updated deepl
- Updated django from 5.2.12 to 5.2.13
- Updated litellm
- Updated pytest
- Updated ruff
Infrastructure¶
- Added
contents: readpermission to lint CI job
[1.1.1] - 2026-04-06¶
Fixed¶
- Fixed existing translations being overwritten when auto-translating across multiple PO files
[1.1.0] - 2026-03-26¶
Added¶
- Public Python API for programmatic translations (
translatebot.translate())
Fixed¶
- Fixed unsupported LLM parameters being passed to models that don't accept them
- Fixed ambiguous language codes in translation preamble causing incorrect target language
- Fixed LLM response validation to reject malformed JSON structures after parsing
- Strengthened JSON-only instruction in translation prompt to reduce non-JSON responses
- Fixed
--target-langflag to accept multiple values
[1.0.1] - 2026-03-19¶
Fixed¶
- Suppressed harmless Pydantic serialization warnings emitted by litellm during LLM API calls
[1.0.0] - 2026-03-19¶
Changed¶
- Marked project as stable (Production/Stable)
- Updated litellm from 1.82.2 to 1.82.4
[0.9.6] - 2026-03-16¶
Fixed¶
- Fixed DeepL placeholder strategy inconsistency across languages. All languages now use email-shaped placeholders (
ph0@tb.x, …) andtag_handling="html", replacing the previous split between newer-model and legacy-model strategies.
Changed¶
- Updated litellm from 1.82.1 to 1.82.2
- Updated ruff from 0.15.5 to 0.15.6
[0.9.5] - 2026-03-13¶
Fixed¶
- Replaced hardcoded list of DeepL newer-model languages with dynamic detection via the
supports_formalityAPI. Languages are now correctly identified at runtime, so newly added DeepL languages automatically get the right placeholder strategy. - Fixed
html.unescapecorrupting source text that intentionally contains HTML entities (e.g.<,&). Entity decoding now only reverses entities introduced by DeepL'stag_handling="html", leaving original entities intact.
Changed¶
- Updated litellm from 1.82.0 to 1.82.1
[0.9.4] - 2026-03-07¶
Fixed¶
- Fixed DeepL placeholder protection failing for Albanian, Bosnian, Croatian, Macedonian, and Serbian. These languages run on a newer DeepL model that mangles
<x>wrapper tags. Placeholders are now replaced with email-shaped tokens (ph0@tb.x,ph1@tb.x, …) that all models preserve.
[0.9.3] - 2026-03-07¶
Fixed¶
- Fixed DeepL
tag_handling="html"corrupting both format placeholders and real HTML in model field translations. Removed tag handling entirely and switched to<x>wrapper tags that DeepL preserves as opaque text.
[0.9.2] - 2026-03-07¶
Fixed¶
- Fixed source language being included when translating to all configured languages. When no
--target-langis specified, the source language (LANGUAGE_CODE) and its bare base code are now automatically excluded. - Fixed DeepL HTML tag handling mode encoding special characters (
<,>,&) as HTML entities in translation output. These are now decoded back to their original characters.
[0.9.1] - 2026-03-05¶
Fixed¶
- Fixed HTML being stripped from model field translations when using DeepL. Switched from XML to HTML tag handling mode, which properly preserves HTML tags, self-closing elements, and entities during translation.
- Fixed translated model fields not being saved when a translation batch failed partway through. Translations are now saved after each batch to prevent data loss.
[0.9.0] - 2026-02-26¶
Added¶
- Added support for
djangojs.pofiles, enabling translation of JavaScript catalog strings alongside standarddjango.pofiles. - PO file extracted comments are now passed to the LLM as translation context, giving the model access to developer hints and notes from source code (e.g.,
# Translators:markers).
Fixed¶
- Fixed DeepL mangling
{placeholder}style brace placeholders and%(name)sformat strings during translation. Placeholders are now wrapped in protective tags before sending to DeepL and restored afterward.
[0.8.1] - 2026-02-25¶
Added¶
- LiteLLM is now fully optional. DeepL-only users can uninstall
litellmto reduce dependency footprint. Attempting to use an LLM provider without litellm installed raises a clear error with install instructions.
Fixed¶
- Fixed plural forms (
msgid_plural/msgstr[n]) being silently ignored during translation. Plural entries are now properly translated and already-translated plurals are correctly skipped. - Fixed DeepL occasionally appending a trailing period to translated strings when the source string does not end with one.
Changed¶
- Updated litellm from 1.81.14 to 1.81.15
[0.8.0] - 2026-02-23¶
Added¶
- Added DeepL translation provider as an alternative to LiteLLM. Configure via
TRANSLATEBOT_PROVIDER = 'deepl'setting. Supports language code mapping, batching respecting DeepL limits, and comprehensive error handling. - Introduced
TranslationProviderABC for extensible provider architecture - Added
deeploptional dependency group (pip install translatebot-django[deepl])
Changed¶
- Refactored translation command to use provider abstraction for extensibility
- Extracted
batch_by_tokens()as a reusable utility function - Updated litellm from 1.81.13 to 1.81.14
[0.7.1] - 2026-02-21¶
Fixed¶
- Fixed batching off-by-one error in PO file and model translation paths where the overflowing item was incorrectly included in the current batch instead of starting a new one
Changed¶
- Enabled branch coverage measurement
- Updated ruff
[0.7.0] - 2026-02-19¶
Added¶
- Added
check_translationsmanagement command for CI pipelines. Checks all.pofiles for missing or fuzzy translations, exiting with code 1 on failure. Includes an optional--makemessagesflag to runmakemessagesbefore checking.
Changed¶
- Refactored locale directory discovery into a shared
_iter_locale_dirs()helper used by bothget_all_po_files()andget_all_po_paths() - Default locale fallback now uses
settings.BASE_DIRinstead of a relative path
[0.6.1] - 2026-02-18¶
Added¶
- Added
CONTRIBUTING.mdwith development setup, testing, and contribution guidelines - Added pepy.tech downloads badge to README
Changed¶
- Repositioned README messaging to differentiate from AI assistants and SaaS platforms
- Updated installation instructions to recommend installing as a dev dependency
- Updated project description
- Updated litellm from 1.81.11 to 1.81.13
- Updated pytest-django from 4.11.1 to 4.12.0
[0.6.0] - 2026-02-16¶
Added¶
- Added per-app
TRANSLATING.mdsupport. Individual Django apps can now provide their ownTRANSLATING.mdfiles with domain-specific translation context (e.g., medical vs legal terminology). App-level context is combined with the project-level context and passed to the LLM.
Changed¶
- Updated litellm from 1.81.9 to 1.81.11
- Updated ruff from 0.15.0 to 0.15.1
[0.5.0] - 2026-02-09¶
Added¶
- Added
--appflag to filter translation by Django app
Changed¶
- Updated litellm from 1.81.4 to 1.81.9
- Updated Django from 5.2.10 to 5.2.11
[0.4.0] - 2026-01-28¶
Added¶
- Added support for
TRANSLATING.mdfiles to provide project-specific translation context. When present in your project root, the content is included in the LLM prompt to guide translations with terminology, tone, and style preferences.
Changed¶
- Updated litellm from 1.81.1 to 1.81.4
[0.3.10] - 2026-01-23¶
Added¶
- Added automatic retry with exponential backoff for rate limit errors (429). Retries up to 5 times with delays of 60s, 120s, 240s, 480s.
Fixed¶
- Fixed translation count mismatch where LLM would occasionally return fewer translations than expected. The prompt now explicitly instructs the model to return exactly the same number of translations as input strings.
[0.3.9] - 2026-01-23¶
Added¶
- Added graceful handling for insufficient API credits with user-friendly error messages
Changed¶
- Refactored API error handling into a reusable context manager
[0.3.8] - 2026-01-23¶
Fixed¶
- Fixed fuzzy entries not being re-translated. Entries marked as fuzzy in PO files are now correctly identified and re-translated, with the fuzzy flag cleared afterward.
[0.3.7] - 2026-01-23¶
Changed¶
- Updated litellm from 1.80.13 to 1.81.1
Fixed¶
- Fixed language code to locale directory conversion for zh-hans (Simplified Chinese)
[0.3.6] - 2026-01-10¶
Fixed¶
- Fixed JSON parsing when LLM adds preamble text before the JSON array (e.g., "Here is the translation:")
[0.3.5] - 2026-01-10¶
Fixed¶
- Fixed JSON parsing errors when LLM models return responses wrapped in markdown code blocks (e.g., ```json ... ```)
[0.3.4] - 2026-01-10¶
Changed¶
- Updated PyPI homepage link to point to GitHub repository
- Updated documentation link to use translatebot.dev/docs/
- Updated changelog link to point to GitHub CHANGELOG.md
- Added documentation link to top of README
[0.3.3] - 2026-01-10¶
Changed¶
- Updated PyPI links to use docs subdomain
[0.3.2] - 2026-01-10¶
Changed¶
- Improved README documentation
- Added Documentation link to PyPI page
[0.3.1] - 2026-01-10¶
Changed¶
- Updated litellm from 1.80.12 to 1.80.13
- Improved README documentation
[0.3.0] - 2026-01-09¶
Changed¶
- Dropped Python 3.9 support to fix filelock security vulnerability
- Simplified README and added links to documentation at translatebot.dev/docs
- Updated litellm from 1.80.11 to 1.80.12
- Updated ruff from 0.14.10 to 0.14.11
Security¶
- Updated filelock to 3.20.1+ to fix Time-of-Check-Time-of-Use (TOCTOU) race condition vulnerability that allowed local attackers to corrupt or truncate arbitrary user files through symlink attacks
Infrastructure¶
- Bumped actions/checkout from 4 to 6
- Bumped actions/setup-python from 5 to 6
[0.2.2] - 2026-01-06¶
Changed¶
- Updated Django 5.2.9 to 5.2.10
- Updated Django 6.0 to 6.0.1
[0.2.1] - 2026-01-04¶
Changed¶
- Updated author metadata formatting in package configuration
[0.2.0] - 2026-01-03¶
Added¶
- Added proper error handling for authentication failures with helpful error messages
- Added support for translating django-modeltranslation model fields with
--modelsflag - Added intelligent batching for large translation jobs to handle token limits
Changed¶
TRANSLATEBOT_MODELsetting now defaults togpt-4o-miniwhen not configured (previously required)- Improved token limit handling for model field translations with intelligent batching
- Updated README to use
uv addinstead ofpip installfor installation instructions - Removed unused
source_langparameter fromModeltranslationBackend - Improved CLI output for dry-run mode to be cleaner and more informative
- Enhanced dry-run output to show accurate counts of entries that would be translated
- Improved modeltranslation integration to properly detect source content in language-specific fields
Fixed¶
- Fixed
--dry-runflag to properly skip LLM API calls instead of contacting the provider - Fixed dry-run mode showing incorrect entry counts (was always showing 0)
- Fixed dry-run mode for model field translation showing 0 fields when records exist
- Fixed dry-run output showing excessive separator lines when no translations needed
- Fixed incorrect model names in README (updated to current Claude 4.5, Gemini 2.5/3, etc.)
- Corrected misleading README claims about source language auto-detection
- Added missing link to django-modeltranslation project in README features
- Fixed queryset filtering to check language-specific fields instead of base field
- Fixed source text extraction to use populated language fields instead of relying on fallback
Infrastructure¶
- Achieved 100% test coverage across entire codebase
- Added comprehensive test suite for authentication error handling
- Added tests for dry-run mode edge cases
- Added tests for batching logic with large datasets
[0.1.4] - 2025-12-31¶
Changed¶
- Improved CLI output with emojis to match the landing page demo
[0.1.3] - 2025-12-29¶
Changed¶
- Added PyPI badge to README
- Grouped badges in README
[0.1.2] - 2025-12-28¶
Added¶
- Added Changelog and a link to it on the PyPI page
[0.1.1] - 2025-12-28¶
Changed¶
- Added project URLs to PyPI page (Homepage, Repository, Issues)
- Added Poetry installation instructions to README
Infrastructure¶
- Added Codecov integration for test coverage reporting
[0.1.0] - 2025-12-28¶
Added¶
- Initial release
- Django management command
translatebotfor translating .po files - Support for OpenAI-compatible LLM providers via LiteLLM
- Automatic detection of untranslated and fuzzy entries
- Preserves existing translations by default
- Configurable via Django settings or command-line arguments
- Support for Django 4.2, 5.0, 5.1, 5.2, and 6.0
- Support for Python 3.9 through 3.14