Skip to content

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_MODEL defaults to gpt-6-luna instead of gpt-4o-mini, which dates from July 2024. gpt-6-luna is 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 set TRANSLATEBOT_MODEL. To keep the old model, set TRANSLATEBOT_MODEL = "gpt-4o-mini".
  • gpt-6-luna is a reasoning model, so requests to it are sent with reasoning_effort="low" and without temperature.
  • Python 3.10: set TRANSLATEBOT_MODEL explicitly (for example TRANSLATEBOT_MODEL = "gpt-4o-mini"). litellm is capped below 1.98 there, and those releases don't recognise gpt-6-luna as a reasoning model: they drop reasoning_effort and still send temperature=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_effort for gpt-6-luna.
  • No litellm release includes gpt-6-luna in its bundled model list yet; litellm downloads the current list when it's imported. Offline, or with LITELLM_LOCAL_MODEL_COST_MAP=True, the token limits of gpt-6-luna can'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 --models its 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 by compilemessages) applies to python-format and python-brace-format entries: named placeholders and {fields} must match the source and keep their type (%(n)d can't become %(n)s), positional %s/%d must 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 against msgid_plural and 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, which compilemessages skips (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, and translate() reports their number in TranslateResult.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 usable Plural-Forms header). It used to get the singular, which lacked the count placeholder and broke compilemessages.

[1.4.1] - 2026-10-01

Fixed

  • A blank TRANSLATEBOT_API_KEY (an empty or whitespace-only string, e.g. from env("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 empty Bearer token, 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_TIMEOUT setting: seconds to wait for each LLM API request (default 300). Previously no timeout was set, so a stalled provider could hang translate indefinitely.
  • 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-After instead of always waiting at least 60 seconds.
  • A warning is logged for PO files with plural entries but no usable Plural-Forms header (missing, or makemessages' nplurals=INTEGER placeholder); their plural forms can't be translated individually.

Fixed

  • PO files: messages that share a msgid but differ in msgctxt (pgettext("month", "May") vs pgettext("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-Forms header, with example counts per form; previously the English plural was reused for every form past the first (5 pliki instead of 5 plików). DeepL keeps the previous behaviour.
  • PO files: a translation made with one app's TRANSLATING.md could 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_translated and 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.po and djangojs.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_MODEL and 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 SlugField is stored as a slug again. Providers translate a slug as prose (moja-lijepa-kuca → My beautiful house); the result is now slugified (honouring allow_unicode) and cut to the field's max_length at 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 PRODAJU as Apartment 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_update received 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.lock with 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.lock with 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.lock with 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" to translate() or --llm-model gpt-4o to the translate management command to override settings.TRANSLATEBOT_MODEL for a single call. Useful for A/B testing models or running higher-quality translations on specific languages without mutating Django settings.
  • Raises CommandError when the model parameter is passed together with the DeepL provider, since DeepL has no model concept.

[1.2.0] - 2026-04-12

Added

  • translate() now returns a TranslateResult dataclass with statistics: strings_found, strings_translated, po_files, model_fields_found, model_fields_translated, target_langs, and dry_run

Changed

  • Updated deepl
  • Updated django from 5.2.12 to 5.2.13
  • Updated litellm
  • Updated pytest
  • Updated ruff

Infrastructure

  • Added contents: read permission 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-lang flag 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, …) and tag_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_formality API. Languages are now correctly identified at runtime, so newly added DeepL languages automatically get the right placeholder strategy.
  • Fixed html.unescape corrupting source text that intentionally contains HTML entities (e.g. <, &). Entity decoding now only reverses entities introduced by DeepL's tag_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-lang is 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.po files, enabling translation of JavaScript catalog strings alongside standard django.po files.
  • 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)s format 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 litellm to 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 TranslationProvider ABC for extensible provider architecture
  • Added deepl optional 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_translations management command for CI pipelines. Checks all .po files for missing or fuzzy translations, exiting with code 1 on failure. Includes an optional --makemessages flag to run makemessages before checking.

Changed

  • Refactored locale directory discovery into a shared _iter_locale_dirs() helper used by both get_all_po_files() and get_all_po_paths()
  • Default locale fallback now uses settings.BASE_DIR instead of a relative path

[0.6.1] - 2026-02-18

Added

  • Added CONTRIBUTING.md with 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.md support. Individual Django apps can now provide their own TRANSLATING.md files 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 --app flag 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.md files 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 --models flag
  • Added intelligent batching for large translation jobs to handle token limits

Changed

  • TRANSLATEBOT_MODEL setting now defaults to gpt-4o-mini when not configured (previously required)
  • Improved token limit handling for model field translations with intelligent batching
  • Updated README to use uv add instead of pip install for installation instructions
  • Removed unused source_lang parameter from ModeltranslationBackend
  • 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-run flag 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 translatebot for 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