Символы валют и их позиция

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

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

Библиотека Globalize решает эти задачи через систему CLDR-данных и предоставляет единый API для корректного форматирования валют в различных локалях.


Подключение Globalize для работы с валютами

Для форматирования валют требуется:

  1. подключить Globalize;
  2. загрузить CLDR-данные;
  3. указать текущую локаль;
  4. использовать форматтер валют.

Пример базовой настройки:

const Globalize = require("globalize");

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

Globalize.locale("en");

После загрузки можно создавать форматтер валют.


Форматирование валюты

Базовый пример:

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

console.log(formatter(1500));
// $1,500.00

Метод currencyFormatter() возвращает функцию форматирования, которая автоматически применяет правила текущей локали.


Символ валюты и его позиция

Различия между локалями

Одинаковая валюта может отображаться по-разному в зависимости от языка и региона.

Пример:

const amount = 1500;

Английская локаль США

Globalize.locale("en-US");

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

console.log(formatter(amount));

Результат:

$1,500.00

Символ валюты расположен перед числом без пробела.


Французская локаль

Globalize.locale("fr");

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

console.log(formatter(amount));

Результат:

1 500,00 $US

Особенности:

  • символ валюты расположен после числа;
  • используется неразрывный пробел;
  • дробная часть отделяется запятой;
  • для доллара используется обозначение $US.

Русская локаль

Globalize.locale("ru");

const formatter = Globalize.currencyFormatter("RUB");

console.log(formatter(amount));

Результат:

1 500,00 ₽

Символ рубля располагается после числа.


Автоматическое определение позиции символа

Globalize не требует ручной настройки позиции валюты. Библиотека использует данные Unicode CLDR и автоматически определяет:

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

Это особенно важно для многоязычных интерфейсов.


Использование различных валют

Евро

Globalize.locale("de");

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

console.log(formatter(2999.95));

Результат:

2.999,95 €

В немецкой локали:

  • точка используется как разделитель тысяч;
  • запятая — разделитель дробной части;
  • символ евро ставится после числа.

Японская иена

Globalize.locale("ja");

const formatter = Globalize.currencyFormatter("JPY");

console.log(formatter(5000));

Результат:

¥5,000

Для иены дробная часть отсутствует.


Фунт стерлингов

Globalize.locale("en-GB");

const formatter = Globalize.currencyFormatter("GBP");

console.log(formatter(120));

Результат:

£120.00

Использование кодов валют вместо символов

В некоторых случаях символ валюты может быть неоднозначным.

Например:

  • $ используется для USD, CAD, AUD;
  • kr используется в нескольких странах;
  • £ может обозначать разные валюты.

Globalize позволяет выводить код валюты.


Формат с кодом валюты

const formatter = Globalize.currencyFormatter("USD", {
  style: "code"
});

console.log(formatter(100));

Результат:

USD 100.00

Формат с названием валюты

const formatter = Globalize.currencyFormatter("USD", {
  style: "name"
});

console.log(formatter(100));

Результат:

100.00 US dollars

Сравнение стилей

Символ

Globalize.currencyFormatter("EUR")(100);
€100.00

Код

Globalize.currencyFormatter("EUR", {
  style: "code"
})(100);
EUR 100.00

Название

Globalize.currencyFormatter("EUR", {
  style: "name"
})(100);
100.00 euros

Особенности пробелов

Обычный и неразрывный пробел

Многие локали используют не обычный пробел, а специальный неразрывный символ.

Пример французской локали:

1 500,00 €

Используются:

  • narrow no-break space;
  • non-breaking space.

Это предотвращает перенос символа валюты на новую строку.


Почему нельзя форматировать вручную

Ошибка:

price + " $" 

Проблемы такого подхода:

  • неправильная позиция валюты;
  • отсутствие локализации;
  • некорректные пробелы;
  • проблемы с RTL-языками;
  • ошибки округления;
  • отсутствие поддержки национальных стандартов.

Работа с отрицательными значениями

Стандартное отображение

Globalize.locale("en");

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

console.log(formatter(-100));

Результат:

-$100.00

Локальные особенности

В некоторых странах отрицательные суммы оформляются иначе:

  • через скобки;
  • с пробелами;
  • с другим положением минуса.

Globalize автоматически применяет нужные правила CLDR.


Позиция валюты в RTL-языках

RTL-языки требуют особого подхода.

Пример арабской локали:

Globalize.locale("ar");

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

console.log(formatter(100));

