Skip to content

Render multilingual documents

A template version can carry translations — a set of language keys, each mapping the template’s translatable strings to that language. Pass a language at render time and Pagr renders that variant. Omit it and you get the template’s base content.

  • A template version that defines translations. A version with none accepts no language at all.
  • The exact language key as defined on the version — fr, nl-BE, whatever the template author used. Matching is exact, not fuzzy.
  1. Find out which languages the version defines.

    Fetch the version and read its translations field — a JSON object keyed by language. null means the version has no translations at all.

  2. Pass the language key when you render.

    It’s a query parameter on the wire (?language=fr) and an option in every SDK. It works on single renders, synchronous batches and async jobs alike.

  3. Read the language back off the result.

    The rendered document’s language field echoes what was rendered, so a stored document is self-describing.

  4. Handle 400 as a data problem, not a bug.

    Catch it and surface the available languages — the message already lists them.

import json
version = await client.get_template_version(TEMPLATE_ID)
languages = list(json.loads(version.translations)) if version.translations else []
print("Available languages:", languages or "none")
from pagr import ApiError
try:
result = await client.render(
TEMPLATE_ID,
{"Title": "Facture Acme T3", "Amount": 42},
language="fr",
include_document=True,
)
print(result.document.language) # "fr"
result.document.save("out/")
except ApiError as exc:
# 400 — the message lists the languages that ARE available
print(exc.status_code, exc.code, exc)

The same language option works on batches and async jobs — one language per request, applied to every document in it:

# One language per request — loop to produce several
for lang in ["fr", "nl", "de"]:
result = await client.render_batch(
TEMPLATE_ID, documents, language=lang, include_document=True,
)
result.save_all(f"out/{lang}/")
  • Matching is exact. fr and fr-FR are different keys. Requesting fr-FR against a version that defines only fr is a 400, not a fallback — the API uses the same lookup the renderer uses, so validation can never accept a language rendering would have to guess at.
  • One language per request. There’s no “render all languages” call. Loop over the languages you need; each is a separate request and a separate credit charge.
  • language is a query parameter, not a body field. If you’re calling the HTTP API directly, don’t put it in documents.
  • The stored document records the language. language on the rendered document and on Documents tells you which variant a stored PDF is — and it’s a filterable field, so you can list all French documents directly. See Listing & pagination.
  • language is null, not "", when the template has no translations or the render didn’t specify one.
  • Credit is charged per rendered page, so three languages of a 4-page invoice costs 12 pages, not 4.
  • The 400 message is useful — surface it. It enumerates the available languages, which is exactly what a caller who guessed wrong needs to see. Don’t swallow it into a generic “render failed”.