Testland
Browse all skills & agents

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-runner
View source

pseudo-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

  • Pre-translation: validate the app's l10n infrastructure works before paying for translation.
  • After major UI work: confirm new components handle l10n correctly.
  • Continuous: the team's "always-on" l10n smoke test.

Step 1 - Pick a pseudo-localization library

Per stack:

StackLibrary / approach
i18next (JS/TS)i18next-pseudo plugin
FormatJSManual middleware in the message extractor
Djangodjango-modeltranslation + custom locale
Railsi18n-pseudo
AnythingBuild 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 is

The 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

SymptomUnderlying issue
Pure-English text on the pageString not wrapped in t() (untranslated)
Truncation (...)Container too narrow for translated text
Layout broken (overlapping elements)CSS doesn't accommodate longer strings
Mojibake / garbled charactersEncoding misconfigured
Missing characters / boxes ()Font doesn't support extended Latin
Bidi text rendered wrong directionMixing 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-patternWhy it failsFix
Pseudo-loc in productionReal users see garbled UI.Dev / staging only (Step 2).
0% length expansionMisses truncation issues.30-50% expansion (Step 2).
ASCII-only pseudo-locMisses encoding issues.Use accented Latin (or non-Latin script for some chars).
Pseudo-loc as substitute for real translationPseudo-loc verifies infrastructure; not translator quality.Use both: pseudo-loc continuously, real translation per release.
Skipping screenshot baseline under pseudo-locLayout regressions visible only when locale activated.Pseudo-loc + visual regression (Step 5).

Limitations

  • Doesn't replace real translation. Pseudo-loc tests the infrastructure; humans still need to translate for real users.
  • Can't test pluralization rules. ICU MessageFormat rules don't auto-apply; pseudo-loc just transforms the surface text.
  • Right-to-left support varies. Some libraries also offer a RTL pseudo-locale (en-XB) that mirrors text direction; valuable but rare.

References

  • W3C i18n documentation at w3.org/International/.
  • i18n-string-coverage - static-scan complement.
  • rtl-rendering-tester - RTL-specific tests.
  • playwright-snapshots - visual regression for catching pseudo-loc regressions.
  • references/manual-transform.md - manual transform script.

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).