Hardcoded strings quietly block localization — here's how to get them out
A string that lives inside your source code — literally typed between quotation marks in a function somewhere — feels harmless while you're writing it. It compiles, it runs, it shows the text you wanted. The problem shows up later, when someone tries to localize the game and discovers that text buried in code cannot be exported, cannot be counted, and cannot be handed to a translator without also handing them your codebase.
Why hardcoded text blocks the whole pipeline
Localization tooling — whether a spreadsheet, a translation management file, or an automated export step — works by pulling text out of a defined location: a resource file, a database table, a structured export. It cannot see into arbitrary source code to find a string, because there is no reliable, safe way to distinguish user-facing text from a log message, a debug label, or an internal constant just by reading code.
This means every hardcoded string is, in practice, invisible to your localization process until a human manually finds it, extracts it, and moves it somewhere the pipeline can see. Doing that after the fact, across a whole codebase, is slower and more error-prone than doing it as you write each string in the first place.
Moving text to keyed resource files
The standard fix is to externalize strings: instead of writing text directly where it's used, you store it in a resource file (JSON, CSV, a strings table — the format matters less than the discipline) under a stable key, and reference that key from code. The code asks for the text by key; the resource file is the single place translators and your export pipeline actually touch.
// Before: string is invisible to any localization tool
showMessage("Not enough gold to buy this item.");
// After: code references a key, text lives in a resource file
showMessage(t("shop.error.insufficient_gold"));
// strings/en.json
{
"shop.error.insufficient_gold": "Not enough gold to buy this item."
}String concatenation is a localization bug, not a shortcut
A subtler version of the same problem is building a sentence out of separately translated fragments and joining them at runtime. It looks convenient — reuse the word 'gold', reuse the word 'item', assemble a sentence — but word order, and even whether a language needs a word at all in that position, differs across languages. A sentence assembled from independently translated fragments cannot be reordered by the translator, because the translator never sees the whole sentence — only the pieces.
// Fragile: word order is baked into the concatenation "You received " + itemName + " x" + count // A German or Japanese translator cannot fix word order here — // they only ever see the fragments, never the assembled sentence.
Placeholder-based composition instead
The fix is to translate the whole sentence as one unit, with placeholders marking where dynamic values are inserted. The translator sees the full sentence in context and can move the placeholders to fit their language's grammar, because the sentence structure — not just the individual words — is theirs to translate.
// strings/en.json
{ "inventory.received": "You received {itemName} x{count}" }
// strings/ja.json — word order differs, and that's fine:
// the translator controls the whole sentence, not fragments
{ "inventory.received": "{itemName} を {count} 個手に入れた" }Where to draw the line
Not every string in a codebase needs to go through this pipeline — internal log messages, developer-only debug output, and configuration keys are not user-facing and don't belong in a translation file; putting them there just adds noise a translator has to skip past. The dividing line is simple: if a player can see it, it's externalized with a key; if only a developer ever sees it, it stays in code.
The earlier this convention is established in a project, the cheaper it stays. Retrofitting externalization onto a codebase with years of accumulated hardcoded strings is a real, multi-week engineering task; enforcing it from the first UI string a project writes costs almost nothing.