Skip to main content
Version: 0.3.0

Custom Locales

When building regional adaptations intended for broad reuse—such as a custom "Europe East" package published to Typst Universe—you should construct standalone locale definitions from scratch.

note

For performance reasons, it is highly discouraged to use locale.<lang>-<region>.with(...) to architect an entirely new custom language or region. Extensive runtime patching slows down compilation times. If you want to create your own locale, you must use the build-locale factory function alongside a raw language dictionary and a region builder function.

info

We are deeply committed to providing backwards and forwards compatibility for the locale ecosystem. A custom locale developed for v0.3 will work seamlessly in all future versions. The underlying system utilizes a Cascading deep-merge. If new text strings, tax Grounds, or settings are introduced to invoice-pro in future updates, your locale will automatically fall back to the sensible defaults defined in the internal master schemas.


Architecture of a Locale

The locale engine separates linguistic translation from mathematical and regional formatting. A complete locale is generated by passing two components into the build-locale factory:

  1. Language Dictionary: A dictionary containing structural translations (e.g., column headers, legal texts).
  2. Region Builder Function: A function that takes the constructed language dictionary and returns a dictionary defining formatting functions, Normalized rounding logic, and default tax rates.

Understanding the Base Schemas

Every custom dictionary you provide is deep-merged against base-language and base-region. You are not required to provide a complete translation of every single string or formatting rule. You only need to define the elements you wish to overwrite; all omitted fields will safely fall back to the base English/Neutral defaults.

tip

Review the internal base-language and base-region structures to understand the minimum viable overrides required. Only override fields that deviate from the standard fallback.


Example: Building a "Europe East" Package

Let's walk through creating a standalone package containing a Polish locale (pl-PL).

1. Creating the Language Dictionary

Create a dictionary containing your translations. Notice how we only override the fields we care about.

// lang/pl.typ
#import "@preview/invoice-pro:0.3.0": locale

// Define the language overrides for Polish
#let pl = (
meta: (
lang: "pl",
),
document: (
invoice: "Faktura",
),
line-items: (
position: "Lp.",
description: "Opis",
quantity: "Ilość",
unit-price: "Cena jedn.",
total: "Razem",
vat: "VAT",
),
summary: (
sum: "Suma",
vat-tax: "Podatek VAT",
total: "Do zapłaty",
),
)

2. Creating the Region Builder

The region configuration is defined as a function: (lang) => dictionary. It receives the finalized language dictionary so that it can utilize translated strings if a specific tax configuration requires standard regional text.

Here, we define how currencies are formatted, how values are Normalized, and the standard VAT rate for Poland. This logic remains consistent whether the system is utilizing Forward/Backward Calculation for line-items.

// region/pl.typ
#import "@preview/invoice-pro:0.3.0": locale, data

// The region builder function
#let region-pl = (lang) => (
meta: (
region: "pl",
),
format: (
// Format currency to append 'zł' and use comma decimals
currency: (val) => {
let rounded = calc.round(val, digits: 2)
str(rounded).replace(".", ",") + " zł"
},
// Customize date formatting
date: (val) => if type(val) == datetime {
val.display("[day].[month].[year]")
} else {
// Handle date ranges safely
val.first().display("[day].[month].[year]") + " - " + val.last().display("[day].[month].[year]")
}
),
tax: (
// Set standard Polish VAT
default-vat: data.tax.vat(23%),
)
)

3. Compiling with the Factory

Finally, expose your newly minted locale to the public by utilizing the build-locale factory function.

// lib.typ
#import "@preview/invoice-pro:0.3.0": locale

// 1. Import your definitions
#import "lang/pl.typ": pl
#import "region/pl.typ": region-pl

// 2. Build and export the highly-optimized locale
#let pl-pl = locale.build-locale(pl, region-pl)

Users of your published package can now simply import your locale and pass it directly into the document root:

#import "@preview/invoice-pro:0.3.0": invoice
#import "@preview/invoice-pro-europe-east:0.1.0": pl-pl

#show: invoice.with(
locale: pl-pl,
// ...
)

Locale Factory API Reference

Module Functions

FunctionCode / SignatureDescription
build-locale#let build-locale(lang, region)Combines a language dictionary and a region builder function with the internal master schemas, outputting an optimized, immutable locale function ready for document ingestion.

Component Parameters

When defining the lang parameter for the factory, refer to the language override keys. When defining the region parameter, your function must return a dictionary conforming to the following structure:

KeyTypeDescription
metadictionaryContains the region string identifier (e.g., "pl", "cz").
formatdictionaryFunctions controlling the conversion of integers/floats/dates to strings (e.g., currency, date, percent).
normalizedictionaryFunctions determining rounding logic for money, money-fine, and infer-tax parameters.
taxdictionaryContains default data.tax objects to be applied globally (e.g., default-vat, small-enterprise-special-scheme).