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().
API Surface
Section titled “API Surface”| Method | Signature | Use for |
|---|---|---|
formatNumber | (value, options?) => string | Numbers, percentages, units |
formatCurrency | (value, currency, options?) => string | Money — pass an ISO 4217 code ('USD', 'EUR', 'UAH') |
formatDate | (value, options?) => string | Dates and times — Date or epoch milliseconds |
formatRelativeTime | (value, unit, options?) => string | Relative times — “2 hours ago”, “in 3 days” |
dir | 'ltr' | 'rtl' getter | Document/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.
Numbers
Section titled “Numbers”import { useI18n } from '@comvi/react';
function PriceTag({ amount }: { amount: number }) { const { formatNumber } = useI18n(); return <span>{formatNumber(amount, { maximumFractionDigits: 2 })}</span>;}<script setup lang="ts">import { useI18n } from '@comvi/vue';import { computed } from 'vue';
const props = defineProps<{ amount: number }>();const { formatNumber, locale } = useI18n();
const formattedAmount = computed(() => { void locale.value; // Track locale changes for re-formatting return formatNumber(props.amount, { maximumFractionDigits: 2 });});</script>
<template> <span>{{ formattedAmount }}</span></template><script lang="ts">import { useI18n } from '@comvi/svelte';const { formatNumber, locale } = useI18n();export let amount: number;// Reading $locale here makes the template re-render on language change.$: formatted = ($locale, formatNumber(amount, { maximumFractionDigits: 2 }));</script>
<span>{formatted}</span>import { i18n } from './i18n';
document.querySelector('#price')!.textContent = i18n.formatNumber(1234.5, { maximumFractionDigits: 2 });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"Currency
Section titled “Currency”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 dateformatDate(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' });Relative Time
Section titled “Relative Time”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');}Text Direction (dir)
Section titled “Text Direction (dir)”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>;}<script setup lang="ts">import { useI18n } from '@comvi/vue';const { dir } = useI18n();</script>
<template> <div :dir="dir"><slot /></div></template>i18n.on('localeChanged', () => { document.documentElement.dir = i18n.dir;});Pair this with CSS logical properties (margin-inline-start, padding-inline-end) so layouts mirror automatically.
Composing With Translations
Section titled “Composing With Translations”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.