Skip to content

Changelog

All notable changes to django-traduire are listed here. The format follows Keep a Changelog, and the project follows Semantic Versioning.

[0.3.0] - 2026-09-08

The release about rich text that actually survives the trip.

v0.2 translated rich text by cutting the document on top-level tag boundaries. A document built around a single element — a table, a <figure>, a <div> wrapper, anything an editor emits — has exactly one top-level element, so the whole thing was handed to the provider as one oversized request. Providers answer that with a truncation, an error, or scrambled rows. Tables were the common case, and they were the case that broke.

v0.3 stops slicing documents. It plans them.

Changed

  • Rich text is now translated segment by segment, never sliced. The document is parsed once; structural markup is set aside and only the prose inside each block — a heading, a list item, a table cell — is sent. Every request therefore fits the provider's budget by construction, whatever the document looks like. A 100-row table, a table nested in a table, a 30-level <div> stack and a 20 000-character article all go through as many small requests as they need.
  • supports_html backends changed shape. DeepL, Google Cloud and OpenAI now receive one block of prose at a time with its inline markup left in (Un secteur <strong>vital</strong>.) instead of whole document slices. Structural tags never reach a provider at all.
  • Provider answers are verified before they are stored. Markup that does not nest, or a tag the segment never carried, is refused and the segment falls back to translating its text nodes one by one — every tag preserved. A provider that invents <script> can no longer get it into a field the template renders as safe.
  • split_html() cuts deeper when it has to. An element larger than the budget is opened up and cut inside instead of being returned whole. It is no longer used by the translation pipeline and is kept as a public utility.

Added

  • Attributes a reader sees are translated: alt, title, placeholder, aria-label, aria-description, <option label>, <th abbr>, <table summary>. Only the value is rewritten — quoting style, attribute order and every other attribute are left exactly as the editor wrote them. href, src, class and id are never touched. New TRANSLATE_ATTRIBUTES setting, on by default.
  • Django and Jinja template syntax is protected. {{ user.name }}, {% if x %} and {# … #} sitting inside prose are set aside like markup instead of being translated into nonsense.
  • django_traduire.htmltokens — the tokenizer, extracted into its own module. It round-trips a document byte for byte: tags, attributes, comments, doctypes and processing instructions come back exactly as they were written.
  • &nbsp; and &shy; survive. v0.2 decoded them to raw code points, which reads the same in a browser but confuses the next person to open the editor.
  • More tags understood: <figure>, <figcaption>, <details>, <summary>, <dialog>, <colgroup>, <optgroup>. More tags left alone: <template>, <iframe>, <canvas>, <object>, <noscript>.
  • Placeholders are recognised even when the provider rewrites the brackets — Google returns full-width [[0]] for CJK targets, and most providers add spaces.

Fixed

  • A table, or any document wrapped in one element, is no longer sent as a single oversized request. This is the bug this release exists for.
  • An empty answer from a provider no longer wipes the text it was meant to translate; the original is kept.
  • A segment with more inline tags than any provider will return in order now falls back to text-node translation instead of being silently skipped.
  • looks_like_html() no longer mistakes a < b for markup, and now recognises every HTML5 element rather than a short hand-picked list.
  • A request is never cut through a tag. chunk_text() cuts on word boundaries, and a tag carries spaces of its own — <a href="/x" title="a b"> could be split in two.

[0.2.0] - 2026-08-20

The release about long texts and rich text.

Added

  • Automatic chunking of long fields (django_traduire.chunking). Every provider caps the size of a request; a 20 000-character article used to come back truncated, or not at all. Text is now cut on the most natural boundary that fits — paragraph, then sentence, then word — translated piece by piece, and glued back exactly: "".join(chunks) == original.
  • HTML-aware translation (django_traduire.richtext). Rich text keeps its structure: <h1>…<h6>, <p>, <ul>/<li>, <table>, <blockquote>, and the inline formatting inside them (<strong>, <em>, <b>, <i>, <a href>, <br>). Two strategies, picked automatically:
  • providers that read markup (DeepL, Google Cloud, OpenAI) receive HTML fragments cut on top-level tag boundaries;
  • providers that do not (the free Google endpoint) receive the sentence with its inline tags replaced by placeholders, restored afterwards. A sentence stays whole across a <strong>, instead of being translated in fragments.
  • <script>, <style>, <code>, <pre>, <kbd>, <samp>, <var>, <svg> and <math> are never sent to a provider.
  • GoogleFreeBackend — Google Translate with no API key, no billing account and no extra dependency (django_traduire.backends.google_free). It is now the default backend, so pip install django-traduire translates out of the box. It talks to the public endpoint the Google Translate web page uses: not a contractual API, rate-limited by IP, with retries and rate_limit options to stay polite. For contractual traffic, keep DeepL, Google Cloud or OpenAI.
  • Column overflow is handled instead of crashing. A translation longer than the column's max_length — very common when translating a CharField(200) title into German — is truncated on a word boundary and logged. Configurable with ON_TOO_LONG ("truncate", "skip", "error").
  • New settings: HTML_MODE, HTML_FIELDS, MAX_CHARS, ON_TOO_LONG, FAIL_SILENTLY.
  • New arguments: fields= on translate_instance(), fields= and limit= on translate_queryset(), --fields and --limit on manage.py traduire.
  • Named exceptions: TraduireError, ConfigurationError, BackendError. Settings are validated when they are read, with a message that says what to fix.

Changed

  • Backend API: a backend now implements translate_raw(texts, source, target, is_html=False) — one request to the provider — instead of translate_batch(). Chunking, grouping and markup handling live in BaseBackend and are inherited for free. translate_batch() remains the public entry point for callers. Custom backends must rename their method.
  • Default backend is now GoogleFreeBackend (was DeepLBackend, which could never work without an auth_key).
  • AUTO_TRANSLATE is connected from the app config instead of at import time, and a provider failure can no longer break a save().
  • Importing django_traduire no longer imports modeltranslation, so the package can be imported before Django settings are configured.
  • The package is checked with ruff, ruff format, mypy and pytest with warnings promoted to errors — all four with zero output.

Fixed

  • Long articles silently coming back empty or truncated.
  • Rich text losing its tags, or having them translated as words.
  • translate_instance() failing the whole save when one translation overflowed its column.
  • A translation identical to the source was dropped instead of being stored, so the field was translated again on every run.

[0.1.0] - 2026

  • First release: DeepL, Google Cloud and OpenAI backends, translate_instance, translate_queryset, manage.py traduire, admin actions, AUTO_TRANSLATE.