Consistent number and date formatting across locales
Format inconsistency is one of the most common internationalization bugs. The price shows $1,234.56 in the US and €1.234,56 in Germany - but the German format is pulled from a hardcoded Intl.NumberFormat call in a component, not from the translation system. Three months later someone updates the US format in the locale configuration but forgets to update the German component. Now the two are out of sync. Customers filing support tickets think the price is wrong when it is just formatted wrong.
Messagevisor centralizes number, date, time, currency, and other format presets in locale definitions. Applications call SDK format helpers that read from the locale's authored presets. Format changes go through pull requests and CI, exactly like translation changes. No format logic lives in application code.
Defining format presets in locales#
Format presets live in locale YAML files under a formats key, organized by category. The most commonly used categories are number, date, and time.
description: English basedirection: ltrformats: 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 - 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)direction: ltrinheritFormatsFrom: eninheritTranslationsFrom: en# No formats override - all presets from en apply as-isdescription: English (United Kingdom)direction: ltrinheritFormatsFrom: eninheritTranslationsFrom: enformats: number: money: style: currency currency: GBP currencyDisplay: symboldescription: German (Germany)direction: ltrformats: 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)direction: ltrformats: 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"The deep-merge behavior means de-DE only needs to declare the format keys where German differs from the inherited base. Other keys 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.

