IntlPull
Technical
11 min read

Pseudo-localization: The QA Technique Every i18n Team Needs

Master pseudo-localization for i18n QA. Learn to detect hardcoded strings, test string expansion, find UI bugs, and integrate into CI pipelines.

IntlPull Team
IntlPull Team
Feb 12, 2026
On this page
Summary

Master pseudo-localization for i18n QA. Learn to detect hardcoded strings, test string expansion, find UI bugs, and integrate into CI pipelines.

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:

JavaScript
1function 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:

JavaScript
1function 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:

JavaScript
1function 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!

Combines all techniques for comprehensive testing:

JavaScript
1function 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:

JavaScript
1function 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:

TSX
1import { 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

TypeScript
1import { 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:

CSS
1/* 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

TypeScript
1import { 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:

JavaScript
1// ❌ 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

TypeScript
1function 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

JavaScript
1import 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

TSX
1import { 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

JavaScript
1// 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:

JSON
1{
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

YAML
1name: 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

JSON
1{
2  "scripts": {
3    "test:pseudo": "playwright test --grep @pseudo"
4  }
5}

Playwright Tests

TypeScript
1import { 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
TypeScript
1// 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.

Tags
pseudo-localization
qa
testing
i18n
string-expansion
hardcoded-strings
IntlPull Team
IntlPull Team
Engineering

Building tools to help teams ship products globally. Follow us for more insights on localization and i18n.