Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Comprehensive Playwright testing guide covering E2E, component, API, visual, accessibility, and security tests.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
testing-patterns/i18n.md
1# Internationalization (i18n) Testing23## Table of Contents451. [Locale Configuration](#locale-configuration)62. [Testing Multiple Locales](#testing-multiple-locales)73. [RTL Layout Testing](#rtl-layout-testing)84. [Date, Time & Number Formats](#date-time--number-formats)95. [Translation Verification](#translation-verification)106. [Visual Regression for i18n](#visual-regression-for-i18n)1112## Locale Configuration1314### Setting Browser Locale1516```typescript17// playwright.config.ts18import { defineConfig, devices } from "@playwright/test";1920export default defineConfig({21projects: [22{23name: "english",24use: {25...devices["Desktop Chrome"],26locale: "en-US",27timezoneId: "America/New_York",28},29},30{31name: "german",32use: {33...devices["Desktop Chrome"],34locale: "de-DE",35timezoneId: "Europe/Berlin",36},37},38{39name: "japanese",40use: {41...devices["Desktop Chrome"],42locale: "ja-JP",43timezoneId: "Asia/Tokyo",44},45},46{47name: "arabic",48use: {49...devices["Desktop Chrome"],50locale: "ar-SA",51timezoneId: "Asia/Riyadh",52},53},54],55});56```5758### Per-Test Locale Override5960```typescript61test("test in French locale", async ({ browser }) => {62const context = await browser.newContext({63locale: "fr-FR",64timezoneId: "Europe/Paris",65});6667const page = await context.newPage();68await page.goto("/");6970// Verify French content71await expect(page.getByRole("button", { name: "Connexion" })).toBeVisible();7273await context.close();74});75```7677### Accept-Language Header7879```typescript80test("server-side locale detection", async ({ browser }) => {81const context = await browser.newContext({82locale: "es-ES",83extraHTTPHeaders: {84"Accept-Language": "es-ES,es;q=0.9,en;q=0.8",85},86});8788const page = await context.newPage();89await page.goto("/");9091// Server should respond with Spanish content92await expect(page.locator("html")).toHaveAttribute("lang", "es");93});94```9596## Testing Multiple Locales9798### Parameterized Locale Tests99100```typescript101const locales = [102{ locale: "en-US", greeting: "Hello", button: "Sign In" },103{ locale: "de-DE", greeting: "Hallo", button: "Anmelden" },104{ locale: "fr-FR", greeting: "Bonjour", button: "Se connecter" },105{ locale: "ja-JP", greeting: "こんにちは", button: "ログイン" },106];107108for (const { locale, greeting, button } of locales) {109test(`login page in ${locale}`, async ({ browser }) => {110const context = await browser.newContext({ locale });111const page = await context.newPage();112113await page.goto("/login");114115await expect(page.getByText(greeting)).toBeVisible();116await expect(page.getByRole("button", { name: button })).toBeVisible();117118await context.close();119});120}121```122123### Locale Fixture124125```typescript126// fixtures/i18n.ts127import { test as base } from "@playwright/test";128129type LocaleFixtures = {130localePage: (locale: string) => Promise<Page>;131};132133export const test = base.extend<LocaleFixtures>({134localePage: async ({ browser }, use) => {135const pages: Page[] = [];136137const createLocalePage = async (locale: string) => {138const context = await browser.newContext({ locale });139const page = await context.newPage();140pages.push(page);141return page;142};143144await use(createLocalePage);145146// Cleanup147for (const page of pages) {148await page.context().close();149}150},151});152153// Usage154test("compare locales", async ({ localePage }) => {155const enPage = await localePage("en-US");156const dePage = await localePage("de-DE");157158await enPage.goto("/pricing");159await dePage.goto("/pricing");160161const enPrice = await enPage.getByTestId("price").textContent();162const dePrice = await dePage.getByTestId("price").textContent();163164expect(enPrice).toContain("$");165expect(dePrice).toContain("€");166});167```168169### Testing Locale Switching170171```typescript172test("user can switch locale", async ({ page }) => {173await page.goto("/");174175// Initial locale (from browser)176await expect(page.locator("html")).toHaveAttribute("lang", "en");177178// Switch to German179await page.getByRole("button", { name: "Language" }).click();180await page.getByRole("menuitem", { name: "Deutsch" }).click();181182// Verify switch183await expect(page.locator("html")).toHaveAttribute("lang", "de");184await expect(page.getByRole("heading", { level: 1 })).toContainText(185/Willkommen/,186);187188// Verify persistence (reload)189await page.reload();190await expect(page.locator("html")).toHaveAttribute("lang", "de");191});192```193194## RTL Layout Testing195196### Setting Up RTL Tests197198```typescript199// playwright.config.ts200export default defineConfig({201projects: [202{203name: "rtl-arabic",204use: {205locale: "ar-SA",206// RTL is usually set by the app based on locale207},208},209{210name: "rtl-hebrew",211use: {212locale: "he-IL",213},214},215],216});217```218219### Verifying RTL Direction220221```typescript222test("RTL layout is applied", async ({ page }) => {223await page.goto("/");224225// Check document direction226await expect(page.locator("html")).toHaveAttribute("dir", "rtl");227228// Or check computed style229const direction = await page.evaluate(() => {230return window.getComputedStyle(document.body).direction;231});232expect(direction).toBe("rtl");233});234```235236### RTL-Specific Element Positioning237238```typescript239test("sidebar is on the right in RTL", async ({ page }) => {240await page.goto("/dashboard");241242const sidebar = page.getByTestId("sidebar");243const main = page.getByTestId("main-content");244245const sidebarBox = await sidebar.boundingBox();246const mainBox = await main.boundingBox();247248// In RTL, sidebar should be to the right of main content249expect(sidebarBox!.x).toBeGreaterThan(mainBox!.x);250});251```252253### RTL Visual Regression254255```typescript256test("RTL layout matches snapshot", async ({ page }) => {257await page.goto("/");258259// Screenshot for RTL comparison260await expect(page).toHaveScreenshot("homepage-rtl.png", {261// Separate snapshots per locale/direction262fullPage: true,263});264});265266// LTR comparison267test("LTR layout matches snapshot", async ({ browser }) => {268const context = await browser.newContext({ locale: "en-US" });269const page = await context.newPage();270271await page.goto("/");272await expect(page).toHaveScreenshot("homepage-ltr.png", { fullPage: true });273});274```275276### Testing Bidirectional Text277278```typescript279test("bidirectional text renders correctly", async ({ page }) => {280await page.goto("/profile");281282// Mixed LTR/RTL content283const nameField = page.getByTestId("full-name");284285// Arabic name with English email286await expect(nameField).toContainText("محمد ([email protected])");287288// Verify text doesn't overlap or break289const box = await nameField.boundingBox();290expect(box!.width).toBeGreaterThan(100); // Content not collapsed291});292```293294## Date, Time & Number Formats295296### Testing Date Formats297298```typescript299test("dates are formatted per locale", async ({ browser }) => {300const testDate = new Date("2024-03-15");301302const formats = [303{ locale: "en-US", expected: "March 15, 2024" },304{ locale: "en-GB", expected: "15 March 2024" },305{ locale: "de-DE", expected: "15. März 2024" },306{ locale: "ja-JP", expected: "2024年3月15日" },307];308309for (const { locale, expected } of formats) {310const context = await browser.newContext({ locale });311const page = await context.newPage();312313await page.goto(`/event?date=${testDate.toISOString()}`);314315const dateDisplay = page.getByTestId("event-date");316await expect(dateDisplay).toContainText(expected);317318await context.close();319}320});321```322323### Testing Number Formats324325```typescript326test("numbers are formatted per locale", async ({ browser }) => {327const testNumber = 1234567.89;328329const formats = [330{ locale: "en-US", expected: "1,234,567.89" },331{ locale: "de-DE", expected: "1.234.567,89" },332{ locale: "fr-FR", expected: "1 234 567,89" },333];334335for (const { locale, expected } of formats) {336const context = await browser.newContext({ locale });337const page = await context.newPage();338339await page.goto(`/stats?value=${testNumber}`);340341await expect(page.getByTestId("formatted-number")).toHaveText(expected);342343await context.close();344}345});346```347348### Testing Currency Formats349350```typescript351test("currency displays correctly", async ({ browser }) => {352const price = 99.99;353354const currencies = [355{ locale: "en-US", currency: "USD", expected: "$99.99" },356{ locale: "de-DE", currency: "EUR", expected: "99,99 €" },357{ locale: "ja-JP", currency: "JPY", expected: "¥100" }, // JPY has no decimals358{ locale: "en-GB", currency: "GBP", expected: "£99.99" },359];360361for (const { locale, currency, expected } of currencies) {362const context = await browser.newContext({ locale });363const page = await context.newPage();364365await page.goto(`/product?price=${price}¤cy=${currency}`);366367await expect(page.getByTestId("price")).toContainText(expected);368369await context.close();370}371});372```373374## Translation Verification375376### Checking for Missing Translations377378```typescript379test("no missing translations", async ({ page }) => {380await page.goto("/");381382// Common patterns for missing translations383const missingPatterns = [384/\{\{.*\}\}/, // Handlebars-style385/\$\{.*\}/, // Template literal style386/t\(["'][\w.]+["']\)/, // i18n key exposed387/MISSING_TRANSLATION/, // Common placeholder388/\[UNTRANSLATED\]/, // Another placeholder389];390391const bodyText = await page.locator("body").textContent();392393for (const pattern of missingPatterns) {394expect(bodyText).not.toMatch(pattern);395}396});397```398399### Detecting Text Overflow400401```typescript402test("translations fit UI containers", async ({ browser }) => {403const locales = ["en-US", "de-DE", "fr-FR", "es-ES"];404const issues: string[] = [];405406for (const locale of locales) {407const context = await browser.newContext({ locale });408const page = await context.newPage();409await page.goto("/");410411const overflowing = await page.evaluate(() => {412const elements = document.querySelectorAll("button, .label, h1, h2, h3");413return Array.from(elements)414.filter(415(el) =>416(el as HTMLElement).scrollWidth > (el as HTMLElement).clientWidth,417)418.map((el) => `${el.tagName}: "${el.textContent?.substring(0, 20)}..."`);419});420421if (overflowing.length > 0)422issues.push(`${locale}: ${overflowing.join(", ")}`);423await context.close();424}425426expect(issues).toEqual([]);427});428```429430## Visual Regression for i18n431432### Locale-Specific Snapshots433434```typescript435// playwright.config.ts436export default defineConfig({437snapshotPathTemplate:438"{testDir}/__snapshots__/{projectName}/{testFilePath}/{arg}{ext}",439440projects: [441{ name: "en-US", use: { locale: "en-US" } },442{ name: "de-DE", use: { locale: "de-DE" } },443{ name: "ja-JP", use: { locale: "ja-JP" } },444{ name: "ar-SA", use: { locale: "ar-SA" } },445],446});447```448449```typescript450// test file451test("homepage visual", async ({ page }) => {452await page.goto("/");453454// Snapshot auto-saved to {projectName}/homepage.png455await expect(page).toHaveScreenshot("homepage.png");456});457```458459### Critical Element Screenshots460461```typescript462test("navigation in all locales", async ({ page }) => {463await page.goto("/");464465// Just the nav - catches overflow, truncation466const nav = page.getByRole("navigation");467await expect(nav).toHaveScreenshot("navigation.png");468});469470test("buttons dont truncate", async ({ page }) => {471await page.goto("/checkout");472473const ctaButton = page.getByRole("button", {474name: /checkout|kaufen|acheter/i,475});476await expect(ctaButton).toHaveScreenshot("checkout-button.png");477});478```479480### Font Loading for i18n481482```typescript483test("wait for fonts before screenshot", async ({ page }) => {484await page.goto("/");485486// Wait for fonts (important for CJK, Arabic)487await page.evaluate(() => document.fonts.ready);488await page.waitForFunction(() =>489document.fonts.check("16px 'Noto Sans Arabic'"),490);491492await expect(page).toHaveScreenshot("with-fonts.png");493});494```495496## Anti-Patterns to Avoid497498| Anti-Pattern | Problem | Solution |499| ------------------------- | ------------------------------- | ------------------------------- |500| Hardcoded text assertions | Breaks in other locales | Use test IDs or parameterize |501| Single locale testing | Misses i18n bugs | Test multiple locales |502| Ignoring RTL | Layout broken for RTL users | Dedicated RTL project |503| No font wait | Screenshots with fallback fonts | Wait for `document.fonts.ready` |504505## Related References506507- **Clock Mocking**: See [clock-mocking.md](../advanced/clock-mocking.md) for timezone testing508- **Mobile Testing**: See [mobile-testing.md](../advanced/mobile-testing.md) for device-specific locales509