Translations and formatting
Keep one template and render it in every language your customers read. The labels live in translation files, the request names the language, and numbers and dates are formatted to match.
{ "invoice": "Rechnung", "dueDate": "Fällig am" }<h1>{{localize "invoice"}} {{invoice.number}}</h1><p>{{localize "dueDate"}}: {{invoice.dueDate}}</p>A request with "locale": "de-CH" prints Rechnung and Fällig am; one with "locale": "en-GB"
reads en-GB.json instead.
Add translation files
Section titled “Add translation files”- In Studio’s explorer, open the menu of the template’s folder and choose
New entity → Localization. Name the file for its language tag, such as
de-CH.jsonoren-GB.json. It can also go in alocalizationsfolder inside the template’s folder. - Write the labels as one JSON object, with the same keys in every file.
- Print a label with
{{localize "key"}}, or read it fromlocalizationin the data, such as{{localization.invoice}}. - Pick the language in the Preview box of the preview toolbar. The box appears once the template has a translation file.
- Publish the template. Production renders use the files from the published version on.
What to know about labels:
localizelooks up top-level keys. A key the file does not have prints the key itself.localizealso works insidehelpers.jsfunctions.- The chosen file is added to the data as
localization, unless your data already has alocalizationproperty; then yours wins. - When a translation file applies, the render data must be a JSON object.
Choose the language
Section titled “Choose the language”The template’s own language is the locale in its page.json, shown in Studio as Language
under PDF properties in the preview toolbar. A render uses it unless something picks another:
-
Reports API: send
localewith the request. Without it, the language isen-US.{"format": "pdf","template": { "name": "Invoice" },"data": { "invoice": { "number": "INV-2041" } },"locale": "de-CH"}See Render a report by name for runnable code in each client.
-
Renders API: send
localewith a template version render. Without it, the render uses thepage.jsonlocale. A script can still change it, as below. -
A script: set
req.options.localization.languagein abeforeRenderscript, for example from your data:scripts/language.js function beforeRender(req, res) {req.options.localization.language = req.data.customer.language ?? "en-GB";}A script’s choice wins over the request’s
locale. See Handlebars templates for how scripts run.
A locale or script language must be a language tag such as en or de-CH; anything else
fails with 400 BAD_REQUEST. The render’s language also becomes the PDF’s document language.
Which file a render reads
Section titled “Which file a render reads”The render reads the first file that exists:
- The file for the render’s language.
- The file for the
localeinpage.json. en-GB.json.
File names must match the whole language tag, ignoring case. A de request does not find
de-CH.json, and de-CH does not fall back to de.json. So a Reports API request without
locale reads en-US.json if the template has one, and otherwise the page.json language’s
file.
Format numbers and amounts
Section titled “Format numbers and amounts”In an HTML template, write a helper in the template’s helpers.js tab.
Number.prototype.toLocaleString() does not group digits and there is no Intl object, so build
the text yourself. Keeping the separators in the translation files lets one helper serve every
language:
{ "groupSeparator": "’", "decimalSeparator": "." }function formatMoney(amount, currency) { const [whole, cents] = Number(amount).toFixed(2).split("."); const grouped = whole.replace(/\B(?=(\d{3})+(?!\d))/g, localize("groupSeparator"));
return `${currency} ${grouped}${localize("decimalSeparator")}${cents}`;}<td>{{formatMoney total "CHF"}}</td>With de-CH and a total of 1234567.5, this prints CHF 1’234’567.50.
Format dates
Section titled “Format dates”Helpers and scripts have two ways to print dates: require("moment") and the toLocale…
methods of Date.
Moment
Section titled “Moment”require("moment") returns a built-in subset of Moment.
Keep the pattern and Moment’s language in the translation files too:
{ "language": "de-CH", "dateFormat": "D. MMMM YYYY" }const moment = require("moment");
function formatDate(value) { return moment(value) .locale(String(localize("language"))) .format(String(localize("dateFormat")));}{{formatDate invoice.dueDate}} prints 30. November 2026 in de-CH and, with
"dateFormat": "D MMMM YYYY" in en-GB.json, 30 November 2026 in en-GB.
| Supported | Details |
|---|---|
moment(), moment(value) |
Now, or whatever new Date(value) accepts, such as ISO 8601 strings and epoch milliseconds. |
.format(pattern) |
The tokens below. Text in [brackets] is literal. Without a pattern, ISO 8601 with offset. |
.locale(name), .lang(name) |
Month and weekday names. moment.locale(name) sets the default. |
.isSame(other, "day") |
Same calendar day. Without "day", the same instant. |
.diff(other) |
Difference in milliseconds only. |
moment.duration(ms) |
.days(), .hours(), .minutes(), .seconds(), .milliseconds(). |
.toDate(), .valueOf(), .isValid() |
As in Moment. |
Format tokens: YYYYYY, YYYYY, YYYY, YY, MMMM, MMM, MM, M, Do, DD, D,
dddd, ddd, dd, HH, H, hh, h, mm, m, ss, s, SSS, A, a, ZZ and Z.
- An unsupported token, or a unit other than milliseconds for
.diffandmoment.duration, fails the render with an error naming it, instead of printing something wrong. moment(text, format)ignores the format argument.- In languages such as Polish and Russian, a month name after a day number takes its declined form, as in Moment.
Date methods
Section titled “Date methods”toLocaleDateString, toLocaleTimeString and toLocaleString take a language and the usual
weekday, year, month, day, hour, minute, second and hour12 options:
function longDate(value) { return new Date(value).toLocaleDateString("de-CH", { day: "numeric", month: "long", year: "numeric", });}Date languages
Section titled “Date languages”Month and weekday names, and the date layouts of the toLocale… methods, exist for en,
en-GB, sv, nb, da, fi, et, de, fr, es, it, nl, pl, pt, cs, lt,
lv, ru and uk. A regional tag such as de-AT uses its language’s names, and other
languages fall back to English.
Time zones
Section titled “Time zones”Helpers and scripts show dates in the server’s time zone, and the timeZone option of the
toLocale… methods is ignored. Convert times to the reader’s zone before you send them: a
date-time without an offset, such as 2026-11-30T09:00:00, prints as written.
Component and spreadsheet templates
Section titled “Component and spreadsheet templates”-
Component templates format numbers, currency, percentages and dates with a
formaton a value binding, including atimeZonefor dates, without any code. Without alocalein the format, the render’s language is used. Translation files work there too. -
Spreadsheet templates built from sheets and columns take a label per language for a
titleorheader:{"header": { "default": "Amount", "de": "Betrag", "lt-LT": "Suma" },"value": "sum","format": "decimal"}The render’s language picks the label: an exact match first, then the language without its region (
de-CHfindsde), thendefault.defaultis required. Up to 20 entries, each label up to 200 characters. -
Workbook templates designed in Excel use helpers,
localizeand translation files in their cells like HTML templates.