Skip to content

Number & Currency Formatting — Locale-Aware Number and Currency Display

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about Number & Currency Formatting. We cover key concepts, practical examples, and best practices to help you master this topic.

Locale-aware number formatting displays numbers, currencies, and percentages according to cultural conventions using the Intl.NumberFormat API.

What You'll Learn

By the end of this tutorial, you'll understand how to use Intl.NumberFormat for locale-aware number, currency, and percentage formatting, how to handle different digit separators, and how to format compact numbers for international audiences.

Why It Matters

Number formats are culturally specific. "1,234.56" is one thousand two hundred thirty-four in the US but could be misinterpreted in Europe where commas are decimal separators. Currency symbols vary ($, €, ¥, £, ر.س), and their position (before or after the number) differs by locale. Incorrect number formatting causes confusion, especially in financial applications.

Real-World Use

A global e-commerce checkout shows "1,234.56" in the US, "1.234,56" in Germany, "1 234,56" in France, and "¥1,235" in Japan — all from the same number using Intl.NumberFormat with the user's locale and appropriate currency code.

Number Format Variations

graph LR
    A[Number 1234567.89
by Locale] --> B[US: 1,234,567.89] A --> C[Germany: 1.234.567,89] A --> D[France: 1 234 567,89] A --> E[Switzerland: 1'234'567.89] A --> F[India: 12,34,567.89] A --> G[Saudi Arabia: ١٬٢٣٤٬٥٦٧٫٨٩] style A fill:#4a90d9,color:#fff

Intl.NumberFormat Basics

// i18n/number-formatting.js — Intl.NumberFormat usage
class NumberFormatter {
    constructor(locale = 'en-US') {
        this.locale = locale;
    }

    // Basic decimal formatting
    decimal(number, options = {}) {
        return new Intl.NumberFormat(this.locale, {
            style: 'decimal',
            minimumFractionDigits: options.minFraction ?? 0,
            maximumFractionDigits: options.maxFraction ?? 2,
            ...options
        }).format(number);
    }

    // Currency formatting
    currency(amount, currencyCode = 'USD') {
        return new Intl.NumberFormat(this.locale, {
            style: 'currency',
            currency: currencyCode,
            currencyDisplay: 'symbol'
        }).format(amount);
    }

    // Percentage formatting
    percent(number) {
        return new Intl.NumberFormat(this.locale, {
            style: 'percent',
            minimumFractionDigits: 1,
            maximumFractionDigits: 2
        }).format(number);
    }

    // Compact notation (1K, 1M, 1B)
    compact(number, notation = 'compact') {
        return new Intl.NumberFormat(this.locale, {
            notation: notation,
            compactDisplay: 'short'
        }).format(number);
    }

    // Engineering notation
    engineering(number) {
        return new Intl.NumberFormat(this.locale, {
            notation: 'engineering'
        }).format(number);
    }

    // Scientific notation
    scientific(number) {
        return new Intl.NumberFormat(this.locale, {
            notation: 'scientific'
        }).format(number);
    }

    // With unit
    unit(number, unit) {
        return new Intl.NumberFormat(this.locale, {
            style: 'unit',
            unit: unit,
            unitDisplay: 'long'
        }).format(number);
    }

    // Sign display
    withSign(number) {
        return new Intl.NumberFormat(this.locale, {
            signDisplay: 'always'
        }).format(number);
    }

    // Demo
    demo() {
        const num = 1234567.89;
        console.log(`Locale: ${this.locale}`);
        console.log(`  Decimal:        ${this.decimal(num)}`);
        console.log(`  Currency (USD): ${this.currency(1234.56, 'USD')}`);
        console.log(`  Currency (EUR): ${this.currency(1234.56, 'EUR')}`);
        console.log(`  Percent:        ${this.percent(0.856)}`);
        console.log(`  Compact:        ${this.compact(num)}`);
    }
}

// Demo across locales
console.log('=== US English ===');
new NumberFormatter('en-US').demo();

console.log('\\n=== German ===');
new NumberFormatter('de-DE').demo();

console.log('\\n=== French ===');
new NumberFormatter('fr-FR').demo();

console.log('\\n=== Indian English ===');
new NumberFormatter('en-IN').demo();

