Three words are often used interchangeably, yet they describe different stages of the work. Internationalization, abbreviated i18n, is preparing the program for many languages and markets: the code does not hard-code the language, number format, or writing direction, but moves them into extension points. Localization, l10n, is producing one concrete local version: Russian, German, Arabic. Translation, converting the text, is only one part of localization, and far from the hardest. If the architecture is not prepared, no amount of translation will save it: strings will join up wrong, numbers will format incorrectly, and dates will shift by a day.
Take the product page of a multilingual store. It is seen at once by shoppers from different countries, by search crawlers, and across different locales. The page has a cart badge with a counter, a price, a delivery time, and a list of specifications. The naive approach feels natural: build the English version first and add translation later by swapping the strings. While there is a single language everything works, and the temptation to defer i18n is strong.
It breaks on the very first real phrase. The developer assembles the cart counter from pieces: a number, a space, a word. The price from a currency sign and an amount. The code reads easily and looks harmless.
The trouble is that word order, grammatical case, and agreement differ between languages. In English it is 3 items; in Russian 3 tovara, and one item is already another form. The currency sign sits before the amount in some locales and after it in others; group and decimal separators differ too. A translator handed the fragment items sees neither the number nor the context and cannot choose the right form. Gluing sentences out of fragments is a design error, not a minor oversight.
The professional solution reverses the flow. Text is not a literal in the code but an entry in a message catalog, reached by a key. The number, price, and date are not stitched onto the string by hand but supplied as named parameters that the platform formats. The translator receives the whole message with placeholders and sees what goes where.
A separate concern is the plural, because languages divide quantities differently. English has two forms, Russian four, Arabic six. The ICU MessageFormat describes this explicitly: one message lists all the forms, and the engine picks the right one by the rules of the locale (the data comes from Unicode CLDR, the common repository of locale data). The # symbol substitutes the number itself.
{count, plural,
=0 {Нет файлов}
one {# файл}
few {# файла}
many {# файлов}
other {# файла}
}The same mechanism handles gender and variant selection: the select construct substitutes different wordings depending on the user's gender or the type of entity. Flexibility has a price. Literals in the code turn into keys, a translation catalog appears, along with string-extraction tooling and a translation memory, and the developer must think through a message's parameters in advance. For a single-language prototype this is overkill.
It becomes justified the moment a product gains a second language or a real plan for one - reworking the glued strings after the fact costs more than laying the frame from the start. The price of skipping it shows on the same store: during a promotion the interface reads 1 items in cart and Delivery in 1 days, the Russian shopper reads it as sloppiness, trust in the checkout drops, and conversion with it. One unselected plural costs real money.
const text = count + ' files';
const price = '$' + value.toFixed(2);const text = t('files', { count });
const price = new Intl.NumberFormat(locale, {
style: 'currency',
currency
}).format(value);