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”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")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); }}curl "https://api.pagr.eu/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, nlStep 2: render a language variant
Section titled “Step 2: render a language variant”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";}curl -X POST "https://api.pagr.eu/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.
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);}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”- Translations and languages — how a template gets its translations in the first place.
- Render a document — where the
languagequery parameter is defined. - Templates & versions — where
translationscomes from. - Browse and download documents — filtering stored documents by language.
