Когда текст вынесен в каталог сообщений, остаётся вторая половина задачи: данные внутри этих сообщений. Цена, дата доставки, количество отзывов, перечень характеристик - всё это выводится по правилам локали. Locale здесь - это набор правил отображения, обозначенный тегом вида ru-RU или de-DE по стандарту BCP 47. Тег говорит платформе, как показывать числа и даты, но сам по себе не решает, в какой валюте цена и в каком часовом поясе срок.
Наивно эти правила пишут руками. Дату собирают из день-месяц-год через шаблон, сумму режут на разряды регулярным выражением, дробную часть округляют через toFixed и приклеивают знак валюты. Для одной локали это работает и выглядит аккуратно.
Ломается на второй локали. Разделитель дробной части в русском и немецком - запятая, в английском точка. Разделитель тысяч - пробел, точка или запятая в зависимости от рынка. Порядок частей даты меняется: день-месяц-год, месяц-день-год, год-месяц-день. Где-то 24 часа, где-то 12 с AM и PM; неделя начинается то с понедельника, то с воскресенья. Ручное форматирование - это, по сути, плохая копия базы CLDR, которую придётся чинить под каждый рынок.
Платформа уже носит эту базу в себе. Семейство Intl - это готовые форматтеры, читающие правила из Unicode CLDR, встроенного в движок: DateTimeFormat для дат и времени, NumberFormat для чисел, процентов и валют, RelativeTimeFormat для относительного времени, ListFormat для перечислений, PluralRules для выбора формы числа и Collator для сравнения строк. Их не надо подключать - они есть в браузере и в среде выполнения.
В одном вызове видно сразу несколько форматтеров. DateTimeFormat со стилем long выдаёт дату словами по правилам ru-RU и в нужном часовом поясе. ListFormat со связкой conjunction соединяет элементы через запятые и союз и по русским правилам. RelativeTimeFormat с numeric auto превращает минус один день во вчера, а не в 1 день назад. Сортировку по алфавиту делает Collator: он ставит буквы в правильном для локали порядке, где ё и буквы с диакритикой встают на свои места, а не в конец таблицы Unicode.
Главное - не путать локаль с валютой, часовым поясом и единицами измерения. Язык интерфейса не определяет, чем платит покупатель: магазин выставляет цену в евро и русскому, и немцу, если торгует из еврозоны. Часовой пояс срока доставки - это факт склада, а не браузера пользователя. Единицы (метры или мили, килограммы или фунты) диктует рынок, а не Accept-Language. Эти параметры должны приходить из бизнес-контекста и подставляться в форматтер явно.
Цена такого подхода почти нулевая: Intl встроен и быстр. Две тонкости всё же есть. Создание форматтера дороже самого форматирования, поэтому экземпляр стоит создавать один раз и переиспользовать, а не в цикле. И вывод зависит от версии движка и данных CLDR, поэтому в тестах нельзя сверять форматированную строку буквально - проверяют факт вызова и параметры, а не точные символы.
Применять это стоит для любых данных, которые видит человек. Провал же нагляден: жёстко зашитый знак доллара на витрине в евро вводит покупателя в заблуждение о цене; дата 07/08 в США читается как 8 июля (месяц-день), а в России как 7 августа (день-месяц) - одна строка, две разные даты; срок доставки без часового пояса уезжает на сутки, и посылку ждут не в тот день. Каждый такой промах - это возврат, тикет в поддержку и потерянное доверие.
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);