Skip to the content.

Internationalization is more than swapping the words

Internationalization is more than swapping the words

Internationalization gets mistaken for “translate the strings,” because that is the visible part. It is the easy part. The hard part is everything else a locale changes: how dates and numbers and currency are formatted, how plurals work (some languages have six plural forms, not two), which direction the text runs, and whether your layout survives a language where the same label is twice as long. A page that swaps English for German but breaks its buttons, mis-formats every price, and mangles plurals is not localized — it is translated and broken. Building for i18n from the start is far cheaper than retrofitting it, because it touches formatting, layout, and data throughout.

Translation is one layer; a locale also changes format, plurals, direction and layout A stack: strings (translation) on top, then formatting (dates, numbers, currency), plurals, text direction (LTR/RTL), and layout that must absorb longer words. strings (the visible, easy layer) formats: dates · numbers · currency plurals (0,1,few,many…) · gender direction: LTR / RTL layout that absorbs 2x-longer words
Translation is the top layer. Everything under it — formats, plurals, direction, and a layout that flexes — is the work that actually makes an app localized.

Format with the platform, don’t hand-roll

Never concatenate a date or a price by hand — the rules differ per locale and the platform already knows them. Intl formats dates, numbers, and currency correctly for any locale, including separators, symbols, and ordering:

// let the platform apply each locale's rules — don't build "$1,234.50" by hand
new Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR" }).format(1234.5);
// → "1.234,50 €"   (comma decimal, dot thousands, symbol after, in German)
new Intl.DateTimeFormat("ja-JP").format(new Date());   // → "2026/07/14" in Japanese order

Hand-formatting is where localization quietly breaks: an American date shown to a European reader is not just ugly, it is wrong (is 03/04 March 4th or April 3rd?).

Plurals and interpolation are not string concatenation

“1 item” vs “2 items” looks trivial in English and is a trap: many languages have several plural categories, and gluing a number to a suffix cannot express them. Use a message format that takes the count and picks the right form per locale:

// ICU MessageFormat: the library picks the correct plural form for the locale
const msg = new IntlMessageFormat(
  "{count, plural, =0 {No items} one {# item} other {# items}}", locale
);
msg.format({ count: 0 });   // "No items"  — and Polish/Russian get their own 'few'/'many'

Never build a sentence by concatenating pieces, either — word order differs across languages, so the whole sentence is one translatable message with named slots.

Design the layout and direction to flex

The two things that silently break are length and direction. German and Finnish words can be twice the English length, so a button sized to “Save” clips “Speichern unter” — design flexible, wrapping layouts, never fixed pixel widths around text. And right-to-left languages (Arabic, Hebrew) mirror the entire layout, so use direction-agnostic CSS (logical properties like margin-inline-start, not margin-left) and let dir="rtl" flip it:

/* logical properties flip automatically for RTL — margin-left would not */
.card { padding-inline-start: 1rem; text-align: start; }

Internationalization done right is a set of habits applied from the start: format with Intl, express plurals and sentences as whole messages, and build layouts that absorb longer text and mirror for RTL. Retrofitting these later means auditing every date, price, sentence, and fixed width in the app — which is exactly why it is a foundation, not a feature. The design-localized-app exercise walks through all of it together, which is where “more than translation” stops being a slogan and becomes a checklist.