Skip to main content
Back to Blog
Guide
2026-02-23

Internationalization Testing — i18n, l10n, and Locale Automation

Complete guide to internationalization and localization testing. Covers i18n validation, locale-specific bugs, date and currency formatting, RTL layouts, and automation strategies.

Internationalization testing is the practice of verifying that your application can adapt to different languages, regions, and cultural conventions without engineering changes. If your product serves -- or plans to serve -- users outside a single locale, i18n testing is not optional. A date displayed as 03/04/2026 means March 4th to an American user and April 3rd to a European user. A price shown as $1,000.00 is meaningless to a user who expects 1.000,00 EUR. A form label that reads perfectly in English may overflow its container when translated into German. And an entire layout can break when rendered in a right-to-left language like Arabic or Hebrew.

This guide covers the complete internationalization and localization testing stack -- from understanding the difference between i18n and l10n, to catching locale-specific bugs in dates, numbers, and currencies, to validating RTL layouts, managing translation quality, handling Unicode edge cases, implementing pseudo-localization, and integrating all of it into your CI/CD pipeline. Whether you are internationalizing a greenfield application or retrofitting an existing product for new markets, these techniques will help you ship software that works correctly for every user, everywhere.


Key Takeaways

  • Internationalization (i18n) is architecture; localization (l10n) is content -- i18n makes your code locale-agnostic, while l10n adapts content (translations, formats, imagery) for a specific locale. Both need dedicated testing strategies.
  • Date, time, number, and currency formatting are the most common sources of locale bugs -- hardcoded formats like MM/DD/YYYY or $ symbols will break in non-US locales. Always use the Intl API or equivalent locale-aware formatters.
  • RTL layout testing is essential for Arabic, Hebrew, Urdu, and Farsi -- CSS logical properties (margin-inline-start instead of margin-left) and bidirectional text handling require dedicated visual and functional validation.
  • Translation expansion causes UI breakage -- German and Finnish translations are typically 30-40% longer than English. Pseudo-localization catches these issues before real translations arrive.
  • Character encoding bugs are silent and destructive -- UTF-8 should be enforced everywhere (database, API, HTML, HTTP headers), and your tests should include CJK characters, emoji, diacritics, and special symbols.
  • AI coding agents with i18n skills can automate locale bug detection -- from finding hardcoded strings to validating RTL layouts, specialized QA skills catch i18n defects that generic testing misses entirely.

i18n vs l10n: What's the Difference?

The terms internationalization and localization are often used interchangeably, but they refer to distinct engineering concerns. Understanding the difference is critical because each requires a different testing approach.

Internationalization (i18n)

Internationalization is the process of designing and building your application so that it can support multiple locales without requiring code changes. It is an architectural concern. i18n work includes:

  • Externalizing all user-facing strings into resource files or translation management systems instead of hardcoding them in source code
  • Using locale-aware APIs for formatting dates, numbers, currencies, and plurals instead of manual string manipulation
  • Supporting bidirectional text by using CSS logical properties and proper HTML dir attributes
  • Handling variable-length text in UI components so that longer translations do not break layouts
  • Supporting Unicode (UTF-8) throughout the entire stack -- database, API, file storage, and rendering

The "18" in i18n refers to the 18 letters between the "i" and the "n" in "internationalization." Similarly, the "10" in l10n refers to the 10 letters in "localization."

Localization (l10n)

Localization is the process of adapting your internationalized application for a specific locale. It is a content and configuration concern. l10n work includes:

  • Translating strings into the target language, accounting for context, tone, and cultural conventions
  • Configuring locale-specific formats for dates, times, numbers, currencies, addresses, and phone numbers
  • Adapting images and icons that contain text or culturally specific symbols
  • Adjusting content for cultural norms -- color meanings, reading direction, name formats, and form field expectations

Why Both Need Testing

i18n testing validates that your code architecture correctly handles locale-switching, dynamic text, and formatting delegation. You test i18n even in a single-language application to ensure the foundation is solid. l10n testing validates that each specific locale works correctly -- that French translations display properly, that Japanese line-breaking is correct, and that Arabic layouts mirror as expected.

