Форматирование денежных сумм

Интернационализация денежных значений — одна из ключевых задач при разработке веб-приложений, работающих с несколькими языками и регионами. Формат отображения валют отличается в зависимости от локали:

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

Библиотека Globalize предоставляет механизм локализованного форматирования денежных значений на основе данных CLDR.


Подключение модулей

Для работы с валютами необходимы:

const Globalize = require("globalize");

require("cldr-data").entireSupplemental();
require("cldr-data").entireMainFor("en", "ru", "de", "fr");

Globalize.load(
    require("cldr-data/supplemental/likelySubtags.json"),
    require("cldr-data/supplemental/numberingSystems.json"),
    require("cldr-data/supplemental/plurals.json"),
    require("cldr-data/supplemental/currencyData.json"),
    require("cldr-data/main/en/numbers.json"),
    require("cldr-data/main/en/currencies.json"),
    require("cldr-data/main/ru/numbers.json"),
    require("cldr-data/main/ru/currencies.json"),
    require("cldr-data/main/de/numbers.json"),
    require("cldr-data/main/de/currencies.json"),
    require("cldr-data/main/fr/numbers.json"),
    require("cldr-data/main/fr/currencies.json")
);

Создание объекта локали

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

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


Базовое форматирование валюты

Метод currencyFormatter() создаёт функцию форматирования.

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

console.log(formatter(1500.5));

Результат:

$1,500.50

Форматирование в разных локалях

Одинаковое значение отображается по-разному.

const value = 1500.5;

console.log(en.currencyFormatter("USD")(value));
console.log(ru.currencyFormatter("USD")(value));
console.log(de.currencyFormatter("USD")(value));
console.log(fr.currencyFormatter("USD")(value));

Результаты:

$1,500.50
1 500,50 $
1.500,50 $
1 500,50 $US

Форматирование российских рублей

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

console.log(formatter(78500.75));

Результат:

78 500,75 ₽

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

const euroFormatter = de.currencyFormatter("EUR");

console.log(euroFormatter(9999.99));

Результат:

9.999,99 €

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

Некоторые валюты не используют дробную часть.

const yenFormatter = en.currencyFormatter("JPY");

console.log(yenFormatter(1500.75));

Результат:

¥1,501

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


Использование опций форматирования

Метод currencyFormatter() принимает объект настроек.

const formatter = en.currencyFormatter("USD", {
    minimumFractionDigits: 4
});

console.log(formatter(10));

Результат:

$10.0000

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

const formatter = en.currencyFormatter("USD", {
    maximumFractionDigits: 1
});

console.log(formatter(15.789));

Результат:

$15.8

Минимальное количество цифр целой части

const formatter = en.currencyFormatter("USD", {
    minimumIntegerDigits: 5
});

console.log(formatter(25));

Результат:

$00,025.00

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

По умолчанию используется стандартный символ.

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

console.log(formatter(100));

Результат:

€100.00

Использование международного кода валюты

Опция style: "code" выводит ISO-код валюты.

const formatter = en.currencyFormatter("EUR", {
    style: "code"
});

console.log(formatter(100));

Результат:

EUR 100.00

Использование полного названия валюты

const formatter = en.currencyFormatter("EUR", {
    style: "name"
});

console.log(formatter(100));

Результат:

100.00 euros

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

const formatter = ru.currencyFormatter("EUR", {
    style: "name"
});

console.log(formatter(100));

Результат:

100,00 евро

Форматирование отрицательных значений

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

console.log(formatter(-3500));

Результат:

-$3,500.00

В некоторых локалях формат отличается.

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

console.log(formatter(-3500));

Результат:

-3.500,00 €

Работа с нулевыми значениями

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

console.log(formatter(0));

Результат:

0,00 ₽

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

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

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

function formatPrice(value) {
    return new Globalize("en")
        .currencyFormatter("USD")(value);
}

Правильно:

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

function formatPrice(value) {
    return formatter(value);
}

Универсальная функция форматирования

function createCurrencyFormatter(locale, currency) {
    const globalize = new Globalize(locale);

    return globalize.currencyFormatter(currency);
}

const usd = createCurrencyFormatter("en", "USD");
const rub = createCurrencyFormatter("ru", "RUB");

console.log(usd(1000));
console.log(rub(1000));

Динамический выбор локали

function formatMoney(locale, currency, value) {
    const globalize = new Globalize(locale);

    return globalize.currencyFormatter(currency)(value);
}

