Translations and settings
This page shows how to translate labels, and set a language, a time zone and a default currency, so that one template can print an invoice in French for one request and in German for the next. They can live in the template, in each request, and in dictionaries the service holds.
The four settings
Section titled “The four settings”| Setting | Does |
|---|---|
lang |
Picks the translations t uses. With a language CLDR has data for, it also sets how number, currency and date write numbers, money and names |
timezone, or tz |
The zone date shows moments in: an offset such as +02:00, or an IANA zone such as Europe/Paris |
currency |
The code currency uses when given none |
| translations | The text t prints for each key |
Each can be set in three places. For the same setting, the first that sets it wins:
- The request’s fields:
lang,timezone,currencyandtranslations. - A
{% settings %}tag in the template. - The template’s custom document properties.
Translations add up rather than replace each other: see how t looks a key up.
They work the same in Office templates, text templates and PDF tags. A PDF holds no settings of its own, so only the request applies there.
Settings in the request
Section titled “Settings in the request”/v1/render, /v1/render/batch and /v1/templates/validate take these fields, as multipart fields or as members of a JSON body:
| Field | Example | Notes |
|---|---|---|
lang |
fr-CA |
Any case, with - or _. It becomes fr-ca |
translations |
{"fr": {"total": "Montant"}, "en-gb": {"total": "Total"}} |
JSON, by language, each a flat object of keys and texts |
timezone |
Europe/Paris |
An offset, UTC, or an IANA zone |
currency |
EUR |
At most 16 characters |
cat > label.txt <<'EOF'{{ "total" | t }}: {{ total | currency }}, {{ issued | date("%d %B %Y %H:%M") }}EOFcurl -F template=@label.txt \ -F 'data={"total":1500,"issued":"2026-07-01T10:00:00Z"}' \ -F lang=de -F currency=EUR -F timezone=Europe/Berlin \ -F 'translations={"de": {"total": "Summe"}}' \ http://localhost:9027/v1/render -o out.txt# The no-break space CLDR puts before the euro sign, shown here as _.test "$(perl -CSD -pe 's/\x{a0}/_/g' out.txt)" = \ 'Summe: 1.500,00_€, 01 Juli 2026 12:00'A field that cannot be used is a 400 that names it, so a typing mistake is not silently ignored:
| Sent | Answer |
|---|---|
lang=French |
'lang' is "French", which is not a language tag such as fr, fr-CA or en-us |
timezone=Mars/Olympus |
'timezone': timezone "Mars/Olympus" is neither an offset like +02:00 nor an IANA zone like Europe/Paris |
translations={"fr": {"total": 5}} |
'translations.fr' has a number for "total"; texts are strings |
translations holds at most 10,000 entries and 2 MiB in all, with keys of up to 256 bytes and texts of up to 16 KiB.
Settings in the template
Section titled “Settings in the template”The {% settings %} tag sets the same things inside the template, with t.<key> for a translation and t.<key>.<lang> for one in a language:
{% settings lang="fr" currency="EUR" timezone="Europe/Paris" t.billed.fr="Facturé à" t.billed.de="Rechnung an" %}- Type it anywhere in the document. It applies to the whole file wherever it sits, and is removed from the output.
- In an Office template, a paragraph holding only this tag stays as an empty paragraph, so put it in a line that has other text. In a text template the tag leaves nothing, not even its line’s text, though a line break after it stays.
- Several
{% settings %}tags add up. For a repeated setting, the last one wins. - Values must be quoted text.
lang=languageis ignored with no error, because settings are read when the template is parsed, before any data exists. Send what varies as a request field instead.
This example puts the settings in the sample invoice:
mkdir t && cd t && unzip -q ../invoice.docxexport TAG='{% settings lang="fr" currency="EUR" timezone="+02:00"'TAG+=' t.billed.fr="Facturé à" t.billed.de="Rechnung an" %}'TAG+='{{ "billed" | t }}, {{ 5 | currency }},'TAG+=' {{ "2026-11-06T23:30:00Z" | date("%d/%m %H:%M") }}'perl -pi -e 's/Billed to/$ENV{TAG}/' word/document.xmlzip -qr ../tagged.docx . && cd ..
curl -F template=@tagged.docx -F "data=<invoice.json" \ http://localhost:9027/v1/render -o out.docxunzip -p out.docx word/document.xml | perl -CSD -pe 's/\x{a0}/_/g' | grep -o 'Facturé à, 5,00_€, 07/11 01:30'
# The request's language wins over the template's.curl -F template=@tagged.docx -F "data=<invoice.json" -F lang=de \ http://localhost:9027/v1/render -o out.docxunzip -p out.docx word/document.xml | perl -CSD -pe 's/\x{a0}/_/g' | grep -o 'Rechnung an, 5,00_€, 07/11 01:30'Settings in document properties
Section titled “Settings in document properties”The same settings can be custom document properties instead, so that nothing has to be typed on the page. Name each property after its setting (lang, currency, timezone or tz, t.<key>) and give it a text value:
- In Word, Excel and PowerPoint: File > Properties > Custom.
- In LibreOffice: File > Properties > Custom Properties.
A {% settings %} tag wins over a property that sets the same thing. Translations add up: a t.<key> property and a t.<key> in the tag both count, and for the same key the tag’s wins. Properties with other names are ignored.
They are read from docProps/custom.xml in a DOCX, XLSX or PPTX, and from meta.xml in an ODT, ODS or ODP. This example writes the property by hand, as Word would save it:
mkdir t && cd t && unzip -q ../invoice.docxmkdir -p docPropscat > docProps/custom.xml <<'EOF'<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/custom-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"> <property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" pid="2" name="currency"> <vt:lpwstr>EUR</vt:lpwstr> </property></Properties>EOFperl -pi -e 's/Billed to/{{ 5 | currency }}/' word/document.xmlzip -qr ../tagged.docx . && cd ..
curl -F template=@tagged.docx -F "data=<invoice.json" \ http://localhost:9027/v1/render -o out.docxunzip -p out.docx word/document.xml | grep -o 'EUR 5.00'How t finds a translation
Section titled “How t finds a translation”{{ "key" | t }}, or {{ value | t }} with a key from the data, looks in the language, then in each shorter tag it falls back to, then in the template’s translations with no language, and prints the key itself when nothing has it. For lang=fr-ca:
fr-ca: the request’stranslations, then the service’s dictionaries, then the template’st.key.fr-ca.fr: the same three, in the same order.- The template’s
t.key, with no language. - The key, printed as it is, with no error.
So a more exact language beats a higher source: a template’s t.total.fr-ca wins over a request’s fr entry. Within one language, the request beats the dictionaries, and the dictionaries beat the template.
cat > t.txt <<'EOF'{% settings t.total="Total" t.tax.fr-ca="TPS" t.note="Note" %}{{- "total" | t }}|{{ "tax" | t }}|{{ "note" | t }}|{{ "other" | t }}EOFcurl -F template=@t.txt -F lang=fr-CA \ -F 'translations={"fr": {"total": "Montant", "tax": "Taxe"}}' \ http://localhost:9027/v1/render -o out.txttest "$(cat out.txt)" = 'Montant|TPS|Note|other'A template written before languages fell back keeps working: lang="fr-FR" with t.total.fr-FR still finds it, and a lang that is not a language tag still finds t.key.<lang> as written.
Dictionaries on the service
Section titled “Dictionaries on the service”Set RUSTICDOC_TRANSLATIONS_DIR to a directory of <lang>.json files, each a flat object of keys and texts:
{"total": "Montant", "due": "Échéance"}- File names are language tags in any case:
fr.json,fr-CA.json. Other files, such as notes, are ignored if they do not end in.json. - They are read when the service starts. A file that is not a flat object of strings, a
.jsonfile whose name is not a language, or a missing directory stops the service from starting, naming the file, so a broken deploy is noticed at once. - They are read again when a file changes: a request checks the directory’s sizes and times at most once a second, so nothing is parsed per request. A file that turns bad while the service runs is logged as a warning, and its last good version stays in use. A deleted file stops applying.
- Each file may be 1 MiB, with the same limits per entry as the request field. There may be 200 files and 4 MiB in all.
With a request lang, a render receives the dictionaries for that language and those it falls back to. Without one, it receives every dictionary, since the template’s own lang may pick one.
Finding the keys to translate
Section titled “Finding the keys to translate”rusticdoc translate reads Office and text templates, collects the keys they pass to t as literal text ("total" | t, wherever it sits in a tag) and the keys their settings translate, and writes them to <dir>/<lang>.json:
rusticdoc translate --lang fr --out translations templates/- The file is created when missing. When it exists, its translations are kept, and new keys are added with an empty text for a translator to fill.
- Keys in the file that no template uses any more are listed, and kept.
- Directories are searched below, for
.docx,.xlsx,.pptx,.odt,.ods,.odpand text templates. - A key that comes from data, as in
{{ label | t }}, cannot be known before a render, so it is not listed.
The result is a dictionary ready for RUSTICDOC_TRANSLATIONS_DIR. rusticdoc render takes --lang, --translations FILE and --timezone, and reads the same directory, so the command line renders as the service does.
Checking translations
Section titled “Checking translations”/v1/templates/validate with a lang, from the request or the template, warns about each tag passing t a literal key that has no translation in that language or one it falls back to. A translation with no language does not count, since it is what the warning is about. Warnings do not make ok false.
printf '{{ "total" | t }} {{ "due" | t }}' > v.txtcurl -F template=@v.txt -F lang=fr \ -F 'translations={"fr": {"total": "Montant"}}' \ http://localhost:9027/v1/templates/validate -o v.jsonjq -e '.ok and (.diagnostics | length == 1) and (.diagnostics[0].message | startswith("no fr translation for \"due\""))' v.jsonrusticdoc validate --lang fr does the same on the command line.