← Back to Blog

Flutter E-commerce Localization: Complete Guide to Multi-Currency and Multi-Language Shopping Apps

flutterecommercelocalizationcurrencypaymentshopping

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.