Aspecti18n Testingl10n Testing
FocusCode architecture and locale readinessLocale-specific content and behavior
WhenDuring development, before translationsAfter translations and locale configs
WhoEngineers and QAQA, translators, and local reviewers
Automated?Highly automatableMix of automated and manual review
ExamplesNo hardcoded strings, Intl API usageFrench date formats, Japanese truncation

Common i18n Bugs

Internationalization bugs are insidious because they often only manifest in specific locales, which means they slip through English-only testing. Here are the most frequent categories of i18n bugs and how to catch them.

Hardcoded Strings

The most fundamental i18n bug is user-facing text written directly in source code rather than loaded from a translation resource file. Hardcoded strings cannot be translated and will display in the development language regardless of the user's locale.

// Bad: hardcoded string
const message = 'No results found';

// Good: externalized string
const message = t('search.noResults');

Hardcoded strings are easy to introduce during rapid development and surprisingly difficult to find in large codebases. Static analysis tools and linters like eslint-plugin-i18next can flag them automatically.

String Concatenation with Variables

Building sentences by concatenating strings with variables is a classic i18n antipattern. Different languages have different word orders, and concatenation locks you into a single grammatical structure.

// Bad: concatenation assumes English word order
const msg = 'Welcome, ' + userName + '! You have ' + count + ' items.';

// Good: interpolation allows translators to reorder
const msg = t('welcome.message', { name: userName, count: count });
// English: "Welcome, {name}! You have {count} items."
// Japanese: "{name}さん、ようこそ!{count}件のアイテムがあります。"

Date and Number Format Assumptions

Hardcoding date formats or number separators is one of the most common causes of locale testing failures. What looks like a minor formatting choice can cause real confusion.

Text Truncation from Translation Expansion

English is one of the more compact languages. When translated, text commonly expands by 30-40% for languages like German, Finnish, and Greek. UI elements with fixed widths or tight padding will overflow or truncate.

Common i18n Bug Reference

Bug TypeExampleImpactDetection
Hardcoded strings"Submit" in JSXUntranslatable UILinter, static analysis
String concatenation"Hello " + nameBroken word orderCode review, linter
Hardcoded date formatMM/DD/YYYYAmbiguous datesLocale switching test
Hardcoded currency"$" + amountWrong currency symbolMulti-locale test
Fixed-width containerswidth: 120px on buttonText overflow in GermanPseudo-localization
ASCII-only validation/^[a-zA-Z]+$/Rejects valid names (Ñ, ü, ç)Unicode input tests
Hardcoded pluralscount + " items"Wrong for singular, wrong for languages with complex plural rulesICU MessageFormat check
LTR-only layoutmargin-left: 16pxBroken in RTL localesRTL visual test
Sort by byte valuearray.sort()Wrong order for accented charactersLocale-aware sort test

Testing Date, Time, and Number Formats

Date, time, and number formatting is where internationalization testing gets concrete. These are not theoretical concerns -- they are the bugs your users actually report. The core principle is simple: never format dates, times, numbers, or currencies manually. Always delegate to locale-aware APIs.

Date and Time Formatting

The same date can be represented completely differently across locales:

LocaleDate FormatExample
en-USMM/DD/YYYY03/04/2026
en-GBDD/MM/YYYY04/03/2026
de-DEDD.MM.YYYY04.03.2026
ja-JPYYYY/MM/DD2026/03/04
ko-KRYYYY. MM. DD.2026. 03. 04.
ar-SADD/MM/YYYY (Hijri calendar)٠٩/٠٨/١٤٤٧

Use the Intl.DateTimeFormat API (or its equivalent in your language) instead of building date strings manually:

// Locale-aware date formatting
const date = new Date('2026-03-04T14:30:00Z');

// US English
new Intl.DateTimeFormat('en-US', {
  year: 'numeric',
  month: 'long',
  day: 'numeric',
}).format(date);
// "March 4, 2026"

// German
new Intl.DateTimeFormat('de-DE', {
  year: 'numeric',
  month: 'long',
  day: 'numeric',
}).format(date);
// "4. März 2026"

// Japanese
new Intl.DateTimeFormat('ja-JP', {
  year: 'numeric',
  month: 'long',
  day: 'numeric',
}).format(date);
// "2026年3月4日"

