Skip to main content

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 User entity (add PreferredLanguageCode)
  • 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

  1. Content created in English (default)
  2. Translation jobs created for target languages
  3. Translations added via LocalizedString entries
  4. API serves translations based on user preference
  5. 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 LanguageCode for language-specific queries
  • Index on EntityType for 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 LocalizedString entity
  • ✅ Add LanguageCode enum
  • ✅ 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 PreferredLanguageCode to 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

  1. Always provide English fallback: If translation missing, use English
  2. Cache translations: Don't query database for every text lookup
  3. Batch translations: Load all translations for an entity at once
  4. Track translation status: Know what's translated and what's not
  5. Validate translations: Ensure translations are appropriate for context
  6. Consider context: Some terms may need different translations in different contexts
  7. Handle plurals: Some languages have complex pluralization rules
  8. 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!