Quick answer: Store a hash of the source string with every translation. When the source changes, the hash mismatches and the translation is automatically flagged as stale. Notify the language’s translator with a diff, fall back to English in production until the new translation arrives, and add a CI check that fails the build if any release-blocker string is stale.

A patch ships with reworded tooltips, a new tutorial, and a tweaked error message. The English looks great. Two weeks later a Japanese player reports that the tooltip on a fire spell still says it deals lightning damage. The English string was rewritten but the Japanese translation still references the old text. This is localization string drift, and it is the most common reason multilingual games end up with confusing or contradictory UI. The fix is automatic detection — you cannot rely on developers remembering to flag every changed string for translation.

The Source of Truth Is the Source Language

Pick one language as the canonical source. For most studios this is English; the other translations all derive from it. The source language file lives in your repo alongside the code and is updated by developers as a normal part of the patch. Translation files are owned by translators and updated through your localization pipeline.

Every translation entry references a source key and contains both the translated text and a hash of the source text at the time the translation was made. The hash is what makes drift detection automatic.

// strings.en.json (source)
{
  "spell.fireball.tooltip": "Hurls a ball of fire that explodes on impact, dealing 40 fire damage."
}

// strings.ja.json (translation)
{
  "spell.fireball.tooltip": {
    "text": "\u706b\u306e\u7403\u3092\u6295\u3052\u3066...",
    "source_hash": "a3f9c2e1",
    "updated_at": "2026-03-12T10:00:00Z"
  }
}

The source_hash is a stable hash (FNV-1a, xxHash, or even truncated SHA-256) of the source string at the time the translation was written. When you load the translation, you recompute the hash of the current source string and compare. If they match, the translation is current. If they do not, it is stale.

Detect Drift on Load and at Build Time

Drift detection runs in two places: at runtime when loading translations, and at build time as a CI check.

The runtime check produces a metric: percentage of strings stale per language. Send it to your telemetry. A sudden spike means a developer landed a string change without a translation update. The metric should be near zero in stable releases and acceptable to be higher on internal builds where translators have not yet caught up.

function isStale(translation, sourceText) {
    return hash(sourceText) !== translation.source_hash;
}

function getString(key, lang) {
    const source = sourceStrings[key];
    const trans = translations[lang][key];

    if (!trans || isStale(trans, source)) {
        staleCounter.inc(lang);
        return getFallback(key, source, trans);
    }
    return trans.text;
}

The CI check is stricter. Run a script as part of your release build that scans every language for stale strings and fails the build if any release-blocker key is stale. Mark keys as release-blockers when they touch onboarding, monetization, or legal text — places where falling back to English is unacceptable.

Notify Translators With Diffs, Not Just Lists

When the CI job detects new stale strings, it should generate a per-language report and send it to the translator. The report must include both the old and new source text, not just the key. A translator who sees only “the string at spell.fireball.tooltip is stale” has to dig through git history to figure out what changed.

Generate a side-by-side diff of the old source (from the source_hash version) and the new source. If you keep a history table of source strings indexed by hash, you can pull the exact old text. If not, you can fall back to a sentinel showing “source has changed” and let the translator compare the current English to their translation.

Send the report through whatever channel the translator uses — email, a translation management system like Crowdin or Lokalise, or a shared Slack channel. The signal you want is a regular, predictable cadence: “here are the strings that need attention this week.”

Fall Back to English Gracefully

When a translation is stale at runtime, you have a choice: show the outdated translation or fall back to English. Both are wrong; pick the less-wrong one based on context.

For purely cosmetic strings (flavor text, achievement descriptions), the outdated translation is usually fine and falling back to English would be jarring. For functional strings (tooltips that describe gameplay mechanics, tutorial instructions), falling back to English is safer because an outdated tooltip can mislead the player into a wrong action.

Annotate each string in your source files with a drift_policy: "keep_stale" or "fallback_to_source". The runtime resolver uses the policy when the hash mismatches. Default to "keep_stale" for safety — sudden English text in a Japanese UI is a worse player experience than a slightly outdated translation in most cases.

In development builds, regardless of policy, render stale translations with a small visual indicator (a colored border around the text, or a prefix like [STALE]). This makes drift visible during playtesting and gets caught before it ships.

“We added hash-based drift detection in a patch and immediately discovered that 11% of our German strings were out of date. Half were tooltips that had been rewritten three patches ago. Once translators got a weekly digest with diffs, our stale rate dropped to under 1% and stayed there.”

Related Issues

For broader QA process improvements, see automated QA testing for indie game studios. For text-heavy game testing tips, read bug reporting best practices for game teams.

Run a one-off audit this week: hash every source string, compare to translations, and count the stale rate per language. The result will probably surprise you.