Zum Inhalt springen

Jinja2PDF

Eine Vorlage, viele Sprachen

Texte kommen über t() aus dem Wörterbuch der Vorlage, und die locale der Anfrage wählt Sprache sowie Zahlen- und Datumsformat. Das deutsche Rendering sagt „Auftragsbestätigung“ und „113,50 €“, das englische „Order confirmation“ und „€113.50“, aus denselben Daten.

Kostenloser Plan, keine Kreditkarte. Sie erhalten eine eigene Kopie im Editor, veröffentlicht und bereit für die API.

Gerendertes Ergebnis

de-DE: PDF · 1 Seite · 17 KB ↓ herunterladenen-GB: PDF · 1 Seite · 17 KB ↓ herunterladen

Was dieses Beispiel zeigt

  • i18n.json enthält ein Wörterbuch je Sprache; t('title', { number: order.number }) füllt den Platzhalter {number} des gewählten.
  • Die locale der Anfrage entscheidet alles auf einmal: das Wörterbuch, money (113,50 € oder €113.50) und das lange Datum, das date('PPPP') ausgibt.
  • Die Daten sind für jede Sprache dieselben, der Aufrufer sendet also nie übersetzte Texte oder formatierte Zahlen.
  • Ohne locale in der Anfrage gilt die Einstellung der Vorlage (en-GB).

Verwendete Helfer

Vorlagenordner

templates/order-confirmation/template.htmlJinja2
<h1>{{ t('title', { number: order.number }) }}</h1>
<p>{{ t('thanks', { name: order.customer }) }}</p>

<table>
  {% for item in order.items %}
  <tr><td>{{ item.name }}</td><td class="num">{{ item.total | money }}</td></tr>
  {% endfor %}
  <tr class="grand">
    <td>{{ t('total') }}</td>
    <td class="num">{{ order.items | sum('total') | money }}</td>
  </tr>
</table>

