Rich text API¶
Translating a document¶
django_traduire.richtext
¶
HTML-aware translation: translate the words, keep the markup.
A rich-text field is not plain text. Sending <table><tr><td>Le cacao</td>…
to a provider as-is either strips the tags, scrambles the rows, or comes back
truncated because the table alone is larger than one request. This module
walks the document, hands the provider prose that fits, and puts the markup
back exactly where it was.
The pipeline has one shape, whatever the provider:
- :func:
tokenizeturns the document into a flat list of tokens. - Tokens are grouped into parts: structural markup on one side, runs of translatable content on the other. A heading, a list item and a table cell are each their own run.
- Every run is cut into segments that fit the provider's request budget,
on token boundaries — never in the middle of a tag, and in
inline_htmlmode never between a tag and the one that closes it. - A segment is sent either with its inline markup replaced by
[[n]]placeholders (providers that read plain text, such as the free Google endpoint) or as real inline HTML (DeepL, Google Cloud, an LLM). - Answers are put back in place; the markup is restored byte for byte.
Attributes a reader sees — alt, title, placeholder,
aria-label — are translated too, and only their value is rewritten.
Content of <script>, <style>, <code>, <pre>, <template>
and their friends is never touched, and neither is Django or Jinja template
syntax ({{ … }}, {% … %}) sitting inside prose.
Part
dataclass
¶
A stretch of the document: structural markup, or a translatable run.
Attributes:
| Name | Type | Description |
|---|---|---|
kind |
str
|
|
tokens |
tuple[Token, ...]
|
The tokens of the part, in document order. |
segments |
tuple[Segment, ...]
|
The segments of a |
Source code in src/django_traduire/richtext.py
Segment
dataclass
¶
A slice of a translatable run that fits one request.
Attributes:
| Name | Type | Description |
|---|---|---|
ident |
int
|
Identifier of the segment inside the document. |
tokens |
tuple[Token, ...]
|
The tokens this segment covers, in document order. |
lead |
str
|
Whitespace stripped from the front of the request text. |
core |
str
|
What is actually sent to the provider. |
trail |
str
|
Whitespace stripped from the end of the request text. |
markup |
tuple[Token, ...]
|
Inline markup set aside, in placeholder order. Empty when the segment is sent as HTML. |
translatable |
bool
|
False when the segment holds no prose at all. |
Source code in src/django_traduire/richtext.py
AttributeRequest
dataclass
¶
One attribute value waiting for its translation.
Source code in src/django_traduire/richtext.py
translate_html(html, translate, *, inline_html=False, max_chars=None, translate_attributes=True)
¶
Translate the prose of an HTML document, keeping its structure intact.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
html
|
str
|
The document to translate. |
required |
translate
|
Callable[[list[str]], list[str]]
|
Callable receiving a list of strings and returning their translations, in the same order and the same number. |
required |
inline_html
|
bool
|
True when the provider reads markup. Inline tags are then
sent as HTML instead of |
False
|
max_chars
|
int | None
|
Characters one request may carry. Runs longer than this are cut on token boundaries. None means no budget. |
None
|
translate_attributes
|
bool
|
Translate |
True
|
Returns:
| Name | Type | Description |
|---|---|---|
str |
str
|
The document with translated prose and untouched markup. |
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
BackendError
|
If |
Source code in src/django_traduire/richtext.py
build_parts(html, max_chars=None, inline_html=False)
¶
Split a document into structural markup and budgeted translatable runs.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
html
|
str
|
The document to plan. |
required |
max_chars
|
int | None
|
Characters one request may carry, or None for no limit. |
None
|
inline_html
|
bool
|
True when the provider reads markup, so inline tags stay in the text instead of becoming placeholders. |
False
|
Returns:
| Type | Description |
|---|---|
list[Part]
|
list[Part]: The document, in order. |
Source code in src/django_traduire/richtext.py
split_html(html, max_chars)
¶
Cut html into fragments of at most max_chars, on tag boundaries.
The concatenation of the fragments is exactly html. Cuts are taken
between top-level elements first; a single element larger than
max_chars — a long table, a nested list — is opened up and cut one
level deeper, as many times as it takes. Fragments produced that way are
slices of the document rather than balanced elements, which is why
:func:translate_html plans its own segments instead of calling this.
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
Source code in src/django_traduire/richtext.py
tokenize(html)
¶
Parse html into tokens, refusing a document that is not editorial.
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
TraduireError
|
If the document holds more than
:data: |
Source code in src/django_traduire/richtext.py
Reading and writing markup¶
django_traduire.htmltokens
¶
Read a rich-text document as a flat list of tokens, and write it back.
Everything that knows what an HTML tag is lives here: which tags open a
block, which ones hide code rather than prose, which attributes carry words a
reader sees. :mod:django_traduire.richtext builds on it and never parses
markup itself.
The contract of this module is that a document survives a round trip::
"".join(render_token(token) for token in tokenize(html)) == html
up to entity decoding — ô comes back as ô, comes back
as . Tags, attributes, comments, doctypes and processing
instructions are re-emitted exactly as they were written.
Token
dataclass
¶
One piece of a parsed document: a run of text, or a piece of markup.
Attributes:
| Name | Type | Description |
|---|---|---|
kind |
str
|
|
value |
str
|
Decoded text, or the markup exactly as it was written. |
tag |
str
|
Tag name, lowercased, for markup. |
role |
str
|
|
verbatim |
bool
|
Text that must be re-emitted without escaping. |
attrs |
tuple[tuple[str, str | None], ...]
|
Attributes of a start tag, as |
index |
int
|
Position of the token in the document, used to attach a translated attribute to the tag it came from. |
Source code in src/django_traduire/htmltokens.py
tokenize(html)
¶
Parse html into tokens.
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
Source code in src/django_traduire/htmltokens.py
render_token(token, attributes=None)
¶
Return the source form of a token, with translated attributes applied.
Source code in src/django_traduire/htmltokens.py
escape_text(text)
¶
Escape a decoded run of text back into document form.
&, < and > become entities again, and so do the invisible
characters an editor writes as or ­ — left as raw code
points they survive the database but confuse the next person to open the
editor.
Source code in src/django_traduire/htmltokens.py
looks_like_html(text)
¶
Return True when text carries markup a plain-text provider would break.
translatable_attributes(token)
¶
Return the (name, value) attributes of a tag that carry prose.
Source code in src/django_traduire/htmltokens.py
rewrite_attributes(source, values)
¶
Return the start tag source with the given attribute values replaced.
Only the value is touched: quoting style, attribute order, spacing and every other attribute are left exactly as the editor wrote them.
Source code in src/django_traduire/htmltokens.py
Splitting plain text¶
django_traduire.chunking
¶
Split long plain text into chunks a translation provider will accept.
Every provider caps the size of a single request. A 20 000-character article sent as one string comes back truncated — or not at all. This module cuts the text on the most natural boundary that fits (paragraph, then sentence, then word, then character) and guarantees that reassembling the pieces reproduces the original exactly::
"".join(chunk_text(text, 4000)) == text
chunk_text(text, max_chars)
¶
Cut text into pieces of at most max_chars characters.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
text
|
str
|
The text to split. |
required |
max_chars
|
int
|
Maximum size of one piece, at least :data: |
required |
Returns:
| Type | Description |
|---|---|
list[str]
|
list[str]: Pieces whose concatenation is exactly |
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
TraduireError
|
If the text would need more than
:data: |
Source code in src/django_traduire/chunking.py
split_edges(text)
¶
Return (leading_whitespace, core, trailing_whitespace).
Providers strip the whitespace around what they translate. Keeping the edges aside and restoring them afterwards preserves the original layout.