Once the text is moved into a message catalog, the second half of the task remains: the data inside those messages. The price, the delivery date, the number of reviews, the list of specifications - all of it is rendered by the rules of the locale. A locale here is a set of display rules identified by a tag like ru-RU or de-DE under the BCP 47 standard. The tag tells the platform how to show numbers and dates, but on its own it does not decide which currency the price is in or which time zone the deadline uses.
The naive way writes these rules by hand. The date is assembled from day-month-year through a template, the amount is cut into groups with a regular expression, the fractional part is rounded with toFixed and a currency sign is glued on. For one locale this works and looks tidy.
It breaks on the second locale. The decimal separator in Russian and German is a comma, in English a dot. The thousands separator is a space, a dot, or a comma depending on the market. The order of the date parts changes: day-month-year, month-day-year, year-month-day. Somewhere it is 24 hours, somewhere 12 with AM and PM; the week starts on Monday in one place and Sunday in another. Hand formatting is essentially a poor copy of the CLDR database that you will have to patch for every market.
The platform already carries that database. The Intl family is a set of ready formatters that read their rules from Unicode CLDR built into the engine: DateTimeFormat for dates and times, NumberFormat for numbers, percentages, and currencies, RelativeTimeFormat for relative time, ListFormat for enumerations, PluralRules for choosing a number's form, and Collator for comparing strings. Nothing needs to be installed - they are present in the browser and in the runtime.
A single call already shows several formatters at work. DateTimeFormat with the long style produces the date in words by the ru-RU rules and in the required time zone. ListFormat with the conjunction type joins the items with commas and the word and by Russian rules. RelativeTimeFormat with numeric auto turns minus one day into yesterday rather than 1 day ago. Alphabetical sorting is done by Collator: it places letters in the order correct for the locale, where the Russian yo and accented letters fall into place instead of at the tail of the Unicode table.
The crucial thing is not to confuse the locale with currency, time zone, and units. The interface language does not determine what the shopper pays in: a store based in the eurozone quotes the price in euros to Russians and Germans alike. The time zone of a delivery slot is a fact about the warehouse, not the user's browser. The units - meters or miles, kilograms or pounds - are dictated by the market, not by Accept-Language. These parameters must come from the business context and be passed to the formatter explicitly.
The cost of this approach is close to zero: Intl is built in and fast. Two subtleties do exist. Constructing a formatter is more expensive than the formatting itself, so an instance should be created once and reused rather than inside a loop. And the output depends on the engine version and CLDR data, so a test must not compare the formatted string literally - you check that the call happened and with which parameters, not the exact characters.
This should be applied to any data a person sees. The failure is vivid: a hard-coded dollar sign on a euro storefront misleads the shopper about the price; the date 07/08 reads in the US as July 8 (month-day) and in Russia as August 7 (day-month) - the same string, two different dates; a delivery slot without a time zone drifts by a day, and the parcel is expected on the wrong one. Each such slip is a return, a support ticket, and lost trust.
const date = new Intl.DateTimeFormat('ru-RU', {
dateStyle: 'long',
timeZone: 'Asia/Tashkent'
}).format(new Date());
const list = new Intl.ListFormat('ru-RU', {
style: 'long',
type: 'conjunction'
}).format(['метрики', 'логи', 'трассы']);
const relative = new Intl.RelativeTimeFormat('ru-RU', {
numeric: 'auto'
}).format(-1, 'day');const price = new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR'
}).format(1299.5);
const names = ['Яблоко', 'ёлка', 'арбуз'];
names.sort(new Intl.Collator('ru').compare);