Как форматировать списки измерений с единицами?

Отображайте несколько измерений, например 5 км, 10 км, 15 км, с учетом локализации списка с помощью Intl API в JavaScript

Введение

Приложения, которые показывают измерения, часто выводят сразу несколько значений. Например, фитнес-приложение может показывать интервалы как «5 км, 10 км, 15 км». Приложение погоды может отображать температуры за неделю: «20°C, 22°C, 25°C, 23°C». В рецепте могут быть указаны ингредиенты: «2 чашки, 1 столовая ложка, 3 чайные ложки».

В таких списках есть две задачи интернационализации. Во-первых, каждое измерение должно быть оформлено с учетом локали и единицы измерения. Во-вторых, сам список должен использовать правильные знаки препинания и разделители для выбранного языка. В английском это запятые и иногда союз «and». В других языках используются другие разделители и грамматические правила.

В JavaScript есть два API для решения этой задачи. Intl.NumberFormat форматирует отдельные измерения с единицами. Intl.ListFormat объединяет несколько значений в грамматически правильный список. В этом уроке объясняется, как использовать оба API вместе, чтобы форматировать списки измерений так, как ожидают пользователи в любой локали.

Для списков измерений нужны два этапа форматирования

Когда вы форматируете список измерений, нельзя пропускать ни один из этапов. Если вы оформите только список, но не измерения, то получите правильные разделители, но некорректное отображение единиц. Если оформить только измерения, но не список, то будут правильные единицы, но неверные разделители.

const distances = [5, 10, 15];

// Wrong: list formatted but not measurements
console.log(distances.join(', '));
// Output: "5, 10, 15" (missing units)

// Wrong: measurements formatted but not list
const formatter = new Intl.NumberFormat('en-US', {
  style: 'unit',
  unit: 'kilometer'
});
console.log(distances.map(d => formatter.format(d)).join(', '));
// Output: "5 km, 10 km, 15 km" (hardcoded comma might be wrong for some locales)

Правильный подход — сначала форматировать измерения, а затем получившийся массив строк оформлять как список.

const distances = [5, 10, 15];

const numberFormatter = new Intl.NumberFormat('en-US', {
  style: 'unit',
  unit: 'kilometer'
});

const formattedMeasurements = distances.map(d => numberFormatter.format(d));
// Result: ["5 km", "10 km", "15 km"]

const listFormatter = new Intl.ListFormat('en-US', {
  type: 'unit'
});

console.log(listFormatter.format(formattedMeasurements));
// Output: "5 km, 10 km, 15 km"

Этот подход работает для любых типов измерений и любой локали. Сначала форматируйте каждое измерение с единицей, затем массив отформатированных строк — как список.

Используйте тип unit для списков измерений

Конструктор Intl.ListFormat принимает опцию type, которая управляет тем, как элементы списка объединяются. Опция type: 'unit' форматирует списки в соответствии с принятыми стандартами для технических и научных данных.

const measurements = ['5 km', '10 km', '15 km'];

const unitList = new Intl.ListFormat('en-US', {
  type: 'unit'
});

console.log(unitList.format(measurements));
// Output: "5 km, 10 km, 15 km"

Списки с type: 'unit' не используют союзы вроде «и» или «или». Между элементами ставятся простые разделители. Это соответствует тому, как обычно записывают измерения в технических текстах.

Для сравнения, type: 'conjunction' добавляет «и» перед последним элементом.

const measurements = ['5 km', '10 km', '15 km'];

const conjunctionList = new Intl.ListFormat('en-US', {
  type: 'conjunction'
});

console.log(conjunctionList.format(measurements));
// Output: "5 km, 10 km, and 15 km"

Форма с союзом звучит естественно в обычном тексте, но выглядит некорректно в технических контекстах. При отображении нескольких измерений используйте type: 'unit', чтобы следовать стандартам научного и технического письма.

Форматируйте списки с расстояниями

Для измерения расстояний используются идентификаторы единиц, такие как kilometer, meter, mile и foot. После форматирования каждого значения с единицей объедините их в список.

const distances = [5, 10, 15, 20];