console.log('\\n=== Arabic (Saudi Arabia) ===');
new NumberFormatter('ar-SA').demo();

console.log('\\n=== Japanese ===');
new NumberFormatter('ja-JP').demo();

Currency Formatting Details

// i18n/currency-formatting.js — Advanced currency formatting
class CurrencyFormatter {
    constructor(locale = 'en-US') {
        this.locale = locale;
    }

    // Format with currency symbol
    symbol(amount, currency) {
        return new Intl.NumberFormat(this.locale, {
            style: 'currency',
            currency: currency,
            currencyDisplay: 'symbol'
        }).format(amount);
    }

    // Format with currency code (USD, EUR, JPY)
    code(amount, currency) {
        return new Intl.NumberFormat(this.locale, {
            style: 'currency',
            currency: currency,
            currencyDisplay: 'code'
        }).format(amount);
    }

    // Format with currency name
    name(amount, currency) {
        return new Intl.NumberFormat(this.locale, {
            style: 'currency',
            currency: currency,
            currencyDisplay: 'name'
        }).format(amount);
    }

    // Format without trailing zeros
    smart(amount, currency) {
        return new Intl.NumberFormat(this.locale, {
            style: 'currency',
            currency: currency,
            minimumFractionDigits: 0,
            maximumFractionDigits: 2
        }).format(amount);
    }

    // Accounting format (negative in parentheses)
    accounting(amount, currency) {
        return new Intl.NumberFormat(this.locale, {
            style: 'currency',
            currency: currency,
            currencySign: 'accounting'
        }).format(amount);
    }

    // Demo all display types
    demo(amount = 1234.56) {
        const currencies = ['USD', 'EUR', 'GBP', 'JPY', 'SAR', 'INR'];

        console.log(`Locale: ${this.locale}`);
        currencies.forEach(curr => {
            console.log(`  ${curr}: ${this.symbol(amount, curr)}`);
        });
    }
}

console.log('=== Currency Display Types (en-US) ===');
const usCurr = new CurrencyFormatter('en-US');
console.log('Symbol:', usCurr.symbol(1234.56, 'USD'));     // $1,234.56
console.log('Code:', usCurr.code(1234.56, 'USD'));         // USD 1,234.56
console.log('Name:', usCurr.name(1234.56, 'USD'));         // 1,234.56 US dollars
console.log('Accounting (negative):', usCurr.accounting(-500, 'USD')); // ($500.00)

console.log('\\n=== Currency by Locale ===');
new CurrencyFormatter('en-US').demo();
new CurrencyFormatter('de-DE').demo();
new CurrencyFormatter('ja-JP').demo();
new CurrencyFormatter('ar-SA').demo();
new CurrencyFormatter('en-IN').demo();

Unit Formatting

// i18n/unit-formatting.js — Formatting numbers with units
const { NumberFormat } = require('intl');

const locales = ['en-US', 'de-DE', 'ja-JP'];

const units = [
    { value: 5, unit: 'kilometer' },
    { value: 100, unit: 'kilogram' },
    { value: 25, unit: 'liter' },
    { value: 72, unit: 'hour' },
    { value: 500, unit: 'megabyte' },
    { value: 98.6, unit: 'fahrenheit' },
    { value: 37, unit: 'celsius' },
    { value: 120, unit: 'mile-per-hour' },
    { value: 2.5, unit: 'cup' },
];

locales.forEach(locale => {
    console.log(`\\n=== ${locale} Unit Formatting ===`);
    units.forEach(({ value, unit }) => {
        const formatted = new Intl.NumberFormat(locale, {
            style: 'unit',
            unit: unit,
            unitDisplay: 'long'
        }).format(value);
        console.log(`  ${value} ${unit}${formatted}`);
    });
});

Integration with i18next

// i18n/i18next-numbers.js — Number formatting with i18next formatters
import i18next from 'i18next';

