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.
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.