const numberFormatter = new Intl.NumberFormat('en-US', {
  style: 'unit',
  unit: 'kilometer'
});

const formattedDistances = distances.map(d => numberFormatter.format(d));

const listFormatter = new Intl.ListFormat('en-US', {
  type: 'unit'
});

console.log(listFormatter.format(formattedDistances));
// Output: "5 km, 10 km, 15 km, 20 km"

Та же схема работает и для миль.

const distances = [3, 6, 9];

const numberFormatter = new Intl.NumberFormat('en-US', {
  style: 'unit',
  unit: 'mile'
});

const formattedDistances = distances.map(d => numberFormatter.format(d));

const listFormatter = new Intl.ListFormat('en-US', {
  type: 'unit'
});

console.log(listFormatter.format(formattedDistances));
// Output: "3 mi, 6 mi, 9 mi"

Можно форматировать расстояния с десятичными знаками, задав параметры форматирования чисел.

const distances = [5.2, 10.7, 15.3];

const numberFormatter = new Intl.NumberFormat('en-US', {
  style: 'unit',
  unit: 'kilometer',
  maximumFractionDigits: 1
});

const formattedDistances = distances.map(d => numberFormatter.format(d));

const listFormatter = new Intl.ListFormat('en-US', {
  type: 'unit'
});

console.log(listFormatter.format(formattedDistances));
// Output: "5.2 km, 10.7 km, 15.3 km"

Форматтер чисел округляет значения и выставляет нужное количество знаков после запятой до того, как форматтер списка объединяет их.

Форматируйте списки с весами

Для измерения веса используется тот же подход с идентификаторами единиц, такими как kilogram, pound, ounce и gram.

const weights = [50, 75, 100];

const numberFormatter = new Intl.NumberFormat('en-US', {
  style: 'unit',
  unit: 'kilogram'
});

const formattedWeights = weights.map(w => numberFormatter.format(w));

const listFormatter = new Intl.ListFormat('en-US', {
  type: 'unit'
});

console.log(listFormatter.format(formattedWeights));
// Output: "50 kg, 75 kg, 100 kg"

Можно также отобразить вес в фунтах.

const weights = [110, 165, 220];

const numberFormatter = new Intl.NumberFormat('en-US', {
  style: 'unit',
  unit: 'pound'
});

const formattedWeights = weights.map(w => numberFormatter.format(w));

const listFormatter = new Intl.ListFormat('en-US', {
  type: 'unit'
});

console.log(listFormatter.format(formattedWeights));
// Output: "110 lb, 165 lb, 220 lb"

Форматтер чисел автоматически использует правильные сокращения для каждой единицы.

Форматируйте списки с температурами

Для измерения температуры используются идентификаторы единиц, такие как celsius и fahrenheit.

const temperatures = [20, 22, 25, 23, 21];

const numberFormatter = new Intl.NumberFormat('en-US', {
  style: 'unit',
  unit: 'celsius'
});

const formattedTemperatures = temperatures.map(t => numberFormatter.format(t));

const listFormatter = new Intl.ListFormat('en-US', {
  type: 'unit'
});

console.log(listFormatter.format(formattedTemperatures));
// Output: "20°C, 22°C, 25°C, 23°C, 21°C"

Форматтеры температуры автоматически добавляют символы градусов в вывод.

Фаренгейт работает так же.

const temperatures = [68, 72, 77, 73, 70];

const numberFormatter = new Intl.NumberFormat('en-US', {
  style: 'unit',
  unit: 'fahrenheit'
});

const formattedTemperatures = temperatures.map(t => numberFormatter.format(t));

const listFormatter = new Intl.ListFormat('en-US', {
  type: 'unit'
});

console.log(listFormatter.format(formattedTemperatures));
// Output: "68°F, 72°F, 77°F, 73°F, 70°F"

Шаблон остаётся одинаковым для разных типов измерений. Меняется только идентификатор единицы.

Форматирование объёма в списках

Для измерения объёма используются идентификаторы единиц, такие как liter, gallon, milliliter и fluid-ounce.

const volumes = [1, 2, 3];