Результат может выглядеть так:

‏100.00 US$

Библиотека корректно обрабатывает:

  • направление текста;
  • bidi-символы;
  • позицию валюты;
  • пробелы.

Настройка минимального и максимального количества знаков

Фиксированное количество дробных знаков

const formatter = Globalize.numberFormatter({
  style: "currency",
  currency: "USD",
  minimumFractionDigits: 2,
  maximumFractionDigits: 2
});

console.log(formatter(25));

Результат:

$25.00

Отключение дробной части

const formatter = Globalize.numberFormatter({
  style: "currency",
  currency: "USD",
  maximumFractionDigits: 0
});

console.log(formatter(25.75));

Результат:

$26

Различие между currencyFormatter и numberFormatter

currencyFormatter

Специализированный метод для валют:

Globalize.currencyFormatter("USD");

Подходит для большинства задач.


numberFormatter

Более гибкий вариант:

Globalize.numberFormatter({
  style: "currency",
  currency: "USD"
});

Позволяет дополнительно управлять:

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

Поддержка нестандартных валют

Форматирование криптовалют

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

const formatter = Globalize.numberFormatter({
  minimumFractionDigits: 8,
  maximumFractionDigits: 8
});

console.log(formatter(0.12345678) + " BTC");

Результат:

0.12345678 BTC

Кэширование форматтеров

Создание форматтера — относительно дорогая операция.

Неправильно:

function format(price) {
  return Globalize.currencyFormatter("USD")(price);
}

Каждый вызов создаёт новый форматтер.


Правильный подход

const usdFormatter =
  Globalize.currencyFormatter("USD");

function format(price) {
  return usdFormatter(price);
}

Форматирование нескольких валют

Интернет-магазин

const prices = [
  { value: 10, currency: "USD" },
  { value: 20, currency: "EUR" },
  { value: 3000, currency: "JPY" }
];

Создание набора форматтеров

const formatters = {
  USD: Globalize.currencyFormatter("USD"),
  EUR: Globalize.currencyFormatter("EUR"),
  JPY: Globalize.currencyFormatter("JPY")
};

prices.forEach(item => {
  console.log(
    formatters[item.currency](item.value)
  );
});

Типичные ошибки

Жёсткое добавление символа валюты

Плохо:

"$" + amount

Игнорирование локали

Плохо:

amount.toFixed(2)

Такой код не учитывает:

  • разделители;
  • положение валюты;
  • национальные правила.

Хранение форматированных строк

Плохо:

const price = "$100.00";

Лучше хранить:

{
  value: 100,
  currency: "USD"
}

Форматирование должно происходить только на этапе отображения.


Интеграция с интерфейсами

React

const usdFormatter =
  Globalize.currencyFormatter("USD");

function Price({ value }) {
  return (
    <span>{usdFormatter(value)}</span>
  );
}

Vue

methods: {
  formatPrice(value) {
    return this.formatter(value);
  }
}

Angular

formatPrice(value: number) {
  return this.usdFormatter(value);
}

Производительность при большом количестве данных

При отображении тысяч цен:

  • не создавать форматтеры внутри циклов;
  • кэшировать экземпляры;
  • переиспользовать локали;
  • минимизировать повторную загрузку CLDR.

Архитектура сервиса форматирования

Централизованный сервис

class CurrencyService {
  constructor(locale) {
    Globalize.locale(locale);

    this.formatters = {};
  }

  format(value, currency) {
    if (!this.formatters[currency]) {
      this.formatters[currency] =
        Globalize.currencyFormatter(currency);
    }

    return this.formatters[currency](value);
  }
}

Практический пример локализации

США

$1,999.99

Германия

1.999,99 €

Россия

1 999,99 ₽

Япония

¥1,999

Франция

1 999,99 €

Использование нескольких локалей одновременно

const en = new Globalize("en");
const fr = new Globalize("fr");
const ru = new Globalize("ru");

const usd = en.currencyFormatter("USD");
const eur = fr.currencyFormatter("EUR");
const rub = ru.currencyFormatter("RUB");

console.log(usd(100));
console.log(eur(100));
console.log(rub(100));

Проверка корректности локализации

При тестировании валют необходимо проверять:

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

Внутренний механизм Globalize

Globalize использует данные CLDR:

  • numbers.json
  • currencies.json
  • supplemental-данные

На основе этих файлов библиотека определяет:

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

Благодаря этому форматирование остаётся одинаково корректным для десятков языков и регионов.