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.
Before you start
Section titled “Before you start”- A template version that defines translations. A version with none accepts no
languageat all. - The exact language key as defined on the version —
fr,nl-BE, whatever the template author used. Matching is exact, not fuzzy.
How it works
Section titled “How it works”-
Find out which languages the version defines.
Fetch the version and read its
translationsfield — a JSON object keyed by language.nullmeans the version has no translations at all. -
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. -
Read the language back off the result.
The rendered document’s
languagefield echoes what was rendered, so a stored document is self-describing. -
Handle
400as a data problem, not a bug.Catch it and surface the available languages — the message already lists them.
Step 1: discover the available languages
Section titled “Step 1: discover the available languages”curl "https://pagr-prd-api-public.azurewebsites.net/v1/templates/8bec66ff-6f3d-4c1e-9a2b-1f0e5d7c4a90/versions/latest" \ -H "Authorization: Bearer pagr_prod_xxxxxxxx" | jq -r '.translations'// translations is a JSON *string* on the wire — parse it, then read its keys"{\"fr\":{\"invoice.title\":\"Facture\"},\"nl\":{\"invoice.title\":\"Factuur\"}}"// ⇒ available languages: fr, nlimport 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")const version = await client.getTemplateVersion(TEMPLATE_ID);const languages = version.translations ? Object.keys(JSON.parse(version.translations)) : [];console.log('Available languages:', languages.length ? languages : 'none');TemplateVersion version = client.getTemplateVersion(templateId);Set<String> languages = version.getTranslations() == null ? Set.of() : JsonParser.parseString(version.getTranslations()) .getAsJsonObject().keySet();System.out.println("Available languages: " + languages);using System.Text.Json;
var version = await client.GetTemplateVersionAsync(templateId);var languages = version.Translations is null ? [] : JsonDocument.Parse(version.Translations).RootElement .EnumerateObject().Select(p => p.Name).ToList();
Console.WriteLine($"Available languages: {string.Join(", ", languages)}");require "json"
version = client.template_version(TEMPLATE_ID)languages = version.translations ? JSON.parse(version.translations).keys : []puts "Available languages: #{languages.empty? ? 'none' : languages.join(', ')}"const auto version = client.get_template_version(kTemplateId);std::vector<std::string> languages;if (version.translations.has_value()) { for (const auto& [key, _] : nlohmann::json::parse(*version.translations).items()) { languages.push_back(key); }}Step 2: render a language variant
Section titled “Step 2: render a language variant”curl -X POST "https://pagr-prd-api-public.azurewebsites.net/v1/render/8bec66ff-6f3d-4c1e-9a2b-1f0e5d7c4a90?language=fr" \ -H "Authorization: Bearer pagr_prod_xxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "documents": [ { "Title": "Facture Acme T3", "Amount": 42 } ], "includeDocument": true }'language is a query parameter, not a body field.
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)import { ApiError } from 'pagr';
try { const result = await client.render( TEMPLATE_ID, { Title: 'Facture Acme T3', Amount: 42 }, { language: 'fr', includeDocument: true }, ); console.log(result.document!.language); // 'fr' await result.document!.save('./out');} catch (err) { // 400 — the message lists the languages that ARE available if (err instanceof ApiError) console.log(err.statusCode, err.code, err.message); else throw err;}import org.example.exception.ApiException;
try { RenderResult result = client.render(templateId, data, RenderOptions.builder().language("fr").includeDocument(true).build());
System.out.println(result.getDocument().getLanguage()); // "fr" result.getDocument().save(Path.of("out"));} catch (ApiException exc) { // 400 — the message lists the languages that ARE available System.out.println(exc.getStatusCode() + " " + exc.getMessage());}try{ var result = await client.RenderAsync( templateId, new { Title = "Facture Acme T3", Amount = 42 }, language: "fr", includeDocument: true);
Console.WriteLine(result.Document!.Language); // "fr" await result.Document.SaveAsync("out");}catch (PagrApiException exc){ // 400 — the message lists the languages that ARE available Console.WriteLine($"{exc.StatusCode} {exc.Message}");}begin result = client.render( TEMPLATE_ID, { "Title" => "Facture Acme T3", "Amount" => 42 }, language: "fr", include_document: true, ) puts result.document.language # "fr" result.document.save("out")rescue Pagr::ApiError => e # 400 — the message lists the languages that ARE available warn "#{e.status_code} #{e.message}"endtry { const auto result = client.render( kTemplateId, json_data, {.include_document = true, .language = "fr"});
if (result.document->language.has_value()) { std::cout << *result.document->language << "\n"; // "fr" }} catch (const pagr::PagrApiException& exc) { // 400 — the message lists the languages that ARE available std::cout << exc.what() << "\n";}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 severalfor lang in ["fr", "nl", "de"]: result = await client.render_batch( TEMPLATE_ID, documents, language=lang, include_document=True, ) result.save_all(f"out/{lang}/")for (const lang of ['fr', 'nl', 'de']) { const result = await client.renderBatch(TEMPLATE_ID, documents, { language: lang, includeDocument: true, }); await result.saveAll(`./out/${lang}`);}for (String lang : List.of("fr", "nl", "de")) { BatchRenderResult result = client.renderBatch(templateId, documents, RenderOptions.builder().language(lang).includeDocument(true).build()); result.saveAll(Path.of("out", lang));}foreach (var lang in new[] { "fr", "nl", "de" }){ var result = await client.RenderBatchAsync( templateId, documents, language: lang, includeDocument: true); await result.SaveAllAsync(Path.Combine("out", lang));}%w[fr nl de].each do |lang| result = client.render_batch(TEMPLATE_ID, documents, language: lang, include_document: true) result.save_all(File.join("out", lang))endfor (const std::string& lang : {"fr", "nl", "de"}) { const auto result = client.render_batch( kTemplateId, documents, {.include_document = true, .language = lang}); result.save_all("out/" + lang);}Variation: translations for a stateless render
Section titled “Variation: translations for a stateless render”A stateless render has no stored version to read translations from, so you supply them inline alongside the template and data. The shape is an object keyed by language, each mapping translation keys to strings:
{ "template": { "…": "the template DSL" }, "data": { "Amount": 42 }, "translations": { "fr": { "invoice.title": "Facture", "invoice.total": "Total" }, "nl": { "invoice.title": "Factuur", "invoice.total": "Totaal" } }}curl -X POST "https://pagr-prd-api-public.azurewebsites.net/v1/render?language=fr" \ -H "Authorization: Bearer pagr_prod_xxxxxxxx" \ -H "Content-Type: application/json" \ -d @body.json --output facture.pdfpdf_bytes = await client.render_stateless( template=template_dsl, data={"Amount": 42}, translations={"fr": {"invoice.title": "Facture"}}, language="fr",)// TypeScript puts translations and language in a single options objectconst pdfBytes = await client.renderStateless( templateDsl, { Amount: 42 }, { translations: { fr: { 'invoice.title': 'Facture' } }, language: 'fr', },);byte[] pdf = client.renderStateless( templateDsl, Map.of("Amount", 42), Map.of("fr", Map.of("invoice.title", "Facture")), "fr");byte[] pdf = await client.RenderStatelessAsync( templateJson, """{ "Amount": 42 }""", translationsJson: """{ "fr": { "invoice.title": "Facture" } }""", language: "fr");pdf = client.render_stateless( template_dsl, { "Amount" => 42 }, { "fr" => { "invoice.title" => "Facture" } }, language: "fr",)const auto pdf = client.render_stateless( template_dsl, nlohmann::json{{"Amount", 42}}, nlohmann::json{{"fr", {{"invoice.title", "Facture"}}}}, std::string("fr"));Other considerations
Section titled “Other considerations”- Matching is exact.
frandfr-FRare different keys. Requestingfr-FRagainst a version that defines onlyfris a400, 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.
languageis a query parameter, not a body field. If you’re calling the HTTP API directly, don’t put it indocuments.- The stored document records the language.
languageon 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. languageisnull, 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
400message 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”.
Related articles
Section titled “Related articles”- Render a document — where the
languagequery parameter is defined. - Render statelessly — inline templates and translations.
- Templates & versions — where
translationscomes from. - Browse and download documents — filtering stored documents by language.
