Skip to content
ExponaBLOG
PLAYWRIGHT & BROWSER AUTOMATION · DAY 056 / 100

Testing localization: Playwright lessons from production

Verify translated UI.

Expona Admin9 min read

Introduction

Localization testing is often an afterthought in automation strategies, yet it's one of the most visible failure points in production. Testing localization: Playwright lessons from production means treating translated UIs with the same rigor as your core features—verifying that strings render correctly, layouts adapt to different text lengths, and bidirectional text behaves as expected.

We've seen production incidents where a German translation pushed buttons off-screen, Arabic layouts reversed unexpectedly, and Japanese characters truncated silently. Playwright makes it practical to catch these issues before they reach users, but the approach differs from testing a single language. This article walks through the patterns we've refined across e-commerce and SaaS products serving 20+ language markets.

Set locale per project

The foundation of localization testing is controlling which locale your tests run under. Playwright provides locale in the browser context configuration, which affects not only language strings but also date/time formatting, number formatting, and currency display.

Rather than creating separate test suites for each language, we configure locale at the project level so you can run the full test suite against multiple locales in parallel. This approach scales better than duplicating tests and keeps your test logic DRY.

typescript
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  projects: [
    {
      name: 'chromium-en-US',
      use: { ...devices['Desktop Chrome'], locale: 'en-US' },
    },
    {
      name: 'chromium-de-DE',
      use: { ...devices['Desktop Chrome'], locale: 'de-DE' },
    },
    {
      name: 'chromium-ar-SA',
      use: { ...devices['Desktop Chrome'], locale: 'ar-SA' },
    },
    {
      name: 'chromium-ja-JP',
      use: { ...devices['Desktop Chrome'], locale: 'ja-JP' },
    },
  ],
});

The locale setting automatically handles system-level formatting. However, your application's UI strings typically come from an i18n library (like i18next, Intl, or a custom system). You'll still need to ensure your test environment loads the correct translation files. For server-rendered or API-driven translations, set the Accept-Language header in your API calls:

typescript
// Set locale via Accept-Language header
test('checkout form displays in German', async ({ page }) => {
  // For API-driven translations
  await page.setExtraHTTPHeaders({ 'Accept-Language': 'de-DE' });
  
  await page.goto('https://app.example.com/checkout');
  
  // Verify German labels appear
  await expect(page.locator('button:has-text("Zahlungsart")')).toBeVisible();
});

If your frontend loads translations from a CDN or local bundle, coordinate with your build process so test environments pull the right language files. Some teams inject a query parameter or environment variable to force a specific locale:

typescript
test('payment methods display in French', async ({ page }) => {
  // Inject locale via query param if your app supports it
  await page.goto('https://app.example.com/checkout?lang=fr-FR');
  
  // Verify French labels
  await expect(page.locator('text=Carte de crédit')).toBeVisible();
});

Assert key strings per language

Once locale is configured, verify that your UI displays the correct translated strings. The critical point here is testing semantic meaning, not just presence of any text. A missing translation fallback might still show English text, which your test could miss if you're only checking for text visibility.

Use a centralized translation map in your tests to assert the expected strings for each language. This keeps assertions maintainable and prevents hardcoding translated text scattered throughout your test files.

typescript
// translations.test.ts
const TRANSLATIONS = {
  'en-US': {
    checkoutTitle: 'Checkout',
    paymentMethod: 'Payment Method',
    confirmOrder: 'Confirm Order',
    freeShipping: 'Free Shipping',
  },
  'de-DE': {
    checkoutTitle: 'Kasse',
    paymentMethod: 'Zahlungsart',
    confirmOrder: 'Bestellung bestätigen',
    freeShipping: 'Versand frei',
  },
  'ja-JP': {
    checkoutTitle: 'チェックアウト',
    paymentMethod: 'お支払い方法',
    confirmOrder: '注文を確認する',
    freeShipping: '送料無料',
  },
};

test('checkout labels match expected translations', async ({ page }, testInfo) => {
  // Extract locale from project name
  const locale = testInfo.project.name.split('-').slice(1).join('-');
  const expectedStrings = TRANSLATIONS[locale];

  await page.goto('https://app.example.com/checkout');

  // Assert each key string
  for (const [key, expectedText] of Object.entries(expectedStrings)) {
    const locator = page.locator(`text=${expectedText}`);
    await expect(locator).toBeVisible({ timeout: 5000 });
  }
});

For applications with dynamic content (product names, user-generated text), validate that placeholders and formatting work correctly across languages. Languages with longer translations (German, for example) may require more space; shorter languages (Chinese, Japanese) pack more meaning into fewer characters.

typescript
test('currency and number formatting matches locale', async ({ page }, testInfo) => {
  const locale = testInfo.project.name.split('-').slice(1).join('-');
  
  await page.goto('https://app.example.com/cart');
  
  // Check currency symbol and decimal separator
  if (locale === 'de-DE') {
    // German: 19,99 € (comma decimal, euro)
    await expect(page.locator('text=19,99 €')).toBeVisible();
  } else if (locale === 'en-US') {
    // US: $19.99 (period decimal, dollar)
    await expect(page.locator('text=$19.99')).toBeVisible();
  } else if (locale === 'fr-FR') {
    // French: 19,99 € (comma decimal, euro with space)
    await expect(page.locator('text=19,99 €')).toBeVisible();
  }
});

Check right-to-left layouts

