A Playwright plugin for auditing web applications for globalization and internationalization issues. Detects RTL layout problems, encoding issues, layout stability problems, IME input handling, and language/collation mismatches across single or multiple pages.
Install the package from the monorepo workspace:
npm install @pangaea-g11n/playwrightor with yarn:
yarn add @pangaea-g11n/playwrightThe plugin exports one primary function:
runAudit()— Execute a single-page audit in legacy or structured mode
Import runAudit and execute a globalization audit on a single page:
import { test, expect } from '@playwright/test';
import { runAudit } from '@pangaea-g11n/playwright';
import { join } from 'path';
test('should pass globalization audit', async ({ page }) => {
await page.goto('https://example.com');
const result = await runAudit({
page,
target: { name: 'Homepage', path: '/' },
thresholds: {
RTLAnalyzer: 70,
EncodingAnalyzer: 70,
LayoutStabilityAnalyzer: 60,
IMEAnalyzer: 60,
LanguageAnalyzer: 70,
globalizationScore: 70,
},
resultArtifactsDir: join(process.cwd(), 'reports'),
});
// Verify audit passed
expect(result.audited).toBe(true);
expect(result.globalScore).toBeGreaterThanOrEqual(70);
const thresholdFailures = [
...(result.thresholdFailures ?? []),
...(result.analyzerThresholdFailures ?? []),
];
expect(thresholdFailures.length).toBe(0);
});Required Parameters:
page(Playwright Page) — The page to auditthresholds(object) — Score thresholds for pass/fail evaluationRTLAnalyzer(number, 0-100, optional) — Right-to-left layout supportEncodingAnalyzer(number, 0-100, optional) — Text encoding compatibilityLayoutStabilityAnalyzer(number, 0-100, optional) — Layout stability (overflow detection)IMEAnalyzer(number, 0-100, optional) — Input Method Editor supportLanguageAnalyzer(number, 0-100, optional) — Language mismatch detectionCollationAnalyzer(number, 0-100, optional) — Collation/sorting detectionglobalizationScore(number, 0-100, optional) — Overall globalization score threshold- Set any threshold to 0 to disable that analyzer entirely
Structured Mode Parameters:
target(object) — Page identity for artifact naming and reportingname(string) — Human-readable page label (e.g., "Homepage", "Billing")path(string) — Route path relative to app root (e.g., "/", "/billing")
resultArtifactsDir(string, optional) — Directory to write JSON and text report artifacts
Optional Parameters:
analyzerWeights(object) — Custom weights for overall score calculation- Keys are analyzer names, values are numeric weights (default: 1 for each)
errorSensitivity(number, default: 5) — Sensitivity threshold for detecting issueslanguageTarget(string, default: "en") — Locale code for language-dependent analyzers (e.g., "ja", "fr-FR")defaultThreshold(number, default: 50) — Fallback threshold for analyzers not explicitly configuredsuppressReportIfPerfect(boolean, default: true) — Skip formatted report if all analyzer scores are 100timeoutMs(number, optional) — Timeout (in milliseconds) for the entire audit operationretryOnTransientContextError(boolean, default: true) — Retry once if a transient Playwright execution context error occursbeforeAudit(async function, optional) — Pre-audit hook; useful for navigation and setupbeforeRetry(async function, optional) — Hook invoked before retry attempt (e.g., to wait for recovery)changeLang(async function, optional) — Callback to switch application locale before language-dependent analyzers run- Only invoked if
revertLangis also provided
- Only invoked if
revertLang(async function, optional) — Callback to restore original locale after language-dependent analyzers complete- Only invoked if
changeLangis also provided
- Only invoked if
localeDependentAnalyzerNames(string[], optional) — Override the default list of locale-dependent analyzers- Default:
['LanguageAnalyzer', 'CollationAnalyzer']
- Default:
Returns:
runAudit() supports two modes:
- Legacy mode: omit
targetand receive the rawAuditReport. Threshold failures throw. - Structured mode: provide
targetand receiveMenuAuditResult. Threshold failures are returned in the result.
Structured mode returns a MenuAuditResult object:
{
// Audit metadata
name: string; // From target.name
path: string; // From target.path
audited: boolean; // true if audit completed successfully
error?: string; // Error message if audit failed or thresholds unmet
// Scores
globalScore: number | null; // Overall globalization score (0-100)
analyzerScores: Record<string, number>; // Scores per analyzer (0-100)
// Issues and failures
criticalIssues: AuditIssue[]; // Critical-severity issues only
totalIssues: number;
// Threshold evaluation
analyzerThresholdFailures: string[]; // Which analyzers fell below threshold
thresholdFailures: string[]; // Human-readable summary of all threshold misses
// Artifact files (populated after audit completes)
artifactFilePaths?: {
resultFilePath: string; // Path to JSON result file
outputFilePath: string; // Path to formatted text report
};
}For auditing multiple pages in one run, call runAudit() per target page and assert each result
directly:
import { runAudit } from '@pangaea-g11n/playwright';
import { join } from 'path';
const pages = [
{ name: 'Homepage', path: '/' },
{ name: 'Pricing', path: '/pricing' },
{ name: 'Docs', path: '/docs' },
];
const thresholds = {
RTLAnalyzer: 70,
EncodingAnalyzer: 70,
LayoutStabilityAnalyzer: 60,
IMEAnalyzer: 60,
LanguageAnalyzer: 70,
globalizationScore: 70,
};
const resultDir = join(process.cwd(), 'reports', 'globalization');
const results = [];
for (const target of pages) {
const result = await runAudit({
page: testPage,
target,
thresholds,
resultArtifactsDir: resultDir,
timeoutMs: 150000,
beforeAudit: async () => {
await testPage.goto(`https://example.com${target.path}`);
},
});
results.push(result);
}
expect(results.every((r) => r.audited)).toBe(true);
expect(
results.every((r) => {
const thresholdFailures = [
...(r.thresholdFailures ?? []),
...(r.analyzerThresholdFailures ?? []),
];
return thresholdFailures.length === 0;
}),
).toBe(true);Use the per-page text artifacts and/or the @pangaea-g11n/playwright/pangea-reporter reporter for
human-readable output in CI.
Legacy mode throws an error if thresholds are not met:
test('audit with threshold enforcement', async ({ page }) => {
await page.goto('https://example.com');
try {
await runAudit({
page,
thresholds: {
globalizationScore: 95,
},
});
} catch (error) {
console.log(`Audit failed: ${error.message}`);
// Contains details about scores and issues found
}
});Structured mode returns a result with error, thresholdFailures, and audited populated instead
of throwing.
By default, the plugin runs all analyzers in an English-only context. To test language-dependent
behavior (e.g., text mismatches in different locales), provide changeLang and revertLang
callbacks that orchestrate locale switching via your application's UI. Both callbacks must be
provided together — changeLang is only invoked if revertLang is also supplied, and vice versa.
const changeLang = async (): Promise<void> => {
// Click the footer locale selector
const langButton = page.locator('[data-qa="footer-link-LANGUAGE_SELECTOR"]');
await langButton.click();
// Select the target locale (e.g., Japanese)
const jaOption = page.locator('[data-qa="language-selection-ja"]');
await jaOption.click();
// Wait for locale to settle
await page.waitForLoadState('networkidle');
};
const revertLang = async (): Promise<void> => {
// Switch back to English
const langButton = page.locator('[data-qa="footer-link-LANGUAGE_SELECTOR"]');
await langButton.click();
const enOption = page.locator('[data-qa="language-selection-en"]');
await enOption.click();
await page.waitForLoadState('networkidle');
};
const result = await runAudit({
page,
target: { name: 'Homepage', path: '/' },
thresholds: {
RTLAnalyzer: 70,
EncodingAnalyzer: 70,
LayoutStabilityAnalyzer: 60,
IMEAnalyzer: 60,
LanguageAnalyzer: 70,
CollationAnalyzer: 70,
globalizationScore: 70,
},
resultArtifactsDir: join(process.cwd(), 'reports'),
languageTarget: 'ja',
changeLang,
revertLang,
});Audit Flow with Locale Callbacks:
- Non-locale-dependent analyzers (RTL, Encoding, LayoutStability, IME by default) run in the current locale state
changeLang()callback is invoked to switch application locale via UI- Locale-dependent analyzers (Language, Collation by default — override with
localeDependentAnalyzerNames) run in the target locale revertLang()callback is invoked to restore original locale- All phase results are merged into a single report
Important Notes:
- If
changeLangthrows an error, the audit fails andrevertLangis still attempted for cleanup. - If
revertLangthrows an error after a successful audit, the audit result captures the error. - If callbacks are not provided, locale-dependent analyzers run against the page's existing locale state.
- The plugin does not set the
langattribute for you; locale switching is owned by the callbacks. changeLangandrevertLangshould not resolve until the UI has fully settled.
When resultArtifactsDir is provided, runAudit() writes two files per page:
-
JSON result file —
audit-result-{page-slug}.json- Machine-readable audit result (MenuAuditResult)
- Includes scores, issues, threshold failures
-
Text report file —
globalization-audit-output-{page-slug}.txt- Human-readable formatted report
- Shows analyzer scores, issues by severity, remediation suggestions
File paths are returned in the artifactFilePaths property of the runAudit() result for test
reporting.
Override the default equal weighting for analyzers:
const result = await runAudit({
page,
target: { name: 'Homepage', path: '/' },
thresholds: { globalizationScore: 70 },
resultArtifactsDir,
analyzerWeights: {
RTLAnalyzer: 2, // Double importance for RTL issues
EncodingAnalyzer: 0.5, // Lower importance for encoding
LayoutStabilityAnalyzer: 1,
IMEAnalyzer: 1,
LanguageAnalyzer: 1.5,
CollationAnalyzer: 1,
},
});Adjust how aggressively issues are detected:
const result = await runAudit({
page,
target: { name: 'Homepage', path: '/' },
thresholds: { globalizationScore: 70 },
resultArtifactsDir,
errorSensitivity: 8, // Higher = more sensitive, stricter scoring
});Set a fallback score for analyzers not explicitly configured:
const result = await runAudit({
page,
target: { name: 'Homepage', path: '/' },
thresholds: {
RTLAnalyzer: 80,
// Other analyzers will use defaultThreshold
},
resultArtifactsDir,
defaultThreshold: 70,
});Set an analyzer's threshold to 0 to skip it entirely:
const result = await runAudit({
page,
target: { name: 'Homepage', path: '/' },
thresholds: {
RTLAnalyzer: 0, // Disabled, won't run
EncodingAnalyzer: 70,
LayoutStabilityAnalyzer: 70,
IMEAnalyzer: 70,
LanguageAnalyzer: 70,
},
resultArtifactsDir,
});- Pre-audit setup — Use
beforeAuditto navigate and set up the page in a consistent state - Timeout management — Set
timeoutMsgenerously for CI environments (150000 ms = 2.5 minutes recommended) - Retry resilience — Keep
retryOnTransientContextError: trueto handle flaky Playwright contexts - Artifact organization — Store results in versioned directories (e.g.,
reports/v1.0/globalization) for easy comparison - Locale isolation — Use dedicated test accounts for each locale test to avoid contaminating other workers' state
- RTL Support: Detects missing
dirattributes, hardcoded text directions, and improper RTL styling - Text Encoding: Finds encoding issues and problematic character usage
- Layout Stability: Detects content that overflows containers (self-overflow and parent-overflow)
- IME Support: Identifies input fields that may not handle IME composition correctly
Use in your CI pipeline to prevent globalization regressions:
# In package.json scripts
"test": "playwright test",
"test:glob": "playwright test --grep @glob"// Mark specific tests for globalization auditing
test('@glob should audit homepage', async ({ page }) => {
// ... audit code
});- Globalization Audit DevTools Extension - Interactive browser extension for manual auditing
- Core Analyzers - Detailed documentation of RTL, Encoding, Layout Stability, and IME analyzers