Skip to content

Formatting Numbers, Dates & Currency

Every Comvi i18n instance exposes thin wrappers around the platform Intl.* APIs (Intl.NumberFormat, Intl.DateTimeFormat, Intl.RelativeTimeFormat). They use the i18n instance’s current locale as the BCP 47 locale. You don’t need a separate library for common date, number, currency, or relative-time formatting.

The same surface is exposed everywhere: directly on the i18n instance (i18n.formatNumber(...)) and on the framework hook return value (useI18n().formatNumber(...)). The methods format with the locale that is active when they are called. In frameworks, call them inside render/computed/memo code that tracks the current locale so the formatted value updates after setLocale().

MethodSignatureUse for
formatNumber(value, options?) => stringNumbers, percentages, units
formatCurrency(value, currency, options?) => stringMoney — pass an ISO 4217 code ('USD', 'EUR', 'UAH')
formatDate(value, options?) => stringDates and times — Date or epoch milliseconds
formatRelativeTime(value, unit, options?) => stringRelative times — “2 hours ago”, “in 3 days”
dir'ltr' | 'rtl' getterDocument/text direction for the current language

All options arguments forward to the underlying Intl constructor. For formatCurrency, Comvi sets style: 'currency' and the currency code from the second argument; other Intl.NumberFormatOptions such as currencyDisplay or minimumFractionDigits still apply.

import { useI18n } from '@comvi/react';
function PriceTag({ amount }: { amount: number }) {
const { formatNumber } = useI18n();
return <span>{formatNumber(amount, { maximumFractionDigits: 2 })}</span>;
}

Common patterns:

formatNumber(0.85, { style: 'percent' }); // "85%"
formatNumber(1234.5, { maximumFractionDigits: 2 }); // "1,234.5" (en) / "1 234,5" (fr)
formatNumber(2.5, { style: 'unit', unit: 'kilometer' }); // "2.5 km"
formatNumber(1_500_000, { notation: 'compact' }); // "1.5M"
formatCurrency(1234.5, 'USD'); // "$1,234.50"
formatCurrency(1234.5, 'EUR'); // "€1,234.50" (en) / "1 234,50 €" (fr)
formatCurrency(1234.5, 'UAH', { currencyDisplay: 'code' }); // "UAH 1,234.50"

formatCurrency requires the currency code as a separate argument so you don’t accidentally store it in your translation strings. Keep currency in your data layer; let the UI pick how to render it.

formatDate accepts a Date instance or epoch milliseconds.

const ts = Date.UTC(2026, 3, 27, 10, 30);
formatDate(ts); // Locale default date
formatDate(ts, { dateStyle: 'long', timeZone: 'UTC' }); // "April 27, 2026" (en-US)
formatDate(ts, {
dateStyle: 'medium',
timeStyle: 'short',
timeZone: 'UTC',
}); // "Apr 27, 2026, 10:30 AM" (en-US)
formatDate(ts, { weekday: 'long', month: 'long', day: 'numeric' });

For “2 hours ago” / “in 3 days” UI:

formatRelativeTime(-2, 'hour'); // "2 hours ago"
formatRelativeTime(3, 'day'); // "in 3 days"
formatRelativeTime(-1, 'day', { numeric: 'auto' }); // "yesterday"
formatRelativeTime(0, 'day', { numeric: 'auto' }); // "today"

Pass numeric: 'auto' to get word forms (“yesterday”, “today”, “tomorrow”, “last week”, etc.) where the locale supports them; the default 'always' always renders numeric phrasing.

A pattern for relative timestamps from Date:

function formatAgo(date: Date) {
const diffSec = Math.round((date.getTime() - Date.now()) / 1000);
const abs = Math.abs(diffSec);
if (abs < 60) return formatRelativeTime(diffSec, 'second');
if (abs < 3600) return formatRelativeTime(Math.round(diffSec / 60), 'minute');
if (abs < 86_400) return formatRelativeTime(Math.round(diffSec / 3600), 'hour');
return formatRelativeTime(Math.round(diffSec / 86_400), 'day');
}

dir returns 'rtl' for Arabic, Hebrew, Persian, Urdu, and other RTL scripts; 'ltr' otherwise. It updates reactively when the language changes.

function Layout({ children }) {
const { dir } = useI18n();
return <div dir={dir}>{children}</div>;
}

Pair this with CSS logical properties (margin-inline-start, padding-inline-end) so layouts mirror automatically.

When a number or date appears inside a translated sentence, format it first and pass the result as a parameter:

const message = t('orders.shipped', {
count: formatNumber(orderCount),
date: formatDate(shippedAt, { dateStyle: 'long' }),
});
// "12,345 orders shipped on April 27, 2026"

For ICU plural rules driven by a number, pass the raw number (not the formatted string) so the plural rule can inspect it — see Pluralization & ICU.