What is Pseudo-Localization
Pseudo-localization is a software testing technique that transforms source language strings to simulate characteristics of translated text without requiring actual translations. It enables developers to identify internationalization issues early in development by generating "fake" translations that mimic real-world translation properties: longer text length (German, Finnish), special characters (accented letters, diacritics), right-to-left directionality (Arabic, Hebrew), and visual markers that make untranslated hardcoded text instantly visible. A pseudo-localized string like [!! Wélçömé tö öür äpplïçätïöñ !!] immediately reveals whether text is properly externalized—if you see plain English instead of bracketed accented text, you've found a hardcoded string. The technique simulates three critical translation characteristics: expansion (30-40% longer for German/Finnish), accented characters (to test encoding and font support), and boundary markers (brackets to identify concatenation issues and detect truncation). Pseudo-localization costs nothing, requires no translators, and catches 80% of i18n bugs before a single line of real translation work begins. Teams using pseudo-localization catch hardcoded strings, discover layout overflow bugs, identify improper string concatenation, validate encoding support, and ensure translation infrastructure works—all without waiting for expensive translation rounds.
Types of Pseudo-Localization
Accented Pseudo-Locale
Replaces Latin characters with accented equivalents to test encoding and font support:
JavaScript1function accentedPseudo(text) { 2 const charMap = { 3 'a': 'á', 'A': 'Á', 4 'e': 'é', 'E': 'É', 5 'i': 'í', 'I': 'Í', 6 'o': 'ó', 'O': 'Ó', 7 'u': 'ú', 'U': 'Ú', 8 'n': 'ñ', 9 'c': 'ç', 10 's': 'š', 11 'z': 'ž' 12 }; 13 14 return text.replace(/[a-zA-Z]/g, char => charMap[char] || char); 15} 16 17// Examples 18accentedPseudo('Welcome'); // "Wélçómé" 19accentedPseudo('Settings'); // "Séttíñgš" 20accentedPseudo('Save changes'); // "Šávé çháñgéš"
Expanded Pseudo-Locale
Adds 30-40% length to simulate German/Finnish expansion:
JavaScript1function expandedPseudo(text) { 2 const expansion = ' '.repeat(Math.floor(text.length * 0.3)); 3 return `${text}${expansion}`; 4} 5 6// Examples 7expandedPseudo('Save'); // "Save " 8expandedPseudo('Welcome'); // "Welcome " 9expandedPseudo('Log in'); // "Log in "
Bracketed Pseudo-Locale
Adds visual markers to identify boundaries and detect hardcoded text:
JavaScript1function bracketedPseudo(text) { 2 return `[!! ${text} !!]`; 3} 4 5// Examples 6bracketedPseudo('Welcome'); // "[!! Welcome !!]" 7bracketedPseudo('Settings'); // "[!! Settings !!]" 8 9// If you see plain "Welcome" instead of "[!! Welcome !!]", 10// you've found hardcoded text!
Combined Pseudo-Locale (Recommended)
Combines all techniques for comprehensive testing:
JavaScript1function pseudoLocalize(text) { 2 const charMap = { 3 'a': 'á', 'A': 'Á', 'e': 'é', 'E': 'É', 4 'i': 'í', 'I': 'Í', 'o': 'ó', 'O': 'Ó', 5 'u': 'ú', 'U': 'Ú', 'n': 'ñ', 'c': 'ç', 6 's': 'š', 'z': 'ž' 7 }; 8 9 // Apply character substitution 10 let result = text.replace(/[a-zA-Z]/g, char => charMap[char] || char); 11 12 // Add expansion (30%) 13 const expansion = ' '.repeat(Math.floor(text.length * 0.3)); 14 15 // Add brackets 16 return `[!! ${result}${expansion} !!]`; 17} 18 19// Examples 20pseudoLocalize('Welcome to our application'); 21// "[!! Wélçómé tó óúr áppléçátíóñ !!]" 22 23pseudoLocalize('Save changes'); 24// "[!! Šávé çháñgéš !!]"
RTL Pseudo-Locale
Uses RTL override characters to test right-to-left layout:
JavaScript1function rtlPseudo(text) { 2 // U+202E: Right-to-Left Override 3 // U+202C: Pop Directional Formatting 4 return `${text}`; 5} 6 7// This forces text to render RTL, testing layout behavior 8rtlPseudo('Welcome'); // Renders as "emocleW"
Detecting Hardcoded Strings
The primary benefit of pseudo-localization is instantly identifying hardcoded strings:
TSX1import { useTranslation } from 'react-i18next'; 2 3function MyComponent() { 4 const { t } = useTranslation(); 5 6 return ( 7 <div> 8 {/* ✅ This will show: [!! Wélçómé !!] */} 9 <h1>{t('welcome')}</h1> 10 11 {/* ❌ This will show: Settings (plain text!) */} 12 <button>Settings</button> 13 14 {/* ✅ This will show: [!! Šávé !!] */} 15 <button>{t('save')}</button> 16 17 {/* ❌ This will show: Log out (plain text!) */} 18 <a href="/logout">Log out</a> 19 </div> 20 ); 21} 22 23// In pseudo-locale, you'll immediately see: 24// [!! Wélçómé !!] 25// Settings ← HARDCODED! Not translated 26// [!! Šávé !!] 27// Log out ← HARDCODED! Not translated
Automated Hardcoded String Detection
TypeScript1import { test, expect } from '@playwright/test'; 2 3test('should not have hardcoded strings', async ({ page }) => { 4 // Enable pseudo-locale 5 await page.goto('/?lang=pseudo'); 6 7 // Get all text nodes 8 const textContent = await page.evaluate(() => { 9 return document.body.innerText; 10 }); 11 12 // Check for text without [!! ... !!] markers 13 const lines = textContent.split('\n'); 14 const hardcoded = lines.filter(line => { 15 const trimmed = line.trim(); 16 // Skip empty lines 17 if (!trimmed) return false; 18 // Skip numbers and special chars 19 if (/^[0-9s-.,!?]+$/.test(trimmed)) return false; 20 // Check if it's missing pseudo markers 21 return !trimmed.includes('[!!') && /[a-zA-Z]{2,}/.test(trimmed); 22 }); 23 24 if (hardcoded.length > 0) { 25 console.error('Hardcoded strings found:', hardcoded); 26 } 27 28 expect(hardcoded).toHaveLength(0); 29});
Testing String Expansion
German and Finnish translations often 30-40% longer than English. Pseudo-localization simulates this:
CSS1/* This button will overflow with expanded text */ 2.button { 3 width: 100px; 4 overflow: hidden; 5 white-space: nowrap; 6} 7 8/* With pseudo-locale showing "[!! Šávé çháñgéš !!]" 9 instead of "Save changes", you'll see overflow immediately */
Visual Regression Tests for Expansion
TypeScript1import { test, expect } from '@playwright/test'; 2 3test.describe('String expansion handling', () => { 4 test('buttons should not overflow', async ({ page }) => { 5 await page.goto('/?lang=pseudo'); 6 7 const buttons = page.locator('button'); 8 9 for (const button of await buttons.all()) { 10 const box = await button.boundingBox(); 11 const scrollWidth = await button.evaluate(el => el.scrollWidth); 12 const clientWidth = await button.evaluate(el => el.clientWidth); 13 14 // Fail if text is truncated 15 expect(scrollWidth).toBeLessThanOrEqual(clientWidth + 2); 16 } 17 }); 18 19 test('navigation labels should fit', async ({ page }) => { 20 await page.goto('/?lang=pseudo'); 21 22 const navItems = page.locator('.nav-item'); 23 24 for (const item of await navItems.all()) { 25 const isEllipsis = await item.evaluate(el => { 26 return el.scrollWidth > el.clientWidth; 27 }); 28 29 expect(isEllipsis).toBe(false); 30 } 31 }); 32});
Identifying String Concatenation
Improper string concatenation breaks in many languages:
JavaScript1// ❌ BAD: Concatenation 2const message = t('hello') + ', ' + userName + '!'; 3// Pseudo: "[!! Hélló !!], John!" 4// Real German: "Hallo, John!" (comma placement wrong) 5 6// ✅ GOOD: Placeholders 7const message = t('greeting', { name: userName }); 8// Pseudo: "[!! Hélló, {name}! !!]" → "[!! Hélló, John! !!]" 9// Real German: "Hallo John!" (no comma in German)
Detecting Concatenation Issues
TypeScript1function detectConcatenation(text: string): boolean { 2 // Look for patterns like "[!! ... !!], plain text" 3 // or "plain text [!! ... !!]" 4 const pseudoPattern = /[!!s.*?s!!]/; 5 const hasPseudo = pseudoPattern.test(text); 6 7 if (hasPseudo) { 8 // Check if there's plain text adjacent to pseudo markers 9 const parts = text.split(/([!!s.*?s!!])/); 10 const hasPlainText = parts.some(part => 11 !pseudoPattern.test(part) && /[a-zA-Z]{2,}/.test(part) 12 ); 13 14 return hasPlainText; 15 } 16 17 return false; 18} 19 20// Examples 21detectConcatenation('[!! Hélló !!], John!'); // true (concatenation!) 22detectConcatenation('[!! Hélló, John! !!]'); // false (proper placeholder)
Implementing Pseudo-Locale
i18next Implementation
JavaScript1import i18next from 'i18next'; 2 3function pseudoLocalize(text) { 4 const charMap = { 5 'a': 'á', 'e': 'é', 'i': 'í', 'o': 'ó', 'u': 'ú', 6 'A': 'Á', 'E': 'É', 'I': 'Í', 'O': 'Ó', 'U': 'Ú', 7 'n': 'ñ', 'c': 'ç', 's': 'š' 8 }; 9 10 let result = text.replace(/[a-zA-Z]/g, char => charMap[char] || char); 11 const expansion = ' '.repeat(Math.floor(text.length * 0.3)); 12 return `[!! ${result}${expansion} !!]`; 13} 14 15// Generate pseudo translations 16const enTranslations = require('./locales/en.json'); 17const pseudoTranslations = {}; 18 19function pseudoLocaleObject(obj, prefix = '') { 20 Object.entries(obj).forEach(([key, value]) => { 21 const fullKey = prefix ? `${prefix}.${key}` : key; 22 23 if (typeof value === 'string') { 24 pseudoTranslations[fullKey] = pseudoLocalize(value); 25 } else if (typeof value === 'object') { 26 pseudoLocaleObject(value, fullKey); 27 } 28 }); 29} 30 31pseudoLocaleObject(enTranslations); 32 33// Initialize i18next with pseudo locale 34i18next.init({ 35 lng: 'pseudo', 36 resources: { 37 en: { translation: enTranslations }, 38 pseudo: { translation: pseudoTranslations } 39 } 40});
React Integration
TSX1import { I18nextProvider } from 'react-i18next'; 2import i18n from './i18n'; 3 4function App() { 5 const [locale, setLocale] = useState('en'); 6 7 return ( 8 <I18nextProvider i18n={i18n}> 9 <div> 10 <select value={locale} onChange={e => { 11 setLocale(e.target.value); 12 i18n.changeLanguage(e.target.value); 13 }}> 14 <option value="en">English</option> 15 <option value="pseudo">Pseudo (QA)</option> 16 </select> 17 18 <Content /> 19 </div> 20 </I18nextProvider> 21 ); 22}
Build-Time Generation
JavaScript1// scripts/generate-pseudo-locale.js 2const fs = require('fs'); 3const path = require('path'); 4 5function pseudoLocalize(text) { 6 const charMap = { 7 'a': 'á', 'e': 'é', 'i': 'í', 'o': 'ó', 'u': 'ú', 8 'A': 'Á', 'E': 'É', 'I': 'Í', 'O': 'Ó', 'U': 'Ú', 9 'n': 'ñ', 'c': 'ç', 's': 'š' 10 }; 11 12 let result = text.replace(/[a-zA-Z]/g, char => charMap[char] || char); 13 const expansion = ' '.repeat(Math.floor(text.length * 0.3)); 14 return `[!! ${result}${expansion} !!]`; 15} 16 17function transformObject(obj) { 18 if (typeof obj === 'string') { 19 return pseudoLocalize(obj); 20 } else if (Array.isArray(obj)) { 21 return obj.map(transformObject); 22 } else if (typeof obj === 'object' && obj !== null) { 23 const result = {}; 24 Object.entries(obj).forEach(([key, value]) => { 25 result[key] = transformObject(value); 26 }); 27 return result; 28 } 29 return obj; 30} 31 32const enFile = path.join(__dirname, '../src/locales/en.json'); 33const pseudoFile = path.join(__dirname, '../src/locales/pseudo.json'); 34 35const enTranslations = JSON.parse(fs.readFileSync(enFile, 'utf-8')); 36const pseudoTranslations = transformObject(enTranslations); 37 38fs.writeFileSync(pseudoFile, JSON.stringify(pseudoTranslations, null, 2)); 39 40console.log('✅ Generated pseudo-locale at:', pseudoFile);
Add to package.json:
JSON1{ 2 "scripts": { 3 "i18n:pseudo": "node scripts/generate-pseudo-locale.js", 4 "prebuild": "npm run i18n:pseudo" 5 } 6}
CI/CD Integration
GitHub Actions Workflow
YAML1name: Pseudo-Locale QA 2 3on: [push, pull_request] 4 5jobs: 6 pseudo-locale-tests: 7 runs-on: ubuntu-latest 8 9 steps: 10 - uses: actions/checkout@v3 11 - uses: actions/setup-node@v3 12 13 - name: Install dependencies 14 run: npm ci 15 16 - name: Generate pseudo-locale 17 run: npm run i18n:pseudo 18 19 - name: Install Playwright 20 run: npx playwright install --with-deps 21 22 - name: Run pseudo-locale tests 23 run: npm run test:pseudo 24 25 - name: Upload screenshots 26 if: failure() 27 uses: actions/upload-artifact@v3 28 with: 29 name: pseudo-locale-failures 30 path: test-results/
Test Script
JSON1{ 2 "scripts": { 3 "test:pseudo": "playwright test --grep @pseudo" 4 } 5}
Playwright Tests
TypeScript1import { test, expect } from '@playwright/test'; 2 3test.describe('Pseudo-locale QA @pseudo', () => { 4 test.beforeEach(async ({ page }) => { 5 await page.goto('/?lang=pseudo'); 6 }); 7 8 test('should not have hardcoded strings', async ({ page }) => { 9 const body = await page.textContent('body'); 10 const lines = body!.split('\n').filter(l => l.trim()); 11 12 const hardcoded = lines.filter(line => { 13 const trimmed = line.trim(); 14 if (!trimmed || /^[0-9s-.,!?]+$/.test(trimmed)) return false; 15 return !trimmed.includes('[!!') && /[a-zA-Z]{3,}/.test(trimmed); 16 }); 17 18 expect(hardcoded, `Found hardcoded strings: ${hardcoded.join(', ')}`) 19 .toHaveLength(0); 20 }); 21 22 test('should not have text overflow', async ({ page }) => { 23 const overflowing = await page.evaluate(() => { 24 const elements = Array.from(document.querySelectorAll('*')); 25 return elements 26 .filter(el => el.scrollWidth > el.clientWidth + 2) 27 .map(el => ({ 28 tag: el.tagName, 29 class: el.className, 30 text: el.textContent?.substring(0, 50) 31 })); 32 }); 33 34 expect(overflowing, `Elements with overflow: ${JSON.stringify(overflowing)}`) 35 .toHaveLength(0); 36 }); 37 38 test('should handle expanded text in buttons', async ({ page }) => { 39 await expect(page).toHaveScreenshot('pseudo-locale-full.png'); 40 }); 41});
IntlPull's Pseudo-Locale Feature
IntlPull provides built-in pseudo-locale generation:
- One-Click Generation: Generate pseudo-locale for entire project
- Customizable Rules: Adjust expansion percentage, character substitution
- Export Options: Download as JSON, XLIFF, or any supported format
- Preview Mode: Toggle pseudo-locale in visual editor
- QA Reports: Automatically detect potential issues
TypeScript1// IntlPull API 2const client = new IntlPullClient({ apiKey: process.env.INTLPULL_API_KEY }); 3 4// Generate pseudo-locale 5await client.generatePseudoLocale({ 6 projectId: 'proj_123', 7 expansionRate: 0.3, // 30% expansion 8 includeAccents: true, 9 includeBrackets: true, 10 targetLocale: 'pseudo' 11}); 12 13// Download pseudo translations 14const pseudo = await client.downloadTranslations({ 15 projectId: 'proj_123', 16 locale: 'pseudo', 17 format: 'json' 18});
FAQ
Q: Should pseudo-locale be included in production builds? A: No. Use it only in development and QA environments. Add it to your development locale switcher for easy testing.
Q: How do I handle dynamic content in pseudo-locale? A: Ensure your pseudo-localization function is called at runtime for dynamic strings, not just static translations.
Q: Can pseudo-localization catch all i18n bugs? A: No. It catches structural issues (hardcoded text, layout problems) but not linguistic issues (wrong translations, cultural inappropriateness).
Q: How much expansion should I simulate? A: 30% is a good default. German averages 30%, Finnish 35%, some technical terms expand 50%+.
Q: Should I test pseudo-locale manually or automatically? A: Both. Manual testing catches visual issues quickly. Automated tests prevent regressions and run on every PR.
Q: Do I need pseudo-locale if I already have real translations? A: Yes! Pseudo-locale catches issues early before translations exist, and continues to catch new hardcoded strings as you develop.
Q: Can I use pseudo-locale with machine translation? A: Yes, but separately. Use pseudo-locale for structural testing, MT for linguistic testing. They serve different purposes.
