Use cases
Consistent number and date formatting across locales
Formatting inconsistency is one of the most common internationalization bugs, and it usually starts the same way: formatting logic lives in more than one place.
The US price might come from your translation system's locale settings while the German price comes from a hardcoded Intl.NumberFormat call in a component. One path gets updated, the other does not, and the same product shows $1,234.56 in the US and €1.234,56 in Germany with no guarantee they stay aligned.
Without a single source of truth, small format changes slip through review. Customers see mismatched prices or dates, support tickets increase, and teams chase bugs that never belonged in application code.
Messagevisor centralizes number, date, time, currency, and other format presets in locale definitions. Applications call SDK format helpers that read from those presets. Format changes go through pull requests and CI, the same way translation changes do.
Defining format presets in locales#
Format presets live in locale definitions under the formats property, organized by category. The most commonly used categories are number, date, and time.
description: English baseformats: number: decimal: maximumFractionDigits: 2 money: style: currency currencyDisplay: symbol percent: style: percent maximumFractionDigits: 1 compact: notation: compact maximumFractionDigits: 1 date: long: year: numeric month: long day: numeric short: year: numeric month: short day: numeric monthYear: year: numeric month: long time: short: hour: numeric minute: 2-digit full: hour: numeric minute: 2-digit second: 2-digit timeZoneName: shortThese named presets like money, decimal, percent, long, short are the vocabulary your application and translation strings share.
Currency presets may define a fixed currency, but they do not have to. When currency is omitted, the SDK can fill it from instance state or a per-call option. Date, time, and date-time-range presets behave the same way for timeZone.
Locale inheritance for formats#
Regional locales inherit format presets from their parent locale and only need to declare what differs. This avoids duplicating the entire format tree for every regional variant:
description: English (United States)inheritFormatsFrom: eninheritTranslationsFrom: en# No formats override - all presets from `en` apply as-isdescription: English (United Kingdom)inheritFormatsFrom: eninheritTranslationsFrom: enformats: number: money: style: currency currency: GBP currencyDisplay: symboldescription: German (Germany)formats: number: decimal: maximumFractionDigits: 2 money: style: currency currency: EUR currencyDisplay: symbol date: long: year: numeric month: long day: numeric short: year: numeric month: "2-digit" day: "2-digit" time: short: hour: "2-digit" minute: "2-digit"description: Japanese (Japan)formats: number: money: style: currency currency: JPY currencyDisplay: symbol maximumFractionDigits: 0 date: long: year: numeric month: long day: numeric short: year: numeric month: "2-digit" day: "2-digit"Format inheritance is style-based: de-DE only needs to declare the named styles where German differs from the inherited base. When it declares a style such as number.money, that full style object replaces the parent style; other named styles flow through unchanged.
Using named formats in ICU message strings#
Named format presets integrate directly into ICU message syntax. A translation string can reference a preset by name:
description: Total amount shown in billing summarytranslations: en-US: "Total: {amount, number, money}" en-GB: "Total: {amount, number, money}" de-DE: "Gesamt: {amount, number, money}" ja-JP: "合計:{amount, number, money}"The money in {amount, number, money} refers to the money preset defined in the active locale's formats. When the en-US locale resolves {amount, number, money} with amount: 149.99, it produces $149.99. When de-DE resolves the same expression, it produces 149,99 €.
The translation string itself does not change. Only the locale-level format preset changes. This means you can update the currency formatting across every message that uses the money preset by changing one locale file.
Using format helpers in the SDK#
The JavaScript SDK exposes format helpers that use the locale's authored presets:
import { createMessagevisor } from "@messagevisor/sdk";import datafile from "./datafiles/messagevisor-web-en-US.json";const m = createMessagevisor({ datafile, locale: "en-US", context: { platform: "web" },});// Uses the "money" preset from en-US locale formatsm.formatNumber(149.99, "money");// → "$149.99"// Uses the "decimal" presetm.formatNumber(0.1234, "decimal");// → "0.12"// Uses the "long" date presetm.formatDate(new Date("2025-11-15"), "long");// → "November 15, 2025"// Uses the "short" date presetm.formatDate(new Date("2025-11-15"), "short");// → "Nov 15, 2025"// Uses the "short" time presetm.formatTime(new Date("2025-11-15T14:30:00"), "short");// → "2:30 PM"For currency, pass a currency code if it should differ from the locale default:
m.formatNumber(149.99, { style: "currency", currency: "EUR" });Target-level format overrides#
Format presets can be overridden at the target level without modifying the locale definition. This is useful when one platform needs a different convention for the same locale - for example, a checkout flow that should display a more compact currency symbol:
description: Checkout experience datafilelocales: - en-US - en-GB - de-DEformats: en-US: number: money: style: currency currency: USD currencyDisplay: narrowSymbol en-GB: number: money: style: currency currency: GBP currencyDisplay: narrowSymbolTarget-level formats take the highest precedence in the resolution chain, above both locale-level and inherited formats.
Testing format presets#
Locale tests can assert the resolved format presets to catch regressions when inheritance changes or a preset is accidentally overwritten:
locale: en-USassertions: - description: USD money format expectedFormats: number: money: currency: USD currencyDisplay: symbol - description: Long date format inherited from en expectedFormats: date: long: year: numeric month: long day: numericCombining a format assertion with an translation assertion verifies end-to-end behavior:
locale: de-DEassertions: - description: EUR money format expectedFormats: number: money: currency: EUR - description: Total message evaluates to EUR target: web rawMessage: "Gesamt: {amount, number, money}" values: amount: 149.99 expectedTranslation: "Gesamt: 149,99 €"Target tests can also assert format resolution to catch the case where a target-level override does not apply as expected:
target: checkoutassertions: - description: Checkout uses narrow USD symbol locale: en-US expectedFormats: number: money: currencyDisplay: narrowSymbolPer-call format overrides in the SDK#
The SDK also supports per-call format overrides for cases where one screen needs a different format than the default for that locale:
// Override for this specific call onlym.formatNumber(149.99, { style: "currency", currency: "EUR", currencyDisplay: "code",});// → "EUR 149.99"Per-call overrides take precedence over everything - target formats, locale formats, and inherited formats. They are not baked into the datafile and do not require a build step.

