Localization

Internationalization and localization with parameter replacement, pluralization, and namespace support.

ParametersPluralizationNamespacesFile-based

Quick Start

Basic Translation

dart
// Set global locale
Lang.setGlobalLocale('en');
Lang.setFallbackLocale('en');

// Basic translation
String greeting = Lang.t('messages.greeting');
String welcome = Lang.t('messages.welcome', parameters: {'name': 'Alice'});

// Pluralization
String items = Lang.choice('items.count', 5);

// Field translation
String label = Lang.getField('email');

// Check if translation exists
if (Lang.has('messages.greeting')) {
  print('Translation exists');
}

πŸ’‘ Note: Translations are automatically loaded from lang/ directory

⚑ Tip: Use descriptive keys like 'messages.welcome' or 'validation.required'

Directory Structure

bash
project_root/
└── lang/
    β”œβ”€β”€ en/
    β”‚   β”œβ”€β”€ messages.json
    β”‚   β”œβ”€β”€ validation.json
    β”‚   β”œβ”€β”€ fields.json
    β”‚   └── auth.json
    β”œβ”€β”€ fr/
    β”‚   β”œβ”€β”€ messages.json
    β”‚   β”œβ”€β”€ validation.json
    β”‚   └── fields.json
    └── es/
        β”œβ”€β”€ messages.json
        └── validation.json

File Format

json
{
  "greeting": "Hello :name!",
  "welcome": "Welcome to :app",
  "items": "item|items",
  "apples": "You have :count apple|You have :count apples",
  "error_required": "The :field field is required",
  "login": {
    "title": "Sign In",
    "email": "Email Address",
    "password": "Password",
    "button": "Sign In"
  }
}

Parameter Replacement

dart
// Simple parameter replacement
String greeting = Lang.t('messages.greeting', parameters: {'name': 'Alice'});
// Result: "Hello Alice!"

// Multiple parameters
String welcome = Lang.t('messages.welcome', parameters: {
  'name': 'Alice',
  'app': 'MyApp'
});
// Result: "Welcome to MyApp"

// Field names in validation
String error = Lang.t('validation.required', parameters: {'field': 'email'});
// Result: "The email field is required"

// Dynamic values
String notification = Lang.t('notifications.order_placed', parameters: {
  'order_id': order.id,
  'total': order.total.toString(),
});
// Result: "Order #123 placed for $99.99"

Parameter Syntax

  • β€’ :name - Simple parameter replacement
  • β€’ :count - Numeric parameter for pluralization
  • β€’ :field - Field name in validation messages
  • β€’ :app - Application name or dynamic values

Pluralization

dart
// Basic pluralization
String item1 = Lang.choice('items.count', 1); // "1 item"
String item5 = Lang.choice('items.count', 5); // "5 items"

// With parameters
String apples1 = Lang.choice('apples', 1); // "You have 1 apple"
String apples3 = Lang.choice('apples', 3); // "You have 3 apples"

// Complex pluralization
String files = Lang.choice('files.uploaded', fileCount, parameters: {
  'count': fileCount,
  'user': userName,
});
// Result: "John uploaded 1 file" or "John uploaded 5 files"

// In templates
String message = Lang.t('cart.summary', parameters: {
  'item_count': itemCount,
  'total': totalPrice,
});
// Translation: "Your cart has :item_count item|Your cart has :item_count items"

Pluralization Rules

  • β€’ item|items - Singular|Plural form
  • β€’ :count item|:count items - With count parameter
  • β€’ apple|apples - Simple pluralization
  • β€’ Works with any count: 0, 1, 2, 3, etc.

Locale Management

dart
// Set global locale for entire application
Lang.setGlobalLocale('en');
Lang.setFallbackLocale('en');

// Set locale for current request (per-request)
Lang.setRequestLocale('fr');

// Override locale for specific calls
String germanText = Lang.t('messages.hello', locale: 'de');

// Get current locale information
String currentLocale = Lang.getGlobalLocale();
String fallbackLocale = Lang.getFallbackLocale();

// List available locales
List<String> locales = Lang.getAvailableLocales();

