Unifyl — a dual-pane file manager for macOS · Blog

All 15 of our translations were missing the same 620 strings — and every check was green

August 2026 · by the developer of Unifyl

My app ships in 15 languages, and I was proud of the machinery behind that: an automated audit that keeps every localization catalogue in lockstep — same keys, same counts, same placeholder specifiers, every locale, every commit. It had caught real regressions before. It was green.

Then I used the app in Korean for ten minutes, like a user instead of a developer, and found this in Settings: an item labeled "App Language". In English. The control whose entire job is choosing your language introduced itself in the one language the user had just chosen not to use.

How a green check hides 620 failures

The investigation found 620 user-facing strings that existed in no catalogue at all — not untranslated, but absent. SwiftUI makes this failure mode comfortable: Text("App Language") compiles, runs, and renders that literal on every locale. Nothing errors. Nothing even warns.

And the parity audit? It compared the catalogues to each other. A key missing from all 15 files at once is in perfect parity:

A consistency check can only catch members of the set disagreeing with each other. When every member is wrong the same way, consistency is exactly what you have. If the whole class copies the same wrong answer, the grading sheet shows a perfect score.

This is the localization variant of a general trap: the test guarded the list it knew about. The 620 strings were never on any list, so no list-based check could miss them — or find them.

The fix: compare against the source of truth, not the peers

The audit needed a second axis. Alongside "do the catalogues agree with each other," it now extracts every user-facing string from the source code — bare SwiftUI literals, interpolated keys (normalized so \(count) matches its %lld catalogue twin), enum rawValues that reach the UI, alert strings — and asks: does each one exist in the catalogues? The budget for misses is zero, and the check runs in CI so the number can't quietly grow back.

Then came the unglamorous part: writing 620 strings × 15 languages of actual translations. No shortcuts there; it's the tax on the earlier comfort.

What I'd tell past me