pseudo-localization-runner
Configures pseudo-localization for the app (replaces translatable strings with accented variants like "Submit" → "Şüƀɱîţ" + 35% length expansion) - surfaces UI issues without needing actual translators: hardcoded strings, truncation, encoding, and bidi handling. Use when preparing an app for translation (i18n / internationalization), validating l10n infrastructure, or when the user mentions pseudo-localization or localization testing.
Install with skills.sh (any agent)
npx skills add testland/qa --skill pseudo-localization-runnerpseudo-localization-runner
Overview
Pseudo-localization swaps translatable strings for accented, length-expanded variants that stay readable to English QA:
"Submit" → "Şüƀɱîţ" # accent / Latin-extended characters
"Submit" → "[Şüƀɱîţ]" # delimited markers (find unwrapped strings)
"Submit" → "Şüƀɱîţ ↵↵↵" # 35% length expansion (test truncation)The output stays readable to English-speaking QA while surfacing the l10n issues catalogued in Step 4.
When to use
Step 1 - Pick a pseudo-localization library
Per stack:
| Stack | Library / approach |
|---|---|
| i18next (JS/TS) | i18next-pseudo plugin |
| FormatJS | Manual middleware in the message extractor |
| Django | django-modeltranslation + custom locale |
| Rails | i18n-pseudo |
| Anything | Build custom: walk the locale file; transform values |
Step 2 - Configure pseudo-locale
// src/i18n.ts (i18next + i18next-pseudo)
import Pseudo from 'i18next-pseudo';
i18next
.use(Pseudo)
.init({
fallbackLng: 'en',
pseudo: {
enabled: process.env.NODE_ENV !== 'production',
letterMultiplier: 2, // ~35% length expansion
languageToPseudo: 'en', // wrap English strings
repeatedLetters: ['a', 'e', 'i', 'o', 'u'],
},
});The letterMultiplier: 2 doubles vowels (Şü → Şüü) - the 35% length-expansion convention.
Step 3 - Run the app under pseudo-locale
# Activate pseudo-locale by URL param / cookie
APP_URL=http://localhost:3000?lng=en-XA # or whatever the pseudo-locale code isThe app renders with pseudo-translated text. QA / engineers walk the UI looking for issues.
Verify the pseudo-locale is active before walking the UI: confirm at least one visible label renders transformed (e.g. Submit shows as Şüƀɱîţ). If the page still shows plain English, the locale did not activate - fix the lng param or cookie and reload before continuing.
Step 4 - Issues to spot
| Symptom | Underlying issue |
|---|---|
| Pure-English text on the page | String not wrapped in t() (untranslated) |
Truncation (...) | Container too narrow for translated text |
| Layout broken (overlapping elements) | CSS doesn't accommodate longer strings |
| Mojibake / garbled characters | Encoding misconfigured |
Missing characters / boxes (□) | Font doesn't support extended Latin |
| Bidi text rendered wrong direction | Mixing LTR/RTL without proper markers |
Step 5 - Visual regression with pseudo-locale
Combine with playwright-snapshots (in the qa-visual-regression plugin) for automated detection:
// e2e/pseudo-loc.spec.ts
import { test, expect } from '@playwright/test';
test.use({ extraHTTPHeaders: { 'Accept-Language': 'en-XA' } });
test('checkout page renders correctly under pseudo-loc', async ({ page }) => {
await page.goto('/checkout');
await expect(page).toHaveScreenshot('checkout-pseudo.png');
});The screenshot baseline is established under pseudo-locale; future runs catch regressions in l10n-friendliness.
Step 6 - CI integration
- name: Pseudo-localization smoke
run: |
npm run dev:pseudo &
sleep 5
npx playwright test e2e/pseudo-loc.spec.ts
- uses: actions/upload-artifact@v4
if: failure()
with:
name: pseudo-loc-screenshots
path: test-results/Step 7 - Alternative: manual transformation
If no pseudo-localization library exists for your stack, transform the source locale file locally with a one-time script - see references/manual-transform.md.
Worked example
A team enables i18next-pseudo with letterMultiplier: 2 (Step 2) and loads /checkout?lng=en-XA (Step 3). "Submit" renders as Şüƀɱîţ, confirming the wrap works - but a promo badge still shows plain Sale, exposing an unwrapped string (Step 4). The "Place Order" button also truncates because its container is fixed-width and can't absorb the 35% length expansion.
The developer wraps the badge in t() and lets the button widen. A Playwright screenshot baseline checkout-pseudo.png is established under the pseudo-locale (Step 5), so future PRs catch layout regressions automatically. The re-run is clean.
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Pseudo-loc in production | Real users see garbled UI. | Dev / staging only (Step 2). |
| 0% length expansion | Misses truncation issues. | 30-50% expansion (Step 2). |
| ASCII-only pseudo-loc | Misses encoding issues. | Use accented Latin (or non-Latin script for some chars). |
| Pseudo-loc as substitute for real translation | Pseudo-loc verifies infrastructure; not translator quality. | Use both: pseudo-loc continuously, real translation per release. |
| Skipping screenshot baseline under pseudo-loc | Layout regressions visible only when locale activated. | Pseudo-loc + visual regression (Step 5). |
Limitations
References
Manual pseudo-localization transform
View source (opens in new window)Manual pseudo-localization transform
If no pseudo-localization library exists for your stack, transform the source locale file locally with a one-time pass:
function pseudoLocalize(s) {
const map = { a: 'à', e: 'è', i: 'ì', o: 'ò', u: 'ù',
A: 'Á', E: 'É', I: 'Í', O: 'Ó', U: 'Ú',
s: 'š', t: 'ţ' };
let out = '';
for (const c of s) {
out += map[c] || c;
if ('aeiouAEIOU'.includes(c)) out += c; // duplicate vowels for the length expansion
}
return `[${out}]`; // delimiters help spot incomplete wraps
}Run it over the source locale file and load the output as the pseudo-locale. The [...] delimiters make unwrapped strings obvious, and the duplicated vowels drive the length expansion (the 35% convention defined in Step 2).
Related skills
i18n-string-coverage
Build-an-X workflow that scans source code for untranslated strings - finds hardcoded user-facing text not wrapped in the i18n function (`t()`, `i18n.t`, `gettext`, `__()`, etc.), maps gaps to the team's translation file, reports per-language coverage (en: 100%; fr: 87%; es: 60%), gates per-PR for new untranslated strings. Use when the product ships in multiple locales and the team needs continuous coverage tracking.
locale-format-validator
Build-an-X workflow that verifies locale-specific format rendering - dates (US `5/4/2026` vs ISO `2026-05-04` vs DE `04.05.2026` vs JP `2026/05/04`), numbers (US `1,234.56` vs DE `1.234,56` vs FR `1 234,56`), currencies (US `$1,234.56` vs JP `¥1,235` vs DE `1.234,56 €`), times, durations. Use when the product displays locale-sensitive data and the team needs assurance the formatting matches per-locale conventions.
rtl-rendering-tester
Build-an-X workflow for verifying RTL (right-to-left) layouts - runs the test suite under Arabic / Hebrew / Persian / Urdu locales, asserts the `dir="rtl"` attribute is set, verifies layout mirrors correctly (text alignment, icon positions, scrollbar location), uses logical CSS properties (`start`/`end` over `left`/`right`) per W3C guidance, captures per-locale screenshots for visual review. Use when the app supports RTL languages.