const numberFormatter = new Intl.NumberFormat('en-US', {
  style: 'unit',
  unit: 'liter'
});

const formattedVolumes = volumes.map(v => numberFormatter.format(v));

const listFormatter = new Intl.ListFormat('en-US', {
  type: 'unit'
});

console.log(listFormatter.format(formattedVolumes));
// Output: "1 L, 2 L, 3 L"

Измерения объёма поддерживают десятичные значения.

const volumes = [0.5, 1.5, 2.5];

const numberFormatter = new Intl.NumberFormat('en-US', {
  style: 'unit',
  unit: 'liter',
  maximumFractionDigits: 1
});

const formattedVolumes = volumes.map(v => numberFormatter.format(v));

const listFormatter = new Intl.ListFormat('en-US', {
  type: 'unit'
});

console.log(listFormatter.format(formattedVolumes));
// Output: "0.5 L, 1.5 L, 2.5 L"

Форматтер чисел обрабатывает точность десятичных значений до того, как форматтер списка обработает значения.

Форматирование скорости в списках

Для измерения скорости используются составные единицы, такие как kilometer-per-hour и mile-per-hour.

const speeds = [50, 75, 100];

const numberFormatter = new Intl.NumberFormat('en-US', {
  style: 'unit',
  unit: 'kilometer-per-hour'
});

const formattedSpeeds = speeds.map(s => numberFormatter.format(s));

const listFormatter = new Intl.ListFormat('en-US', {
  type: 'unit'
});

console.log(listFormatter.format(formattedSpeeds));
// Output: "50 km/h, 75 km/h, 100 km/h"

Мили в час работают так же.

const speeds = [30, 45, 60];

const numberFormatter = new Intl.NumberFormat('en-US', {
  style: 'unit',
  unit: 'mile-per-hour'
});

const formattedSpeeds = speeds.map(s => numberFormatter.format(s));

const listFormatter = new Intl.ListFormat('en-US', {
  type: 'unit'
});

console.log(listFormatter.format(formattedSpeeds));
// Output: "30 mph, 45 mph, 60 mph"

Составные единицы форматируются автоматически с правильными сокращениями и разделителями.

Локаль определяет формат разделителей в списках

Параметр локали управляет тем, как элементы списка разделяются и пунктуируются. В разных языках используются разные правила форматирования списков.

const distances = [5, 10, 15];

const numberFormatter = new Intl.NumberFormat('en-US', {
  style: 'unit',
  unit: 'kilometer'
});

const formattedDistances = distances.map(d => numberFormatter.format(d));

const enList = new Intl.ListFormat('en-US', {
  type: 'unit'
});

const frList = new Intl.ListFormat('fr-FR', {
  type: 'unit'
});

const deList = new Intl.ListFormat('de-DE', {
  type: 'unit'
});

console.log(enList.format(formattedDistances));
// Output: "5 km, 10 km, 15 km"

console.log(frList.format(formattedDistances));
// Output: "5 km, 10 km, 15 km"

console.log(deList.format(formattedDistances));
// Output: "5 km, 10 km, 15 km"

Хотя сокращения километров похожи в этих локалях, правила пробелов и разделителей могут отличаться. API Intl.ListFormat автоматически учитывает эти особенности форматирования для каждой локали.

В некоторых языках используются разные разделители или схемы пунктуации для списков. API гарантирует, что ваши списки будут соответствовать нужным правилам для каждой локали, и вам не нужно знать эти правила заранее.

Совмещайте локаль чисел и списков

При форматировании списков измерений используйте одну и ту же локаль для форматтера чисел и форматтера списков. Это обеспечит единообразное форматирование во всём выводе.

const distances = [1000, 2000, 3000];

const locale = 'de-DE';

const numberFormatter = new Intl.NumberFormat(locale, {
  style: 'unit',
  unit: 'meter'
});

const formattedDistances = distances.map(d => numberFormatter.format(d));

const listFormatter = new Intl.ListFormat(locale, {
  type: 'unit'
});

console.log(listFormatter.format(formattedDistances));
// Output: "1.000 m, 2.000 m, 3.000 m"

