Localization

Key-to-text lookup, runtime language switching, and UI text that updates live when the player changes language.

Ship your game in multiple languages without hardcoding UI text.

InterfaceILocalizationService
Off switchNullLocalization
AssemblyCommonGameSystem.Localization (optional — requires Unity's built-in uGUI package)
StartupRegistered automatically at boot; you load the translation tables

What it does

Instead of writing "Start" directly into a menu, you write a key — a stable identifier such as "menu.start". The Localization service resolves that key to the text of the current language at runtime. When the player switches language in your settings menu, every on-screen LocalizedText component updates instantly — no scene reload, no restart.

Missing translations never crash the game. A lookup that fails in the current language falls back to the fallback language (English by default), and if that also fails, the key itself is returned. Your UI always shows something.

Translations live in LocalizationTableAsset ScriptableObjects — plain data assets you fill in the Inspector or generate from a spreadsheet. Languages are identified by standard BCP-47 language tags, the same short codes browsers and operating systems use: "en", "ko", "ja", "zh-CN", and so on.

Because the module lives in its own optional assembly, removing uGUI from your project simply excludes the assembly — the rest of the framework still compiles and boots.

Quick start

Resolve the service in a startup script and load your tables once:

using CommonGameSystem.Core;
using UnityEngine;

[DefaultExecutionOrder(100)] // Run after the framework has started.
public class GameStartup : MonoBehaviour
{
    [SerializeField] private LocalizationTableAsset[] tables; // assign in the Inspector

    private ILocalizationService _loc;

    private void Awake()
    {
        _loc = ServiceLocator.Resolve<ILocalizationService>();
        _loc.LoadTables(tables);

        // Optional: start in the player's OS language.
        _loc.SetLocale(LocaleId.FromSystemLanguage(Application.systemLanguage));
    }
}

After that, any script can fetch text with Get:

string title = _loc.Get("menu.title");

API reference

Lookup

  • string Get(string key) — Resolves a key: current language → fallback language → the key itself. Never throws.
  • string Get(string key, params object[] args) — Same lookup, then formats the result with string.Format using the invariant culture. A bad format string returns the unformatted text instead of throwing.
  • bool TryGet(string key, out string value) — Lookup with no side effects. Returns false if the key is missing (value is set to the key). Logs nothing — useful for probing keys without warning spam.

Language switching

  • void SetLocale(string localeId) — Switches the active language. Publishes a LocaleChanged event on the Event Bus and saves the choice through the Configuration service, so it persists across sessions.
  • string CurrentLocale { get; } — The active locale id.
  • IReadOnlyList<string> AvailableLocales { get; } — The locale ids found in the loaded tables. Drive your language-picker UI from this list.
  • string GetLocaleDisplayName(string localeId) — The language's name in its own language (for example, "한국어" for Korean), for a language-picker UI. Falls back to the id if not set.

Setup

  • void LoadTables(IReadOnlyList<LocalizationTableAsset> tables) — Loads the translation tables and activates the saved (or default) language. Call it once at startup.

Static helper

  • LocaleId.FromSystemLanguage(SystemLanguage) — Maps Application.systemLanguage to a BCP-47 tag (for example, SystemLanguage.Korean"ko"). Use it to auto-detect the player's OS language on first launch.

The LocalizedText component

For static UI text, skip the code entirely: attach a LocalizedText component to any uGUI Text or TextMeshPro component and set its key in the Inspector. The component fetches the text on enable and re-fetches it whenever the language changes.

For dynamic text with values in it, use SetArgs:

using CommonGameSystem.Core;
using UnityEngine;

public class ScoreDisplay : MonoBehaviour
{
    private LocalizedText _locText;

    private void Start()
    {
        _locText = GetComponent<LocalizedText>();
    }

    public void SetScore(int newScore)
    {
        // Table entry: "score.current" = "Score: {0}"  →  displays "Score: 42"
        _locText.SetArgs(newScore);
    }
}

Example: a language button

using CommonGameSystem.Core;
using UnityEngine;

public class LanguageMenu : MonoBehaviour
{
    private ILocalizationService _loc;

    private void Start()
    {
        _loc = ServiceLocator.Resolve<ILocalizationService>();
    }

    // Wire this to a UI button, passing "en", "ko", "ja", ...
    public void OnLanguageButtonClicked(string localeId)
    {
        _loc.SetLocale(localeId); // Every LocalizedText on screen updates live.
    }
}

The choice is saved automatically — the next session starts in the language the player picked.

Turning it off

ServiceLocator.Replace<ILocalizationService>(new NullLocalization());

Get(key) returns the key unchanged, SetLocale and LoadTables are silent no-ops, and nothing is logged. Use this to strip localization entirely in lightweight builds or tests.

Common pitfalls

  • Main thread only. Calling Get or SetLocale from a worker thread throws InvalidOperationException. All MonoBehaviour callbacks are safe.
  • Cache the service. Resolving in Update() repeats a dictionary lookup every frame. Cache the reference in Awake() or Start() instead.
  • Locale ids must match exactly. "EN" is not "en", and Application.systemLanguage.ToString() yields "Korean", not "ko". Use LocaleId.FromSystemLanguage() for auto-detection, and lowercase BCP-47 tags in your tables.
  • LocalizedText re-subscribes on every enable. On a pooled UI prefab (damage numbers, toasts), each pool cycle subscribes to and unsubscribes from LocaleChanged. That is correct behavior — reactivation picks up the current language — but watch high-frequency pools. Prefer updating dynamic text with SetArgs.
  • Shutdown order. Services shut down in the reverse of their start order, so the Localization service can be disposed before the Event Bus. If a LocaleChanged event arrives after that, LocalizedText swallows it silently — the text does not update, and nothing crashes.
  • Custom types need link.xml on IL2CPP. If you serialize your own settings types that carry localization keys, make sure your project's link.xml preserves them. The shipped framework assemblies already preserve their own types.