← Back to Blog

Flutter Number and Currency Formatting: Locale-Aware Display Guide

fluttercurrencynumber-formatlocalizationintlformatting

Flutter Number and Currency Formatting: Locale-Aware Display Guide

Master number and currency formatting in Flutter for international audiences. Learn to display prices, percentages, and large numbers that feel native to users worldwide.

Why Number Formatting Matters

Numbers display differently across the world:

Region Number Format Currency
USA 1,234,567.89 $1,234.56
Germany 1.234.567,89 1.234,56 €
France 1 234 567,89 1 234,56 €
India 12,34,567.89 ₹1,234.56
Switzerland 1'234'567.89 CHF 1'234.56

Using US-style formatting for a German user causes confusion. Is "1.234" one thousand two hundred thirty-four, or one point two three four?

Setting Up Number Formatting

Add Required Dependencies

# pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter
  intl: ^0.19.0

Configure Localization

import 'package:flutter_localizations/flutter_localizations.dart';

MaterialApp(
  localizationsDelegates: [
    GlobalMaterialLocalizations.delegate,
    GlobalWidgetsLocalizations.delegate,
  ],
  supportedLocales: [
    Locale('en', 'US'),
    Locale('de', 'DE'),
    Locale('fr', 'FR'),
    Locale('ja', 'JP'),
    Locale('ar', 'SA'),
  ],
)

Using NumberFormat from intl

Basic Number Formatting

import 'package:intl/intl.dart';

final number = 1234567.89;

// Default decimal format
NumberFormat.decimalPattern('en_US').format(number);  // 1,234,567.89
NumberFormat.decimalPattern('de_DE').format(number);  // 1.234.567,89
NumberFormat.decimalPattern('fr_FR').format(number);  // 1 234 567,89

Using Context Locale

class PriceDisplay extends StatelessWidget {
  final double amount;

  PriceDisplay({required this.amount});

  @override
  Widget build(BuildContext context) {
    final locale = Localizations.localeOf(context).toString();
    final formatted = NumberFormat.decimalPattern(locale).format(amount);

    return Text(formatted);
  }
}

NumberFormat Constructors Reference

Decimal Patterns

final value = 1234567.891;
final locale = 'en_US';

// Standard decimal
NumberFormat.decimalPattern(locale).format(value);
// 1,234,567.891

// With specific decimal places
NumberFormat.decimalPatternDigits(
  locale: locale,
  decimalDigits: 2,
).format(value);
// 1,234,567.89

Percentage Formatting

final rate = 0.4567;

NumberFormat.percentPattern(locale).format(rate);
// 46% (rounds and adds % symbol)

// Custom percentage with decimals
final percentFormat = NumberFormat.percentPattern(locale)
  ..minimumFractionDigits = 1
  ..maximumFractionDigits = 2;
percentFormat.format(rate);
// 45.67%

Scientific Notation

final largeNumber = 1234567890.0;

NumberFormat.scientificPattern(locale).format(largeNumber);
// 1E9 (varies by locale)

Compact Numbers

final followers = 1234567;

NumberFormat.compact(locale: locale).format(followers);
// en_US: 1.2M
// de_DE: 1,2 Mio.
// ja_JP: 123万

NumberFormat.compactLong(locale: locale).format(followers);
// en_US: 1.2 million
// de_DE: 1,2 Millionen

Currency Formatting

Basic Currency

final price = 1234.56;

// Currency with locale
NumberFormat.currency(locale: 'en_US', symbol: '\$').format(price);
// $1,234.56

NumberFormat.currency(locale: 'de_DE', symbol: '€').format(price);
// 1.234,56 €

NumberFormat.currency(locale: 'ja_JP', symbol: '¥').format(price);
// ¥1,235 (no decimals for JPY)

Simple Currency (No Symbol)

NumberFormat.simpleCurrency(locale: 'en_US').format(price);
// $1,234.56

NumberFormat.simpleCurrency(locale: 'de_DE').format(price);
// 1.234,56 €

NumberFormat.simpleCurrency(locale: 'ja_JP').format(price);
// ¥1,235

Currency with Custom Decimals

// Force 2 decimal places (even for JPY)
NumberFormat.currency(
  locale: 'ja_JP',
  symbol: '¥',
  decimalDigits: 0,
).format(price);
// ¥1,235