В немецком формате для разделения тысяч используются точки. И форматер чисел, и форматер списков используют немецкие правила, потому что у них общий язык локали.

Использование разных локалей для форматирования чисел и списков приводит к несогласованному выводу.

const distances = [1000, 2000, 3000];

const numberFormatter = new Intl.NumberFormat('de-DE', {
  style: 'unit',
  unit: 'meter'
});

const formattedDistances = distances.map(d => numberFormatter.format(d));

const listFormatter = new Intl.ListFormat('en-US', {
  type: 'unit'
});

console.log(listFormatter.format(formattedDistances));
// Output: "1.000 m, 2.000 m, 3.000 m"

В этом случае получается смешанное форматирование: числа оформляются по немецким правилам, а список — по английским. Всегда используйте одну и ту же локаль для обоих форматеров.

Управляйте стилем отображения списка

Опция style определяет, насколько подробно будет оформлен список. Она принимает три значения: "long", "short" и "narrow".

const measurements = ['5 km', '10 km', '15 km'];

const longList = new Intl.ListFormat('en-US', {
  type: 'unit',
  style: 'long'
});

const shortList = new Intl.ListFormat('en-US', {
  type: 'unit',
  style: 'short'
});

const narrowList = new Intl.ListFormat('en-US', {
  type: 'unit',
  style: 'narrow'
});

console.log(longList.format(measurements));
// Output: "5 km, 10 km, 15 km"

console.log(shortList.format(measurements));
// Output: "5 km, 10 km, 15 km"

console.log(narrowList.format(measurements));
// Output: "5 km 10 km 15 km"

Стили long и short дают похожий результат для списков единиц в английском языке. Стиль narrow использует минимальные пробелы и не ставит разделители между элементами.

В разных локалях стили могут отличаться сильнее. Локаль определяет точное оформление для каждого уровня стиля.

Используйте полные названия единиц

Вы можете форматировать измерения с полными названиями единиц вместо сокращений, если задать unitDisplay: 'long' в форматере чисел.

const distances = [5, 10, 15];

const numberFormatter = new Intl.NumberFormat('en-US', {
  style: 'unit',
  unit: 'kilometer',
  unitDisplay: 'long'
});

const formattedDistances = distances.map(d => numberFormatter.format(d));

const listFormatter = new Intl.ListFormat('en-US', {
  type: 'unit'
});

console.log(listFormatter.format(formattedDistances));
// Output: "5 kilometers, 10 kilometers, 15 kilometers"

Форматер чисел автоматически выбирает единственное и множественное число. Форматер списков объединяет отформатированные строки, независимо от того, используются ли сокращения или полные названия.

const distances = [1, 5];

const numberFormatter = new Intl.NumberFormat('en-US', {
  style: 'unit',
  unit: 'kilometer',
  unitDisplay: 'long'
});

const formattedDistances = distances.map(d => numberFormatter.format(d));

const listFormatter = new Intl.ListFormat('en-US', {
  type: 'unit'
});

console.log(listFormatter.format(formattedDistances));
// Output: "1 kilometer, 5 kilometers"

Форматер чисел использует «километр» для 1 и «километров» для 5. Форматер списков объединяет их с нужными разделителями.

Используйте повторно форматеры для лучшей производительности

Создание экземпляров Intl.NumberFormat и Intl.ListFormat требует загрузки данных локали и обработки опций. Если вы форматируете несколько списков измерений, создайте форматеры один раз и используйте их повторно.

const numberFormatter = new Intl.NumberFormat('en-US', {
  style: 'unit',
  unit: 'kilometer'
});

const listFormatter = new Intl.ListFormat('en-US', {
  type: 'unit'
});

const distanceLists = [
  [5, 10, 15],
  [20, 25, 30],
  [35, 40, 45]
];

distanceLists.forEach(distances => {
  const formattedDistances = distances.map(d => numberFormatter.format(d));
  console.log(listFormatter.format(formattedDistances));
});
// Output:
// "5 km, 10 km, 15 km"
// "20 km, 25 km, 30 km"
// "35 km, 40 km, 45 km"