Timezone Handling

Timezone bugs are among the hardest i18n issues to reproduce and test. A timestamp stored as UTC and displayed in the user's local time can shift the date entirely -- an event at 11 PM UTC on March 4th is March 5th in Tokyo.

// Testing timezone-sensitive displays
const timestamp = '2026-03-04T23:30:00Z';

// New York (UTC-5)
new Intl.DateTimeFormat('en-US', {
  timeZone: 'America/New_York',
  dateStyle: 'full',
  timeStyle: 'long',
}).format(new Date(timestamp));
// "Wednesday, March 4, 2026 at 6:30:00 PM EST"

// Tokyo (UTC+9)
new Intl.DateTimeFormat('ja-JP', {
  timeZone: 'Asia/Tokyo',
  dateStyle: 'full',
  timeStyle: 'long',
}).format(new Date(timestamp));
// "2026年3月5日木曜日 8:30:00 JST"

Notice how the same UTC timestamp renders as March 4th in New York but March 5th in Tokyo. Your tests must cover these boundary cases.

Currency Formatting

Currency formatting varies in symbol, position, and number format:

const amount = 1234567.89;

// US Dollar
new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD',
}).format(amount);
// "$1,234,567.89"

// Euro (German locale)
new Intl.NumberFormat('de-DE', {
  style: 'currency',
  currency: 'EUR',
}).format(amount);
// "1.234.567,89 €"

// Japanese Yen (no decimals)
new Intl.NumberFormat('ja-JP', {
  style: 'currency',
  currency: 'JPY',
}).format(amount);
// "¥1,234,568"

// Indian Rupee (lakh/crore grouping)
new Intl.NumberFormat('en-IN', {
  style: 'currency',
  currency: 'INR',
}).format(amount);
// "₹12,34,567.89"

Number Separators

The decimal separator and thousands grouping separator are swapped between many locales. This is not cosmetic -- it changes the meaning of the number:

LocaleOne Million and a HalfThousands SeparatorDecimal Separator
en-US1,000,000.50CommaPeriod
de-DE1.000.000,50PeriodComma
fr-FR1 000 000,50Non-breaking spaceComma
en-IN10,00,000.50Comma (lakh grouping)Period

Test strategy: For each supported locale, create test fixtures that format known values and assert the output matches the expected locale-specific format. Run these tests as part of your unit test suite to catch regressions immediately.


RTL Layout Testing

Right-to-left (RTL) languages -- Arabic, Hebrew, Urdu, Farsi, and others -- require that the entire UI layout be mirrored. RTL testing is not simply about text direction; it involves navigation order, icon placement, animations, and interactive elements.

What Changes in RTL

When you switch from an LTR to an RTL locale, the following should mirror:

  • Text alignment flips from left to right
  • Navigation flows from right to left (sidebar on the right, back buttons on the right)
  • Icons with directional meaning (arrows, progress indicators) must mirror
  • Margins and padding swap sides
  • Scroll direction remains unchanged (scrollbars stay on the right in most implementations)
  • Numbers, phone numbers, and code remain LTR even within RTL text (bidirectional text)

CSS Logical Properties

The single most impactful architectural decision for RTL support is using CSS logical properties instead of physical properties:

/* Physical properties -- break in RTL */
.card {
  margin-left: 16px;
  padding-right: 24px;
  text-align: left;
  border-left: 2px solid blue;
}

/* Logical properties -- work in both LTR and RTL */
.card {
  margin-inline-start: 16px;
  padding-inline-end: 24px;
  text-align: start;
  border-inline-start: 2px solid blue;
}
Physical PropertyLogical Property
margin-leftmargin-inline-start
margin-rightmargin-inline-end
padding-leftpadding-inline-start
padding-rightpadding-inline-end
text-align: lefttext-align: start
float: leftfloat: inline-start
border-leftborder-inline-start
left: 0inset-inline-start: 0

Playwright RTL Testing

You can automate RTL layout validation with Playwright by setting the HTML dir attribute and taking visual snapshots:

import { test, expect } from '@playwright/test';