console.log(formatMoney("en", "USD", 500));
console.log(formatMoney("ru", "RUB", 500));
console.log(formatMoney("de", "EUR", 500));

Использование пользовательских настроек

const formatter = ru.currencyFormatter("USD", {
    minimumFractionDigits: 3,
    maximumFractionDigits: 3
});

console.log(formatter(15));

Результат:

15,000 $

Форматирование больших денежных значений

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

console.log(formatter(999999999.99));

Результат:

$999,999,999.99

Работа с плавающей точкой

JavaScript использует тип Number, который может создавать проблемы точности.

console.log(0.1 + 0.2);

Результат:

0.30000000000000004

При форматировании:

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

console.log(formatter(0.1 + 0.2));

Результат:

$0.30

Globalize корректно форматирует число, но ошибка вычисления остаётся. Для финансовых расчётов рекомендуется:

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

Форматирование сумм из копеек и центов

function formatCents(cents) {
    const formatter = ru.currencyFormatter("RUB");

    return formatter(cents / 100);
}

console.log(formatCents(105099));

Результат:

1 050,99 ₽

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

const globalize = new Globalize("en");

const usd = globalize.currencyFormatter("USD");
const eur = globalize.currencyFormatter("EUR");
const gbp = globalize.currencyFormatter("GBP");

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

Результат:

$100.00
€100.00
£100.00

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

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

const products = [
    { name: "Ноутбук", price: 120000 },
    { name: "Мышь", price: 2500 },
    { name: "Клавиатура", price: 7000 }
];

products.forEach(product => {
    console.log(
        `${product.name}: ${formatter(product.price)}`
    );
});

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

import Globalize from "globalize";

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

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

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

import Globalize from "globalize";

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

export default {
    props: ["price"],

    computed: {
        formattedPrice() {
            return formatter(this.price);
        }
    }
};

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

import Globalize from "globalize";

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

export class ProductComponent {

    format(value) {
        return formatter(value);
    }
}

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

class CurrencyService {

    constructor(locale) {
        this.globalize = new Globalize(locale);
        this.cache = {};
    }

    formatter(currency) {

        if (!this.cache[currency]) {
            this.cache[currency] =
                this.globalize.currencyFormatter(currency);
        }

        return this.cache[currency];
    }

    format(currency, value) {
        return this.formatter(currency)(value);
    }
}

const service = new CurrencyService("ru");

console.log(service.format("RUB", 1000));
console.log(service.format("USD", 1000));

Ошибки при отсутствии данных валюты

Если CLDR-данные не загружены, возникнет ошибка.

Error: E_MISSING_CLDR

Наиболее частые причины:

  • отсутствует currencies.json;
  • не загружен currencyData.json;
  • указана неподдерживаемая локаль;
  • отсутствуют supplemental-данные.

Проверка поддерживаемой валюты

function safeCurrencyFormatter(locale, currency) {

    try {
        return new Globalize(locale)
            .currencyFormatter(currency);

    } catch (e) {
        console.error("Валюта не поддерживается");
        return null;
    }
}

Сравнение с Intl.NumberFormat

Современный JavaScript содержит встроенный API:

new Intl.NumberFormat("ru", {
    style: "currency",
    currency: "RUB"
}).format(1000);

Результат:

1 000,00 ₽

Globalize отличается:

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

Практический пример интернет-магазина

class ShopFormatter {

    constructor(locale) {
        this.globalize = new Globalize(locale);
    }

    price(value, currency) {

        const formatter =
            this.globalize.currencyFormatter(currency);

        return formatter(value);
    }
}

const shop = new ShopFormatter("ru");

const items = [
    { name: "Телефон", price: 59990, currency: "RUB" },
    { name: "Наушники", price: 199.99, currency: "USD" },
    { name: "Камера", price: 899.5, currency: "EUR" }
];

items.forEach(item => {

    console.log(
        `${item.name}: ${shop.price(
            item.price,
            item.currency
        )}`
    );
});

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

class MultiCurrency {

    constructor(locale) {
        this.globalize = new Globalize(locale);
        this.formatters = {};
    }

    getFormatter(currency) {

        if (!this.formatters[currency]) {

            this.formatters[currency] =
                this.globalize.currencyFormatter(currency);
        }

        return this.formatters[currency];
    }

    format(value, currency) {
        return this.getFormatter(currency)(value);
    }
}

const mc = new MultiCurrency("en");

console.log(mc.format(1200, "USD"));
console.log(mc.format(1200, "EUR"));
console.log(mc.format(1200, "JPY"));