Flutter Localization for E-commerce Apps: Complete Implementation Guide
E-commerce apps have unique localization challenges: currencies, prices, product descriptions, checkout flows, and legal requirements. This guide covers everything you need to build a globally-ready Flutter shopping app.
E-commerce Localization Challenges
E-commerce apps must handle:
- Currency formatting - $10.99 vs 10,99 € vs ¥1,099
- Price localization - Different prices per region
- Product translations - Names, descriptions, sizes
- Checkout flows - Address formats, payment methods
- Legal content - Terms, privacy policies, return policies
- Date/time - Delivery estimates, order history
- Units - Weights, dimensions, sizes
Project Structure
lib/
├── l10n/
│ ├── app_en.arb
│ ├── app_es.arb
│ ├── app_de.arb
│ └── app_fr.arb
├── models/
│ ├── product.dart
│ ├── cart.dart
│ └── order.dart
├── services/
│ ├── localization_service.dart
│ ├── currency_service.dart
│ └── pricing_service.dart
└── widgets/
└── localized/
├── price_text.dart
├── currency_selector.dart
└── localized_product_card.dart
Currency Formatting
Currency Service
// lib/services/currency_service.dart
import 'package:intl/intl.dart';
class CurrencyService {
static final Map<String, CurrencyConfig> _currencies = {
'USD': CurrencyConfig(
code: 'USD',
symbol: '\$',
locale: 'en_US',
decimalDigits: 2,
),
'EUR': CurrencyConfig(
code: 'EUR',
symbol: '€',
locale: 'de_DE',
decimalDigits: 2,
),
'GBP': CurrencyConfig(
code: 'GBP',
symbol: '£',
locale: 'en_GB',
decimalDigits: 2,
),
'JPY': CurrencyConfig(
code: 'JPY',
symbol: '¥',
locale: 'ja_JP',
decimalDigits: 0,
),
'CNY': CurrencyConfig(
code: 'CNY',
symbol: '¥',
locale: 'zh_CN',
decimalDigits: 2,
),
'INR': CurrencyConfig(
code: 'INR',
symbol: '₹',
locale: 'en_IN',
decimalDigits: 2,
),
'BRL': CurrencyConfig(
code: 'BRL',
symbol: 'R\$',
locale: 'pt_BR',
decimalDigits: 2,
),
};
String formatPrice(double amount, String currencyCode) {
final config = _currencies[currencyCode] ?? _currencies['USD']!;
final formatter = NumberFormat.currency(
locale: config.locale,
symbol: config.symbol,
decimalDigits: config.decimalDigits,
);
return formatter.format(amount);
}
String formatPriceCompact(double amount, String currencyCode) {
final config = _currencies[currencyCode] ?? _currencies['USD']!;
final formatter = NumberFormat.compactCurrency(
locale: config.locale,
symbol: config.symbol,
decimalDigits: config.decimalDigits,
);
return formatter.format(amount);
}
CurrencyConfig? getConfig(String currencyCode) {
return _currencies[currencyCode];
}
List<String> get supportedCurrencies => _currencies.keys.toList();
}
class CurrencyConfig {
final String code;
final String symbol;
final String locale;
final int decimalDigits;
const CurrencyConfig({
required this.code,
required this.symbol,
required this.locale,
required this.decimalDigits,
});
}
Price Widget
// lib/widgets/localized/price_text.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../services/currency_service.dart';
import '../../providers/store_provider.dart';
class PriceText extends StatelessWidget {
final double amount;
final TextStyle? style;
final bool showOriginal;
final double? originalAmount;
const PriceText({
super.key,
required this.amount,
this.style,
this.showOriginal = false,
this.originalAmount,
});
@override
Widget build(BuildContext context) {
final storeProvider = context.watch<StoreProvider>();
final currencyService = CurrencyService();
final formattedPrice = currencyService.formatPrice(
amount,
storeProvider.currency,
);
if (showOriginal && originalAmount != null && originalAmount! > amount) {
final formattedOriginal = currencyService.formatPrice(
originalAmount!,
storeProvider.currency,
);
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
formattedPrice,
style: style?.copyWith(
color: Colors.red,
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 8),
Text(
formattedOriginal,
style: TextStyle(
decoration: TextDecoration.lineThrough,
color: Colors.grey,
fontSize: (style?.fontSize ?? 14) * 0.85,
),
),
],
);
}
return Text(formattedPrice, style: style);
}
}
// Usage:
PriceText(
amount: 29.99,
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
)
PriceText(
amount: 19.99,
originalAmount: 29.99,
showOriginal: true,
)
Product Localization
Localized Product Model
// lib/models/product.dart
class Product {
final String id;
final Map<String, String> names; // locale -> name
final Map<String, String> descriptions; // locale -> description
final Map<String, double> prices; // currency -> price
final List<String> images;
final Map<String, List<String>> sizes; // locale -> sizes
final String category;
const Product({
required this.id,
required this.names,
required this.descriptions,
required this.prices,
required this.images,
required this.sizes,
required this.category,
});
String getName(String locale) {
return names[locale] ?? names['en'] ?? '';
}
String getDescription(String locale) {
return descriptions[locale] ?? descriptions['en'] ?? '';
}
double getPrice(String currency) {
return prices[currency] ?? prices['USD'] ?? 0;
}
List<String> getSizes(String locale) {
return sizes[locale] ?? sizes['en'] ?? [];
}
factory Product.fromJson(Map<String, dynamic> json) {
return Product(
id: json['id'],
names: Map<String, String>.from(json['names'] ?? {}),
descriptions: Map<String, String>.from(json['descriptions'] ?? {}),
prices: Map<String, double>.from(
(json['prices'] as Map).map((k, v) => MapEntry(k, v.toDouble())),
),
images: List<String>.from(json['images'] ?? []),
sizes: (json['sizes'] as Map?)?.map(
(k, v) => MapEntry(k.toString(), List<String>.from(v)),
) ??
{},
category: json['category'] ?? '',
);
}
}
Localized Product Card
// lib/widgets/localized/localized_product_card.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../../models/product.dart';
import '../../providers/store_provider.dart';
import 'price_text.dart';
class LocalizedProductCard extends StatelessWidget {
final Product product;
final VoidCallback? onTap;
final VoidCallback? onAddToCart;
const LocalizedProductCard({
super.key,
required this.product,
this.onTap,
this.onAddToCart,
});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final storeProvider = context.watch<StoreProvider>();
final locale = Localizations.localeOf(context).languageCode;
return Card(
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: onTap,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Product Image
AspectRatio(
aspectRatio: 1,
child: Image.network(
product.images.first,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => Container(
color: Colors.grey[200],
child: const Icon(Icons.image, size: 48),
),
),
),
Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Product Name (localized)
Text(
product.getName(locale),
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
// Price (currency-aware)
PriceText(
amount: product.getPrice(storeProvider.currency),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
// Add to Cart Button
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: onAddToCart,
child: Text(l10n.addToCart),
),
),
],
),
),
],
),
),
);
}
}
ARB File Structure for E-commerce
{
"@@locale": "en",
"appTitle": "Shop",
"home": "Home",
"categories": "Categories",
"cart": "Cart",
"account": "Account",
"addToCart": "Add to Cart",
"buyNow": "Buy Now",
"outOfStock": "Out of Stock",
"inStock": "In Stock",
"lowStock": "Only {count} left",
"@lowStock": {
"placeholders": {
"count": {"type": "int"}
}
},
"productDetails": "Product Details",
"description": "Description",
"specifications": "Specifications",
"reviews": "Reviews",
"reviewCount": "{count, plural, =0{No reviews} =1{1 review} other{{count} reviews}}",
"@reviewCount": {
"placeholders": {
"count": {"type": "int"}
}
},
"selectSize": "Select Size",
"selectColor": "Select Color",
"quantity": "Quantity",
"cartEmpty": "Your cart is empty",
"cartItems": "{count, plural, =1{1 item} other{{count} items}}",
"@cartItems": {
"placeholders": {
"count": {"type": "int"}
}
},
"subtotal": "Subtotal",
"shipping": "Shipping",
"tax": "Tax",
"total": "Total",
"freeShipping": "Free Shipping",
"estimatedDelivery": "Estimated delivery: {date}",
"@estimatedDelivery": {
"placeholders": {
"date": {"type": "String"}
}
},
"checkout": "Checkout",
"shippingAddress": "Shipping Address",
"billingAddress": "Billing Address",
"sameAsShipping": "Same as shipping address",
"paymentMethod": "Payment Method",
"placeOrder": "Place Order",
"orderConfirmed": "Order Confirmed!",
"orderNumber": "Order #{number}",
"@orderNumber": {
"placeholders": {
"number": {"type": "String"}
}
},
"firstName": "First Name",
"lastName": "Last Name",
"email": "Email",
"phone": "Phone",
"address": "Address",
"addressLine2": "Apt, Suite, etc. (optional)",
"city": "City",
"state": "State/Province",
"zipCode": "ZIP/Postal Code",
"country": "Country",
"creditCard": "Credit Card",
"paypal": "PayPal",
"applePay": "Apple Pay",
"googlePay": "Google Pay",
"returnPolicy": "Return Policy",
"returnPolicyText": "Free returns within 30 days of delivery.",
"privacyPolicy": "Privacy Policy",
"termsOfService": "Terms of Service"
}
Checkout Flow Localization
Address Form with Locale-Aware Fields
// lib/widgets/checkout/address_form.dart
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class AddressForm extends StatefulWidget {
final String countryCode;
final Function(Map<String, String>) onSubmit;
const AddressForm({
super.key,
required this.countryCode,
required this.onSubmit,
});
@override
State<AddressForm> createState() => _AddressFormState();
}
class _AddressFormState extends State<AddressForm> {
final _formKey = GlobalKey<FormState>();
final _controllers = <String, TextEditingController>{};
@override
void initState() {
super.initState();
for (final field in _getFieldsForCountry(widget.countryCode)) {
_controllers[field.key] = TextEditingController();
}
}
@override
void dispose() {
for (final controller in _controllers.values) {
controller.dispose();
}
super.dispose();
}
List<AddressField> _getFieldsForCountry(String countryCode) {
// Different countries have different address formats
switch (countryCode) {
case 'US':
return [
AddressField('firstName', isRequired: true),
AddressField('lastName', isRequired: true),
AddressField('address', isRequired: true),
AddressField('addressLine2'),
AddressField('city', isRequired: true),
AddressField('state', isRequired: true),
AddressField('zipCode', isRequired: true),
];
case 'GB':
return [
AddressField('firstName', isRequired: true),
AddressField('lastName', isRequired: true),
AddressField('address', isRequired: true),
AddressField('addressLine2'),
AddressField('city', isRequired: true),
AddressField('county'),
AddressField('postcode', isRequired: true),
];
case 'JP':
return [
AddressField('postalCode', isRequired: true),
AddressField('prefecture', isRequired: true),
AddressField('city', isRequired: true),
AddressField('address', isRequired: true),
AddressField('building'),
AddressField('lastName', isRequired: true),
AddressField('firstName', isRequired: true),
];
default:
return [
AddressField('firstName', isRequired: true),
AddressField('lastName', isRequired: true),
AddressField('address', isRequired: true),
AddressField('city', isRequired: true),
AddressField('zipCode', isRequired: true),
];
}
}
String _getFieldLabel(String key, AppLocalizations l10n) {
switch (key) {
case 'firstName':
return l10n.firstName;
case 'lastName':
return l10n.lastName;
case 'address':
return l10n.address;
case 'addressLine2':
return l10n.addressLine2;
case 'city':
return l10n.city;
case 'state':
return l10n.state;
case 'zipCode':
case 'postcode':
case 'postalCode':
return l10n.zipCode;
default:
return key;
}
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final fields = _getFieldsForCountry(widget.countryCode);
return Form(
key: _formKey,
child: Column(
children: [
...fields.map((field) => Padding(
padding: const EdgeInsets.only(bottom: 16),
child: TextFormField(
controller: _controllers[field.key],
decoration: InputDecoration(
labelText: _getFieldLabel(field.key, l10n),
border: const OutlineInputBorder(),
),
validator: field.isRequired
? (value) {
if (value == null || value.isEmpty) {
return 'Required';
}
return null;
}
: null,
),
)),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _submit,
child: Text(l10n.checkout),
),
),
],
),
);
}
void _submit() {
if (_formKey.currentState!.validate()) {
final data = <String, String>{};
for (final entry in _controllers.entries) {
data[entry.key] = entry.value.text;
}
widget.onSubmit(data);
}
}
}
class AddressField {
final String key;
final bool isRequired;
const AddressField(this.key, {this.isRequired = false});
}
Date and Delivery Formatting
// lib/services/delivery_service.dart
import 'package:intl/intl.dart';
class DeliveryService {
String formatDeliveryDate(DateTime date, String locale) {
final formatter = DateFormat.yMMMd(locale);
return formatter.format(date);
}
String formatDeliveryRange(
DateTime start,
DateTime end,
String locale,
) {
final formatter = DateFormat.MMMd(locale);
return '${formatter.format(start)} - ${formatter.format(end)}';
}
String getDeliveryEstimate(String locale, {int businessDays = 5}) {
final now = DateTime.now();
var deliveryDate = now;
var daysAdded = 0;
while (daysAdded < businessDays) {
deliveryDate = deliveryDate.add(const Duration(days: 1));
if (deliveryDate.weekday != DateTime.saturday &&
deliveryDate.weekday != DateTime.sunday) {
daysAdded++;
}
}
return formatDeliveryDate(deliveryDate, locale);
}
}
// Usage in widget:
final deliveryService = DeliveryService();
final locale = Localizations.localeOf(context).toString();
Text(
l10n.estimatedDelivery(
deliveryService.getDeliveryEstimate(locale),
),
)
Size Localization
// lib/services/size_service.dart
class SizeService {
// Clothing sizes vary by region
static const Map<String, Map<String, String>> _sizeConversions = {
'US': {
'XS': 'XS',
'S': 'S',
'M': 'M',
'L': 'L',
'XL': 'XL',
},
'EU': {
'XS': '32-34',
'S': '36-38',
'M': '40-42',
'L': '44-46',
'XL': '48-50',
},
'UK': {
'XS': '4-6',
'S': '8-10',
'M': '12-14',
'L': '16-18',
'XL': '20-22',
},
'JP': {
'XS': 'SS',
'S': 'S',
'M': 'M',
'L': 'L',
'XL': 'LL',
},
};
String convertSize(String size, String fromRegion, String toRegion) {
if (fromRegion == toRegion) return size;
// Find the base size key
final fromSizes = _sizeConversions[fromRegion];
final toSizes = _sizeConversions[toRegion];
if (fromSizes == null || toSizes == null) return size;
// Find matching key
String? baseKey;
for (final entry in fromSizes.entries) {
if (entry.value == size) {
baseKey = entry.key;
break;
}
}
if (baseKey == null) return size;
return toSizes[baseKey] ?? size;
}
List<String> getSizesForRegion(String region) {
return _sizeConversions[region]?.values.toList() ?? [];
}
}
Store Provider with Currency and Locale
// lib/providers/store_provider.dart
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
class StoreProvider extends ChangeNotifier {
String _currency = 'USD';
String _country = 'US';
String get currency => _currency;
String get country => _country;
// Map locales to default currencies
static const Map<String, String> _localeCurrencies = {
'en_US': 'USD',
'en_GB': 'GBP',
'de_DE': 'EUR',
'fr_FR': 'EUR',
'es_ES': 'EUR',
'ja_JP': 'JPY',
'zh_CN': 'CNY',
'pt_BR': 'BRL',
};
StoreProvider() {
_loadPreferences();
}
Future<void> _loadPreferences() async {
final prefs = await SharedPreferences.getInstance();
_currency = prefs.getString('currency') ?? 'USD';
_country = prefs.getString('country') ?? 'US';
notifyListeners();
}
Future<void> setCurrency(String currency) async {
_currency = currency;
notifyListeners();
final prefs = await SharedPreferences.getInstance();
await prefs.setString('currency', currency);
}
Future<void> setCountry(String country) async {
_country = country;
notifyListeners();
final prefs = await SharedPreferences.getInstance();
await prefs.setString('country', country);
}
void setFromLocale(Locale locale) {
final localeString = '${locale.languageCode}_${locale.countryCode}';
final currency = _localeCurrencies[localeString];
if (currency != null) {
setCurrency(currency);
}
if (locale.countryCode != null) {
setCountry(locale.countryCode!);
}
}
}
Best Practices for E-commerce Localization
1. Separate Content from Code
// Store product translations in database, not code
final product = await api.getProduct(
id: productId,
locale: currentLocale,
currency: currentCurrency,
);
2. Handle Missing Translations
String getLocalizedName(String locale) {
return names[locale]
?? names[locale.split('_').first] // Try language only
?? names['en'] // Fallback to English
?? 'Product'; // Ultimate fallback
}
3. Consider RTL for Arabic Markets
Directionality(
textDirection: isRtl ? TextDirection.rtl : TextDirection.ltr,
child: PriceText(amount: product.price),
)
4. Test with Real Data
// Test different currencies and formats
testWidgets('displays price correctly in EUR', (tester) async {
await tester.pumpWidget(
StoreProvider(currency: 'EUR'),
child: PriceText(amount: 29.99),
);
expect(find.text('29,99 €'), findsOneWidget);
});
Conclusion
E-commerce localization requires attention to:
- Currency formatting and conversion
- Product content translation
- Locale-specific address forms
- Size and unit conversions
- Date formatting for delivery
Invest in a solid localization architecture early - it's much harder to retrofit later.
Need help managing translations for your e-commerce app? FlutterLocalisation offers team collaboration and AI-powered translations to keep your product catalog localized across all markets.