Modules
ICU module
ICU module adds ICU message syntax support to Messagevisor. It is the module you want for interpolation, plurals, select logic, named number/date/time formats, inline ICU skeletons, and rich text tags.
Install#
$ npm install --save @messagevisor/module-icu @messagevisor/sdkReact applications usually install:
$ npm install @messagevisor/sdk @messagevisor/react @messagevisor/module-icuProject setup#
Register the module in messagevisor.config.js in your Messagevisor project so CLI evaluation, examples, tests, catalog output, and generated workflow checks all use the same behavior:
const { createICUModule } = require("@messagevisor/module-icu");module.exports = { modules: [createICUModule()],};If you intentionally want inline ICU skeleton styles such as {amount, number, ::currency/GBP}, also enable icuSkeleton for linting:
const { createICUModule } = require("@messagevisor/module-icu");module.exports = { modules: [createICUModule()], icuSkeleton: true,};Prefer named locale formats for shared product copy. Inline skeletons are best for one-off strings where a reusable preset would be noise.
SDK setup#
The SDK does not interpret ICU syntax by default. Add the module to your application's runtime instance:
import { createMessagevisor } from "@messagevisor/sdk";import { createICUModule } from "@messagevisor/module-icu";// either import or fetch in the runtimeimport datafile from "./datafiles/messagevisor-web-en-US.json";const m = createMessagevisor({ datafile, modules: [createICUModule()],});m.translate("cart.items", { count: 3 });m.formatMessage("Hello {name}", { name: "Ada" });Without the module, Hello {name} stays Hello {name}. With the module, it becomes Hello Ada.
React setup#
React uses the same SDK instance. Configure ICU on the SDK, then pass that instance to MessagevisorProvider:
import { createMessagevisor } from "@messagevisor/sdk";import { createICUModule } from "@messagevisor/module-icu";import { MessagevisorProvider, useTranslation } from "@messagevisor/react";import datafile from "./datafiles/messagevisor-web-en-US.json";const m = createMessagevisor({ datafile, modules: [createICUModule({ ignoreTags: false })],});export function AppRoot() { return ( <MessagevisorProvider instance={m} defaultRichTextElements={{ strong: (chunks) => <strong>{chunks}</strong>, link: (chunks) => <a href="/terms">{chunks}</a>, }} > <App /> </MessagevisorProvider> );}function App() { const title = useTranslation("dashboard.welcome", { name: "Ada" }); return <h1>{title}</h1>;}Use ignoreTags: false when messages contain rich text tags such as <link>terms</link>. Rich tag callbacks and non-string renderable output are JavaScript-enhanced capabilities. Portable SDKs in other languages may support the string ICU subset without supporting rich callback output.
Supported ICU shapes#
| ICU type | Example | Messagevisor support |
|---|---|---|
| Interpolation | {name} | Values object passed to SDK, tests, examples, or CLI |
plural | {count, plural, one {...} other {...}} | Locale-aware cardinal plural rules |
select | {gender, select, male {...} other {...}} | String equality branches |
selectordinal | {rank, selectordinal, one {...} other {...}} | Locale-aware ordinal plural rules |
number | {amount, number, money} | Uses formats.number.<style> |
date | {startsAt, date, long} | Uses formats.date.<style> |
time | {startsAt, time, short} | Uses formats.time.<style> |
| Rich text tags | <link>terms</link> | Requires ignoreTags: false and tag handlers |
| Inline skeletons | {amount, number, ::currency/USD} | Supported by intl-messageformat; enable icuSkeleton for linting |
Messagevisor format families such as relative and dateTimeRange are available through SDK formatter helpers, but ICU message strings use the number, date, and time style families.
Named format styles#
Named ICU styles reference locale format presets. Define the style in a locale in your Messagevisor project:
description: English (United States)formats: number: money: style: currency currency: USD currencyDisplay: symbol compactShort: notation: compact compactDisplay: short distanceKm: style: unit unit: kilometer unitDisplay: short date: long: year: numeric month: long day: numeric time: short: hour: numeric minute: 2-digitThen reference those names in messages:
translations: en-US: "Total: {amount, number, money}. Delivery: {distance, number, distanceKm}."Named formats keep product copy stable while letting locale owners tune the display behavior in one place.
Number examples#
Decimal#
translations: en: "Score: {score, number, decimal}"formats: number: decimal: maximumFractionDigits: 2Currency#
translations: en-US: "Total: {amount, number, money}"formats: number: money: style: currency currencyDisplay: symbolIf currency is omitted, Messagevisor uses the SDK instance currency at runtime. Define currency in the preset when that style should always use a fixed currency. Per-call options still win over both:
m.translate("billing.total", { amount: 12 }, { currency: "EUR" });Compact and scientific notation#
formats: number: compactShort: notation: compact compactDisplay: short scientific: notation: scientifictranslations: en: "Audience: {count, number, compactShort}. Scientific: {count, number, scientific}."Unit#
formats: number: distanceKm: style: unit unit: kilometer unitDisplay: shorttranslations: en: "Distance: {distance, number, distanceKm}"Date and time examples#
formats: date: long: year: numeric month: long day: numeric time: event: hour: numeric minute: 2-digittranslations: en: "Starts on {startsAt, date, long} at {startsAt, time, event}"If timeZone is omitted, Messagevisor uses the SDK instance time zone or the runtime default. Define timeZone in the preset when the style should always render in a fixed zone. Values are passed to Intl.DateTimeFormat, so use identifiers that your JavaScript runtime supports:
m.translate( "event.startsAt", { startsAt: new Date("2026-05-06T10:00:00Z") }, { timeZone: "Europe/Amsterdam" });Plural#
translations: en: "{count, plural, =0 {No items} one {# item} other {# items}}" nl: "{count, plural, =0 {Geen items} one {# item} other {# items}}"Plural categories are locale-aware. English commonly uses one and other; other locales may use zero, two, few, or many. The # placeholder renders the numeric value.
Select#
translations: en: "{role, select, admin {Admin joined} owner {Owner joined} other {Member joined}}"select matches exact string values. Always include an other branch.
Selectordinal#
translations: en: "You are {rank, selectordinal, one {#st} two {#nd} few {#rd} other {#th}} in line"selectordinal uses ordinal rules, not cardinal plural rules.
Nesting#
You can nest plurals inside select branches:
translations: en: > {status, select, trial {{days, plural, one {# day left} other {# days left}}} paid {Your plan is active} other {Choose a plan} }Keep nested ICU readable. For complex messages, add examples and tests so reviewers can see each branch.
Rich text#
Rich text tags are useful when the same translation needs to render links, strong text, or other React elements:
translations: en: "Read the <link>terms</link> for <strong>{product}</strong>."JavaScript usage:
const m = createMessagevisor({ datafile, modules: [createICUModule({ ignoreTags: false })],});m.translate("legal.terms", { product: "Messagevisor", link: (chunks) => `[${chunks.join("")}]`, strong: (chunks) => chunks.join("").toUpperCase(),});React usage is usually cleaner through defaultRichTextElements:
<MessagevisorProvider instance={m} defaultRichTextElements={{ link: (chunks) => <a href="/terms">{chunks}</a>, strong: (chunks) => <strong>{chunks}</strong>, }}> <LegalCopy /></MessagevisorProvider>Per-call module options#
Each module can read options by its module name. The ICU module defaults to the name icu.
m.formatMessage( "Read <link>terms</link>.", { link: (chunks) => `[${chunks.join("")}]` }, { moduleOptions: { icu: { ignoreTags: true, }, }, });Use this sparingly. It is best for one rendering path that needs to treat tags differently.
Testing ICU messages#
Message tests are the safest way to lock down ICU behavior:
message: cart.itemsassertions: - description: Empty cart locale: en target: web values: count: 0 expectedTranslation: No items - description: Multiple items locale: en target: web values: count: 3 expectedTranslation: 3 itemsLocale examples are useful for demonstrating reusable formats. They can be checked from the terminal with npx messagevisor examples --onlyLocales and reviewed visually in the generated Catalog:
examples: - description: Compact audience count rawMessage: "Audience: {count, number, compactShort}" values: count: 1200CLI evaluation#
Use evaluate for fast debugging:
$ npx messagevisor evaluate --rawMessage="{count, plural, one {# item} other {# items}}" --locale=en --values='{"count":3}'$ npx messagevisor evaluate --message=cart.items --locale=en --values='{"count":5}'For sets-based projects, include --set=<set>.
Edge cases and behavior notes#
ICU behavior depends on locale#
Plural and ordinal categories come from the active locale. The same message can evaluate differently across locales.
Named formats must exist#
If a message references {amount, number, money}, the active locale's resolved formats.number.money must exist, either directly or through inheritFormatsFrom.
Module registration must match across environments#
Project config, SDK setup, React setup, tests, examples, and catalog generation should all register ICU consistently. Otherwise a message can pass review but render literally in an application.
Relative time and date-time range are SDK formatter helpers#
Use m.formatRelativeTime() and m.formatDateTimeRange() for those families. They are part of Messagevisor formats, but not named ICU styles in message strings.
Locale grammar#
Translate the sentence and its grammar, not the source ICU tree. Keep argument names and compatible value types, but allow argument order, repetition, and grammatical structure to change. A language may need a plural expression where the source uses a plain number, or different cardinal and ordinal categories. Always include other and use the categories for the target locale rather than copying English categories blindly.
For example, the same numeric count value can support different grammar:
translations: en: "Items: {count, number}" pl: "{count, plural, one {# element} few {# elementy} many {# elementów} other {# elementu}}"Preserve application select keys such as admin, named format references, required rich text tags, and the meaning of deliberate exact selectors such as =0. Changes to those contracts need review, not automatic translation. Add examples for each relevant branch, including zero and fractional counts where appropriate, and review output with a speaker of the target language.