// Cryptocurrency with more decimals
NumberFormat.currency(
  locale: 'en_US',
  symbol: '₿',
  decimalDigits: 8,
).format(0.00123456);
// ₿0.00123456

Currency Code Instead of Symbol

NumberFormat.currency(
  locale: 'en_US',
  name: 'USD',
).format(price);
// USD 1,234.56

NumberFormat.currency(
  locale: 'de_DE',
  name: 'EUR',
).format(price);
// 1.234,56 EUR

Compact Currency

NumberFormat.compactCurrency(locale: 'en_US', symbol: '\$').format(1234567);
// $1.2M

NumberFormat.compactSimpleCurrency(locale: 'en_US').format(1234567);
// $1.2M

Numbers in ARB Files

Basic Number Placeholder

{
  "totalItems": "Total: {count} items",
  "@totalItems": {
    "placeholders": {
      "count": {
        "type": "int",
        "format": "decimalPattern",
        "example": "1234"
      }
    }
  }
}

Currency Placeholder

{
  "price": "Price: {amount}",
  "@price": {
    "placeholders": {
      "amount": {
        "type": "double",
        "format": "currency",
        "optionalParameters": {
          "symbol": "$",
          "decimalDigits": 2
        },
        "example": "99.99"
      }
    }
  }
}

Compact Number Placeholder

{
  "followerCount": "{count} followers",
  "@followerCount": {
    "placeholders": {
      "count": {
        "type": "int",
        "format": "compact",
        "example": "1200000"
      }
    }
  }
}

Percentage Placeholder

{
  "completionRate": "{rate} complete",
  "@completionRate": {
    "placeholders": {
      "rate": {
        "type": "double",
        "format": "percentPattern",
        "example": "0.85"
      }
    }
  }
}

All Number Formats Available

{
  "@@locale": "en",

  "decimalExample": "{value}",
  "@decimalExample": {
    "placeholders": {
      "value": {
        "type": "double",
        "format": "decimalPattern"
      }
    }
  },

  "percentExample": "{value}",
  "@percentExample": {
    "placeholders": {
      "value": {
        "type": "double",
        "format": "percentPattern"
      }
    }
  },

  "scientificExample": "{value}",
  "@scientificExample": {
    "placeholders": {
      "value": {
        "type": "double",
        "format": "scientificPattern"
      }
    }
  },

  "compactExample": "{value}",
  "@compactExample": {
    "placeholders": {
      "value": {
        "type": "int",
        "format": "compact"
      }
    }
  },

  "compactLongExample": "{value}",
  "@compactLongExample": {
    "placeholders": {
      "value": {
        "type": "int",
        "format": "compactLong"
      }
    }
  },

  "currencyExample": "{value}",
  "@currencyExample": {
    "placeholders": {
      "value": {
        "type": "double",
        "format": "currency",
        "optionalParameters": {
          "symbol": "€",
          "decimalDigits": 2
        }
      }
    }
  },

  "compactCurrencyExample": "{value}",
  "@compactCurrencyExample": {
    "placeholders": {
      "value": {
        "type": "double",
        "format": "compactCurrency",
        "optionalParameters": {
          "symbol": "$"
        }
      }
    }
  }
}

Custom Number Formatting

Custom Decimal Places

final format = NumberFormat('#,##0.000', 'en_US');
format.format(1234.5);  // 1,234.500

Custom Patterns

Pattern Example Input Output
#,##0 1234.56 1,235
#,##0.00 1234.5 1,234.50
#,##0.### 1234.5 1,234.5
0000 42 0042
+#,##0;-#,##0 -1234 -1,234
// Custom pattern with locale
final format = NumberFormat('#,##0.00', 'de_DE');
format.format(1234.5);  // 1.234,50

Phone Number Style

String formatPhone(String number, String locale) {
  // US: (555) 123-4567
  // DE: 0555 123 4567
  // Custom formatting based on locale
  if (locale.startsWith('en_US')) {
    return '(${number.substring(0, 3)}) ${number.substring(3, 6)}-${number.substring(6)}';
  }
  return number;
}

Handling Edge Cases

Null and Zero Values

