Localization
Ship your game in more than one language. Text, plural forms, and even artwork that needs to differ per language.
How it works
You keep one file per language in Assets/Localization/, named after the language code:
Assets/Localization/en.json
Assets/Localization/es.json
Assets/Localization/ja.jsonThe file name is the language code — es.json is Spanish. Nothing inside the file says which language it is, so renaming the file is how you change that.
Each file is a flat list of keys and the text they stand for. en.json is your source of truth; the others are translations of it.
{
"hud.coins": "Coins: {0}",
"language.name": "English",
"menu.play": "Play",
"menu.quit": "Quit"
}Three rules make the tooling work:
- Keys are dotted names, like
menu.play— they describe where the text is used, not what it says.menu.playsurvives someone deciding the button should read "Start". - Keys are sorted alphabetically. The validator checks this, and it's what keeps a diff between two translations readable.
language.nameis reserved — put the language's own name for itself there ("Español", "日本語"). That's what a language picker shows, because a Spanish speaker looking for their language is not looking for the word "Spanish".
Text that translates itself
The easiest path needs no code at all. A Text Component has a Localization Key field: set it to menu.play and the label shows whatever the current language's catalog says. Change the language and every one of those labels follows, with nothing to subscribe to and nothing to refresh.
Clear the field to go back to plain, untranslated text.
Text a script builds
For item names, dialogue, or anything assembled at runtime:
label.Text = Loc.Get("menu.play");Loc.Get always returns something: the current language, falling back to English, falling back to the key itself. A missing translation shows up as menu.play on screen — visible and obviously wrong, rather than a blank space you don't notice until someone reports it.
Values inside text
Placeholders are numbered, and Loc.Format fills them in:
{ "hud.coins": "Coins: {0}" }label.Text = Loc.Format("hud.coins", coinCount);Numbered placeholders rather than inline substitution because word order changes between languages — a translator has to be able to move {0} to the front of the sentence.
Plurals
"1 enemy" and "2 enemies" is the easy case. Polish has four forms; Japanese has one. Loc.Plural picks the right one for the current language:
{
"enemy.defeated.one": "Defeated {0} enemy",
"enemy.defeated.other": "Defeated {0} enemies"
}label.Text = Loc.Plural("enemy.defeated", killCount);You write sibling keys sharing a prefix, one per plural category — zero, one, two, few, many, other. The count both picks the category and fills {0}.
A single .one key is not a plural
A plural is detected by finding two or more siblings with different category suffixes. So chapter.one on its own stays an ordinary string, which is what you want — but it also means a plural with only .one written and no .other yet won't behave as a plural. The validator catches this.
Switching languages
Loc.Language = "es";Everything using a Text Component's Localization Key updates on its own. If your script built some text by hand, subscribe so you can rebuild it:
protected override void OnCreate()
{
Loc.OnLanguageChanged += RefreshLabels;
}The choice persists, so the game comes back in the same language next time.
Building a language picker
Loc.Available gives you every language it found, each with its code and its own name for itself — exactly what a dropdown needs:
foreach ((string code, string nativeName) in Loc.Available)
AddLanguageOption(code, nativeName);Don't call it every frame; it builds a small array each time. Populate the menu once when it opens.
On first run the game starts in the operating system's language if you have a catalog for it, and in English if you don't.
Artwork that differs per language
Sometimes it isn't text — a sign in the level, a logo with words baked into it, a voice line. Put a variant next to the original with the language code before the extension:
Assets/Textures/Sign.png
Assets/Textures/Sign.es.png
Assets/Textures/Sign.ja.pngReference Sign.png as normal. In Spanish the engine finds Sign.es.png and uses it; where no variant exists it falls back to the base file. You only make variants for the handful of assets that actually need one.
The Localization panel
Settings → Localization → Open Panel… opens the editor's own view of your catalogs — which keys exist, which languages are missing them, and the CSV/XLIFF import and export buttons described below.
Checking your translations
Run the validator over your localization folder:
python3 scripts/validate_loc.py Assets/LocalizationIt checks the things that are tedious to check by hand and embarrassing to ship:
- The file is valid JSON, UTF-8, and every value is a string
- Keys are in alphabetical order (
--fixsorts them for you) - Every language has exactly the keys
en.jsonhas — nothing missing, nothing left over from a key you renamed - Placeholders match — if English says
{0}and the translation drops it, that's a crash or a blank number, and it's caught here - Every plural covers the categories its language actually requires
Run it before you ship, and ideally whenever translations come back.
Fonts have to have the glyphs
A font with no Japanese glyphs renders Japanese as boxes. The engine checks font coverage when it builds your game and fails the build rather than shipping tofu — so if you add a language, check the font that will display it.
Working with translators
Translators don't want JSON. Catalogs round-trip through CSV and XLIFF, which is what translation tools actually take, and the export records a fingerprint of the English text for each key — so when you reword an English string, the next export can tell you which translations are now out of date instead of leaving them quietly wrong.
Habits worth having
- Key by location, not by content.
menu.play, notplay_button_says_play. - Never concatenate translated fragments.
Loc.Get("you_have") + count + Loc.Get("apples")is unbuildable in most languages. One key, one whole sentence, with placeholders. - Leave room in your UI. German is routinely 30% longer than English. A button sized exactly to "Play" will not fit "Wiedergabe".
- Start early. Adding keys as you build is easy; extracting hard-coded strings from a finished game is not.
See also
- Game UI — Text Components and the Localization Key field
- Saving and settings — the
languagesetting