i18n Key Management: Keeping Translations in Sync Across Locales
Adding a second language to your app is straightforward. Keeping five languages in sync as features change over six months is where things break down. Missing keys, stale translations, inconsistent plural forms, and duplicated strings creep in silently. Here is how to manage translation keys at scale.
The Translation File Structure
Most i18n libraries (react-intl, i18next, vue-i18n) use JSON or YAML files keyed by locale. A typical structure:
locales/
en/
common.json
dashboard.json
errors.json
ar/
common.json
dashboard.json
errors.json
fr/
common.json
dashboard.json
errors.json
Splitting by feature module (dashboard, errors) keeps files manageable and lets teams work on different sections without merge conflicts.
// locales/en/dashboard.json
{
"dashboard.title": "Dashboard",
"dashboard.welcome": "Welcome back, {{name}}",
"dashboard.stats.orders": "Orders",
"dashboard.stats.revenue": "Revenue",
"dashboard.empty": "No data available for this period"
}
Key Naming Conventions
Consistent key names prevent duplication and make searching easy. Use a hierarchical dot notation:
[namespace].[section].[element].[modifier]
Examples:
auth.login.button.submit- the submit button on the login formorders.list.empty- empty state for the orders listerrors.network.timeout- network timeout error messagecommon.actions.save- reusable save action
Rules that help:
- Use lowercase with dots as separators.
- Start with the page or module name.
- End with the UI element or purpose.
- Prefix reusable strings with
common.. - Never use the English text as the key. Keys should be stable identifiers.
Detecting Missing and Unused Keys
As features evolve, translation files drift. A key gets added to English but not to Arabic. A component gets deleted but its keys remain. Automated checks catch this:
// Compare two locale files and find differences
function findMissingKeys(
reference: Record<string, string>,
target: Record<string, string>
): { missing: string[]; extra: string[] } {
const refKeys = new Set(Object.keys(reference));
const targetKeys = new Set(Object.keys(target));
return {
missing: [...refKeys].filter((k) => !targetKeys.has(k)),
extra: [...targetKeys].filter((k) => !refKeys.has(k)),
};
}
// Usage
const en = require("./locales/en/common.json");
const ar = require("./locales/ar/common.json");
const diff = findMissingKeys(en, ar);
if (diff.missing.length > 0) {
console.error("Missing in Arabic:", diff.missing);
process.exit(1);
}
Run this in CI to block merges that introduce translation gaps.
Handling Plurals
Pluralization rules vary dramatically across languages. English has two forms (singular and plural). Arabic has six. Russian has three. The ICU MessageFormat standard handles this:
{
"items.count": "{count, plural, =0 {No items} one {1 item} other {{count} items}}"
}
For Arabic, the plural categories are: zero, one, two, few, many, and other. Each requires a distinct translation:
{
"items.count": "{count, plural, =0 {لا عناصر} one {عنصر واحد} two {عنصران} few {{count} عناصر} many {{count} عنصرًا} other {{count} عنصر}}"
}
If your i18n library does not support ICU format, use the Intl.PluralRules API to determine the correct category:
const rules = new Intl.PluralRules("ar");
rules.select(0); // "zero"
rules.select(1); // "one"
rules.select(2); // "two"
rules.select(5); // "few"
rules.select(100); // "other"
Interpolation and Variables
Translation strings frequently include dynamic values. Use named placeholders instead of positional ones:
// Prefer named placeholders
"welcome": "Welcome back, {{userName}}"
// Avoid positional placeholders
"welcome": "Welcome back, %1"
Named placeholders are self-documenting. Translators understand what {{userName}} represents without consulting the code.
RTL Considerations
When supporting both LTR and RTL languages, translation files should not contain directional CSS or HTML. Instead, handle directionality at the component level:
function Price({ amount, currency }: { amount: number; currency: string }) {
return (
<span dir="ltr" className="tabular-nums">
{amount.toFixed(2)} {currency}
</span>
);
}
Numbers, URLs, and code snippets within translated text should be wrapped in dir="ltr" spans to prevent rendering issues.
Automation Strategies
Extract keys from code. Tools like i18next-parser scan your source for t("key") calls and generate a list of used keys. Diff this against your locale files to find unused translations.
Machine translation for first drafts. Use translation APIs to generate initial translations, then have native speakers review them. This dramatically speeds up the process for adding new locales.
Pseudolocalization for testing. Replace English text with accented characters and padding to test UI resilience:
function pseudoLocalize(text: string): string {
const map: Record<string, string> = {
a: "ä", e: "ë", i: "ï", o: "ö", u: "ü",
A: "Ä", E: "Ë", I: "Ï", O: "Ö", U: "Ü",
};
const accented = text.replace(/[aeiouAEIOU]/g, (c) => map[c] || c);
return `[${accented} ~~~]`; // Padding reveals truncation
}
This exposes layout issues where text is cropped or containers overflow before real translations arrive.
CI Checklist
- Verify all locales have identical key sets.
- Validate JSON syntax in all translation files.
- Check that all plural categories are provided for each locale.
- Warn on keys longer than 100 characters (often a sign of embedded HTML).
- Fail on duplicate keys within a single file.
Try our Localization Manager to compare, merge, and validate translation files instantly — right in your browser, no upload required.