i18next.init({
    lng: 'en',
    resources: {
        en: {
            translation: {
                'price': 'Price: {price, number, currency}',
                'discount': 'Save {percent, number, percent}',
                'distance': '{km, number, unit} from city center',
                'compact_views': '{views, number, compact} views',
                'revenue': 'Revenue: {amount, number, accounting}'
            }
        }
    },
    interpolation: {
        format: (value, format, lng) => {
            if (format === 'currency') {
                return new Intl.NumberFormat(lng, {
                    style: 'currency',
                    currency: 'USD'
                }).format(value);
            }
            if (format === 'percent') {
                return new Intl.NumberFormat(lng, {
                    style: 'percent',
                    maximumFractionDigits: 0
                }).format(value);
            }
            if (format === 'compact') {
                return new Intl.NumberFormat(lng, {
                    notation: 'compact'
                }).format(value);
            }
            if (format === 'accounting') {
                return new Intl.NumberFormat(lng, {
                    style: 'currency',
                    currency: 'USD',
                    currencySign: 'accounting'
                }).format(value);
            }
            return value;
        }
    }
});

// Usage in app:
// t('price', { price: 29.99 })        // Price: $29.99
// t('discount', { percent: 0.20 })     // Save 20%
// t('compact_views', { views: 1500 })  // 1.5K views

Common Mistakes

  1. Assuming the decimal separator is always ".". Many locales use "," for decimals and "." for thousands (1.234,56 in Germany). Always use Intl.NumberFormat instead of toFixed() + string concatenation.
  2. Hardcoding currency symbol positions. "$1,234.56" places the symbol before the number. In some locales, the symbol goes after (1.234,56 €) or uses a different character (ر.س for Saudi Riyal). Let Intl.NumberFormat handle positioning.
  3. Using the wrong currency code. "€123" is ambiguous — which of the 25+ countries using the Euro? Use locale-appropriate currency codes: EUR for Euros, USD for US Dollars, JPY for Japanese Yen.
  4. Forcing decimal places for currencies without sub-units. JPY and KRW have no decimal sub-units. "¥1,234.56" is incorrect — it should be "¥1,235" (or "¥1,234" if the price is exact). Use maximumFractionDigits based on the currency.
  5. Not handling compact notation for large numbers. Displaying "1234567890" instead of "1.2B" wastes space and reduces readability. Use notation: 'compact' for social media counts, file sizes, and analytics.

Practice Questions

  1. How does Intl.NumberFormat handle digit grouping separators across locales?
  2. What currency display options are available in Intl.NumberFormat?
  3. How do you format negative currency values in accounting style?
  4. Why should you never hardcode decimal or thousand separators?
  5. How does compact notation differ from engineering notation?

Challenge: Build a multi-currency pricing table that displays the same product price ($29.99) in 8+ locale-currency combinations (en-US/USD, de-DE/EUR, ja-JP/JPY, ar-SA/SAR, en-IN/INR, fr-FR/EUR, zh-CN/CNY, pt-BR/BRL). Include currency symbol display, accounting format for discounts, compact notation for view counts, and percentage formatting for tax rates.

FAQ

What is Intl.NumberFormat?

Intl.NumberFormat is a built-in JavaScript API that formats numbers according to locale-specific conventions. It handles decimal and thousand separators, currency symbols, percentages, units, and compact notation. Available in all modern browsers and Node.js.

How do I format currencies for different locales?

Pass the locale and currency code: new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }). The API handles symbol placement, decimal separators, and grouping automatically.

What currency codes should I use?

Use ISO 4217 three-letter currency codes: USD, EUR, GBP, JPY, SAR, INR, BRL, etc. Never use currency symbols ($, €, £) as identifiers — they're ambiguous and locale-dependent.

How do I format percentages with Intl.NumberFormat?

Set style: 'percent'. The input should be a decimal (0.856 for 85.6%). The API multiplies by 100 and adds the locale-appropriate percent sign.

Can I use Intl.NumberFormat for non-decimal number systems?

Yes. Arabic locales use Eastern Arabic numerals (١٢٣). Hindi locales use Devanagari digits (१२३). The locale string determines which numeral system to use.

Mini Project

Build a global pricing dashboard: a table showing product prices in 10+ countries with locale-appropriate formatting (currency symbol, decimal/thousand separators, currency code display option), a toggle between symbol/code/name display, percentage discount formatting, compact view counts, accounting format for refunds, and unit formatting for weights and dimensions.

What's Next

You've mastered number and currency formatting. Next, learn about RTL Layout for building right-to-left layouts for Arabic and Hebrew.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro