Смешанные единицы

В реальных интерфейсах часто требуется отображать не одну единицу измерения, а комбинацию нескольких величин: расстояние вместе с дробными частями, время в формате «часы и минуты», скорость, выраженную через базовую единицу расстояния и времени, бытовые форматы вроде «килограммы и граммы». Такие значения относятся к категории составных (или смешанных) единиц.

В контексте интернационализации задача усложняется тем, что каждая локаль по-своему определяет порядок следования компонентов, правила округления и допустимые сокращения. Библиотека Globalize опирается на CLDR (Unicode Common Locale Data Repository), где для единиц измерения описаны не только базовые символы, но и правила комбинирования и форматирования.


Единицы измерения и их представление в CLDR

CLDR классифицирует единицы измерения как самостоятельные сущности с набором правил:

  • полное название (например, meter, hour)
  • сокращённая форма (m, h)
  • формы множественного числа
  • правила форматирования в зависимости от числа
  • предпочтения локали

Для смешанных единиц важна не только сама единица, но и шаблоны их комбинирования.

Например:

  • время: 1 hour 30 minutes
  • расстояние: 1 kilometer 250 meters
  • масса: 2 kilograms 300 grams

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


Форматирование единиц в Globalize

Globalize предоставляет механизм форматирования единиц через данные CLDR и соответствующие API форматирования.

Базовая идея заключается в том, что единица — это объект с типом и значением, а форматтер преобразует его в строку с учётом локали.

Пример единичного форматирования:

Globalize.locale("en");

const numberFormatter = Globalize.numberFormatter();
const unitFormatter = Globalize.unitFormatter("kilometer");

unitFormatter(5);
// "5 kilometers"

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


Логика составных значений

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

Основная задача — разложить значение на компоненты и отформатировать каждый из них в соответствии с локалью.

Пример разложения времени:

  • 90 минут → 1 час 30 минут
  • 3725 секунд → 1 час 2 минуты 5 секунд

Это требует двух операций:

  1. нормализация исходного значения
  2. распределение по единицам

Нормализация значений

Нормализация — это приведение значения к набору базовых единиц.

Для времени:

  • секунды → минуты → часы

Для массы:

  • граммы → килограммы

Для расстояния:

  • миллиметры → метры → километры

Алгоритм нормализации:

  1. берётся исходное значение в минимальной единице
  2. выполняется последовательное деление на коэффициенты перехода
  3. остаток переносится в текущую единицу

Пример:

function normalizeSeconds(totalSeconds) {
    const hours = Math.floor(totalSeconds / 3600);
    totalSeconds %= 3600;

    const minutes = Math.floor(totalSeconds / 60);
    const seconds = totalSeconds % 60;

    return { hours, minutes, seconds };
}

Локализованное форматирование компонентов

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

Globalize.locale("en");

const nf = Globalize.numberFormatter();

const unitHour = Globalize.unitFormatter("hour");
const unitMinute = Globalize.unitFormatter("minute");

const time = normalizeSeconds(3725);

const result =
    unitHour(time.hours) + " " +
    unitMinute(time.minutes);

Результат будет зависеть от локали:

  • en: 1 hour 2 minutes
  • fr: 1 heure 2 minutes
  • de: 1 Stunde 2 Minuten

Различия касаются не только слов, но и грамматических форм.


Правила множественных форм

CLDR содержит таблицы plural rules, которые определяют форму слова в зависимости от числа.

Например:

  • 1 minute → minute
  • 2 minutes → minutes
  • 0 minutes → minutes

Globalize использует эти правила автоматически при вызове unitFormatter.

Это критично для смешанных единиц, так как каждый компонент может иметь собственную форму:

const unitMinute = Globalize.unitFormatter("minute");

unitMinute(1); // "1 minute"
unitMinute(2); // "2 minutes"

Составные единицы и порядок компонентов

В разных языках порядок компонентов может меняться.

Примеры:

  • английский: 1 hour 30 minutes
  • японский: 1時間30分
  • немецкий: 1 Stunde 30 Minuten

В Globalize порядок задаётся не вручную, а через CLDR-шаблоны, где определены правила соединения единиц.

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


Интервалы и компактные представления

Смешанные единицы часто используются для компактных интервалов:

  • длительность видео
  • расстояние маршрута
  • временные промежутки

Пример стратегии компактного отображения:

  • 59 секунд → 59 s
  • 1 минута 5 секунд → 1:05
  • 1 час 2 минуты → 1:02:00

Globalize не навязывает формат «часы:минуты», но предоставляет инструменты для единиц, на основе которых такие форматы строятся.


Многоуровневые единицы

Некоторые значения требуют трёх и более уровней детализации.

Пример: длительность 1 день 3 часа 12 минут

function normalizeDuration(seconds) {
    const days = Math.floor(seconds / 86400);
    seconds %= 86400;

    const hours = Math.floor(seconds / 3600);
    seconds %= 3600;

    const minutes = Math.floor(seconds / 60);

    return { days, hours, minutes };
}

Далее каждый компонент форматируется через соответствующий unitFormatter.


Согласование единиц в интерфейсе

При работе с несколькими единицами важно соблюдать единый стиль:

  • не смешивать сокращения и полные формы
  • не использовать разные системы измерения внутри одной строки
  • учитывать локаль при выборе разделителей
  • избегать ручной конкатенации без форматирования

Пример некорректного подхода:

result = time.hours + "h " + time.minutes + " min";

Корректный подход:

const hour = Globalize.unitFormatter("hour")(time.hours);
const minute = Globalize.unitFormatter("minute")(time.minutes);

result = hour + " " + minute;

Округление и потеря точности

При разложении на смешанные единицы неизбежно возникает округление.

Пример:

  • 1.75 часа

    • 1 час
    • 0.75 часа → 45 минут

Алгоритм должен учитывать:

  • направление округления (вниз для стабильности отображения)
  • перенос остатка в младшую единицу
  • локальные правила отображения дробных значений

Практика построения универсального форматтера

Обобщённый форматтер смешанных единиц обычно состоит из трёх слоёв:

  1. слой нормализации

    • перевод в базовую единицу
  2. слой декомпозиции

    • разбиение на компоненты
  3. слой локализации

    • применение Globalize unitFormatter к каждому компоненту
function formatMixedTime(seconds) {
    const t = normalizeSeconds(seconds);

    const parts = [];

    if (t.hours) {
        parts.push(Globalize.unitFormatter("hour")(t.hours));
    }

    if (t.minutes) {
        parts.push(Globalize.unitFormatter("minute")(t.minutes));
    }

    if (t.seconds) {
        parts.push(Globalize.unitFormatter("second")(t.seconds));
    }

    return parts.join(" ");
}

Влияние локали на структуру результата

Локаль может менять не только слова, но и:

  • наличие пробелов
  • использование двоеточий вместо пробелов
  • порядок единиц
  • обязательность отображения нулевых значений

Например:

  • en: 1 hour 0 minutes
  • fr: 1 h
  • ja: 1時間

Это означает, что универсальный алгоритм не может быть полностью статическим и должен учитывать правила CLDR для каждой локали отдельно.