Переход с других библиотек

Многие JavaScript-проекты начинали интернационализацию с простых решений: ручных словарей, i18next, Polyglot.js, moment.js, Intl, Numeral.js или самописных модулей локализации. По мере роста приложения появляются проблемы:

  • несогласованное форматирование дат и чисел;
  • дублирование правил локализации;
  • несовместимость библиотек;
  • отсутствие поддержки CLDR;
  • сложность работы с множественными локалями;
  • проблемы с plural forms;
  • ручная обработка валют и единиц измерения.

Globalize решает эти задачи за счёт интеграции с CLDR и стандартизированного подхода к интернационализации.


Отличия Globalize от других библиотек

Сравнение с Intl API

Нативный Intl предоставляет базовые механизмы локализации:

new Intl.NumberFormat("fr").format(12345.67);

Globalize строится поверх CLDR и добавляет:

  • гибкую загрузку локалей;
  • message formatting;
  • pluralization;
  • runtime-компиляцию;
  • precompile-подход;
  • единый API для всех типов локализации.

Пример с Intl:

const formatter = new Intl.DateTimeFormat("de");
formatter.format(new Date());

Пример с Globalize:

const dateFormatter = Globalize("de").dateFormatter({
  datetime: "medium"
});

dateFormatter(new Date());

Главное отличие — Globalize требует явной загрузки данных CLDR.


Переход с ручной локализации

Типичная структура старого проекта

Во многих проектах локализация выглядит так:

const translations = {
  en: {
    hello: "Hello"
  },
  ru: {
    hello: "Привет"
  }
};

function t(locale, key) {
  return translations[locale][key];
}

Подобный подход быстро становится неуправляемым:

  • отсутствуют plural rules;
  • нет форматирования валют;
  • нет склонений;
  • нет региональных особенностей;
  • сложно поддерживать большие словари.

Замена словарей на Globalize

Старый подход

function price(value) {
  return value + " USD";
}

Новый подход

const formatter = Globalize("en").currencyFormatter("USD");

formatter(1200);

Результат:

$1,200.00

Для русской локали:

const formatter = Globalize("ru").currencyFormatter("USD");

formatter(1200);

Результат:

1 200,00 $

Формат автоматически определяется локалью.


Переход с Numeral.js

Особенности Numeral.js

Numeral.js ориентирован только на числа:

numeral(1000).format("0,0");

Недостатки:

  • ограниченная поддержка локалей;
  • отсутствие полноценного CLDR;
  • слабая поддержка pluralization;
  • нет message formatting.

Эквиваленты в Globalize

Форматирование чисел

Numeral.js:

numeral(12345.67).format("0,0.00");

Globalize:

const formatter = Globalize("en").numberFormatter({
  minimumFractionDigits: 2
});

formatter(12345.67);

Форматирование процентов

Numeral.js:

numeral(0.56).format("0%");

Globalize:

const formatter = Globalize("en").numberFormatter({
  style: "percent"
});

formatter(0.56);

Переход с moment.js

Почему проекты уходят от moment.js

Основные причины:

  • большой размер;
  • mutable API;
  • устаревшая архитектура;
  • проблемы tree-shaking;
  • отсутствие активного развития.

Замена форматирования дат

moment.js:

moment(date).format("DD.MM.YYYY");

Globalize:

const formatter = Globalize("ru").dateFormatter({
  skeleton: "yMd"
});

formatter(date);

Замена относительного времени

moment.js:

moment().fromNow();

Globalize:

const formatter = Globalize("en").relativeTimeFormatter("day");

formatter(-1);

Результат:

yesterday

Переход с i18next

Основное различие архитектур

i18next фокусируется на переводах.

Globalize ориентирован на:

  • CLDR;
  • ICU message syntax;
  • форматирование;
  • plural rules;
  • региональные стандарты.

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

  • i18next — переводы;
  • Globalize — форматирование.

Но при полной миграции Globalize способен заменить оба слоя.


Замена системы переводов

i18next

i18next.t("welcome");

Globalize

const formatter = Globalize("ru").messageFormatter("welcome");

formatter();

Работа с параметрами

i18next:

i18next.t("hello", {
  name: "Анна"
});

Globalize:

const formatter = Globalize("ru").messageFormatter("hello");

formatter({
  name: "Анна"
});

Переход на ICU MessageFormat

Старый подход

{
  "items": "У вас {{count}} товаров"
}

Такой вариант не учитывает plural forms.


Новый подход

{
  "items": "{count, plural, one{У вас # товар} few{У вас # товара} many{У вас # товаров} other{У вас # товара}}"
}

Использование:

const formatter = Globalize("ru").messageFormatter("items");

formatter({ count: 5 });

Переход с Polyglot.js

Ограничения Polyglot.js

Polyglot.js подходит для небольших проектов, но имеет ограничения:

  • минимальный набор plural forms;
  • отсутствие CLDR;
  • нет локализованного форматирования;
  • нет поддержки валют;
  • нет date formatting.

Замена pluralization

Polyglot.js:

polyglot.t("cars", 5);

Globalize:

const formatter = Globalize("ru").messageFormatter("cars");

formatter({ count: 5 });

