LoL Engine Localization System¶
Overview¶
The LoL Engine Localization System enables seamless multi-language support for your game with runtime language switching, localized assets, and formatting for numbers, dates, and currencies. It's built for performance with LRU caching and supports language fallback chains for missing translations.
Core Features¶
- Multi-Language Support: Support for any Unity
SystemLanguage - Runtime Language Switching: Change language dynamically without restarting
- CSV-Based Storage: Easy-to-edit CSV files for translators
- Language Fallback: Serializable fallback chain for missing translations
- Asset Localization: Localized sprites, audio, and other Unity assets
- Font Localization: Per-language font switching for CJK, Arabic, Cyrillic, etc.
- Plural Rules: CLDR plural categories (zero/one/two/few/many/other) for 30+ languages
- Case Transforms: UpperCase, LowerCase, TitleCase, UpperFirst applied after lookup
- RTL Support: Automatic text direction and alignment flipping for Arabic/Hebrew
- Per-Language Memory: Split CSV files, load/unload per language, auto-split on build
- Localized Audio: Component-based audio clip switching on language change
- Text Formatting: Culture-specific number, date, and currency formatting
- Parameter Substitution: Dynamic text with named parameters
- LRU Caching: Performance-optimized with configurable cache size
- Event System Integration: React to language changes across your game
- ServiceAwaiter Compatible: Safe initialization with other engine services
Requirements¶
- LoL Engine with ImprovedGameInitializer
- TextMeshPro (recommended for UI text)
- CSV localization file
StringTable.csvinResources/Localization/Tables/ - LocalizationConfig asset in
Resources/Configs/
Setup & Configuration¶
1. Create LocalizationConfig Asset¶
- Right-click in Project:
Create > LoLEngine > Localization > Config - Name it
DefaultLocalizationConfig - Place it in
Assets/[YourProject]/Resources/Configs/
IMPORTANT: Config MUST be in Resources folder
Configure the asset:
Default Language: English
Use System Language: [x]
Supported Languages:
├── English
├── Spanish
├── French
├── German
└── Japanese
Localization Format: CSV
Localization Tables Path: "Localization/Tables" (within Resources folder)
Language Fallbacks:
├── Spanish → English
├── French → English
├── German → English
└── Japanese → English
String Cache Size: 1000
Use Lazy Loading: [x]
Preload All Languages: (load on-demand for better performance)
2. Configure ImprovedGameInitializer¶
Ensure localization is enabled in ServiceConfiguration:
Enable Localization Service: [x]
3. Create CSV Localization File¶
Create a CSV file at:
Assets/[YourProject]/Resources/Localization/Tables/StringTable.csv
File Structure (editor):
Resources/
└── Localization/
└── Tables/
└── StringTable.csv # Monolithic file — source of truth
At build time (or via menu: Tools > LoL Engine > Localization > Split CSV by Language), per-language files are generated automatically for better runtime memory:
Resources/
└── Localization/
└── Tables/
├── StringTable.csv # Source of truth (editor)
├── StringTable_English.csv # Auto-generated (Key,Value)
├── StringTable_French.csv
└── StringTable_German.csv
CSV File Format (StringTable.csv):
The CSV header must use SystemLanguage enum names exactly. All translations for all languages are in ONE file:
Key,English,Spanish,French,German,Japanese
UI_PLAY,Play,Jugar,Jouer,Spielen,プレイ
UI_QUIT,Quit,Salir,Quitter,Beenden,終了
UI_SETTINGS,Settings,Configuración,Paramètres,Einstellungen,設定
UI_BACK,Back,Atrás,Retour,Zurück,戻る
UI_CONTINUE,Continue,Continuar,Continuer,Fortsetzen,続ける
UI_NEW_GAME,New Game,Nuevo Juego,Nouvelle Partie,Neues Spiel,新しいゲーム
GAME_PAUSED,Game Paused,Juego Pausado,Jeu en Pause,Spiel Pausiert,一時停止
GAME_OVER,Game Over,Juego Terminado,Fin du Jeu,Spiel Vorbei,ゲームオーバー
PLAYER_HEALTH,Health,Salud,Santé,Gesundheit,体力
PLAYER_SCORE,Score: {0},Puntuación: {0},Score: {0},Punktzahl: {0},スコア: {0}
ITEM_SWORD_NAME,Iron Sword,Espada de Hierro,Épée de Fer,Eisenschwert,鉄の剣
ITEM_SWORD_DESC,A sturdy blade for beginners,Una hoja resistente para principiantes,Une lame robuste pour débutants,Eine robuste Klinge für Anfänger,初心者向けの丈夫な刃
DIALOG_WELCOME,Welcome {name}!,¡Bienvenido {name}!,Bienvenue {name}!,Willkommen {name}!,ようこそ {name}!
TUTORIAL_MOVE,Use WASD to move,Usa WASD para moverte,Utilisez WASD pour vous déplacer,Verwende WASD zum Bewegen,WASDで移動
ACHIEVEMENT_UNLOCK,Achievement Unlocked: {achievement},Logro Desbloqueado: {achievement},Succès Débloqué: {achievement},Erfolg Freigeschaltet: {achievement},実績解除: {achievement}
Important CSV Rules:
1. Header row must contain "Key" followed by SystemLanguage enum names (e.g., "English", "Spanish", "French")
2. One row per key with all language translations
3. Use {parameterName} or {0} for parameter substitution
4. Use double quotes if your text contains commas: "Hello, world!"
Getting Started¶
Accessing the Localization Service¶
using LoLEngine.Core.Localization.Interfaces;
using LoLEngine.Core.ServiceManagement.Service;
using LoLEngine.Runtime;
using UnityEngine;
public class LocalizedUI : MonoBehaviour
{
private ILocalizationService _localization;
void Start()
{
ServiceAwaiter.WaitForServices(this, OnServicesReady, OnTimeout);
}
private void OnServicesReady()
{
_localization = ServiceLocator.Instance.Get<ILocalizationService>();
if (_localization == null)
{
Debug.LogError("Localization Service not found!");
return;
}
// Subscribe to language change events
_localization.OnLanguageChanged += OnLanguageChanged;
// Update UI with current language
UpdateUIText();
}
private void OnTimeout()
{
Debug.LogError("Services initialization timeout");
}
private void OnLanguageChanged(SystemLanguage newLanguage)
{
Debug.Log($"Language changed to: {newLanguage}");
UpdateUIText();
}
private void UpdateUIText()
{
// Localized text will be shown in examples below
}
void OnDestroy()
{
if (_localization != null)
{
_localization.OnLanguageChanged -= OnLanguageChanged;
}
}
}
Basic Usage¶
Canonical API: Build a
LocString(table + key) and call_localization.Format(loc)(addusing LoLEngine.Core.Localization.LocString;). The string-keyGetText/GetTextFormattedcalls are[Obsolete]— still functional (warning-only), but new code should useLocString. See Localization Migration.
Simple Text Localization¶
public class MenuUI : MonoBehaviour
{
private ILocalizationService _localization;
[SerializeField] private TextMeshProUGUI playButtonText;
[SerializeField] private TextMeshProUGUI quitButtonText;
[SerializeField] private TextMeshProUGUI settingsButtonText;
void Start()
{
_localization = ServiceLocator.Instance.Get<ILocalizationService>();
UpdateButtonText();
}
private void UpdateButtonText()
{
// Canonical pattern: build a LocString (table + key) and Format it.
// Requires: using LoLEngine.Core.Localization.LocString;
playButtonText.text = _localization.Format(new LocString(LocTable.UI, "UI_PLAY"));
quitButtonText.text = _localization.Format(new LocString(LocTable.UI, "UI_QUIT"));
settingsButtonText.text = _localization.Format(new LocString(LocTable.UI, "UI_SETTINGS"));
}
}
Text with Parameters¶
public class PlayerUI : MonoBehaviour
{
private ILocalizationService _localization;
[SerializeField] private TextMeshProUGUI scoreText;
[SerializeField] private TextMeshProUGUI welcomeText;
void Start()
{
_localization = ServiceLocator.Instance.Get<ILocalizationService>();
}
// Method 1: Using string.Format style
public void UpdateScore(int score)
{
// CSV: PLAYER_SCORE,Score: {score}
scoreText.text = _localization.Format(new LocString(LocTable.UI, "PLAYER_SCORE", new DynamicVar("score", score)));
}
// Method 2: Using named parameters (Dictionary)
public void ShowWelcome(string playerName)
{
// CSV: DIALOG_WELCOME,Welcome {name}!
var parameters = new Dictionary<string, string>
{
{ "name", playerName }
};
welcomeText.text = _localization.GetText("DIALOG_WELCOME", parameters);
}
// Complex example with multiple parameters
public void ShowAchievement(string achievementName, int points)
{
// CSV: ACHIEVEMENT_DETAILS,You unlocked {achievement} and earned {points} points!
var parameters = new Dictionary<string, string>
{
{ "achievement", achievementName },
{ "points", points.ToString() }
};
string message = _localization.GetText("ACHIEVEMENT_DETAILS", parameters);
Debug.Log(message);
}
}
Changing Languages¶
Language Selection Menu¶
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
public class LanguageSelector : MonoBehaviour
{
private ILocalizationService _localization;
[SerializeField] private TMP_Dropdown languageDropdown;
void Start()
{
_localization = ServiceLocator.Instance.Get<ILocalizationService>();
// Populate dropdown with available languages
PopulateLanguageDropdown();
// Set current selection
SetCurrentLanguageInDropdown();
// Listen for dropdown changes
languageDropdown.onValueChanged.AddListener(OnLanguageDropdownChanged);
}
private void PopulateLanguageDropdown()
{
languageDropdown.ClearOptions();
var availableLanguages = _localization.AvailableLanguages;
List<string> languageNames = new List<string>();
foreach (var language in availableLanguages)
{
languageNames.Add(GetLanguageDisplayName(language));
}
languageDropdown.AddOptions(languageNames);
}
private void SetCurrentLanguageInDropdown()
{
var currentLanguage = _localization.CurrentLanguage;
var availableLanguages = _localization.AvailableLanguages;
for (int i = 0; i < availableLanguages.Count; i++)
{
if (availableLanguages[i] == currentLanguage)
{
languageDropdown.value = i;
break;
}
}
}
private void OnLanguageDropdownChanged(int index)
{
var availableLanguages = _localization.AvailableLanguages;
if (index >= 0 && index < availableLanguages.Count)
{
SystemLanguage selectedLanguage = availableLanguages[index];
_localization.SetLanguage(selectedLanguage);
Debug.Log($"Language changed to: {selectedLanguage}");
}
}
private string GetLanguageDisplayName(SystemLanguage language)
{
// Return user-friendly names
switch (language)
{
case SystemLanguage.English: return "English";
case SystemLanguage.Spanish: return "Español";
case SystemLanguage.French: return "Français";
case SystemLanguage.German: return "Deutsch";
case SystemLanguage.Japanese: return "日本語";
case SystemLanguage.ChineseSimplified: return "简体中文";
case SystemLanguage.ChineseTraditional: return "繁體中文";
case SystemLanguage.Korean: return "한국어";
case SystemLanguage.Portuguese: return "Português";
case SystemLanguage.Russian: return "Русский";
default: return language.ToString();
}
}
}
Programmatic Language Change¶
public class LanguageManager : MonoBehaviour
{
private ILocalizationService _localization;
void Start()
{
_localization = ServiceLocator.Instance.Get<ILocalizationService>();
}
public void SetEnglish()
{
_localization.SetLanguage(SystemLanguage.English);
}
public void SetSpanish()
{
_localization.SetLanguage(SystemLanguage.Spanish);
}
public void SetFrench()
{
_localization.SetLanguage(SystemLanguage.French);
}
public void GetCurrentLanguage()
{
SystemLanguage current = _localization.CurrentLanguage;
Debug.Log($"Current language: {current}");
}
public void ListAvailableLanguages()
{
var languages = _localization.AvailableLanguages;
Debug.Log($"Available languages: {string.Join(", ", languages)}");
}
}
Localizing UI Components¶
Auto-Updating Text Component¶
Create a component that automatically updates when language changes:
using LoLEngine.Core.Localization.Interfaces;
using LoLEngine.Core.ServiceManagement.Service;
using UnityEngine;
using TMPro;
[RequireComponent(typeof(TextMeshProUGUI))]
public class LocalizedText : MonoBehaviour
{
private ILocalizationService _localization;
private TextMeshProUGUI _textComponent;
[SerializeField] private string localizationKey;
[SerializeField] private bool updateOnLanguageChange = true;
void Awake()
{
_textComponent = GetComponent<TextMeshProUGUI>();
}
void Start()
{
_localization = ServiceLocator.Instance.Get<ILocalizationService>();
if (_localization != null)
{
if (updateOnLanguageChange)
{
_localization.OnLanguageChanged += OnLanguageChanged;
}
UpdateText();
}
}
private void OnLanguageChanged(SystemLanguage newLanguage)
{
UpdateText();
}
private void UpdateText()
{
if (_localization != null && !string.IsNullOrEmpty(localizationKey))
{
_textComponent.text = _localization.GetText(localizationKey);
}
}
public void SetKey(string key)
{
localizationKey = key;
UpdateText();
}
void OnDestroy()
{
if (_localization != null)
{
_localization.OnLanguageChanged -= OnLanguageChanged;
}
}
}
Dynamic UI Updates¶
public class DynamicUIManager : MonoBehaviour
{
private ILocalizationService _localization;
[SerializeField] private TextMeshProUGUI[] allUIText;
void Start()
{
_localization = ServiceLocator.Instance.Get<ILocalizationService>();
_localization.OnLanguageChanged += OnLanguageChanged;
UpdateAllUI();
}
private void OnLanguageChanged(SystemLanguage newLanguage)
{
UpdateAllUI();
}
private void UpdateAllUI()
{
// Find all LocalizedText components in scene
var localizedComponents = FindObjectsOfType<LocalizedText>();
foreach (var component in localizedComponents)
{
// Components will auto-update if they're subscribed
}
// Manually update other UI elements
UpdateMenus();
UpdateDialogs();
UpdateTooltips();
}
private void UpdateMenus() { /* Update menu text */ }
private void UpdateDialogs() { /* Update dialog text */ }
private void UpdateTooltips() { /* Update tooltip text */ }
void OnDestroy()
{
if (_localization != null)
{
_localization.OnLanguageChanged -= OnLanguageChanged;
}
}
}
Font Localization¶
When switching to a language whose characters don't exist in the current font (e.g., Korean, Japanese, Arabic), you need per-language font switching.
Setup¶
- Create a LocalizedFontConfig asset: Right-click >
Create > LoLEngine > Localization > Font Config - Set a default TMP font (used for languages without a specific entry)
- Add per-language entries with the appropriate font, optional fallbacks, and size scale
- Assign the asset to your
LocalizationConfig's Font Config field
LocalizedFontConfig Fields¶
Default Font: NotoSans-Regular (TMP) ← used when no language-specific entry
Default Legacy Font: Arial ← for legacy UI.Text components
Language Fonts:
├── Korean
│ ├── TMP Font: NotoSansKR-Regular
│ ├── Fallbacks: [NotoSansCJK]
│ ├── Legacy Font: (none)
│ └── Size Scale: 1.2 ← multiplied with designer-set font size
├── Japanese
│ ├── TMP Font: NotoSansJP-Regular
│ └── Size Scale: 1.1
└── Arabic
├── TMP Font: NotoSansArabic-Regular
└── Size Scale: 1.0
Automatic Font Switching¶
LocalizedText components automatically apply the correct font on language change when Update Font is enabled (default). No additional code needed.
Standalone Font Applier¶
For dynamic text that builds strings in code (chat, logs, debug), use LocalizedFontApplier:
// Add to any GameObject with Text or TMP_Text
// The component auto-switches font on language change without text localization
[AddComponentMenu("LoL Engine/Localization/Localized Font Applier")]
Programmatic Access¶
TMP_FontAsset font = _localization.GetFontForCurrentLanguage();
Font legacyFont = _localization.GetLegacyFontForCurrentLanguage();
float sizeScale = _localization.GetFontSizeScale(); // 1.0 if none configured
LocalizedFontConfig config = _localization.FontConfig; // null if not configured
Plural Rules¶
Languages have different plural forms. English has 2 (one/other), Arabic has 6, Japanese has 1. The system uses CLDR/ICU plural categories.
CSV Format¶
Use bracket suffixes for plural variants:
Key,English,French,Polish
ITEM_COUNT[one],{count} item,{count} objet,{count} przedmiot
ITEM_COUNT[few],,,{count} przedmioty
ITEM_COUNT[many],,,{count} przedmiotów
ITEM_COUNT[other],{count} items,{count} objets,{count} przedmiotów
Usage¶
// Resolves plural category for current language, looks up KEY[category]
// Falls back to bare KEY if plural variant is missing
string text = _localization.GetPluralText("ITEM_COUNT", itemCount);
// Extension method
string text = "ITEM_COUNT".LocalizePlural(5); // → "5 items" (English)
Supported Categories¶
| Category | Languages that use it |
|---|---|
zero |
Arabic |
one |
English, French, German, Spanish, etc. |
two |
Arabic |
few |
Polish, Russian, Czech, Arabic |
many |
Polish, Russian, Arabic |
other |
All languages (required fallback) |
Case Transforms¶
Apply case transforms after text lookup without creating duplicate keys:
Inspector:
├── Localization Key: "UI_PLAY"
├── Text Transform: UpperCase ← None, UpperCase, LowerCase, UpperFirst, TitleCase
Uses CultureInfo for correct casing (handles Turkish İ/i).
RTL Language Support¶
Arabic and Hebrew are automatically detected as RTL. When active:
- TMP text:
isRightToLeftText = true, alignment flipped (Left↔Right) - Legacy text: Alignment flipped (Left↔Right)
- Layout groups:
LocalizedLayoutGroupcomponent flipsHorizontalLayoutGroup.reverseArrangement
Setup¶
No configuration needed — RTL detection is automatic based on SystemLanguage.Arabic and SystemLanguage.Hebrew.
bool isRTL = _localization.IsCurrentLanguageRTL;
For layout containers, add the LocalizedLayoutGroup component:
[AddComponentMenu("LoL Engine/Localization/Localized Layout Group")]
// Requires: HorizontalLayoutGroup or VerticalLayoutGroup on same GameObject
// Auto-flips child order and alignment for RTL languages
Per-Language Memory Management¶
By default, the system loads from a single monolithic StringTable.csv. For shipping games with many languages, you can split into per-language files.
Splitting CSV¶
Menu: Tools > LoL Engine > Localization > Split CSV by Language
This creates per-language files alongside your CSV:
Resources/Localization/Tables/
├── StringTable.csv ← monolithic (source of truth in editor)
├── StringTable_English.csv ← Key,Value only
├── StringTable_French.csv
├── StringTable_German.csv
└── StringTable_Japanese.csv
Automatic Build Splitting¶
The LanguageSplitBuildPreprocessor runs automatically before every build, ensuring per-language files are up to date.
Runtime Behavior¶
CSVStringProvider.LoadLanguage()tries per-language file first (StringTable_English.csv)- Falls back to monolithic CSV if per-language file doesn't exist
- On language switch, the previous non-default language is unloaded from memory
- Default language is always kept loaded as fallback
Localized Audio¶
Component-Based¶
Add LocalizedAudio to a GameObject with an AudioSource:
Inspector:
├── Localization Key: "VO_INTRO_NARRATOR"
├── Auto Update: [x] ← reload clip on language change
├── Auto Play: [x] ← play clip after loading
Assets should be at:
Resources/Localization/Tables/Assets/
├── English/VO_INTRO_NARRATOR
├── French/VO_INTRO_NARRATOR
└── Japanese/VO_INTRO_NARRATOR
Extension Method¶
audioSource.SetLocalizedAudio("VO_INTRO_NARRATOR");
Advanced Features¶
Localized Assets¶
Localize sprites, audio clips, and other Unity assets:
public class LocalizedAssetExample : MonoBehaviour
{
private ILocalizationService _localization;
[SerializeField] private Image characterPortrait;
[SerializeField] private AudioSource voiceAudio;
void Start()
{
_localization = ServiceLocator.Instance.Get<ILocalizationService>();
_localization.OnLanguageChanged += OnLanguageChanged;
LoadLocalizedAssets();
}
private void OnLanguageChanged(SystemLanguage newLanguage)
{
LoadLocalizedAssets();
}
private void LoadLocalizedAssets()
{
// Load localized sprite
// Place sprites in: Resources/Localization/Assets/en/, /es/, etc.
Sprite localizedSprite = _localization.GetLocalizedAsset<Sprite>("CHARACTER_PORTRAIT");
if (localizedSprite != null)
{
characterPortrait.sprite = localizedSprite;
}
// Load localized audio
AudioClip localizedAudio = _localization.GetLocalizedAsset<AudioClip>("VOICE_GREETING");
if (localizedAudio != null)
{
voiceAudio.clip = localizedAudio;
}
}
void OnDestroy()
{
if (_localization != null)
{
_localization.OnLanguageChanged -= OnLanguageChanged;
}
}
}
Number, Date, and Currency Formatting¶
using System;
public class FormattingExamples : MonoBehaviour
{
private ILocalizationService _localization;
void Start()
{
_localization = ServiceLocator.Instance.Get<ILocalizationService>();
ShowFormattingExamples();
}
private void ShowFormattingExamples()
{
// Number formatting (culture-specific)
double number = 1234567.89;
string formattedNumber = _localization.FormatNumber(number);
// English: "1,234,567.89"
// German: "1.234.567,89"
Debug.Log($"Number: {formattedNumber}");
// Number with custom format
string percentage = _localization.FormatNumber(0.85, "P"); // 85%
string currency = _localization.FormatNumber(99.99, "C"); // $99.99
Debug.Log($"Percentage: {percentage}, Currency: {currency}");
// Date formatting
DateTime now = DateTime.Now;
string formattedDate = _localization.FormatDateTime(now);
// English: "12/25/2024"
// German: "25.12.2024"
Debug.Log($"Date: {formattedDate}");
// Date with custom format
string longDate = _localization.FormatDateTime(now, "D");
// English: "Wednesday, December 25, 2024"
Debug.Log($"Long Date: {longDate}");
// Currency formatting
decimal amount = 49.99m;
string formattedCurrency = _localization.FormatCurrency(amount);
// English (USD): "$49.99"
// Euro: "49,99 €"
Debug.Log($"Currency: {formattedCurrency}");
// Currency with specific code
string euros = _localization.FormatCurrency(amount, "EUR");
Debug.Log($"Euros: {euros}");
}
}
Complex Localization Example¶
public class ItemDescriptionDisplay : MonoBehaviour
{
private ILocalizationService _localization;
[SerializeField] private TextMeshProUGUI itemNameText;
[SerializeField] private TextMeshProUGUI itemDescText;
[SerializeField] private TextMeshProUGUI itemStatsText;
[SerializeField] private Image itemIcon;
public void DisplayItem(string itemId, int damage, float price)
{
_localization = ServiceLocator.Instance.Get<ILocalizationService>();
// Get localized item name
string itemName = _localization.GetText($"ITEM_{itemId}_NAME");
itemNameText.text = itemName;
// Get localized description with parameters
var descParams = new Dictionary<string, string>
{
{ "damage", damage.ToString() }
};
string itemDesc = _localization.GetText($"ITEM_{itemId}_DESC", descParams);
itemDescText.text = itemDesc;
// Format stats with localized numbers and currency
string damageFormatted = _localization.FormatNumber(damage);
string priceFormatted = _localization.FormatCurrency((decimal)price);
itemStatsText.text = _localization.GetTextFormatted(
"ITEM_STATS_FORMAT",
damageFormatted,
priceFormatted
);
// Load localized icon
Sprite icon = _localization.GetLocalizedAsset<Sprite>($"ITEM_{itemId}_ICON");
if (icon != null)
{
itemIcon.sprite = icon;
}
}
}
Language Fallback System¶
The localization system automatically handles missing translations:
User selects: Japanese
└─> Key not found in Japanese column of StringTable.csv
└─> Check fallback language: English column
└─> Key found! Return English translation
└─> Key not found! Return "#KEY_NAME#"
How it works:
1. System looks up the key in the selected language's column
2. If not found, checks the configured fallback language column
3. If still not found, uses default language (English)
4. If nowhere, returns #KEY_NAME# to indicate missing translation
Example Configuration:
// In LocalizationConfig
languageFallbacks:
- Japanese → English
- Korean → English
- Spanish (Mexico) → Spanish → English
- French (Canada) → French → English
Event System Integration¶
React to language changes across your game:
using LoLEngine.Core.Events.Interfaces;
using LoLEngine.Core.Events.Providers;
using LoLEngine.Core.Localization.Events;
using UnityEngine;
public class GlobalLanguageListener : MonoBehaviour,
IEventListener<LocalizationEvents.LanguageChangedEvent>
{
void OnEnable() => this.EventStartListening<LocalizationEvents.LanguageChangedEvent>();
void OnDisable() => this.EventStopListening<LocalizationEvents.LanguageChangedEvent>();
public void OnGameEvent(LocalizationEvents.LanguageChangedEvent evt)
{
Debug.Log($"Language changed via event system: {evt.NewLanguage}");
UpdateAudioLanguage(evt.NewLanguage);
UpdateUILanguage(evt.NewLanguage);
ReloadGameContent(evt.NewLanguage);
}
private void UpdateAudioLanguage(SystemLanguage language) { /* ... */ }
private void UpdateUILanguage(SystemLanguage language) { /* ... */ }
private void ReloadGameContent(SystemLanguage language) { /* ... */ }
}
Best Practices¶
1. Organize Your Keys Hierarchically¶
# UI Keys
UI_MAIN_PLAY,Play
UI_MAIN_QUIT,Quit
UI_SETTINGS_AUDIO,Audio Settings
UI_SETTINGS_VIDEO,Video Settings
# Game Keys
GAME_PAUSED,Game Paused
GAME_OVER,Game Over
GAME_VICTORY,Victory!
# Item Keys
ITEM_WEAPON_SWORD,Sword
ITEM_WEAPON_AXE,Axe
ITEM_ARMOR_HELMET,Helmet
# Dialog Keys
DIALOG_NPC_GREETING,Hello, traveler!
DIALOG_NPC_QUEST,I have a quest for you
2. Use ServiceAwaiter¶
// GOOD
void Start()
{
ServiceAwaiter.WaitForServices(this, OnServicesReady, OnTimeout);
}
// BAD
void Start()
{
_localization = ServiceLocator.Instance.Get<ILocalizationService>(); // May be null
}
3. Always Unsubscribe from Events¶
void OnDestroy()
{
if (_localization != null)
{
_localization.OnLanguageChanged -= OnLanguageChanged;
}
}
4. Handle Missing Translations Gracefully¶
string text = _localization.GetText("SOME_KEY");
if (text.StartsWith("#") && text.EndsWith("#"))
{
// Translation missing, use fallback
text = "Default Text";
Debug.LogWarning($"Translation missing for key: {text}");
}
5. Test With Pseudo-Localization¶
Create a test language with extra characters to catch UI overflow:
# test.csv - Pseudo-localization for UI testing
UI_PLAY,[!!! Plâÿ !!!]
UI_QUIT,[!!! Qüît !!!]
UI_SETTINGS,[!!! Sëttîñgs !!!]
Performance Tips¶
- LRU Caching: The service caches 1000 recently used strings automatically
- Language Preloading: Languages are loaded on-demand and cached
- Avoid Frequent GetText Calls: Cache frequently used strings
- Use Events: Subscribe to language changes instead of polling
// BAD - Calls GetText every frame
void Update()
{
scoreText.text = _localization.GetText("UI_SCORE");
}
// GOOD - Updates only when language changes
void Start()
{
_localization.OnLanguageChanged += UpdateScoreText;
UpdateScoreText(_localization.CurrentLanguage);
}
void UpdateScoreText(SystemLanguage lang)
{
scoreText.text = _localization.GetText("UI_SCORE");
}
Troubleshooting¶
Translations Not Loading¶
Problem: GetText returns #KEY_NAME#
Solutions:
1. Verify StringTable.csv exists at Resources/Localization/Tables/StringTable.csv
2. Check CSV header uses exact SystemLanguage enum names (e.g., "English", "Spanish", not "en", "es")
3. Ensure first column is named "Key"
4. Verify LocalizationConfig exists at Resources/Configs/DefaultLocalizationConfig.asset
5. Check LocalizationConfig's "Localization Tables Path" is set to "Localization/Tables"
6. Check console for CSV parsing errors
7. Ensure the language you're using is listed in Supported Languages in LocalizationConfig
Language Not Changing¶
Problem: SetLanguage() doesn't update UI
Solutions:
1. Subscribe to OnLanguageChanged event
2. Ensure UI components update in the event handler
3. Check if language is in Supported Languages list
4. Verify ServiceLocator has LocalizationService registered
System Language Not Detected¶
Problem: Game always uses default language
Solutions: 1. Enable "Use System Language" in LocalizationConfig 2. Add system language to Supported Languages list 3. Verify Application.systemLanguage is supported
Asset Localization Not Working¶
Problem: GetLocalizedAsset returns null
Solutions: 1. Place assets under the configured localization tables path, using SystemLanguage folder names:
Resources/Localization/Tables/Assets/
├── English/
│ └── character_portrait.png
├── Spanish/
│ └── character_portrait.png
└── French/
└── character_portrait.png
Summary¶
The Localization System provides comprehensive multi-language support:
- CSV-based workflow with auto-split per-language files for builds
- Runtime language switching with automatic memory management
- Per-language font switching (TMP + legacy) for CJK, Arabic, Cyrillic
- CLDR plural rules for 30+ languages
- Case transforms (upper, lower, title, upper-first)
- RTL support with automatic alignment flipping
- Localized assets: sprites, audio clips, and other Unity objects
- Culture-specific number, date, and currency formatting
- Serializable fallback chains
- LRU caching with cache-clear on language switch
- Component-based:
LocalizedText,LocalizedImage,LocalizedAudio,LocalizedFontApplier,LocalizedLayoutGroup - Event-driven updates via
OnLanguageChangedand EventManager integration
Component Reference¶
| Component | Purpose |
|---|---|
LocalizedText |
Text + font + RTL on TMP/Text |
LocalizedImage |
Sprite on Image/SpriteRenderer |
LocalizedAudio |
AudioClip on AudioSource |
LocalizedFontApplier |
Font-only switching (no text lookup) |
LocalizedLayoutGroup |
Layout direction for RTL |
For implementation examples, see:
- Samples~/Localization/LocalizationExample.cs
- Sample config in Samples~/Localization/Resources/Configs/LocalizationSampleConfig.asset