Localization is one of those things that every developer knows they should do from the start — usually after making the mistake of leaving it to the end on their first game. By the time we see it on a porting project, the codebase is full of hardcoded strings scattered across hundreds of MonoBehaviours, UI prefabs, and designer-facing ScriptableObjects. Tracking them all down without breaking anything is exactly as painful as it sounds.

This post covers what we’ve learned from porting Unity games to console and PC storefronts across multiple regions: which tools to use, what the platforms actually require, and the specific traps that will cost you time if you hit them mid-port.


What tools to use for Unity Localization

There are two localization systems we would recommend considering for Unity.

Unity Localization Package

Unity’s own Localization package is the default choice for new projects. It ships with Unity, integrates cleanly with the Editor, and has first-party support for:

  • String tables with key/value pairs per locale
  • Asset tables (textures, audio clips, fonts — all swappable per locale)
  • Smart strings with pluralization rules
  • Google Sheets sync for working with external translators
  • Runtime locale switching

It does what it says on the tin and is the Unity default option. You get a Localization Tables window, a project-wide locale switcher, and LocalizeStringEvent components you can drop directly onto TextMeshProUGUI without writing any code.

using UnityEngine;
using UnityEngine.Localization;
using UnityEngine.Localization.Settings;

public class LocalizedGreeting : MonoBehaviour
{
    [SerializeField] private LocalizedString _greetingKey;

    private void Start()
    {
        var op = _greetingKey.GetLocalizedStringAsync();
        op.Completed += handle => Debug.Log(handle.Result);
    }
}

By default the package initialises asynchronously, loading locale data in the background rather than blocking the main thread. You don’t want a string lookup stalling your frame. The pattern above reflects that: subscribe to Completed and let the operation finish whenever the data is ready.

That said, you can configure the package to preload all locale data up front and then access strings synchronously via GetLocalizedString. Whether that makes sense depends on your project:

using UnityEngine.Localization.Settings;

public class LocalizedGreeting : MonoBehaviour
{
    [SerializeField] private LocalizedString _greetingKey;

    private void Start()
    {
        string greeting = _greetingKey.GetLocalizedString();
        Debug.Log(greeting);
    }
}
  • Async (default) — lower memory footprint at startup, strings load on demand. Works well for large games with many locales where you don’t want to preload everything.
  • Sync after preload — simpler call sites, but you’re paying the preload cost at startup and holding all table data in memory. Fine for smaller games or situations where string lookup code is deeply embedded and async callbacks are impractical to retrofit.

Neither is universally better. Pick based on where your loading budget sits and how much control you have over the call sites.

I2 Localization

I2 Localization is the established third-party alternative, and it has a longer track record than Unity’s own package. Many Unity projects that started pre Unity 2022 are built on I2 because it existed before Unity’s package was stable.

Its main advantages:

  • Synchronous string access via LocalizationManager.GetTranslation("key") — easier to retrofit into existing code
  • Direct Google Sheets integration with two-way sync
  • More control over language fallback chains
  • Works in older Unity versions

If you’re porting an existing project that already uses I2, there’s usually no reason to migrate. If you’re starting fresh, Unity’s own package is the better long-term bet.


Localise From Day One — Even if You’re Not Localising Yet

The argument against is always “we’re only launching in English, we don’t need it yet.” The argument for is: retrofitting localization into an existing codebase always costs more than just using localization from day one.

When you ship in English only but architect for localization from day one, it means adding new localization post launch is just the cost of localization not retrofitting.

The pattern to avoid:

public class QuestUI : MonoBehaviour
{
    [SerializeField] private TextMeshProUGUI _questTitle;

    private void ShowQuest(Quest quest)
    {
        _questTitle.text = "Quest Complete: " + quest.name;
    }
}

What it would look like with localization

public class QuestUI : MonoBehaviour
{
    [SerializeField] private TextMeshProUGUI _questTitle;
    [SerializeField] private LocalizedString _questCompleteKey;

    private void ShowQuest(Quest quest)
    {
        _questTitle.text =  _questCompleteKey.GetLocalizedString(quest.name);
    }
}

The string table entry looks like: Quest Complete: {0} in English, Mission accomplie : {0} in French. Same key, same code, different output per locale.


Pluralization: It’s More Complex Than You Think

When it comes to localization, pluralization is the biggest brain twister, as it changes how you may even want to set up the text in the first place.

You have a UI element that reads: “Player has 1 gold coin” / “Player has 5 gold coins”. Makes total sense in English — if the count is greater than 1, just add an “s” to the end.


 string message = $"Player has {count} gold coin";
 if (count > 1){
    message += "s";
 }

That works for English and is an example we have seen in codebases we have reviewed.

