All articles
CI/CD

Detecting Stale Translations in Your CI Pipeline Before They Ship

Abstract illustration suggesting a detection or alert moment in a pipeline flow

The scenario is common enough that every localization engineer has a version of it. A product string changes in English -- maybe a button label, maybe an error message, maybe a tooltip. The change looks small. The developer updates the source file, the test suite passes, CI is green, the PR merges. Three days after the release, a support ticket arrives from a French user: the UI is showing an English string where French text is expected. The French translation was not updated because nothing in the pipeline caught it.

Stale translations are invisible to standard CI. They do not break compilation. They do not fail unit tests. They do not trigger lint warnings. By the time they surface, they are already in production, already degrading the experience for your non-English users.

Why Standard CI Does Not Catch This

CI pipelines validate correctness of code, not completeness of localization. Your test suite can assert that a button renders with the correct key (`checkout.confirm.cta`), but it typically cannot assert that every locale file has a current translation for that key. Doing that reliably requires comparing the source file (usually English) against every locale file and verifying that every key in the source has a corresponding translation that was updated after the source text last changed.

That comparison is not zero-effort. A naive key-presence check will pass even when a translation exists but is months out of date. A hash-based check will flag keys whose translation predates the last source-text change -- but only if you store source-text hashes alongside translations, which most locale file formats do not do by default.

The result is a class of regressions that CI consistently misses: source string changes that leave one or more locale translations pointing at the old text.

The Stale Detection Model

Effective stale detection at the CI level requires tracking not just key presence but translation freshness. There are a few approaches, with different tradeoffs:

Hash-Based Staleness

Augment each locale entry with a hash of the source text it was translated from. When the source text changes, the hash changes, and the translation is flagged as stale. Tools like XLIFF support a `state` attribute that can encode translation review status; JSON and YAML formats require metadata stored separately or inline via comment conventions.

The challenge is discipline: every translation commit needs to update the hash alongside the translated text. If translators work in a TMS that doesn't propagate hashes back to the locale file, the chain breaks.

TMS Sync State as a CI Gate

If your TMS tracks translation state per string (which most modern ones do), you can expose that state as a CI artifact. Before merging a PR that changes source strings, a CI step queries the TMS: are all affected strings in an "approved" or "translated" state for all target locales? If not, the PR does not merge until they are -- or until the team explicitly marks the translation debt as accepted and tracked.

This model works well but requires your TMS to have an API that reflects string-level state. It also requires a mapping between your locale file keys and the TMS's internal identifiers, which can be fragile if maintained manually.

Source-File Diff as Stale Signal

A simpler approach that works without TMS integration: in CI, parse the git diff for the PR and extract all changed keys in source locale files. For each changed key, verify that the corresponding key in every target locale file was also changed in the same diff -- or that the PR is annotated with a tracking reference to an open translation task. If neither condition is met, the CI step warns or fails.

This is approximate -- it does not distinguish between a translation that was correctly updated and one that was blindly force-updated with stale text -- but it catches the most common case: a developer changes a source string and no corresponding translation change is made.

Implementing a Basic Stale Check

Here is the pattern we recommend for teams that want stale detection without full TMS integration:

  1. In your CI pipeline, after the PR diff is available, run a script that parses your source locale file (e.g., `en.json`) and builds a map of key to source-text hash.
  2. For each target locale file, compare the stored hashes against the source. Any key where the source hash has changed since the translation was last written is stale.
  3. Report stale keys as a CI artifact. Treat them as warnings initially; promote to blocking failures once the team has established a translation update workflow that can respond before merge.

The transition from warning to blocking is important. A stale check that can always be bypassed provides information but no gate. Once translators are integrated into the PR workflow and can respond to stale signals within the PR cycle, blocking is appropriate and prevents the class of issues it was designed to catch.

Integrating With Your Translation Workflow

Stale detection at the CI stage is most useful when it is connected to an action. A warning that says "these 12 strings are stale" is actionable if the translator can see the warning and queue those strings immediately. It is useless if the warning lives in a CI log that only the developer sees.

The most effective patterns we have seen route the stale signal directly to the translation queue. When CI detects a stale key, it opens a translation task in the TMS for that specific string, tagged to the branch and commit that introduced the staleness. Translators see the task immediately. They do not need to wait for the localization PM to notice the CI log and manually create the task.

This closes the loop: the developer changes a string, CI detects the staleness, the translation task is created automatically, the translator updates the locale, and the next run of CI either passes or confirms the remaining debt. The French user does not get an English string. The support ticket does not arrive.

The Branch Timing Problem

One complication worth addressing: stale detection is most useful when it fires on the branch where the source change happened, before that branch merges. If you only run stale detection on the main branch after merge, the window to fix it before production shrinks significantly -- and if your release cadence is fast, it may be zero.

Run stale detection as a PR check, not a post-merge check. The earlier in the pipeline the signal fires, the more time there is to resolve the translation debt before the change ships. The goal is catching staleness at the same moment you catch a failing test -- before merge, not after.

Localization that moves with your code.

Try Tonguesage free. Connect a repo and see your first sync in minutes.

Get started free