Implementing language switching without breaking save data
Adding a language selector to the options menu looks like an afternoon of work: a dropdown, a call into the localization system, done. What actually consumes the time is everything around it — deciding which language to show before the player has ever opened that menu, redrawing a UI that is already on screen, and making sure a save file created in one language still makes sense when it is loaded in another.
First-launch detection and save data are usually built by different people at different times, which is exactly why they collide. The save bug in particular tends to surface months after release, when a player switches language and discovers that half their quest log is permanently frozen in the old one.
This article covers both, in the order you will meet them: choosing a language before anyone has chosen one, switching it while the game is running, and keeping language out of the data you persist.
Deciding the language on the very first launch
On a brand-new install the player has never touched your options menu, so you have to guess. Two details decide whether that guess feels right to the player, and both matter more than the detection code itself.
The first is distinguishing has never chosen from chose the language that happens to be the default. If you store the auto-detected result the same way you store a deliberate choice, you cannot tell them apart later. Keep a flag saying the value was detected rather than chosen, or leave the setting absent until the player picks. Otherwise a player who deliberately selected English gets silently flipped back the next time their platform reports a different language, and from their point of view your game changed its mind.
The second is matching language tags properly rather than by string equality. A system reporting a regional variant should resolve to the closest thing you support, then to the base language, and only then to your default — so a Brazilian Portuguese system should reach Portuguese rather than falling all the way through to English. Chinese needs particular care, because the meaningful distinction is the script rather than the region, and a bare language code without a script is genuinely ambiguous. Write the resolution as a small function with tests; it is the kind of code that looks trivial and is wrong in three places.
One more thing that costs nothing and saves support mail: make the language selector reachable from the very first screen, and label each option in its own language and script rather than translating the names into the current UI language. A player who launched into a language they cannot read needs to recognise their own on sight.
With those settled, the order of preference runs from the strongest signal to the weakest:
- An explicit choice the player has already made, stored in your settings file — this always wins, including over any later change to the system language
- The language the store or platform launched your game with, which players often set per game and which is a stronger signal than the OS setting
- The operating system UI language
- Your source language, when nothing above matches anything you support
The setting belongs to the player, not to the playthrough
Language is a per-player, per-device preference in the same family as volume and key bindings. It belongs in your options or config file. It does not belong inside a save slot.
If language lives in the save, three things go wrong. Loading an old save silently changes the UI language, which reads as a bug. Starting a new game reverts to the default, discarding a choice the player made deliberately. And a player who cannot read the current language may not be able to reach a save at all in order to fix it — the setting has to be changeable from the title screen without loading anything.
Cloud-synced configuration deserves an explicit decision here rather than a default. A player with a system in one language on the desktop and another on a handheld will have one device overwrite the other's choice if the language field syncs. Either exclude language from the synced set or accept the behaviour knowingly, but do not discover it from a bug report.
Switching at runtime means re-resolving everything on screen
Plenty of games sidestep this by asking the player to restart. That is honest and it works, but it is an unpleasant moment, and on some platforms it is not an acceptable answer. If you do support switching in place, the rule is simple to state and easy to violate: nothing may hold a copy of an already-resolved string.
The practical implementation is a language-changed event that every text component subscribes to and responds to by re-reading its key. That has a design consequence worth stating plainly: every text component must retain its key, not just the string it displayed. A component that was handed a finished string at construction time has no way to refresh itself, and no amount of event plumbing will fix it.
Fonts and layout have to move at the same moment. A language change may change which font is used, because glyph coverage differs; it may change line height, because scripts have different vertical metrics; and it may change text direction entirely. Anything that cached a measured width, a wrap position, or a laid-out line is invalid the instant the language changes, so the re-measure has to be part of the same event rather than something that happens on the next screen.
Test the switch specifically from inside a screen, not from the title menu. Switching language while sitting in the main menu is the case that always works. Switching it while a shop, an inventory tooltip and a confirmation dialog are all open is the case that finds the bugs.
The stale copies hide in predictable places, and every one of them produces the same symptom — a screen that is half in the new language and half in the old:
- Labels populated once when a screen was created and never touched again
- Text produced by string formatting and then cached, such as a score or timer line
- Tooltips and description panels built on demand and memoised
- Lists sorted with locale-aware collation, which have to be re-sorted
- Numbers, dates and currency values formatted under the previous locale
- Text rasterised into a texture or atlas, which has to be regenerated
- Any menu that is already open — including the pause screen containing the language dropdown itself
The bug that poisons saves: storing rendered text
The most damaging language bug in a shipped game is not on screen at all. It is inside the save file, and it happens whenever the game persists text that was already rendered in the player's language instead of the identifiers needed to render it again.
It shows up in the same handful of places in almost every project: quest log entries stored as finished sentences, item names copied into an inventory record, a dialogue backlog or history in a story game, auto-generated save slot labels containing a chapter title, statistics and end-of-run summaries, and custom loadout names pre-filled from item names.
The consequences compound. A player who switches language finds part of the game permanently in the old one, with no way to repair it short of starting over. Text stored in a save is also a compatibility hazard: if the save was written under a legacy per-language encoding, or contains characters the current font cannot render, the player sees mojibake or missing glyphs in a place they cannot fix. And every improvement you later make to a translation — including simple typo fixes — will never reach existing saves.
// poisons the save
{ "quest": { "title": "Find the missing courier", "step": "Search the docks" } }
// survives any language change
{ "quest": { "id": "q_courier", "stepIndex": 2, "params": { "town": "riverport" } } }What legitimately belongs in the save
Not everything can be an identifier, and the exceptions are worth naming so nobody over-applies the rule. Text the player typed — a character name, a save note, a custom label — is genuinely free text. Store it verbatim, never translate it, and never normalise it. The same applies to any value that was free text at the time it was recorded, such as a procedurally generated name.
Because those fields exist, the save format must be UTF-8 regardless of which language the game is being played in. A per-language encoding chosen to match the current locale is how a save written on one machine becomes unreadable on another, and it is entirely avoidable.
It is also worth recording the language a save was created in as metadata. Not to force the interface back to it, but because it makes support conversations and bug reports far easier, and because it lets you detect that a save contains player text your current font cannot display.
A short test list that catches nearly all of it
None of this needs a formal test plan, but it does need someone to sit down and go through the cases deliberately, because every one of them is invisible during normal development in a single language:
- Launch with no config file present, once per supported platform language, and confirm what you get
- Launch with no config file and an unsupported system language, and confirm the fallback
- Change the system language after the player has made an explicit choice, and confirm the choice still wins
- Open every screen in the game, then switch language while that screen is open
- Switch language mid-conversation, and during a timed or scripted sequence
- Save in one language, switch, reload, then read the quest log, inventory and dialogue history looking for the old language
- Switch to a language that uses a different font and re-check anything that was measured or rasterised earlier