Renaming and removing translation keys without losing work
Renaming a variable in code is a routine refactor a compiler will catch if you miss a spot. Renaming a translation key is a different kind of operation, because nothing downstream of your source file understands 'rename' — it only understands 'this key disappeared' and 'this new key appeared,' which is a much more destructive pair of events than it looks.
A rename is a delete plus an add
From the point of view of a translation file, or a translation management tool, or a translator's task queue, changing checkout_button to checkout_cta_button isn't a rename — it's the disappearance of checkout_button and the sudden appearance of a brand-new, untranslated checkout_cta_button. Every language file that had a translation for the old key loses it, and every language now shows an empty or fallback string for the new key until someone translates it again.
This is true even though the English or source-language meaning didn't change at all — you just changed the identifier. The cost of a rename is not paid by the language you're renaming in; it's paid by every other language, which suddenly needs a retranslation of a string whose meaning never actually changed.
// looks harmless in the source file - "checkout_button": "Buy now" + "checkout_cta_button": "Buy now" // but every other language's translation of checkout_button // is now orphaned, and checkout_cta_button starts empty
Retiring a key without losing its translations
If you do need to rename a key, the safer path is to add the new key first, copy the existing translations from the old key into it under the new name across every language file, and only then remove the old key. This preserves the translated text instead of throwing it away and asking every translator to redo work that was already done — the meaning of the string didn't change, only its identifier did, so there's no reason to lose the words along with the name.
Some teams keep a small mapping of old key to new key during the transition, so tooling can apply the same copy automatically instead of doing it by hand for each language file.
Deprecation over deletion
When a key is no longer referenced by any code, the instinct is to delete it immediately. A gentler approach is to mark it deprecated first — leave the translation in place, but flag in a comment or a separate list that it's no longer used — and delete it only after enough time has passed to be confident nothing still depends on it.
This matters most for keys shared across a codebase in ways that are hard to grep exhaustively for: a key referenced by name in a configuration file, built dynamically from a variable, or used by an external integration that reads the translation files directly. Immediate deletion assumes your search for references was complete; deprecation gives you a safety window if it wasn't.
Finding orphaned keys
An orphaned key is one that still exists in your translation files but that no code actually references anymore — usually left behind after a feature was removed or a key was renamed without cleanup. Orphaned keys aren't just clutter: every one of them still needs to be kept in sync and translated for every new language, which is wasted effort spent maintaining a string nobody will ever see.
Finding them means comparing the full set of keys in your translation files against every key actually referenced in code, and flagging any key present in the files but absent from the code. This is naturally the kind of check that's easy to automate and easy to forget to run manually, since the files themselves give no visual indication that a key is dead.
- Compare the file's key set against every key string used in source code
- Include dynamically built key references in the search, or flag them for manual review
- Treat a large number of orphaned keys as a signal to schedule cleanup, not just noise to ignore
Finding missing keys
The opposite problem is a key that code references but that no translation file defines — usually introduced when a developer adds a new string to the UI but forgets to add the corresponding entry to the source language file, or adds it to the source file but not to every other language. At runtime this typically surfaces as a fallback: the raw key shown to the user, an empty string, or a fallback to another language, depending on how the app is configured to handle it.
Detecting this ahead of time means the same comparison run in the opposite direction — every key used in code should exist in the source language file at minimum, and ideally in every language file the app ships. Running this check as part of a build or a routine review catches the gap before a user sees a raw key on screen instead of a translated sentence.