← Back to blogs

ICU MessageFormat Basics for Real Projects

A practical ICU syntax guide for placeholders, number/date/time formatting, and writing localization-safe messages that scale in production.

ICU MessageFormat is the foundation of many modern localization pipelines. It allows you to keep translatable copy in resource files while still injecting runtime values like user names, counts, dates, prices, and status labels.

When teams skip a structured ICU approach, they often end up concatenating strings in code, which creates grammar bugs, translator confusion, and inconsistent copy across locales. A stable ICU template strategy fixes this by keeping sentence logic in one message instead of spread across conditionals.

This guide focuses on practical implementation: how to write maintainable ICU messages, how to keep variable contracts stable, and how to prevent common formatting regressions before they reach production.

Start with clear variable contracts

Use descriptive variable names and keep them stable across all locales. If your source message uses `{totalAmount}`, every translation must keep the same variable key. Renaming variables after translators start work is one of the fastest ways to create runtime failures.

Provide translators with context: what each variable means, expected data type, and where the string appears. Even technically valid ICU strings can become linguistically incorrect when context is missing.

  • Good: `Hello {firstName}`
  • Good: `Your total is {amount, number}`
  • Avoid vague names like `{value}` or `{x}` in production keys

Use typed arguments instead of preformatted strings

Whenever possible, pass raw values and let ICU format them for the active locale. For example, pass a number for amount and format it with ICU rather than formatting currency manually in code before interpolation.

This keeps your localization layer locale-aware and avoids inconsistencies between screens. It also helps QA test behavior centrally because formatting logic lives in one place.

  • Number: `Total: {amount, number}`
  • Date: `Updated on {updatedAt, date, medium}`
  • Time: `Last sync at {updatedAt, time, short}`
  • Currency style: `Balance: {amount, number, ::currency/USD}`

Keep full sentence meaning in one message

Do not split one sentence into multiple translation keys just to inject a variable in the middle. Different languages reorder sentence parts, and split keys often produce awkward or incorrect grammar.

Instead, keep each user-facing sentence complete in one ICU message. This gives translators freedom to produce natural output in their language without being constrained by source-language word order.

Add a lightweight validation workflow

At minimum, validate ICU syntax in pull requests and run a preview test for your top locales before release. This catches broken braces, missing branches, and variable mismatches early.

If your team ships frequently, add a small smoke suite that renders high-traffic strings with realistic variable values. You will catch far more issues than syntax-only checks.

Key takeaways

  • Keep variable names stable and descriptive.
  • Let ICU handle locale-aware formatting for number/date/time values.
  • Translate full sentences, not fragmented pieces.
  • Add PR and CI validation for ICU messages.

Frequently asked questions

Should we preformat numbers and dates before passing them into ICU?

Prefer passing raw values and formatting in ICU. It keeps formatting locale-aware and consistent across features.

How many locales should we test before each release?

Test at least your highest-traffic locales plus one language with more complex plural behavior to detect template quality issues early.