// Middleware example for automatic locale detection
class LocaleMiddleware extends Middleware {
  @override
  Future<void> handle(Request request, Response response, Next next) async {
    // Detect locale from Accept-Language header
    String locale = _detectLocaleFromHeader(request.header('accept-language'));
    Lang.setRequestLocale(locale);

    await next();

    // Clean up request-specific locale
    // (This happens automatically when request ends)
  }
}

Locale Types

  • β€’ Global Locale - Application-wide default locale
  • β€’ Request Locale - Per-request locale override
  • β€’ Fallback Locale - Used when translation is missing
  • β€’ Override Locale - One-time locale for specific calls

Namespace Support

dart
// Load package-specific translations
Lang.loadNamespace('auth', 'en', {
  'login': 'Sign In',
  'register': 'Create Account',
  'forgot_password': 'Forgot Password',
});

Lang.loadNamespace('auth', 'fr', {
  'login': 'Se Connecter',
  'register': 'CrΓ©er un Compte',
  'forgot_password': 'Mot de Passe OubliΓ©',
});

// Use namespaced translations
String loginButton = Lang.t('login', namespace: 'auth');

// Mix global and namespaced translations
String pageTitle = Lang.t('auth.page_title', namespace: 'auth');
String welcomeMessage = Lang.t('messages.welcome'); // Global namespace

// Package structure example
class AuthPackage {
  static void initialize() {
    // Load translations for this package
    Lang.loadNamespace('auth', 'en', _englishTranslations);
    Lang.loadNamespace('auth', 'fr', _frenchTranslations);
  }

  static String loginTitle() => Lang.t('login.title', namespace: 'auth');
  static String loginButton() => Lang.t('login.button', namespace: 'auth');
}

Namespace Use Cases

  • β€’ Package-specific translations
  • β€’ Feature-specific translations
  • β€’ Third-party library translations
  • β€’ Modular application translations

Field Translations

dart
// Translate field labels
String emailLabel = Lang.getField('email'); // Uses "fields.email" key
String passwordLabel = Lang.getField('password');
String firstNameLabel = Lang.getField('first_name');

// With namespace
String customField = Lang.getField('custom_field', namespace: 'my_package');

// In forms
class UserForm {
  Map<String, String> getFieldLabels() {
    return {
      'email': Lang.getField('email'),
      'password': Lang.getField('password'),
      'first_name': Lang.getField('first_name'),
      'last_name': Lang.getField('last_name'),
    };
  }
}

// Validation integration
class Validator {
  static String getErrorMessage(String field, String rule, [Map<String, dynamic>? params]) {
    String key = 'validation.${field}_$rule';
    if (!Lang.has(key)) {
      key = 'validation.$rule'; // Fallback to generic rule message
    }
    return Lang.t(key, parameters: {'field': Lang.getField(field), ...?params});
  }
}

Field Translation Structure

json
{
  "email": "Email Address",
  "password": "Password",
  "first_name": "First Name",
  "last_name": "Last Name",
  "phone": "Phone Number",
  "address": "Address",
  "city": "City",
  "country": "Country",
  "postal_code": "Postal Code"
}

Custom Parameter Replacers

dart
// Add custom replacer for date formatting
Lang.addParameterReplacer((key, value, params) {
  if (key == 'date' && value is DateTime) {
    return _formatDate(value, params['locale'] ?? 'en');
  }
  return ':$key'; // Default replacement
});

// Add currency formatter
Lang.addParameterReplacer((key, value, params) {
  if (key == 'currency' && value is num) {
    return _formatCurrency(value, params['currency'] ?? 'USD');
  }
  return ':$key';
});

// Add number formatter
Lang.addParameterReplacer((key, value, params) {
  if (key == 'number' && value is num) {
    return _formatNumber(value, params['locale'] ?? 'en');
  }
  return ':$key';
});

// Usage with custom replacers
String invoice = Lang.t('invoice.due_date', parameters: {
  'date': DateTime.now().add(Duration(days: 30)),
  'amount': 99.99,
  'currency': 'EUR',
});
// Translation: "Invoice due on March 15, 2024 for €99.99"

// Complex object formatting
Lang.addParameterReplacer((key, value, params) {
  if (key == 'user' && value is User) {
    return value.fullName;
  }
  if (key == 'product' && value is Product) {
    return '${value.name} (${value.sku})';
  }
  return ':$key';
});

String orderMessage = Lang.t('order.shipped', parameters: {
  'user': currentUser,
  'product': orderedProduct,
  'tracking': trackingNumber,
});

Replacer Use Cases

  • β€’ Date/time formatting
  • β€’ Currency formatting
  • β€’ Number formatting
  • β€’ Custom object serialization
  • β€’ Complex conditional formatting

Fallback and Missing Translations

dart
// Set fallback locale
Lang.setFallbackLocale('en');

// Translation lookup order:
// 1. Try 'fr' locale with 'auth' namespace
// 2. Try 'fr' locale with global namespace
// 3. Try 'en' fallback locale with 'auth' namespace
// 4. Try 'en' fallback locale with global namespace
// 5. Return key itself if not found

String message = Lang.t('auth.login', locale: 'fr', namespace: 'auth');

// Handle missing translations gracefully
String safeTranslate(String key, {Map<String, dynamic>? parameters}) {
  if (Lang.has(key)) {
    return Lang.t(key, parameters: parameters);
  }

  // Log missing translation
  Khadem.logger.warning('Missing translation for key: $key');

  // Return a user-friendly fallback
  return _humanizeKey(key);
}

String _humanizeKey(String key) {
  return key.split('.').last.replaceAll('_', ' ').toUpperCase();
}

// Check translation existence
if (Lang.has('messages.greeting')) {
  return Lang.t('messages.greeting');
} else {
  return 'Hello!'; // Fallback
}

// Get raw translation value
String? rawTranslation = Lang.get('messages.greeting');
if (rawTranslation != null) {
  // Process raw translation
}

Fallback Strategy

  1. 1. Try requested locale with namespace
  2. 2. Try requested locale without namespace
  3. 3. Try fallback locale with namespace
  4. 4. Try fallback locale without namespace
  5. 5. Return the key itself as last resort

Translation Management

dart
// Check if translation exists
bool exists = Lang.has('messages.greeting');
bool existsInNamespace = Lang.has('login', namespace: 'auth');

// Get raw translation value
String? rawValue = Lang.get('messages.greeting');
String? namespacedValue = Lang.get('login', namespace: 'auth');

// Clear translation cache (useful during development)
Lang.clearCache();

// Get available locales
List<String> locales = Lang.getAvailableLocales();

// Switch translation provider
Lang.use(CustomLangProvider());

// Load translations programmatically
Lang.loadNamespace('notifications', 'en', {
  'welcome': 'Welcome to our platform!',
  'order_placed': 'Your order has been placed',
  'payment_received': 'Payment received successfully',
});
dart
// Debug translation loading
void debugTranslations() {
  print('Available locales: ${Lang.getAvailableLocales()}');
  print('Current global locale: ${Lang.getGlobalLocale()}');
  print('Fallback locale: ${Lang.getFallbackLocale()}');

  // Test specific translations
  final testKeys = [
    'messages.greeting',
    'validation.required',
    'auth.login',
  ];

  for (final key in testKeys) {
    final exists = Lang.has(key);
    final value = Lang.get(key);
    print('$key: ${exists ? 'EXISTS' : 'MISSING'} - $value');
  }
}

// Validate translation completeness
void validateTranslations(String locale) {
  final requiredKeys = [
    'messages.greeting',
    'messages.welcome',
    'validation.required',
    'validation.email',
    'auth.login',
    'auth.register',
  ];

  final missingKeys = <String>[];
  for (final key in requiredKeys) {
    if (!Lang.has(key)) {
      missingKeys.add(key);
    }
  }

  if (missingKeys.isNotEmpty) {
    Khadem.logger.warning('Missing translations for $locale: $missingKeys');
  }
}

// Monitor translation usage
class TranslationMonitor {
  static final Map<String, int> _usage = {};

  static String translate(String key, {Map<String, dynamic>? parameters}) {
    _usage[key] = (_usage[key] ?? 0) + 1;
    return Lang.t(key, parameters: parameters);
  }

