Localization Testing & QA Checklist for Developers (2026)

A layered QA system, not a flat list. Starts from why a green test suite tells you nothing about German — assertions on English copy lock the suite to one locale, so the German path is never exercised. Then five layers ordered by cost: fourteen static catalog checks with a working Python validator covering placeholder integrity across printf, python-named, i18next and ICU styles plus plural arity and whitespace; CI gates with msgfmt --check-format, extraction-drift detection and a hardcoded-string lint rule, wired into a full GitHub Actions workflow; pseudo-localization with three locales (en-XA, en-XL, ar-XB) and an honest account of what it cannot catch; automated UI checks including the ICU data trap that breaks date assertions on a Node upgrade, collation, Turkish case folding, bidi isolation and diacritic-insensitive search; a five-locale matrix chosen by failure class rather than revenue; what human linguistic QA is actually for. Plus a symptom-to-missing-layer table, a one-day triage plan with time estimates, a PR-template definition of done, and a grouped master checklist. Expanded September 2026 with three sections on the gap between what you validate and what you ship: the compiled MO drops fuzzy and untranslated entries, so a four-entry catalog with one translated, one fuzzy and one empty string ships exactly one — which is why “80% done” and “80% of the UI is translated” are different claims; the locale data that lives in the runtime rather than your repo, where Intl never throws for a missing locale but silently resolves xx-YY to en-US, so a small-icu build or a minimal container ships perfect German words with American dates and every layer still passes; and the exact plural values a test must cover, computed from CLDR rather than guessed. That last table overturns several assumptions — French and Portuguese put 0 in the singular while Spanish and Italian do not, Romanian puts 0 in “few” and does not reach “other” until 20, Welsh is non-monotonic with many=6 and other=4, and the fourth Russian, Polish and Ukrainian form is unreachable by any integer because CLDR reserves it for decimals.

Back to the PO-File blog