Formatting Reference
lib/format.ts provides locale-aware formatting utilities for numbers, currency, percentages, pluralization, and bulk-delete messages. Every function accepts a BCP 47 locale string and uses Intl.NumberFormat / Intl.PluralRules internally. Formatter instances are memoized per (locale, options) so rendering hundreds of formatted values on an index page doesn't pay the construction cost per cell.
These are server-side helpers used in generated CRUD routes, list views, and custom templates. They're not exposed as template helpers — use the template-level {= display_currency(value) } / {~ percent(value) } helpers in .ree files instead.
display_currency()
function display_currency(
val: number,
locale?: string,
hide_zero?: boolean,
symbol?: string,
): string;
Formats a number as EUR currency with locale-aware grouping and the desired symbol.
| Parameter | Type | Default | Description |
|---|---|---|---|
val | number | (required) | The amount to format |
locale | string | "sl-si" | BCP 47 locale for number formatting |
hide_zero | boolean | false | When true, returns "" if the value is 0 |
symbol | string | "€" | Currency symbol displayed after the number |
Uses Intl.NumberFormat with currencyDisplay: "code" and replaces "EUR" with the custom symbol, so the output always shows € regardless of the locale's default currency display:
display_currency(1234.56); // → "1.234,56 €" (sl-si)
display_currency(1234.56, "en-us"); // → "€1,234.56" (en-us)
display_currency(0, "en-us", true); // → "" (hide zero)
display_currency(99.99, "en-us", false, "$"); // → "$99.99"
currency_no_cents()
function currency_no_cents(
val: number,
locale?: string,
hide_zero?: boolean,
): string;
Same as display_currency() but rounds to an integer first — no decimal places. Useful for display-only price summaries where cents are noise:
currency_no_cents(1234.56); // → "1.235 €" (sl-si, rounded)
currency_no_cents(1234.56, "en-us"); // → "€1,235" (en-us, rounded)
currency_no_cents(0, "en-us", true); // → "" (hide zero defaults to true)
Note: hide_zero defaults to true for this function (unlike display_currency() where it defaults to false).
decimal()
function decimal(
val: number,
locale?: string,
hide_zero?: boolean,
fraction_digits?: number,
): string;
Formats a decimal number with locale-aware grouping and configurable fraction digits.
| Parameter | Type | Default | Description |
|---|---|---|---|
val | number | (required) | The number to format |
locale | string | "sl-si" | BCP 47 locale |
hide_zero | boolean | false | When true, returns "" if value is 0 |
fraction_digits | number | 2 | Minimum fraction digits to display |
decimal(1234.5); // → "1.234,50" (sl-si)
decimal(1234.5, "en-us"); // → "1,234.50" (en-us)
decimal(1234.5, "en-us", false, 0); // → "1,235" (rounded, no decimals)
decimal(0, "en-us", true); // → "" (hide zero)
percent()
function percent(
val: number,
locale?: string,
): string;
Formats a percentage value using Intl.NumberFormat with style: "percent". The value is divided by 100 internally per Intl convention — pass 25 to display "25%", not 0.25.
percent(25); // → "25%" (sl-si)
percent(25, "en-us"); // → "25%" (en-us)
percent(0); // → "0%"
percent(undefined as any); // → "0%" (undefined → 0)
display_percent()
function display_percent(
val: number,
locale?: string,
): string;
Same as percent() but with configurable fraction digits (0–2). Better for display where exact percentages look cleaner:
display_percent(25); // → "25%" (en-us, no decimals)
display_percent(33.3333); // → "33.33%" (en-us, 2 decimals)
display_percent(0); // → "0%"
Uses minimumFractionDigits: 0, maximumFractionDigits: 2 — whole percentages show without decimals, fractional ones show up to 2 places.
plural()
function plural(
translation_string: string,
count: number,
locale?: string,
): string;
Selects the correct plural form from a pipe-separated translation string using Intl.PluralRules. The string must have up to 5 forms separated by |:
"zero|one|two|few|other"
| Index | CLDR Category | Example (English) |
|---|---|---|
| 0 | zero | "0 records" |
| 1 | one | "1 record" |
| 2 | two | "2 records" (Arabic, Welsh, etc.) |
| 3 | few | "{count} records" (Slavic languages) |
| 4 | other | "{count} records" — the universal fallback |
If a form index is missing, it falls back to the last available form. {count} in the selected form is replaced with the locale-formatted number.
// English: "one" for 1, "other" for everything else
plural("0 records|1 record|{count} records|{count} records|{count} records", 1, "en-us");
// → "1 record"
plural("0 records|1 record|{count} records|{count} records|{count} records", 5, "en-us");
// → "5 records"
// Slovenian: "one" for 1, "two" for 2, "few" for 3-4, "other" for 5+
plural("0 zapisov|1 zapis|{count} zapisa|{count} zapisi|{count} zapisov", 2, "sl-si");
// → "2 zapisa"
plural("0 zapisov|1 zapis|{count} zapisa|{count} zapisi|{count} zapisov", 7, "sl-si");
// → "7 zapisov"
format_bulk_delete_message()
function format_bulk_delete_message(
msg: { bulk_deleted?: string; bulk_errors?: string; },
deleted_count: number,
error_count: number,
label?: string,
locale?: string,
): string;
Composes a bulk-delete result message from two pluralized translation keys. Used by the generated CRUD bulk-delete handler.
| Parameter | Type | Default | Description |
|---|---|---|---|
msg | { bulk_deleted?, bulk_errors? } | (required) | Translation key object from the route's translations |
deleted_count | number | (required) | Number of successfully deleted records |
error_count | number | (required) | Number of records that failed to delete |
label | string | "record" | Fallback noun used when translation keys are missing |
locale | string | "en-us" | BCP 47 locale |
Each key is a pipe-separated plural string. If a key is missing from msg, a fallback is constructed from the label:
// Successful bulk delete, no errors
format_bulk_delete_message(
{ bulk_deleted: "No records deleted|{count} record deleted|{count} records deleted|{count} records deleted|{count} records deleted" },
3, 0, "product", "en-us",
);
// → "3 records deleted"
// Partial failure: some records couldn't be deleted
format_bulk_delete_message(
{
bulk_deleted: "No records deleted|{count} record deleted|{count} records deleted|{count} records deleted|{count} records deleted",
bulk_errors: "|{count} record failed|{count} records failed|{count} records failed|{count} records failed",
},
5, 2, "product", "en-us",
);
// → "5 records deleted, 2 records failed"
When error_count is 0, the error suffix is omitted entirely — only the deletion message is returned.
Memoization
All formatters use cached Intl.NumberFormat and Intl.PluralRules instances, keyed by (locale, JSON-stringified options). Constructing a new Intl.NumberFormat costs tens of microseconds — negligible for a single call, but rendering 200 rows on an index page would pay that cost 200 times. The cache eliminates the constructor overhead after the first call per (locale, options) pair.
// First call: constructs Intl.NumberFormat("sl-si", { style: "currency", ... })
display_currency(100, "sl-si");
// Subsequent calls: hits the cache, just calls .format()
display_currency(200, "sl-si");
display_currency(300, "sl-si");
The cache is module-scoped and lives for the lifetime of the process — no eviction, no TTL, just a Map that grows with the number of distinct (locale, options) combinations seen.
Template Helpers vs Server Helpers
These functions are server-side — they're used in route handlers (index.ts) and called before the response is rendered. In .ree templates, use the template-level helpers instead:
Server (lib/format.ts) | Template (.ree) |
|---|---|
display_currency(v, l) | {~ display_currency(v, l) } |
percent(v, l) | {~ display_percent(v, l) } |
plural(s, n, l) | Not available — use translation keys with {_ ... } |
The template-level display_currency() and display_percent() are defined in lib/template_helpers.ts and available in every .ree file without imports. They delegate to the same Intl APIs but accept props.locale automatically.