  static void reportUsage() {
    print('Translation usage statistics:');
    _usage.entries.toList()
      ..sort((a, b) => b.value.compareTo(a.value))
      ..forEach((entry) => print('${entry.key}: ${entry.value} times'));
  }
}

Management Operations

  • β€’ has(key) - Check if translation exists
  • β€’ get(key) - Get raw translation value
  • β€’ clearCache() - Clear translation cache
  • β€’ getAvailableLocales() - List available locales

Advanced Usage Patterns

Middleware for Request Localization

Set locale based on request headers or user preferences

dart
// Locale detection middleware
class LocaleDetectionMiddleware extends Middleware {
  @override
  Future<void> handle(Request request, Response response, Next next) async {
    String locale = _determineLocale(request);

    // Set request-specific locale
    Lang.setRequestLocale(locale);

    // Add locale to response for client-side use
    response.header('X-App-Locale', locale);

    await next();
  }

  String _determineLocale(Request request) {
    // 1. Check URL parameter (?lang=en)
    String? urlLocale = request.query('lang');
    if (urlLocale != null && _isValidLocale(urlLocale)) {
      return urlLocale;
    }

    // 2. Check Accept-Language header
    String? acceptLanguage = request.header('accept-language');
    if (acceptLanguage != null) {
      String detected = _parseAcceptLanguage(acceptLanguage);
      if (_isValidLocale(detected)) {
        return detected;
      }
    }

    // 3. Check user preference (from session/database)
    String? userLocale = await _getUserPreferredLocale(request);
    if (userLocale != null && _isValidLocale(userLocale)) {
      return userLocale;
    }

    // 4. Fallback to global default
    return Lang.getGlobalLocale();
  }

  bool _isValidLocale(String locale) {
    return Lang.getAvailableLocales().contains(locale);
  }

  String _parseAcceptLanguage(String acceptLanguage) {
    // Parse Accept-Language header (e.g., "en-US,en;q=0.9,fr;q=0.8")
    // Return the highest priority valid locale
    return 'en'; // Simplified implementation
  }

  Future<String?> _getUserPreferredLocale(Request request) async {
    // Check user session or database for preferred locale
    return null; // Implementation depends on auth system
  }
}

Validation Messages

Localized validation error messages

dart
// Localized validation
class LocalizedValidator {
  static Map<String, String> validateUser(Map<String, dynamic> data) {
    final errors = <String, String>{};

    // Email validation
    if (data['email'] == null || data['email'].toString().isEmpty) {
      errors['email'] = Lang.t('validation.required', parameters: {
        'field': Lang.getField('email')
      });
    } else if (!isValidEmail(data['email'])) {
      errors['email'] = Lang.t('validation.email', parameters: {
        'field': Lang.getField('email')
      });
    }

    // Password validation
    if (data['password'] == null || data['password'].toString().length < 8) {
      errors['password'] = Lang.t('validation.min_length', parameters: {
        'field': Lang.getField('password'),
        'min': '8'
      });
    }

    // Age validation
    final age = data['age'];
    if (age != null && (age < 13 || age > 120)) {
      errors['age'] = Lang.t('validation.between', parameters: {
        'field': Lang.getField('age'),
        'min': '13',
        'max': '120'
      });
    }

    return errors;
  }
}

// Custom validation rules with localization
class LocalizedValidationRule {
  static String required(String field) {
    return Lang.t('validation.required', parameters: {
      'field': Lang.getField(field)
    });
  }

  static String minLength(String field, int length) {
    return Lang.t('validation.min_length', parameters: {
      'field': Lang.getField(field),
      'length': length.toString()
    });
  }

  static String maxLength(String field, int length) {
    return Lang.t('validation.max_length', parameters: {
      'field': Lang.getField(field),
      'length': length.toString()
    });
  }

  static String between(String field, num min, num max) {
    return Lang.t('validation.between', parameters: {
      'field': Lang.getField(field),
      'min': min.toString(),
      'max': max.toString()
    });
  }
}

Email Templates

Localized email content and subjects