<p>{{ t('delivery') }}: {{ order.date | dateAdd(3, 'days') | date('PPPP') }}</p>
templates/order-confirmation/style.cssCSS
body { font-family: 'Noto Sans', sans-serif; font-size: 10pt; color: #1e293b; }
h1 { margin: 0 0 3mm; font-size: 16pt; color: #312e81; }
table { width: 100%; margin: 7mm 0; border-collapse: collapse; }
td { padding: 2mm 0; border-bottom: 1px solid #e2e8f0; }
.num { text-align: right; }
.grand td { border-top: 1.5px solid #1e293b; border-bottom: 0; font-weight: 700; }
templates/order-confirmation/settings.jsonJSON
{
  "pdf": {
    "metadata": {
      "title": "{{ t('title', { number: order.number }) }}"
    }
  },
  "paper": {
    "format": "A5"
  },
  "margin": {
    "top": "16mm",
    "right": "14mm",
    "bottom": "16mm",
    "left": "14mm"
  },
  "locale": "en-GB",
  "currency": "EUR"
}
templates/order-confirmation/i18n.jsonJSON
{
  "en": {
    "title": "Order confirmation {number}",
    "thanks": "Thank you for your order, {name}.",
    "total": "Total",
    "delivery": "Expected delivery"
  },
  "de": {
    "title": "Auftragsbestätigung {number}",
    "thanks": "Vielen Dank für Ihre Bestellung, {name}.",
    "total": "Gesamt",
    "delivery": "Voraussichtliche Lieferung"
  }
}
templates/order-confirmation/data/default.jsonJSON
{
  "order": {
    "number": "A-1042",
    "customer": "Jane",
    "date": "2026-09-10",
    "items": [
      {
        "name": "Desk lamp",
        "total": 89
      },
      {
        "name": "Linen shade",
        "total": 24.5
      }
    ]
  }
}

Über die API rendern

Speichern Sie die Beispieldaten als data.json und senden Sie sie mit dem Namen der Vorlage. TypeScript und Python nutzen die SDKs; jede andere Sprache schickt dasselbe JSON mit ihrer Standardbibliothek an die REST-API. Renderings mit einem Schlüssel, der mit ff_test_ beginnt, werden nicht gezählt; im Free-Tarif tragen sie ein kleines Wasserzeichen.

render.shShell
jq '{template: "order-confirmation", data: ., locale: "de-DE"}' data.json \
  | curl https://api-eu.formfeed.dev/v1/renders \
      -H "Authorization: Bearer ff_test_..." \
      -H "Content-Type: application/json" \
      --data-binary @-
render.tsTypeScript
import { Formfeed } from '@formfeed/sdk';
import data from './data.json' with { type: 'json' };

const formfeed = new Formfeed({ apiKey: process.env.FORMFEED_API_KEY! });

const render = await formfeed.renders.create({
  template: 'order-confirmation',
  data,
  locale: 'de-DE',
});
console.log(render.status, render.download_url);
render.pyPython
import json
import os

from formfeed import Formfeed

formfeed = Formfeed(api_key=os.environ["FORMFEED_API_KEY"])

with open("data.json", encoding="utf-8") as file:
    data = json.load(file)

render = formfeed.renders.create(
    template="order-confirmation",
    data=data,
    locale="de-DE",
)
print(render.status, render.download_url)
render.phpPHP
<?php
$body = [
    'template' => 'order-confirmation',
    'data' => json_decode(file_get_contents('data.json'), true),
    'locale' => 'de-DE',
];

$curl = curl_init('https://api-eu.formfeed.dev/v1/renders');
curl_setopt_array($curl, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('FORMFEED_API_KEY'),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode($body),
]);
$render = json_decode(curl_exec($curl), true);

echo $render['status'], ' ', $render['download_url'], PHP_EOL;
main.goGo
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
)

func main() {
	data, err := os.ReadFile("data.json")
	if err != nil {
		panic(err)
	}
	body, _ := json.Marshal(map[string]any{
		"template": "order-confirmation",
		"data":     json.RawMessage(data),
		"locale":   "de-DE",
	})

	request, _ := http.NewRequest("POST", "https://api-eu.formfeed.dev/v1/renders", bytes.NewReader(body))
	request.Header.Set("Authorization", "Bearer "+os.Getenv("FORMFEED_API_KEY"))
	request.Header.Set("Content-Type", "application/json")

	response, err := http.DefaultClient.Do(request)
	if err != nil {
		panic(err)
	}
	defer response.Body.Close()

	var render struct {
		Status      string `json:"status"`
		DownloadURL string `json:"download_url"`
	}
	json.NewDecoder(response.Body).Decode(&render)
	fmt.Println(render.Status, render.DownloadURL)
}
Program.csC#
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;

var body = new
{
    template = "order-confirmation",
    data = JsonDocument.Parse(File.ReadAllText("data.json")).RootElement,
    locale = "de-DE",
};

using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("FORMFEED_API_KEY"));

var response = await http.PostAsJsonAsync("https://api-eu.formfeed.dev/v1/renders", body);
var render = await response.Content.ReadFromJsonAsync<JsonElement>();

Console.WriteLine($"{render.GetProperty("status")} {render.GetProperty("download_url")}");
Render.javaJava
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;

public class Render {
  public static void main(String[] args) throws Exception {
    String data = Files.readString(Path.of("data.json"));
    String body = """
        {
          "template": "order-confirmation",
          "locale": "de-DE",
          "data": %s
        }
        """.formatted(data);

    HttpRequest request = HttpRequest.newBuilder(URI.create("https://api-eu.formfeed.dev/v1/renders"))
        .header("Authorization", "Bearer " + System.getenv("FORMFEED_API_KEY"))
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(body))
        .build();

    HttpResponse<String> response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
    System.out.println(response.body()); // status, download_url, page_count, units …
  }
}
render.rbRuby
require "json"
require "net/http"

body = {
  template: "order-confirmation",
  data: JSON.parse(File.read("data.json")),
  locale: "de-DE",
}

response = Net::HTTP.post(
  URI("https://api-eu.formfeed.dev/v1/renders"),
  JSON.generate(body),
  "Authorization" => "Bearer #{ENV.fetch("FORMFEED_API_KEY")}",
  "Content-Type" => "application/json"
)
render = JSON.parse(response.body)

puts render["status"], render["download_url"]

Heute mit dem Rendern beginnen

Der kostenlose Plan enthält 100 Einheiten im Monat und kostenlose Testrenderings. Wechseln Sie den Plan, wenn Sie mehr brauchen.