Skip to content

ICU Message Format — Standard Syntax for Translation Strings

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about ICU Message Format. We cover key concepts, practical examples, and best practices to help you master this topic.

ICU Message Format provides a standardized syntax for translation strings with pluralization, gender, selection, and variable formatting across platforms.

What You'll Learn

By the end of this tutorial, you'll understand the ICU MessageFormat syntax, how to handle pluralization and gender selection, how to use complex formatting with numbers and dates, and how to integrate ICU with JavaScript i18n libraries.

Why It Matters

Simple string interpolation like "You have {count} items" breaks for languages with complex plural rules. Arabic has 6 plural forms, Russian has 4, and Japanese has none. ICU MessageFormat is the industry standard for expressing these rules in a way that translators — not developers — can maintain.

Real-World Use

A messaging app uses ICU MessageFormat for notifications like "{name} added {gender, select, male {his} female {her} other {their}} photo." Translators handle Arabic's dual form ("2 messages" vs "messages" in English is simple, but Arabic requires different words for 1, 2, and 3+). Developers never touch plural logic again.

ICU Syntax Overview

graph LR
    A[ICU MessageFormat] --> B[Simple text
Variable replacement] A --> C[Plural
{count, plural, one {# item} other {# items}}] A --> D[Select
{gender, select, male {he} female {she} other {they}}] A --> E[Number formatting
{price, number, ::currency/USD}] A --> F[Date formatting
{date, date, long}] A --> G[Nesting
Compose complex messages] style A fill:#4a90d9,color:#fff

Basic ICU Syntax

// i18n/icu-basics.js — ICU MessageFormat with JavaScript
const { MessageFormat } = require('@messageformat/core');

// Simple variable replacement
const mf1 = new MessageFormat('en');
const msg1 = mf1.compile('Hello, {name}!');
console.log(msg1({ name: 'Alice' }));
// Output: Hello, Alice!

// Multiple variables
const mf2 = new MessageFormat('en');
const msg2 = mf2.compile('{name} has {count} messages.');
console.log(msg2({ name: 'Bob', count: 5 }));
// Output: Bob has 5 messages.

Pluralization

// i18n/icu-plurals.js — Pluralization across locales
const { MessageFormat } = require('@messageformat/core');

// English plurals (one, other)
const enPlural = new MessageFormat('en');
const enMsg = enPlural.compile(
    'You have {count, plural, =0 {no messages} one {# message} other {# messages}}.'
);

console.log(enMsg({ count: 0 }));
// Output: You have no messages.

console.log(enMsg({ count: 1 }));
// Output: You have 1 message.

console.log(enMsg({ count: 42 }));
// Output: You have 42 messages.

// Arabic plurals (zero, one, two, few, many, other)
const arPlural = new MessageFormat('ar');
const arMsg = arPlural.compile(
    'لديك {count, plural, ' +
    'zero {لا توجد رسائل} ' +
    'one {رسالة واحدة} ' +
    'two {رسالتان} ' +
    'few {{count} رسائل} ' +
    'many {{count} رسالة} ' +
    'other {{count} رسالة}}.'
);

console.log(arMsg({ count: 0 }));
// Output: لديك لا توجد رسائل

console.log(arMsg({ count: 1 }));
// Output: لديك رسالة واحدة

console.log(arMsg({ count: 2 }));
// Output: لديك رسالتان

console.log(arMsg({ count: 5 }));
// Output: لديك 5 رسائل

// Russian plurals (one, few, many, other)
const ruPlural = new MessageFormat('ru');
const ruMsg = ruPlural.compile(
    'У вас {count, plural, ' +
    'one {{count} сообщение} ' +
    'few {{count} сообщения} ' +
    'many {{count} сообщений} ' +
    'other {{count} сообщения}}.'
);

console.log(ruMsg({ count: 1 }));
// Output: У вас 1 сообщение

console.log(ruMsg({ count: 2 }));
// Output: У вас 2 сообщения

console.log(ruMsg({ count: 10 }));
// Output: У вас 10 сообщений

Gender and Select

// i18n/icu-select.js — Gender and conditional selection

// Select: choose between options based on a variable
const selectForm = new MessageFormat('en');
const selectMsg = selectForm.compile(
    '{name} added {gender, select, ' +
    'male {his} ' +
    'female {her} ' +
    'other {their}} ' +
    'photo.'
);

console.log(selectMsg({ name: 'Alice', gender: 'female' }));
// Output: Alice added her photo.

console.log(selectMsg({ name: 'Bob', gender: 'male' }));
// Output: Bob added his photo.

console.log(selectMsg({ name: 'Alex', gender: 'non-binary' }));
// Output: Alex added their photo.

// Combining select and plural
const complexMsg = new MessageFormat('en');
const complex = complexMsg.compile(
    '{name} {gender, select, ' +
    'male {updated his} ' +
    'female {updated her} ' +
    'other {updated their}} ' +
    '{count, plural, one {profile picture} other {profile pictures}}.'
);

console.log(complex({ name: 'Alice', gender: 'female', count: 1 }));
// Output: Alice updated her profile picture.

console.log(complex({ name: 'Bob', gender: 'male', count: 3 }));
// Output: Bob updated his profile pictures.

Number and Date Formatting

// i18n/icu-formatting.js — Number and date formatting in ICU

// Number formatting with currency
const numForm = new MessageFormat('en');
const numMsg = numForm.compile(
    'Total: {price, number, ::currency/USD}'
);

console.log(numMsg({ price: 1234.56 }));
// Output: Total: $1,234.56

// Date formatting (locale-aware)
const dateForm = new MessageFormat('en');
const dateMsg = dateForm.compile(
    'Published: {date, date, long}'
);

console.log(dateMsg({ date: new Date('2026-12-31') }));
// Output: Published: December 31, 2026

// German date format
const deDateForm = new MessageFormat('de');
const deDateMsg = deDateForm.compile(
    'Veroffentlicht: {date, date, long}'
);

console.log(deDateMsg({ date: new Date('2026-12-31') }));
// Output: Veroffentlicht: 31. Dezember 2026

// Custom number patterns
const customNum = new MessageFormat('en');
const customNumMsg = customNum.compile(
    'Progress: {percent, number, ::percent}'
);

console.log(customNumMsg({ percent: 0.856 }));
// Output: Progress: 86%

// Ordinal numbers
const ordinal = new MessageFormat('en');
const ordinalMsg = ordinal.compile(
    'You finished {position, number, ::ordinal}!'
);

console.log(ordinalMsg({ position: 3 }));
// Output: You finished 3rd!

Integration with i18next

// i18n/i18next-icu.js — Using ICU MessageFormat with i18next
import i18next from 'i18next';
import ICU from 'i18next-icu';

// Register ICU plugin
i18next.use(ICU).init({
    lng: 'en',
    resources: {
        en: {
            translation: {
                // ICU syntax in translation values
                'welcome': 'Hello, {name}!',
                'notifications': 'You have {count, plural, one {# notification} other {# notifications}}.',
                'friend_added': '{name} added {gender, select, male {his} female {her} other {their}} photo.',
                'total_price': 'Total: {price, number, ::currency/USD}',
                'publish_date': 'Published: {date, date, long}',
                'items_in_cart': 'Cart ({count, plural, =0 {empty} one {# item} other {# items}})'
            }
        },
        ar: {
            translation: {
                'notifications': 'لديك {count, plural, zero {لا توجد إشعارات} one {إشعار واحد} two {إشعاران} few {{count} إشعارات} many {{count} إشعارًا} other {{count} إشعار}}.',
                'friend_added': '{name} {gender, select, male {أضاف} female {أضافت} other {أضاف}} صورة.',
                'total_price': 'الإجمالي: {price, number, ::currency/SAR}',
                'publish_date': 'تاريخ النشر: {date, date, long}'
            }
        }
    }
});

// Usage (same t() function, ICU handles parsing)
console.log(i18next.t('notifications', { count: 1 }));
// Output: You have 1 notification.

console.log(i18next.t('notifications', { count: 42 }));
// Output: You have 42 notifications.

console.log(i18next.t('friend_added', { name: 'Sarah', gender: 'female' }));
// Output: Sarah added her photo.

Common Mistakes

  1. Not escaping curly braces in literal text. If your translation needs literal { or }, double them: {{ and }}. Otherwise ICU interprets them as variable placeholders.
  2. Using confusing plural category names. The categories are: zero, one, two, few, many, other. Not all languages use all categories. English only uses one and other. Always include the "other" category as fallback.
  3. Hardcoding plurals in code instead of translation files. The plural logic belongs in the translation, not in the code. Write "{count, plural, one {item} other {items}}" in the translation file, not in JavaScript.
  4. Forgetting the =N syntax for exact matches. Use =0, =1, =2 for exact number matches that override the plural category. This is useful for "no messages" instead of "0 messages."
  5. Not testing with multiple locales during development. ICU handles Arabic plurals correctly only if you test with actual Arabic locale data. Mocking English plurals during development hides ICU bugs.

Practice Questions

  1. What are the standard ICU plural categories and which ones does English use?
  2. How does the select format differ from plural in ICU?
  3. What does the ::currency/USD syntax do in number formatting?
  4. How do you include literal curly braces in an ICU message?
  5. Why should plural logic live in translation files rather than code?

Challenge: Write ICU messages for a notification system that handles: "X liked your post" (gender-aware: his/her/their), "X and Y others liked your post" (plural: 1 other, 2 others, 3+ others), "X commented on your post Y hours ago" (date-relative formatting), for English, Arabic (6 plural forms), and Russian (4 plural forms).

FAQ

What is ICU MessageFormat?

ICU MessageFormat is a standard syntax for expressing user-facing strings with variables, plurals, gender selection, and locale-aware formatting. It originated from the ICU (International Components for Unicode) library and is used across many platforms and languages.

Do I need a library to use ICU in JavaScript?

Yes. Use the messageformat package (@messageformat/core) or i18next-icu plugin. The native Intl API handles formatting but doesn't parse ICU message syntax.

What's the difference between ICU and i18next syntax?

i18next uses its own simpler syntax ({{count}} item, {{count}} items with _plural suffix). ICU is more powerful but more verbose. i18next-icu bridges both — you can use ICU syntax inside i18next.

Can translators learn ICU easily?

ICU syntax is designed for translators. The plural/select patterns are more readable than code-based conditional logic. Most professional translators working with i18n tools are familiar with ICU.

Does ICU handle RTL text direction?

ICU handles the content direction (text itself), not the layout direction (CSS). The translated string will be in Arabic or Hebrew correctly, but you still need to handle CSS direction (dir attribute) separately.

Mini Project

Build a notification system using ICU MessageFormat: 5 notification types (like, comment, share, follow, mention) each with gender-aware pronouns, pluralization for counts, number formatting for view counts, and date formatting for timestamps. Support English and Arabic. Demonstrate that the same code works for both languages with only translation file changes.

What's Next

You've mastered ICU MessageFormat. Next, learn about Plurals and handling pluralization across languages with different plural rules.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro