Internationalization (i18n) & Localization (l10n)
Overview
The system supports multiple languages through the LocalizedString entity, which provides translations for any text field in any entity. This allows the application to display content in the user's preferred language without requiring schema changes.
Architecture
LocalizedString Entity
The LocalizedString entity provides a polymorphic translation system:
public class LocalizedString
{
public string EntityType { get; set; } // e.g., "Brand", "FishSpecies"
public Guid? EntityGuidId { get; set; } // Entity ID (if Guid)
public int? EntityIntId { get; set; } // Entity ID (if int)
public string FieldName { get; set; } // e.g., "Name", "Description"
public LanguageCode LanguageCode { get; set; } // ISO 639-1 code
public string TranslatedText { get; set; } // The translation
}
LanguageCode Enum
Supports 30+ languages including:
- English: En, EnUs, EnGb, EnCa, EnAu
- Spanish: Es, EsMx, EsEs
- French: Fr, FrCa, FrFr
- European: De, It, Pt, PtBr, Nl, Ru, Pl, Sv, No, Da, Fi
- Asian: Zh, ZhCn, ZhTw, Ja, Ko, Th, Vi
- Other: Ar, Hi, Tr, El, He, Cs, Hu, Ro, Uk
Usage Patterns
Getting Translated Text
// Get brand name in user's language
var brand = context.Brands.First(b => b.Id == brandId);
var userLanguage = LanguageCode.Es; // User's preferred language
var translatedName = context.LocalizedStrings
.FirstOrDefault(ls => ls.EntityType == "Brand"
&& ls.EntityGuidId == brandId
&& ls.FieldName == "Name"
&& ls.LanguageCode == userLanguage)
?.TranslatedText ?? brand.Name; // Fallback to default (English)
// Or use a helper method
var name = GetLocalizedText(context, "Brand", brandId, "Name", userLanguage) ?? brand.Name;
Adding Translations
// Add Spanish translation for a brand
var translation = new LocalizedString
{
EntityType = "Brand",
EntityGuidId = brandId,
FieldName = "Name",
LanguageCode = LanguageCode.Es,
TranslatedText = "Abu García", // Spanish translation
IsPrimary = true
};
context.LocalizedStrings.Add(translation);
context.SaveChanges();
Bulk Translation
// Translate all fish species names to Spanish
var species = context.FishSpecies.ToList();
var translations = species.Select(s => new LocalizedString
{
EntityType = "FishSpecies",
EntityIntId = s.Id,
FieldName = "CommonName",
LanguageCode = LanguageCode.Es,
TranslatedText = GetSpanishTranslation(s.CommonName), // Your translation logic
IsPrimary = true
});
context.LocalizedStrings.AddRange(translations);
context.SaveChanges();
Entities That Support Translation
Core Entities
- Brand: Name, Description
- FishSpecies: CommonName, ScientificName, Description, AlternativeCommonNames
- LureType: Name, Description
- LureSubtype: Name, Description, Notes
- GearCategory: Name, Description
Gear Entities
- Rod: Name
- Reel: Name, Type, LineCapacity
- Lure: Name, Description
- Line: Name, Description
- Apparel: Name, Model, Description
Lookup Tables
- LookupTable: Name, Description
- All enum-derived lookups can be translated
- RodPower, RodAction, LureType, etc.
Other Entities
- FishingSuperstition: Name, Description
- Achievement: Name, Description
- Knot: Name, Description, Instructions, UseCase
- Guide: Brand, Model, Series
Translation Strategy
1. Default Language (English)
- All entities store English text in their primary fields
- English is the fallback language
- No translation entries needed for English
2. User Language Preference
- Store user's preferred language in
Userentity (addPreferredLanguageCode) - API returns translations based on user preference
- Frontend can override with explicit language selection
3. Translation Sources
- Manual: Admin/translator enters translations
- Machine Translation: Use AI/ML services (Google Translate, Azure Translator)
- Community: Allow users to suggest translations (with moderation)
4. Translation Workflow
- Content created in English (default)
- Translation jobs created for target languages
- Translations added via
LocalizedStringentries - API serves translations based on user preference
- Missing translations fall back to English
Implementation Examples
Helper Extension Methods
public static class LocalizationExtensions
{
public static string? GetLocalizedText(
this AppDbContext context,
string entityType,
Guid? entityGuidId,
int? entityIntId,
string fieldName,
LanguageCode languageCode)
{
return context.LocalizedStrings
.FirstOrDefault(ls => ls.EntityType == entityType
&& ls.EntityGuidId == entityGuidId
&& ls.EntityIntId == entityIntId
&& ls.FieldName == fieldName
&& ls.LanguageCode == languageCode)
?.TranslatedText;
}
public static string GetLocalizedTextOrDefault(
this AppDbContext context,
string entityType,
Guid? entityGuidId,
int? entityIntId,
string fieldName,
LanguageCode languageCode,
string defaultValue)
{
return GetLocalizedText(context, entityType, entityGuidId, entityIntId, fieldName, languageCode)
?? defaultValue;
}
}
API Response DTOs
public class BrandDto
{
public Guid Id { get; set; }
public string Name { get; set; } // Translated based on user language
public string? Description { get; set; } // Translated
public string? Country { get; set; }
// ... other fields
}
// In controller/service
public BrandDto GetBrand(Guid id, LanguageCode language)
{
var brand = context.Brands.First(b => b.Id == id);
return new BrandDto
{
Id = brand.Id,
Name = context.GetLocalizedTextOrDefault("Brand", brand.Id, null, "Name", language, brand.Name),
Description = context.GetLocalizedTextOrDefault("Brand", brand.Id, null, "Description", language, brand.Description),
Country = brand.Country
};
}
Future Enhancements
1. User Language Preference
Add to User entity:
public LanguageCode PreferredLanguageCode { get; set; } = LanguageCode.En;
public string? PreferredLanguageCodeCustom { get; set; } // For custom languages
2. Translation Management
- Admin UI for managing translations
- Translation status tracking (translated, needs review, missing)
- Translation completion percentage per language
3. Community Translations
- Allow users to suggest translations
- Translation voting/approval system
- Contributor credits
4. Machine Translation Integration
- Auto-translate on content creation
- Batch translation jobs
- Translation quality scoring
5. Regional Variants
- Support regional language variants (e.g., EsMx vs EsEs)
- Fallback chain: EsMx → Es → En
6. Right-to-Left (RTL) Support
- Track text direction per language
- UI adjustments for RTL languages (Arabic, Hebrew)
Database Considerations
Indexes
- Unique index on
(EntityType, EntityGuidId/EntityIntId, FieldName, LanguageCode) - Index on
LanguageCodefor language-specific queries - Index on
EntityTypefor entity-specific queries
Performance
- Consider caching translations in Redis
- Pre-load common translations on app startup
- Lazy-load translations as needed
Storage
- Translations can significantly increase database size
- Consider archiving old/unused translations
- Compress rarely-accessed translations
Migration Strategy
Phase 1: Infrastructure (Current)
- ✅ Create
LocalizedStringentity - ✅ Add
LanguageCodeenum - ✅ Configure relationships and indexes
- ✅ Seed language codes into lookup tables
Phase 2: Core Translations
- Translate core entities (Brand, FishSpecies, LureType)
- Translate LookupTable entries
- Add translation UI for admins
Phase 3: User Preference
- Add
PreferredLanguageCodeto User - Update API to return translations
- Add language selector in frontend
Phase 4: Advanced Features
- Machine translation integration
- Community translation system
- Translation quality metrics
Best Practices
- Always provide English fallback: If translation missing, use English
- Cache translations: Don't query database for every text lookup
- Batch translations: Load all translations for an entity at once
- Track translation status: Know what's translated and what's not
- Validate translations: Ensure translations are appropriate for context
- Consider context: Some terms may need different translations in different contexts
- Handle plurals: Some languages have complex pluralization rules
- Date/Number formatting: Use user's locale for formatting (separate concern)
Example: Translating Fish Species
// English (default)
var bass = new FishSpecies
{
CommonName = "Largemouth Bass",
ScientificName = "Micropterus salmoides"
};
// Spanish translation
var spanishName = new LocalizedString
{
EntityType = "FishSpecies",
EntityIntId = bass.Id,
FieldName = "CommonName",
LanguageCode = LanguageCode.Es,
TranslatedText = "Perca de Boca Grande"
};
// French translation
var frenchName = new LocalizedString
{
EntityType = "FishSpecies",
EntityIntId = bass.Id,
FieldName = "CommonName",
LanguageCode = LanguageCode.Fr,
TranslatedText = "Achigan à Grande Bouche"
};
Summary
The LocalizedString entity provides a flexible, extensible translation system that:
- ✅ Works with any entity and any field
- ✅ Supports 30+ languages
- ✅ Doesn't require schema changes
- ✅ Allows gradual translation (translate as needed)
- ✅ Provides fallback to English
- ✅ Tracks translation metadata (machine vs manual, confidence, etc.)
This system is ready for international expansion!