Multilingual B2B e-commerce platform for waterless urinal products across 32 European countries with automated VAT handling, PDF invoicing, and CRM integration.
pi-pi.ee — Multilingual B2B Presentation PDF Generator
On-demand PDF presentation generator for 31 European markets — one codebase, one URL pattern, streamed directly from a Next.js route handler with zero storage and ~200 ms generation time.
Tech Stack
Stack
Libraries
Services
Key Results
- 31 languages — one codebase, one URL pattern, zero manual work per market
- ~200 ms end-to-end PDF generation (4-slide A4 with images and tables)
- Content sourced directly from the site's existing i18n files — no translation overhead
- Live sync — any content, price, or spec update on the site instantly reflects in all 31 PDFs
- No storage, no queue — PDF streamed directly in the HTTP response
The Challenge
The pi-pi.ee sales team sells waterless urinal systems across 31 European markets. Reaching distributors and facility managers means communicating in their language — and that communication starts with a product presentation.
The existing workflow: a single PowerPoint file, maintained manually, in one language. When the sales team needed to send a deck to a prospect in Poland or Estonia, someone had to translate it, reformat it, and export it to PDF. Each iteration of the product — new prices, updated specs, different material grades — required the same process all over again. With 31 potential markets, the problem was structurally unsolvable with manual tooling.
The core requirements:
- 31 locales — same layout and branding across all markets, each in the local language
- Shareable link —
pi-pi.ee/en/presentation.pdf,pi-pi.ee/et/presentation.pdf,pi-pi.ee/de/presentation.pdfopens directly in the browser; no login, no form, no download page - Always in sync — content lives in the site's own i18n files; update specs or prices on the site, presentation updates automatically
- Zero infrastructure overhead — no storage buckets, no generation queues, no separate CMS
The Solution
I built a Next.js App Router route handler at /[locale]/presentation.pdf that generates the PDF on demand and streams it directly in the HTTP response. The key architectural decision: the presentation reuses the site's existing next-intl translation files verbatim — no separate content, no translators, no duplication. The same JSON keys that power the product pages, pricing tables, and feature lists on the website are read by the PDF route and composed into a structured sales deck.
The PDF is assembled with @react-pdf/renderer — React components describe the layout declaratively, and Satori-style primitives (View, Text, Image) compile to a PDF byte stream on the server. From a developer perspective, building a slide is identical to building a React component, with the constraint that only a subset of CSS is supported.
Route Handler — Generate and Stream
The route reads the locale from the URL, loads the relevant message namespaces from the site's i18n files, renders the PresentationDocument component tree, and writes the resulting bytes directly to the response:
// app/[locale]/presentation.pdf/route.ts
export async function GET(
_req: Request,
{ params }: { params: Promise<{ locale: string }> }
) {
const { locale } = await params;
const messages = await Promise.all([
getBaseMessages(locale),
getUseCasesMessages(locale),
]);
const pdfBuffer = await renderToBuffer(
<PresentationDocument
messages={messages}
{...extractors(getMessages(locale))}
/>
);
return new Response(Buffer.from(pdfBuffer), {
headers: {
"Content-Type": "application/pdf",
"Content-Disposition": `inline; filename="pi-pi-presentation-${locale}.pdf"`,
},
});
}No file saved to disk. No upload to a storage bucket. The PDF exists only for the duration of the request.
Slide Structure
The presentation is four A4 pages, each with a distinct purpose:
Slide 1 — Cover. Company logo, product hero image, tagline, and the sales manager's name and contact details. The contact block is locale-aware — different markets may have different account managers.
Slide 2 — Manufacturer Capabilities. A grid of proof points: factory certifications, production capacity, material grades, export geography. All text comes from the site's existing capabilities message namespace.
Slide 3 — Dual Product Specs. Side-by-side comparison of the two main product lines with dimensions, flow rates, compatibility, and installation requirements. Uses a two-column View grid with aligned label/value rows.
Slide 4 — LLDPE Material Deep-Dive. The most layout-intensive slide: a 3×2 grid of stat cards (tensile strength, temperature range, chemical resistance, UV stability, recyclability, service life), followed by a comparison table of pi-pi material versus standard alternatives.
Reusing Site i18n Files
The route imports message loaders that are shared with the rest of the Next.js app. There is no separate content management step for the presentation:
// lib/pdf/messages.ts — reads the same files next-intl uses on the site
import { getMessages } from "@/i18n/server";
export async function getPresentationMessages(locale: string) {
const messages = await getMessages({ locale });
return {
cover: messages.presentation.cover,
capabilities: messages.capabilities,
products: messages.products,
material: messages.material,
};
}When the product team edits a spec value in messages/de.json to update a Polish-language page on the site, the next request to /de/presentation.pdf picks up the change automatically. No rebuild, no deploy, no manual sync.
Non-Latin Fonts and Hyphenation
@react-pdf/renderer uses its own text rendering engine and does not inherit system fonts or browser fonts. Without explicit font registration, any character outside the Latin-1 range renders as an empty rectangle. The fix is to register a full Unicode font with an explicit CDN URL before the document renders:
// lib/pdf/fonts.ts
Font.register({
family: "Roboto",
fonts: [
{ src: "https://fonts.gstatic.com/.../Roboto-Regular.ttf", fontWeight: 400 },
{ src: "https://fonts.gstatic.com/.../Roboto-Bold.ttf", fontWeight: 700 },
{ src: "https://fonts.gstatic.com/.../Roboto-Italic.ttf", fontStyle: "italic" },
],
});
// Disable automatic hyphenation globally.
// The built-in algorithm is English-only — applied to non-Latin locales
// it produces incorrect line breaks mid-word.
Font.registerHyphenationCallback((word) => [word]);The hyphenation callback is the less obvious one: @react-pdf/renderer runs a hyphenation pass on every text node by default. For non-English locales this produces broken line breaks. Returning [word] as a single-element array tells the engine to never split the word, which gives correct results for all 31 locales at once.
Stale Module Cache in Development
Because the presentation reads from the same i18n JSON files as the rest of the site, hot-reload should work automatically — edit a message, refresh the PDF URL, see the change. In practice, the PDF route had its own module-level Map that cached parsed message objects between requests. The cache survived Next.js hot-reload, so edits to JSON files had no effect until a full server restart.
The fix is a single environment check:
// lib/pdf/messages.ts
const cache = new Map<string, Messages>();
export async function getMessages(locale: string): Promise<Messages> {
if (process.env.NODE_ENV !== "development" && cache.has(locale)) {
return cache.get(locale)!;
}
const messages = await loadJSON(locale); // same files the site uses
cache.set(locale, messages);
return messages;
}In development, the cache is bypassed entirely — every request re-reads the JSON from disk. In production on Vercel, the module-level cache persists for the lifetime of the serverless function instance, which is the desired behavior: warm requests are fast without redundant I/O.
Results
The presentation is live at pi-pi.ee/[locale]/presentation.pdf for all 31 locales.
| Metric | Value |
|---|---|
| Locales | 31 (full EU coverage + other languages) |
| Generation time | ~200 ms (A4, 4 slides, images + tables) |
| Storage required | None — generated on demand |
| Translators involved | Zero — reuses existing site i18n files |
| Content sync | Automatic — site update = PDF update |
| Deployment | Vercel serverless, scales automatically |
B2B managers now share a single URL in their outreach emails. The recipient gets a professionally formatted presentation in their language — with specs, pricing, and copy that always match the live site — without any manual preparation or translator involvement.
AvailableNeed something similar?
I build custom solutions — from APIs to full products. Let's talk about your project.
Related projects
International e-commerce platform with 30 locales, product configurators, AI chatbot, and fully automated order flow: Stripe → Zoho CRM → Airtable → Mailgun →