String formatPrice(double? price, String locale) {
  if (price == null) return '--';
  if (price == 0) return 'Free';

  return NumberFormat.simpleCurrency(locale: locale).format(price);
}

Negative Numbers

final format = NumberFormat.currency(locale: 'en_US', symbol: '\$');

format.format(-1234.56);
// ($1,234.56) or -$1,234.56 depending on locale

Very Large Numbers

final bigNumber = 9999999999999.99;

// Use compact for readability
NumberFormat.compact(locale: 'en_US').format(bigNumber);
// 10T

// Or scientific for precision
NumberFormat.scientificPattern('en_US').format(bigNumber);
// 1E13

Very Small Numbers

final smallNumber = 0.000001234;

// Scientific is cleaner
NumberFormat.scientificPattern('en_US').format(smallNumber);
// 1.23E-6

// Or custom precision
NumberFormat('0.000000000', 'en_US').format(smallNumber);
// 0.000001234

E-commerce Patterns

Product Price Display

class PriceWidget extends StatelessWidget {
  final double price;
  final double? originalPrice;
  final String currencyCode;

  PriceWidget({
    required this.price,
    this.originalPrice,
    this.currencyCode = 'USD',
  });

  @override
  Widget build(BuildContext context) {
    final locale = Localizations.localeOf(context).toString();
    final format = NumberFormat.simpleCurrency(locale: locale);

    return Row(
      children: [
        Text(
          format.format(price),
          style: TextStyle(
            fontWeight: FontWeight.bold,
            fontSize: 18,
          ),
        ),
        if (originalPrice != null) ...[
          SizedBox(width: 8),
          Text(
            format.format(originalPrice!),
            style: TextStyle(
              decoration: TextDecoration.lineThrough,
              color: Colors.grey,
            ),
          ),
        ],
      ],
    );
  }
}

Order Summary

{
  "orderSummary_subtotal": "Subtotal: {amount}",
  "orderSummary_shipping": "Shipping: {amount}",
  "orderSummary_tax": "Tax ({rate}): {amount}",
  "orderSummary_total": "Total: {amount}",

  "@orderSummary_subtotal": {
    "placeholders": {
      "amount": {
        "type": "double",
        "format": "currency",
        "optionalParameters": {"symbol": "$", "decimalDigits": 2}
      }
    }
  },
  "@orderSummary_shipping": {
    "placeholders": {
      "amount": {
        "type": "double",
        "format": "currency",
        "optionalParameters": {"symbol": "$", "decimalDigits": 2}
      }
    }
  },
  "@orderSummary_tax": {
    "placeholders": {
      "rate": {
        "type": "double",
        "format": "percentPattern"
      },
      "amount": {
        "type": "double",
        "format": "currency",
        "optionalParameters": {"symbol": "$", "decimalDigits": 2}
      }
    }
  },
  "@orderSummary_total": {
    "placeholders": {
      "amount": {
        "type": "double",
        "format": "currency",
        "optionalParameters": {"symbol": "$", "decimalDigits": 2}
      }
    }
  }
}

Discount Display

{
  "discountBadge": "-{percent} OFF",
  "@discountBadge": {
    "placeholders": {
      "percent": {
        "type": "int",
        "example": "20"
      }
    }
  },

  "savingsAmount": "You save {amount}",
  "@savingsAmount": {
    "placeholders": {
      "amount": {
        "type": "double",
        "format": "currency",
        "optionalParameters": {"symbol": "$"}
      }
    }
  }
}

RTL and Number Display

Arabic Numbers

Arabic locales may use Eastern Arabic numerals:

final number = 1234567.89;

NumberFormat.decimalPattern('ar_SA').format(number);
// ١٬٢٣٤٬٥٦٧٫٨٩

// Use Western numerals if preferred
NumberFormat('#,##0.00', 'ar_SA').format(number);
// May still show Eastern numerals depending on locale data

Number Direction in RTL

Widget buildPriceRow(BuildContext context, String label, double amount) {
  final locale = Localizations.localeOf(context).toString();
  final isRtl = Directionality.of(context) == TextDirection.rtl;

  return Row(
    textDirection: isRtl ? TextDirection.rtl : TextDirection.ltr,
    mainAxisAlignment: MainAxisAlignment.spaceBetween,
    children: [
      Text(label),
      // Numbers typically stay LTR even in RTL layouts
      Directionality(
        textDirection: TextDirection.ltr,
        child: Text(
          NumberFormat.simpleCurrency(locale: locale).format(amount),
        ),
      ),
    ],
  );
}

Input Parsing

Parse Formatted Numbers

String parseFormattedNumber(String input, String locale) {
  try {
    final format = NumberFormat.decimalPattern(locale);
    return format.parse(input).toString();
  } catch (e) {
    return input; // Return original if parsing fails
  }
}

// "1,234.56" (en_US) -> 1234.56
// "1.234,56" (de_DE) -> 1234.56

Currency Input Field

class CurrencyInputFormatter extends TextInputFormatter {
  final String locale;
  late final NumberFormat _format;

  CurrencyInputFormatter({required this.locale}) {
    _format = NumberFormat.decimalPatternDigits(
      locale: locale,
      decimalDigits: 2,
    );
  }

  @override
  TextEditingValue formatEditUpdate(
    TextEditingValue oldValue,
    TextEditingValue newValue,
  ) {
    // Remove non-numeric characters
    final numericOnly = newValue.text.replaceAll(RegExp(r'[^0-9]'), '');

    if (numericOnly.isEmpty) {
      return newValue.copyWith(text: '');
    }

    // Convert to cents then format
    final value = int.parse(numericOnly) / 100;
    final formatted = _format.format(value);

    return TextEditingValue(
      text: formatted,
      selection: TextSelection.collapsed(offset: formatted.length),
    );
  }
}

Best Practices

1. Create Reusable Formatters

class AppNumberFormat {
  final String locale;

  AppNumberFormat(this.locale);

  String decimal(num value) {
    return NumberFormat.decimalPattern(locale).format(value);
  }

  String currency(double value, {String symbol = '\$'}) {
    return NumberFormat.currency(locale: locale, symbol: symbol).format(value);
  }

  String compact(num value) {
    return NumberFormat.compact(locale: locale).format(value);
  }

  String percent(double value) {
    return NumberFormat.percentPattern(locale).format(value);
  }
}

// Usage
final formatter = AppNumberFormat(Localizations.localeOf(context).toString());
Text(formatter.currency(99.99));

2. Test Multiple Locales

void main() {
  group('Number formatting', () {
    test('formats currency correctly for US', () {
      final format = NumberFormat.currency(locale: 'en_US', symbol: '\$');
      expect(format.format(1234.56), '\$1,234.56');
    });

    test('formats currency correctly for Germany', () {
      final format = NumberFormat.currency(locale: 'de_DE', symbol: '€');
      expect(format.format(1234.56), '1.234,56 €');
    });
  });
}

3. Handle Currency Dynamically

class CurrencyService {
  String formatPrice(double amount, String currencyCode, String locale) {
    final symbols = {
      'USD': '\$',
      'EUR': '€',
      'GBP': '£',
      'JPY': '¥',
      'INR': '₹',
    };

    final decimals = {
      'JPY': 0,
      'KRW': 0,
    };

    return NumberFormat.currency(
      locale: locale,
      symbol: symbols[currencyCode] ?? currencyCode,
      decimalDigits: decimals[currencyCode] ?? 2,
    ).format(amount);
  }
}

Simplify Number Formatting with FlutterLocalisation

Managing number formats across currencies and locales is complex. FlutterLocalisation.com helps you:

  • Visual format preview - See how numbers appear in each locale
  • Currency configuration - Set up currency symbols and decimals easily
  • Validation - Catch formatting errors before building
  • AI translations - Handle number-related phrases correctly across languages

Stop debugging number format issues. Try FlutterLocalisation free and deliver perfectly formatted numbers in every currency.

Summary

Flutter number and currency formatting requires:

  1. Use intl package - NumberFormat provides locale-aware formatting
  2. Choose correct format - decimal, currency, compact, percent
  3. Handle currency properly - Symbol, decimal places, position vary by locale
  4. Test edge cases - Zero, negative, very large, very small numbers
  5. Build reusable utilities - Consistent formatting across your app

With proper number localization, your app's prices, statistics, and metrics will feel native to users in any country.