Миграция структуры проекта

Старый вариант

/locales
  en.json
  ru.json
/date-utils
/number-utils
/currency-utils

Новый вариант

/cldr
/messages
/globalize

Централизация локализации

До миграции

formatPrice()
formatDate()
translate()
pluralize()

Все механизмы разрознены.


После миграции

Globalize(locale)

Единая точка входа:

const g = Globalize("ru");

g.formatMessage(...);
g.formatNumber(...);
g.formatDate(...);

Изменение подхода к локалям

Старый подход

if (locale === "ru") {
  ...
}

Новый подход

Логика локализации переносится в CLDR.

Вместо ручных условий:

const formatter = Globalize(locale)
  .currencyFormatter("EUR");

Работа с CLDR

Главное изменение при миграции

Globalize требует загрузки CLDR-данных.

Минимальный набор:

const Cldr = require("cldrjs");
const Globalize = require("globalize");

Globalize.load(
  require("cldr-data/main/en/numbers"),
  require("cldr-data/main/en/ca-gregorian"),
  require("cldr-data/supplemental/likelySubtags")
);

Типичные ошибки при миграции

Отсутствует supplemental data

Ошибка:

E_MISSING_CLDR

Причина:

Globalize.load(...)

загружает только часть данных.

Необходимо:

require("cldr-data/supplemental/numberingSystems")
require("cldr-data/supplemental/plurals")

Неправильная инициализация локали

Ошибка:

Globalize.locale("ru");

без загрузки locale data.

Правильный порядок:

Globalize.load(...)
Globalize.locale("ru");

Проблемы bundle size

Частая проблема

CLDR содержит большой объём данных.

Неправильный импорт:

require("cldr-data");

резко увеличивает bundle.


Оптимизация загрузки

Выборочная загрузка

require("cldr-data/main/ru/numbers");
require("cldr-data/main/ru/ca-gregorian");

Lazy loading локалей

async function loadLocale(locale) {
  const data = await import(
    `cldr-data/main/${locale}/numbers.json`
  );

  Globalize.load(data);
}

Переход на precompile

Проблема runtime parsing

Без precompile:

Globalize.messageFormatter(...)

компилирует сообщения в runtime.

При большом количестве переводов это создаёт нагрузку.


Использование globalize-compiler

globalize-compiler extract
globalize-compiler compile

После компиляции:

import compiled from "./compiled-messages";

compiled.formatMessage(...);

Миграция pluralization

Ручной подход

if (count === 1) {
  return "товар";
}

ICU plural rules

"{count, plural,
  one{товар}
  few{товара}
  many{товаров}
}"

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


Изменение архитектуры приложения

До миграции

Обычно:

UI → translate()
UI → formatDate()
UI → formatMoney()

Разные сервисы и библиотеки.


После миграции

UI → Globalize

Единая инфраструктура локализации.


Интеграция с React

Старый код

<span>{i18next.t("hello")}</span>

Новый код

<span>{g.formatMessage("hello")}</span>

Создание глобального i18n-сервиса

Рекомендуемый подход

import Globalize from "globalize";

export function createI18n(locale) {
  const g = new Globalize(locale);

  return {
    t: g.formatMessage.bind(g),
    n: g.formatNumber.bind(g),
    d: g.formatDate.bind(g)
  };
}

Постепенная миграция

Практический подход

Полный переход редко выполняется сразу.

Чаще используется схема:

1. Переводы
2. Числа
3. Валюты
4. Даты
5. Relative time
6. ICU messages

Совместное использование библиотек

Временный гибрид

i18next.t(...)
Globalize.formatNumber(...)

Такой подход позволяет мигрировать поэтапно.


Проверка совместимости переводов

Старые сообщения

{
  "hello": "Привет"
}

Новые ICU-сообщения

{
  "hello": "Привет, {name}"
}

Изменение структуры message catalog

Простые строки

{
  "save": "Сохранить"
}

ICU messages

{
  "save": "{gender, select,
    male{Сохранил}
    female{Сохранила}
    other{Сохранил(а)}
  }"
}

Переход с кастомных date helper

Старый helper

function formatDate(date) {
  return date.toLocaleDateString("ru");
}

Новый helper

const formatter = Globalize("ru")
  .dateFormatter({
    date: "long"
  });

formatter(date);

Унификация локализации

После миграции:

  • даты форматируются одинаково;
  • валюты соответствуют региону;
  • pluralization корректен;
  • единицы измерения локализованы;
  • сообщения используют ICU syntax;
  • уменьшается количество самописного кода.

Когда миграция особенно оправдана

Globalize особенно полезен при:

  • большом количестве локалей;
  • enterprise-приложениях;
  • финансовых системах;
  • e-commerce;
  • SaaS-платформах;
  • сложных plural forms;
  • строгих требованиях к локализации;
  • SSR и precompile-сборке.

Когда миграция может быть избыточной

Использование Globalize не всегда оправдано.

Для небольших проектов иногда достаточно:

  • Intl;
  • i18next;
  • date-fns;
  • простых JSON-словарей.

Globalize наиболее эффективен в крупных системах с полноценной интернационализацией и глубокой зависимостью от CLDR.