test.describe('RTL layout tests', () => {
  test('navigation mirrors in Arabic locale', async ({ page }) => {
    // Set RTL direction
    await page.goto('/');
    await page.evaluate(() => {
      document.documentElement.setAttribute('dir', 'rtl');
      document.documentElement.setAttribute('lang', 'ar');
    });

    // Verify sidebar is on the right side
    const sidebar = page.locator('[data-testid="sidebar"]');
    const box = await sidebar.boundingBox();
    const viewport = page.viewportSize();
    expect(box.x).toBeGreaterThan(viewport.width / 2);

    // Visual snapshot for RTL regression
    await expect(page).toHaveScreenshot('homepage-rtl.png');
  });

  test('bidirectional text renders correctly', async ({ page }) => {
    await page.goto('/profile');
    await page.evaluate(() => {
      document.documentElement.setAttribute('dir', 'rtl');
    });

    // Verify that embedded LTR content (emails, URLs) is still LTR
    const emailField = page.locator('[data-testid="email-display"]');
    const computedDir = await emailField.evaluate(
      (el) => getComputedStyle(el).direction
    );
    expect(computedDir).toBe('ltr');
  });
});

Visual Regression for RTL

Automated visual regression testing is particularly valuable for RTL because layout mirroring issues are visual by nature -- they are difficult to catch with DOM assertions alone. Capture baseline screenshots in both LTR and RTL modes and compare on every pull request.

const locales = [
  { lang: 'en', dir: 'ltr' },
  { lang: 'ar', dir: 'rtl' },
  { lang: 'he', dir: 'rtl' },
];

for (const locale of locales) {
  test(`visual regression - ${locale.lang}`, async ({ page }) => {
    await page.goto('/dashboard');
    await page.evaluate(
      ({ lang, dir }) => {
        document.documentElement.setAttribute('dir', dir);
        document.documentElement.setAttribute('lang', lang);
      },
      locale
    );
    await expect(page).toHaveScreenshot(
      `dashboard-${locale.lang}.png`
    );
  });
}

Translation Testing

Even with a perfectly internationalized codebase, localization testing can fail at the translation layer. Missing translations, incorrect context, and inadequate handling of pluralization rules are common issues.

Missing Translations

The most basic l10n bug is a missing translation key. When a key has no translation for a given locale, the application typically falls back to the default language (usually English) or displays the raw key string. Both outcomes are bad -- one creates a jarring mixed-language experience, the other displays gibberish.

Detection strategy: Export your full set of translation keys and compare across all supported locales. Any key present in the default locale but missing in a target locale is a gap. This can be automated in CI:

# Compare English keys against French keys
diff <(jq -r 'paths | join(".")' locales/en.json | sort) \
     <(jq -r 'paths | join(".")' locales/fr.json | sort)

Translation String Length Expansion

When English text is translated, it almost always gets longer. The expansion factor varies by target language:

Target LanguageTypical ExpansionExample
German30-35%"Save" becomes "Speichern"
Finnish30-40%"Search" becomes "Hae" (shorter!) or "Hakutulokset" (longer for "Search results")
French15-20%"Settings" becomes "Paramètres"
Japanese-10 to +10%Often comparable or shorter in characters but wider in pixels
Arabic20-25%Variable, plus RTL layout shift

Buttons, navigation tabs, table headers, and modal titles are the UI elements most commonly broken by translation expansion. A button that fits "Submit" will not fit "Absenden bestätigen."

Pluralization Rules

English has two plural forms: singular and plural. Many languages have more complex rules. Polish has four forms. Arabic has six. Russian has three. The ICU MessageFormat standard handles this:

// English: 2 forms
{count, plural, one {# item} other {# items}}

// Polish: 4 forms
{count, plural,
  one {# element}
  few {# elementy}
  many {# elementów}
  other {# elementu}
}

// Arabic: 6 forms
{count, plural,
  zero {لا عناصر}
  one {عنصر واحد}
  two {عنصران}
  few {# عناصر}
  many {# عنصرًا}
  other {# عنصر}
}

Test with representative count values: 0, 1, 2, 5, 12, 21, 100, 101. These values trigger different plural forms across most languages.

Context-Dependent Translations

The same English word can require different translations depending on context. "Post" as a noun (a blog post) and "Post" as a verb (to post a comment) are different words in many languages. Translation keys should include context hints:

{
  "post.noun": "Beitrag",
  "post.verb": "Veröffentlichen",
  "save.action": "Speichern",
  "save.noun": "Ersparnis"
}

Character Encoding and Unicode

Character encoding issues are among the most destructive i18n bugs because they corrupt data silently. A user's name stored with the wrong encoding becomes garbled text (Müller instead of Müller), and once data is corrupted, it is extremely difficult to recover.

UTF-8 Everywhere

The single most important encoding rule is: use UTF-8 for everything. This means:

  • Database: Ensure your database and every column uses UTF-8 encoding (or utf8mb4 in MySQL, which supports the full Unicode range including emoji)
  • API responses: Set Content-Type: application/json; charset=utf-8 headers
  • HTML: Include <meta charset="UTF-8"> in the document head
  • File storage: Save source files and translation files as UTF-8 without BOM
  • HTTP requests: Ensure form submissions and API requests use UTF-8 encoding

Testing with Diverse Character Sets

Your i18n test suite should include input and display tests with characters from multiple scripts:

ScriptTest CharactersCommon Issues
Latin Extendedé, ü, ñ, ç, å, ø, ßBroken by ASCII-only validation
CJK (Chinese, Japanese, Korean)你好, こんにちは, 안녕Double-width characters break fixed-width layouts
ArabicمرحباRTL direction, connecting letters
CyrillicПриветLooks like Latin but different codepoints
Devanagariनमस्तेComplex ligatures and combining marks
Emoji🚀🌍👨‍👩‍👧‍👦Multi-byte sequences, ZWJ sequences, varies by platform

Database and API Encoding Validation

Write explicit tests that round-trip special characters through your entire stack:

test('Unicode round-trip through API and database', async () => {
  const testNames = [
    'Müller',           // German umlaut
    '山田太郎',           // Japanese Kanji
    'محمد',            // Arabic
    'O\'Brien',         // Apostrophe
    '🚀 Rocket User', // Emoji
    'Café',             // Accented Latin
  ];

  for (const name of testNames) {
    const response = await api.createUser({ name });
    const fetched = await api.getUser(response.id);
    expect(fetched.name).toBe(name);
  }
});

String Length vs. Character Count

Be aware that string length in JavaScript counts UTF-16 code units, not visible characters. An emoji like 👨‍👩‍👧‍👦 (family emoji) has a .length of 11 but displays as one character. Use Intl.Segmenter for accurate grapheme counting:

const text = '👨‍👩‍👧‍👦';

// Wrong: counts UTF-16 code units
text.length; // 11

// Correct: counts grapheme clusters
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
[...segmenter.segment(text)].length; // 1

Pseudo-Localization

Pseudo-localization is a development technique that replaces English strings with accented or visually modified versions to expose i18n issues without needing real translations. It is one of the most cost-effective i18n testing strategies because it catches problems early, during development, before translators are even involved.

What Pseudo-Localization Looks Like

A pseudo-localized string transforms "Settings" into something like "Ŝéţţïñğş" or "[!!Ŝéţţïñğş!!]". The transformation typically includes:

  • Accented characters replacing ASCII letters to test Unicode rendering (a becomes à, e becomes é)
  • String expansion adding extra characters (padding with underscores or brackets) to simulate translation expansion
  • Bookending with markers like [!! and !!] to make truncation visible -- if you cannot see the end markers, the string is being cut off
  • Right-to-left markers optionally embedded to test bidirectional text handling

Pseudo-Localization Implementation

Many i18n libraries support pseudo-localization out of the box. Here is a simple implementation:

const PSEUDO_MAP: Record<string, string> = {
  a: 'à', b: 'ƃ', c: 'ç', d: 'ď', e: 'é',
  f: 'ƒ', g: 'ğ', h: 'ĥ', i: 'ï', j: 'ĵ',
  k: 'ķ', l: 'ļ', m: 'ḿ', n: 'ñ', o: 'ö',
  p: 'þ', q: 'ƣ', r: 'ŕ', s: 'š', t: 'ţ',
  u: 'ü', v: 'ṽ', w: 'ŵ', x: 'х', y: 'ý',
  z: 'ž',
};

function pseudoLocalize(text: string): string {
  const accented = text
    .split('')
    .map((char) => PSEUDO_MAP[char.toLowerCase()] || char)
    .join('');

  // Add ~35% expansion padding
  const padding = '~'.repeat(Math.ceil(text.length * 0.35));
  return `[!! ${accented}${padding} !!]`;
}

pseudoLocalize('Settings');
// "[!! šéţţïñğš~~~ !!]"

Integrating Pseudo-Localization into Development

The most effective approach is to make pseudo-localization a locale option that developers can toggle during local development:

  1. Add a pseudo locale (e.g., en-XA for pseudo-accented, ar-XB for pseudo-RTL) to your locale configuration
  2. Configure your i18n library to use the pseudo-localization transform when the pseudo locale is active
  3. Add a locale switcher to your development UI so developers can toggle between English and pseudo-localized views
  4. Run visual regression tests in pseudo-locale mode to catch expansion and truncation issues before real translations arrive

Libraries like @formatjs/cli include built-in pseudo-localization transforms. React Intl, i18next, and vue-i18n all support custom formatters that can apply pseudo-localization.

What Pseudo-Localization Catches

IssueHow Pseudo-Localization Reveals It
Hardcoded stringsAppear as un-transformed English amid pseudo text
Text truncationEnd markers !!] are cut off
Layout overflowExpanded text breaks containers
Unicode renderingAccented characters fail to display
Concatenation bugsPseudo text appears fragmented
Missing translationsEnglish keys visible without transformation

CI/CD Integration

Internationalization testing should be automated and integrated into your CI/CD pipeline so that i18n regressions are caught on every pull request, not during manual QA cycles weeks later. For a deeper dive on CI/CD testing pipelines, see our CI/CD pipeline guide.

Automated i18n Checks

Add these checks to your CI pipeline:

1. Missing Translation Keys

Compare translation files across locales and fail the build if any key is present in the source locale but missing in a target locale:

# GitHub Actions example
- name: Check translation completeness
  run: |
    node scripts/check-translations.js --source en --targets fr,de,ja,ar
    # Exits with non-zero if missing keys found

2. Hardcoded String Detection

Use ESLint rules to flag user-facing strings that are not wrapped in translation functions:

{
  "rules": {
    "i18next/no-literal-string": ["error", {
      "ignore": ["data-testid", "className", "key"],
      "ignoreCallee": ["console.log", "console.error"]
    }]
  }
}

3. Pseudo-Localization Visual Tests

Run your visual regression suite with pseudo-localization enabled to catch text expansion and truncation:

- name: Visual regression (pseudo-locale)
  run: |
    LOCALE=en-XA npx playwright test --project=visual

Locale Matrix Testing

Run your full test suite across multiple locales using CI matrix builds:

strategy:
  matrix:
    locale: [en-US, de-DE, ja-JP, ar-SA, fr-FR, zh-CN]
steps:
  - name: Run tests for ${{ matrix.locale }}
    run: LOCALE=${{ matrix.locale }} npx playwright test
    env:
      TEST_LOCALE: ${{ matrix.locale }}

This approach runs your entire E2E suite in each locale, catching locale-specific rendering and functional bugs. The trade-off is increased CI time, so you may choose to run the full matrix on main branch merges and a subset (e.g., en-US, ar-SA, ja-JP) on pull requests.

Visual Regression for Locale Variants

Capture baseline screenshots for every supported locale and compare on each build. This catches:

  • RTL layout breakage in Arabic and Hebrew
  • Text overflow from translation expansion in German and Finnish
  • Font rendering issues with CJK characters
  • Locale-specific component differences (date pickers, number inputs)

Automate i18n Testing with AI Agents

Manual internationalization testing across multiple locales is time-consuming and error-prone. AI coding agents equipped with specialized QA skills can automate the most tedious parts of i18n testing and catch locale bugs that generic testing misses entirely.

Install i18n Testing Skills

QA Skills offers specialized skills designed for internationalization and localization testing:

# Find and fix localization bugs -- hardcoded strings, locale-specific
# formatting, missing translations, and l10n antipatterns
npx @qaskills/cli add localization-bug-finder

# Detect timezone-related bugs -- UTC conversion errors, DST handling,
# timezone-dependent display logic, and cross-timezone data integrity
npx @qaskills/cli add timezone-bug-hunter

These skills teach your AI coding agent to:

  • Scan for hardcoded strings and suggest externalization to translation files
  • Identify date/number formatting antipatterns like manual string concatenation instead of Intl API usage
  • Detect timezone handling bugs including UTC conversion errors, DST boundary issues, and timezone-dependent business logic
  • Flag CSS physical properties that will break in RTL layouts and suggest logical property replacements

Complementary Skills for i18n Testing

For comprehensive locale testing, combine the i18n-specific skills with these related skills:

# Visual regression testing -- catch layout breakage from
# translation expansion and RTL mirroring
npx @qaskills/cli add visual-regression

# Responsive layout testing -- verify that translated content
# works across all viewport sizes
npx @qaskills/cli add responsive-layout-breaker

Browse the full catalog of 95+ QA testing skills at qaskills.sh/skills, or get started with the installation guide.

What AI Agents Catch That Manual Testing Misses

AI coding agents with i18n skills are particularly effective at:

  • Exhaustive scanning: Reviewing every file in a codebase for hardcoded strings, something impractical for manual review on large projects
  • Pattern recognition: Identifying locale-sensitive code patterns (date formatting, string concatenation, CSS physical properties) across diverse codebases
  • Consistency enforcement: Ensuring that every new feature follows i18n best practices before it is merged
  • Regression prevention: Checking that previously fixed i18n issues do not reappear in new code

Frequently Asked Questions

What is the difference between internationalization testing and localization testing?

Internationalization testing (i18n testing) validates that your application's code architecture correctly supports multiple locales -- that strings are externalized, formatting uses locale-aware APIs, layouts support bidirectional text, and Unicode is handled properly throughout the stack. It tests the foundation. Localization testing (l10n testing) validates that a specific locale works correctly -- that French translations are accurate and complete, that German text does not overflow UI elements, and that Arabic layouts are properly mirrored. i18n testing happens during development; l10n testing happens after translations and locale-specific content are applied.

How do you test RTL layouts automatically?

The most effective automated approach combines Playwright or Cypress E2E tests that set the HTML dir="rtl" attribute with visual regression snapshots. Set the document direction to RTL, navigate to key pages, and capture screenshots for comparison against baselines. For functional RTL testing, write assertions that verify element positions (e.g., sidebar should be on the right side of the viewport) and that directional icons are mirrored. Run these tests in your CI pipeline with a locale matrix that includes at least one RTL locale (Arabic or Hebrew).

What is pseudo-localization and why should I use it?

Pseudo-localization replaces English strings with accented or expanded versions (e.g., "Settings" becomes "[!! šéţţïñğš~~~ !!]") to reveal i18n issues without real translations. It catches hardcoded strings (they stay in plain English), text truncation (end markers get cut off), layout overflow (expanded text breaks containers), and Unicode rendering problems. The key advantage is timing: you can find and fix i18n issues during development, before translations arrive, when fixes are cheapest.

How many locales should I test in CI?

At minimum, test three representative locales: one LTR Latin-script language (English or French), one RTL language (Arabic or Hebrew), and one CJK language (Japanese, Chinese, or Korean). This covers the major layout directions and character set categories. If your application supports specific high-priority markets, add those locales. Run the full locale matrix on main branch builds and a subset on pull requests to balance coverage with CI speed.

How do I handle plural rules across languages?

Use the ICU MessageFormat standard (supported by libraries like FormatJS, i18next, and vue-i18n) instead of building conditional logic manually. ICU MessageFormat defines plural categories (zero, one, two, few, many, other) that each language maps to specific number ranges. English uses only one and other. Arabic uses all six categories. Polish uses one, few, many, and other. Your translation files should include all required plural forms for each target language, and your i18n tests should verify rendering with representative count values (0, 1, 2, 5, 12, 21, 100) that trigger different plural categories.

Internationalization Testing — i18n, l10n, and Locale Automation | QASkills.sh