The default fix for this may be something along these lines


    [SerializeField] private LocalizedString _oneGoldCoin; //  "Player has 1 gold coin";

    [SerializeField] private LocalizedString _multipleGoldCoins;//  "Player has {count} gold coins";

    string message = count > 1 ?_multipleGoldCoins.GetLocalizedString(count) :  _oneGoldCoin.GetLocalizedString();

    

You can’t do that. The Unicode CLDR defines plural category rules for every language. The categories are: zero, one, two, few, many, other. Not every language uses all of them, but many languages use categories that English ignores entirely.

Here’s a subset of how dramatically this varies:

LanguagePlural categories used
Englishone, other
Frenchone, many, other
Russianone, few, many, other
Arabiczero, one, two, few, many, other — all six
Polishone, few, many, other (with complex rules)
Japaneseother (no plural distinction at all)
Welshzero, one, two, few, many, other

Arabic is the canonical example of why your count == 1 branch is wrong. Arabic has separate grammatically required forms for: exactly zero, exactly one, exactly two, small numbers (3–10), large numbers (11–99), and everything else. If you only provide one and other forms, your Arabic text is grammatically incorrect for most values.

The ISO 24617 standard and Unicode CLDR are the authoritative sources here.

Unity Localization handles this via Smart Strings with plural selectors:

// String table entry for "gold_count" key:
{count:plural:zero=Player has no gold|one=Player has one gold|other=Player has {count} gold}

I2 Localization has its own pluralization system with a similar per-locale form definition. Both tools let translators define the correct form for each plural category in their language — which is the only correct approach. Your code doesn’t need to know any of this:

    [SerializeField] private LocalizedString _goldCoinText; 

    string message = _goldCoinText.GetLocalizedString(count);

The localization system picks the right plural form at runtime based on the current locale and the count value.


Don’t Forget Your Plugins

Every third-party plugin in your project that surfaces text to players needs to go through your localization system. This is easy to miss because plugins often handle their own UI.

Rewired is a common example on console ports. It has built-in UI for controller assignment and input configuration screens. If you’re shipping in Japanese, that UI needs to be in Japanese. Rewired supports custom string overrides — but you need to wire those overrides through your localization system, not hardcode Japanese strings into the Rewired configuration.

The same applies to:

  • Achievement / trophy notification overlays
  • Any in-engine UI that surfaces error messages (network errors, save failures)
  • Dialogue and subtitle systems (especially third-party ones like Ink or Yarn Spinner)
  • Tutorial or hint systems

The audit question for every plugin: “does this surface any string to the player?” If yes, find out how to override those strings and route them through your localization keys. Most larger plugins include built-in support for Unity Localization or I2. If they don’t, you will need to retrofit the plugin.


Platform-Specific Terminology

Some platforms require you to use their exact approved terminology and localization. So you can’t always just send everything to a localization house without reviewing it first.

A concrete example: the Xbox GDK includes an approved terminology list covering UI strings like “Achievements”, “Game Pass”, “Xbox button”, and save-related language. Microsoft publishes these in the GDK documentation and provides approved translations for every supported locale. If your game refers to something Microsoft calls “Achievements” using a different term, that’s a compliance fail. These approved strings often may not be what your localization house provides. For more complex games, ensure you are pulling these terminology items into their own key. For example, the localization value may be “You have unlocked a new {Achievement}” with a separate entry for “Achievement” which you can fill out yourself based on the GDK requirements.

This matters for localization because you cannot translate these terms yourself. You take the platform-approved translation from their terminology list and use it verbatim. Sending these strings to your localization house without flagging them will get you incorrect translations, even from experienced translators who don’t have access to the platform docs.


Pre-Port Localization Audit Checklist

Before we begin any port that involves localization, we run through this:

  • No hardcoded strings in C# — grep for = " on any MonoBehaviour that has a UI reference
  • No hardcoded strings in prefabs — TextMeshProUGUI / Text components with static text not driven by a localized string
  • All pluralized strings use proper plural categories — no count == 1 ? singular : plural checks
  • Plugins audited — Review every plugin that surfaces text and confirm override hooks are wired to your localization system
  • Platform terminology reviewed — Where are we using terminology
  • Font coverage checked — does the font assets cover every glyph needed for all target languages (CJK, Arabic, Cyrillic)
  • Text expansion budget — German and French typically run 30–40% longer than English; confirm UI layouts handle it
  • Right-to-left support tested — if targeting Arabic or Hebrew, Unity’s RTL text support is enabled and tested

If you’re mid-production and haven’t started localization yet, now is still better than during cert. If you’re planning your next Unity project, build the localization system before you build the first UI prefab. The cost is an afternoon of setup. The cost of not doing it is weeks of archaeology.

If you’re heading into a console port and need a localization audit as part of your HealthCheck, get in touch.