dart
// Localized email templates
class EmailTemplates {
  static Map<String, String> welcomeEmail(User user) {
    return {
      'subject': Lang.t('emails.welcome.subject', parameters: {
        'app': Lang.t('app.name')
      }),
      'greeting': Lang.t('emails.welcome.greeting', parameters: {
        'name': user.firstName
      }),
      'body': Lang.t('emails.welcome.body', parameters: {
        'name': user.firstName,
        'app': Lang.t('app.name'),
        'login_url': _generateLoginUrl(),
      }),
      'signature': Lang.t('emails.signature', parameters: {
        'app': Lang.t('app.name')
      }),
    };
  }

  static Map<String, String> orderConfirmation(Order order) {
    final itemCount = order.items.length;
    return {
      'subject': Lang.t('emails.order.subject', parameters: {
        'order_id': order.id
      }),
      'greeting': Lang.t('emails.order.greeting', parameters: {
        'name': order.customerName
      }),
      'body': Lang.t('emails.order.body', parameters: {
        'order_id': order.id,
        'item_count': itemCount.toString(),
        'items': Lang.choice('emails.order.items', itemCount),
        'total': _formatCurrency(order.total),
      }),
      'signature': Lang.t('emails.signature', parameters: {
        'app': Lang.t('app.name')
      }),
    };
  }

  static Map<String, String> passwordReset(User user, String resetToken) {
    return {
      'subject': Lang.t('emails.password_reset.subject'),
      'greeting': Lang.t('emails.password_reset.greeting', parameters: {
        'name': user.firstName
      }),
      'body': Lang.t('emails.password_reset.body', parameters: {
        'reset_url': _generateResetUrl(resetToken),
        'expiry': '24' // hours
      }),
      'signature': Lang.t('emails.signature', parameters: {
        'app': Lang.t('app.name')
      }),
    };
  }
}

// Email service with localization
class LocalizedEmailService {
  Future<void> sendWelcomeEmail(User user) async {
    final template = EmailTemplates.welcomeEmail(user);

    await Mail.to(user.email).send(Email(
      subject: template['subject']!,
      html: _renderTemplate('welcome', template),
    ));
  }

  Future<void> sendOrderConfirmation(Order order) async {
    final template = EmailTemplates.orderConfirmation(order);

    await Mail.to(order.customerEmail).send(Email(
      subject: template['subject']!,
      html: _renderTemplate('order_confirmation', template),
    ));
  }

  String _renderTemplate(String templateName, Map<String, String> data) {
    // Render email template with localized content
    // Implementation depends on your template engine
    return '';
  }
}

Best Practices

βœ… Do's

  • β€’ Use descriptive, hierarchical keys (e.g., 'auth.login.title')
  • β€’ Set appropriate fallback locales for missing translations
  • β€’ Use parameters for dynamic content instead of string concatenation
  • β€’ Organize translations by feature or domain
  • β€’ Use pluralization for count-dependent messages
  • β€’ Test translations with different locales
  • β€’ Document translation keys and their parameters
  • β€’ Use namespaces for package-specific translations
  • β€’ Cache translations for better performance
  • β€’ Handle missing translations gracefully

❌ Don'ts

  • β€’ Don't hardcode text strings in your application code
  • β€’ Don't use translation keys as fallback text
  • β€’ Don't concatenate translated strings with dynamic content
  • β€’ Don't use generic keys like 'message1', 'text2'
  • β€’ Don't forget to handle pluralization for countable items
  • β€’ Don't ignore missing translation warnings
  • β€’ Don't use complex logic in translation files
  • β€’ Don't forget to clear cache after translation file changes
  • β€’ Don't use different parameter names inconsistently
  • β€’ Don't mix different locales in the same response

Performance Considerations

Optimization Tips

  • β€’ Translations are cached after first load
  • β€’ Use clearCache() to refresh after file changes
  • β€’ Large translation files are loaded entirely into memory
  • β€’ Consider splitting large translation files by feature
  • β€’ Use namespaces to organize and load translations on-demand
  • β€’ Minimize parameter replacement in performance-critical paths

Memory Usage

  • β€’ All loaded translations are kept in memory
  • β€’ Use namespaces to load only required translations
  • β€’ Clear cache periodically in long-running applications
  • β€’ Monitor memory usage with large translation sets

On this page