Engineering guide
Localization Testing and QA Workflow
A release-ready localization QA workflow for pseudolocalization, automation, visual regression, native review, regional devices, and release gates.
Reviewed 19 July 2026 · Editorial owner: localization.guide
The workflow in six gates
1. Static checks
Reject hardcoded UI strings, invalid locale tags, missing keys, placeholder mismatches, and unsafe concatenation in continuous integration.
2. Pseudolocalization
Transform source messages to expose clipping, fixed widths, untranslated strings, token corruption, and bidirectional assumptions before translation.
3. Locale automation
Run number, currency, date, timezone, plural, address, and phone fixtures with explicit locales and timezones.
4. Visual regression
Capture representative long-text, CJK, combining-mark, emoji, and RTL screens at phone, tablet, desktop, and 200% zoom.
5. Linguistic QA
Have qualified native-language reviewers assess meaning, terminology, tone, grammar, cultural suitability, and functional context.
6. Regional validation
Test real device language, region, timezone, keyboard, calendar, numbering system, and currency combinations before release.
A pseudolocalization transform that finds real bugs
A useful pseudo locale keeps messages readable while expanding text, replacing letters with accented variants, preserving interpolation tokens, and surrounding output with unmistakable markers. Use a separate mirrored RTL pseudo locale; simply reversing a whole string does not model the Unicode bidirectional algorithm.
const accentMap: Record<string, string> = {
a: "à", e: "ë", i: "ï", o: "ô", u: "ü",
A: "Å", E: "Ë", I: "Ï", O: "Ö", U: "Û",
};
export function pseudo(message: string): string {
const tokens: string[] = [];
const protectedMessage = message.replace(/{[^}]+}|%w|<[^>]+>/g, (token) => {
tokens.push(token);
return "__TOKEN_" + (tokens.length - 1) + "__";
});
const accented = [...protectedMessage]
.map((char) => accentMap[char] ?? char)
.join("");
const expanded = accented + " " + "~".repeat(Math.ceil(message.length * 0.35));
return "⟦" + expanded.replace(/__TOKEN_(d+)__/g, (_, index) => tokens[Number(index)]) + "⟧";
}- Fail if any source-language message appears unchanged outside an allowlist.
- Verify placeholders, HTML tags, ICU plural branches, and line breaks survive the transformation.
- Include a long expansion locale and an RTL mirrored locale in every pull-request preview.
Locale test matrix
| Area | Automated fixture | Human/device check |
|---|---|---|
| Numbers and currencies | Grouping, digits, signs, minor units, formatToParts | Spacing, bidi marks, local expectations |
| Dates and time | Timezone boundaries, DST, calendars, week starts | Device region versus language |
| Messages | Missing keys, plurals, select branches, placeholders | Meaning, grammar, tone, terminology |
| Forms | Unicode input, validation, autocomplete, paste | Keyboard, IME, field order, error recovery |
| Layout | Overflow assertions and screenshot diffs | Wrapping, clipping, mirrored controls, 200% zoom |
| Fonts | Character coverage smoke set | CJK, Arabic, Hebrew, combining marks, emoji, fallback |
| Addresses and phone | Country fixtures and parsing outcomes | Regional labels, optional fields, user expectations |
Choose representative stress locales
Text and layout
Use German-style long compounds, Finnish inflection, CJK text without spaces, Arabic and Hebrew RTL, Thai line breaking, Vietnamese diacritics, combining marks, and emoji sequences. No single locale is a universal “worst case.”
Regional settings
Separate language from region, timezone, calendar, numbering system, measurement units, and preferred currency. An English UI with an Indian region or an Arabic UI using Latin digits is a valid user configuration.
Browser and device simulation
- Change the operating-system language and region independently; restart the app where the platform requires it.
- Set a timezone that crosses the tested date boundary and include a daylight-saving transition.
- Install and use a regional keyboard or input method editor, then test typing, composition, selection, and paste.
- Request an alternate numbering system through a Unicode locale extension such as `ar-EG-u-nu-latn`.
- Test narrow phone widths, landscape, desktop, browser text zoom, and 200% page zoom.
- Inspect logical navigation order, focus visibility, screen-reader labels, and whether RTL changes meaningfully directional icons.
Automate stable contracts
Set locale and timezone explicitly in tests. Prefer semantic assertions over machine-default output and keep screenshots on a pinned browser/runtime.
import test from "node:test";
import assert from "node:assert/strict";
test("JPY uses its runtime currency precision", () => {
const formatter = new Intl.NumberFormat("ja-JP", {
style: "currency", currency: "JPY",
});
assert.equal(formatter.resolvedOptions().maximumFractionDigits, 0);
});
test("pseudo keeps the interpolation token", () => {
const output = pseudo("Welcome, {name}");
assert.match(output, /{name}/);
assert.match(output, /^⟦/);
});Copyable release gate
- □ Message extraction and placeholder validation pass.
- □ Pseudo, long-text, CJK, and RTL previews have no blocked defects.
- □ Locale fixtures pass with explicit runtime, locale, and timezone.
- □ Native-language reviewers approve the release-candidate build.
- □ Critical flows pass on representative regional devices and input methods.
- □ Accessibility, visual regression, fallback, and missing-translation behavior pass.
- □ Known exceptions have owners, severity, affected locales, and expiry dates.
Primary and official sources
- Pseudolocalization methodology — Microsoft Learn
- How to perform internationalization testing — Microsoft Learn
- Test your app with pseudolocales — Android Developers
- Structural markup and right-to-left text — W3C Internationalization