Right-to-left (RTL) languages like Arabic, Hebrew, and Farsi require mirrored layouts. Text direction changes, which means icons and UI elements must flip. Buttons that sit on the right in LTR should sit on the left in RTL, and margin/padding logic often needs to reverse.

Playwright can verify RTL behavior by checking both the DOM's dir attribute and visual element positions. Test that form inputs, navigation bars, and modal dialogs all reflect the RTL direction.

typescript
test('RTL layout applies to Arabic locale', async ({ page }) => {
  await page.goto('https://app.example.com/checkout?lang=ar-SA');
  
  // Verify  element has dir="rtl"
  const htmlDir = await page.locator('html').getAttribute('dir');
  expect(htmlDir).toBe('rtl');
  
  // Verify key layout elements respect RTL
  const sidebar = page.locator('[data-test-id="sidebar"]');
  const sidebarBox = await sidebar.boundingBox();
  
  // In RTL, sidebar should be on the right (high x coordinate)
  expect(sidebarBox.x).toBeGreaterThan(500);
});

For form layouts, verify that labels appear before or after inputs based on direction. In RTL layouts, form structure often mirrors, which can be a source of bugs if CSS isn't properly set up with logical properties (e.g., margin-inline-start instead of margin-left).

typescript
test('form labels positioned correctly in RTL', async ({ page }) => {
  await page.goto('https://app.example.com/profile?lang=ar-SA');
  
  const nameLabel = page.locator('label:has-text("الاسم")');
  const nameInput = page.locator('#name-input');
  
  const labelBox = await nameLabel.boundingBox();
  const inputBox = await nameInput.boundingBox();
  
  // In RTL, label should appear to the right of (higher x) the input
  expect(labelBox.x).toBeGreaterThan(inputBox.x + inputBox.width);
});

Screenshot tests catch overflow

Visual regression testing is particularly powerful for localization. Different text lengths, fonts, and character widths can cause buttons to overflow, text to truncate unexpectedly, or modals to overflow their containers. Screenshot tests catch these issues that string assertions alone would miss.

Use Playwright's built-in screenshot comparison with a baseline per locale. Store baseline screenshots for each language and screen size combination, then compare against new renders to detect visual drift.

typescript
test.describe('Localization visual regression', () => {
  test('checkout form renders correctly in each locale', async ({ page }, testInfo) => {
    const locale = testInfo.project.name.split('-').slice(1).join('-');
    
    await page.goto('https://app.example.com/checkout');
    await page.waitForLoadState('networkidle');
    
    // Capture screenshot with locale in filename
    await expect(page).toHaveScreenshot(`checkout-${locale}.png`);
  });

  test('product card labels do not overflow', async ({ page }, testInfo) => {
    const locale = testInfo.project.name.split('-').slice(1).join('-');
    
    await page.goto('https://app.example.com/products');
    
    // Take screenshot of first product card
    const productCard = page.locator('[data-test-id="product-card"]').first();
    await expect(productCard).toHaveScreenshot(`product-card-${locale}.png`);
  });
});

Screenshot tests work best when combined with viewport standardization. Test common breakpoints (mobile, tablet, desktop) for each locale to catch responsive design issues that are language-specific:

typescript
test.describe('Mobile RTL layout', () => {
  test.use({ viewport: { width: 375, height: 667 } });

  test('navbar fits on mobile in RTL', async ({ page }) => {
    await page.goto('https://app.example.com?lang=ar-SA');
    await expect(page).toHaveScreenshot('navbar-mobile-rtl.png');
  });
});

When screenshot baselines diverge, review the diff carefully. Legitimate translation differences (longer German labels, compact Japanese) will show visual changes that aren't bugs. Establish a clear review process so the team agrees on which visual changes are expected for each locale.

Common mistakes

  • Forgetting to set locale per project: Running all tests in en-US and assuming they'll pass in other languages leads to missed bugs. Use Playwright's project configuration to test multiple locales in parallel.
  • Hardcoding translated strings in tests: When translation files update, hardcoded assertions become stale. Maintain a translation map or fetch strings dynamically from your i18n bundle.
  • Ignoring number and date formatting: The locale setting in Playwright affects how Intl APIs format numbers and dates. Tests that pass in en-US may fail in de-DE due to comma vs. period decimals.
  • Not testing RTL with CSS Logical Properties: CSS Logical Properties (like margin-inline-start) simplify RTL support, but if your codebase still uses margin-left, RTL tests will catch regressions quickly.
  • Screenshot baselines without locale context: Storing one screenshot baseline across all locales leads to false positives. Always include locale in the baseline filename and review diffs with the full locale context.
  • Testing only major languages: Developers often test en, de, and ar but skip smaller markets. A single test project covering 15 locales runs in the same CI time as one and catches more edge cases.

Key takeaways

  • Use Playwright's project-level locale configuration to run the full test suite across multiple languages in parallel, avoiding test duplication.
  • Maintain a centralized translation map in your tests so assertions reference expected strings by key, not hardcoded text, making tests robust to translation updates.
  • For RTL languages, explicitly verify the dir attribute and check element positioning to ensure layouts actually mirror, not just text direction.
  • Screenshot tests are your best defense against overflow, truncation, and responsive layout issues that vary by language and string length.
  • Test both system-level formatting (currency, dates) via locale and UI string correctness via your i18n library, since they're independent concerns.
  • Include locale in screenshot baselines and review diffs with full context; what looks like a regression may be a legitimate translation-driven layout change.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles