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:
- Use intl package - NumberFormat provides locale-aware formatting
- Choose correct format - decimal, currency, compact, percent
- Handle currency properly - Symbol, decimal places, position vary by locale
- Test edge cases - Zero, negative, very large, very small numbers
- 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.