Этот подход создаёт каждый форматтер только один раз и использует его многократно. Разница в производительности становится заметной при форматировании большого количества списков.

Создайте функцию для повторного использования форматтера

Вы можете инкапсулировать этот двухшаговый паттерн форматирования в функцию для повторного использования.

function formatMeasurementList(values, locale, unit) {
  const numberFormatter = new Intl.NumberFormat(locale, {
    style: 'unit',
    unit: unit
  });

  const formattedValues = values.map(v => numberFormatter.format(v));

  const listFormatter = new Intl.ListFormat(locale, {
    type: 'unit'
  });

  return listFormatter.format(formattedValues);
}

console.log(formatMeasurementList([5, 10, 15], 'en-US', 'kilometer'));
// Output: "5 km, 10 km, 15 km"

console.log(formatMeasurementList([50, 75, 100], 'en-US', 'kilogram'));
// Output: "50 kg, 75 kg, 100 kg"

console.log(formatMeasurementList([20, 22, 25], 'en-US', 'celsius'));
// Output: "20°C, 22°C, 25°C"

Эта функция работает с любым типом измерения и локалью. Её можно расширить, чтобы принимать дополнительные параметры форматирования.

function formatMeasurementList(values, locale, unit, options = {}) {
  const numberFormatter = new Intl.NumberFormat(locale, {
    style: 'unit',
    unit: unit,
    ...options
  });

  const formattedValues = values.map(v => numberFormatter.format(v));

  const listFormatter = new Intl.ListFormat(locale, {
    type: 'unit'
  });

  return listFormatter.format(formattedValues);
}

console.log(formatMeasurementList(
  [5.123, 10.789, 15.456],
  'en-US',
  'kilometer',
  { maximumFractionDigits: 1 }
));
// Output: "5.1 km, 10.8 km, 15.5 km"

console.log(formatMeasurementList(
  [1, 5, 10],
  'en-US',
  'kilometer',
  { unitDisplay: 'long' }
));
// Output: "1 kilometer, 5 kilometers, 10 kilometers"

Функция передаёт дополнительные параметры в форматтер чисел, что позволяет управлять количеством знаков после запятой, отображением единиц и другими настройками форматирования.

Форматируйте списки для локали пользователя

Вместо жёстко заданной локали можно использовать языковые предпочтения браузера пользователя. Свойство navigator.language возвращает предпочитаемую локаль пользователя.

const userLocale = navigator.language;

const distances = [5, 10, 15];

const numberFormatter = new Intl.NumberFormat(userLocale, {
  style: 'unit',
  unit: 'kilometer'
});

const formattedDistances = distances.map(d => numberFormatter.format(d));

const listFormatter = new Intl.ListFormat(userLocale, {
  type: 'unit'
});

console.log(listFormatter.format(formattedDistances));
// Output varies by user's locale

Такой подход отображает списки измерений в соответствии с ожиданиями каждого пользователя. Разные пользователи видят одни и те же данные, отформатированные по правилам своей локали.

Отображение списков измерений в приложениях

Этот паттерн можно использовать везде, где вы показываете пользователям несколько измерений. Например, в фитнес-приложениях для отображения промежуточных результатов, в погодных приложениях для прогноза температур, в кулинарных приложениях для количества ингредиентов и в научных приложениях для экспериментальных данных.

const splitTimes = [5, 10, 15, 20];

const numberFormatter = new Intl.NumberFormat(navigator.language, {
  style: 'unit',
  unit: 'kilometer',
  maximumFractionDigits: 1
});

const formattedTimes = splitTimes.map(t => numberFormatter.format(t));

const listFormatter = new Intl.ListFormat(navigator.language, {
  type: 'unit'
});

const result = listFormatter.format(formattedTimes);

document.getElementById('split-times').textContent = result;
// Displays: "5 km, 10 km, 15 km, 20 km" (or locale equivalent)

Отформатированные строки работают как любые другие строковые значения. Их можно вставлять в текст, атрибуты или любой другой контекст, где вы показываете информацию пользователям.