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. Try requested locale with namespace
- 2. Try requested locale without namespace
- 3. Try fallback locale with namespace
- 4. Try fallback locale without namespace
- 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