Language & money

Override any string, ship a whole translation, run right-to-left, and format currency correctly anywhere in the world.

@storvexa/pdf-builder Chapter 9 of 14
Documentation

Overriding a string

Every visible string in the builder is a key — 191 of them. Block names, field labels, tabs, design names, template names, categories, buttons, empty states, validation messages, errors, dialogs, toasts, keyboard shortcuts and screen-reader announcements. There are no literal strings hiding in a component.

<PdfBuilder labels={{ 'toolbar.save': 'Save draft', 'block.lineItems.name': 'Order lines', 'panel.blocks': 'Components', }} />

To see the whole vocabulary:

import { LABEL_KEYS, en } from '@storvexa/pdf-builder' console.log(Object.keys(LABEL_KEYS).length) // 191 console.log(en['toolbar.save']) // the English default
labels is for changing wording in one language. To support a second language, use a locale pack instead — otherwise you end up rebuilding the labels object every time the user switches.

Shipping a translation

const frFR = { locale: 'fr-FR', direction: 'ltr', labels: { 'toolbar.save': 'Enregistrer', 'panel.blocks': 'Blocs', // …anything you omit falls back to English }, dateFormats: ['dd/MM/yyyy'], numberFormat: { minimumFractionDigits: 2 }, } <PdfBuilder localePacks={[frFR]} locale="fr-FR" />
A partial pack is a valid pack. Translate the strings that matter to your users and let the rest fall back to English. You do not need all 191 before you ship.

Offer several and let the user choose:

<PdfBuilder localePacks={[frFR, deDE, arSA]} locales={['en-US', 'fr-FR', 'de-DE', 'ar-SA']} />

Resolution order

First match wins, most specific first:

1. a field-level override in the `fields` prop 2. the `labels` prop 3. the selected locale pack 4. the en-US fallback 5. the key itself — visible only in development
Seeing a raw key like block.foo.name on screen? That means the key exists nowhere — usually a typo, or a custom block whose labelKey has no matching label. It is deliberately visible rather than silently blank, so it gets fixed before release.

Right-to-left

Set direction: 'rtl' on the locale pack. The builder chrome mirrors, the canvas mirrors, and blocks receive ctx.direction so their internal structure can mirror too.

{ locale: 'ar-SA', direction: 'rtl', labels: { … } }
Writing a custom block? Use logical CSS properties. margin-inline-start, not margin-left. Then your block mirrors for free instead of being the one thing on the page that faces the wrong way.

Currency

A document has one primary currency, stored as a code in tokens.currency. Formatting happens at render time through Intl.NumberFormat, so USD, EUR, GBP, INR, CAD, AUD, JPY, CNY, AED, SAR, CHF and every other ISO 4217 code work with no configuration at all.

createDocument({ locale: 'ja-JP', currency: 'JPY' }) // ¥1,235 — no decimals, correctly createDocument({ locale: 'ar-KW', currency: 'KWD' }) // three decimals, correctly
Zero-decimal and three-decimal currencies are handled for you. JPY has no minor unit; KWD and BHD have three. You do not configure that — the platform knows.

Tune presentation through tokens:

tokens: { currency: 'EUR', currencyDisplay: 'code', // symbol | narrowSymbol | code | name minimumFractionDigits: 2, useGrouping: true, }

Custom currencies

Loyalty points, credits, an internal unit, a cryptocurrency — register it and it behaves like any other.

<PdfBuilder currencies={[ 'USD', 'EUR', // bare ISO codes are fine { code: 'PTS', label: 'Loyalty points', // …in the same array as objects symbol: 'pts', decimalDigits: 0 }, ]} />

And if your business has formatting rules no standard covers, take the whole thing over:

<PdfBuilder formatCurrency={(value, ctx) => { if (ctx.currency === 'PTS') return `${value.toLocaleString(ctx.locale)} pts` return undefined // fall back to the built-in formatter }} />
You will rarely need formatCurrency. It exists for genuinely non-standard money. For normal currencies the platform is already correct in every locale, and more thoroughly than a hand-written formatter will be.

Money rules

The package never converts currency. No exchange rates, no conversion, ever. Changing the document's currency changes how amounts are written, not what they are worth. Converting is arithmetic on someone's money and it belongs in your application, with your rates and your audit trail.
Never store a formatted string. Amounts are numbers; the currency is a code. Saved JSON contains 1234.5 and 'EUR', never '€1.234,50'. A formatted string cannot be re-formatted for another locale, and it silently freezes one reader's conventions into the document.
Never hardcode a currency symbol in a renderer. Not in a block, not in a design thumbnail, not in a template fixture. Use ctx.formatMoney(value). Every visible amount in the package goes through one function, which is why two builders on one page can use different currencies without interfering.
Language and currency are independent. Changing the locale changes presentation only. Changing a country preset may suggest a currency, but never converts amounts and never overwrites what a user authored.