ScreenshotNeo

BlogHow-to

How to bulk capture screenshots of pages in multiple languages with Playwright

Capture localized pages in bulk with Playwright by setting a browser locale per job, waiting for the right content, and saving collision-free screenshots.

By the ScreenshotNeo team4 October 202610 min read

To bulk capture screenshots of pages in multiple languages with Playwright, create a separate browser context for each locale, navigate to the appropriate page, wait for a site-specific signal that the localized content is ready, then save the screenshot to a path that includes the locale and page identifier. For bigger batches, run each locale and URL pair as a Playwright Test case with a bounded worker count. Setting a browser locale emulates locale behavior; it does not guarantee that a site will serve translated content.

1. Choose how the site selects a language

First determine whether the site uses language-specific routes, browser language negotiation, or both. If it has explicit localized URLs such as /fr/ and /de/, use those URLs for predictable capture targets. If it negotiates language from browser settings, set the context locale and verify the result with a visible, site-specific signal such as a translated heading.

Keep the locale and URL together in the input data. Locale identifiers such as en-US, de-DE, and ja-JP are examples; use the locales your site supports. A locale setting is not a translation command: routing, translations, fallback behavior, and page readiness remain application-specific.

2. Install Playwright and prepare output folders

This example uses Playwright’s JavaScript library and Chromium. In a new project, install the package and browser:

npm init -y
npm install playwright
npx playwright install chromium

Save the following as capture-locales.mjs. Create the output directory before running it:

mkdir -p screenshots

3. Capture locale and URL pairs with the Playwright library

The script below runs captures sequentially. It creates a fresh context per locale, uses a per-target readiness selector, and writes one full-page PNG to a locale-specific path. Replace the example URLs, selectors, and page keys with values for your site.

import { chromium } from 'playwright';
import { mkdir } from 'node:fs/promises';

const targets = [
  {
    locale: 'en-US',
    url: 'https://example.com/en',
    pageKey: 'home',
    readySelector: 'h1',
  },
  {
    locale: 'de-DE',
    url: 'https://example.com/de',
    pageKey: 'home',
    readySelector: 'h1',
  },
  {
    locale: 'ja-JP',
    url: 'https://example.com/ja',
    pageKey: 'home',
    readySelector: 'h1',
  },
];

const browser = await chromium.launch();
try {
  for (const target of targets) {
    const { locale, url, pageKey, readySelector } = target;
    const context = await browser.newContext({ locale });
    try {
      const page = await context.newPage();
      await page.goto(url, { waitUntil: 'domcontentloaded' });

      // Prefer a meaningful application signal over an arbitrary delay.
      await page.locator(readySelector).waitFor({ state: 'visible' });

      // Confirm the site actually selected the expected localized content.
      const heading = await page.locator(readySelector).innerText();
      if (!heading.trim()) {
        throw new Error(`Empty localized heading for ${locale}: ${url}`);
      }

      const outputDir = `screenshots/${locale}`;
      await mkdir(outputDir, { recursive: true });
      await page.screenshot({
        path: `${outputDir}/${pageKey}.png`,
        fullPage: true,
        animations: 'disabled',
      });
      console.log(`Saved ${locale}: ${outputDir}/${pageKey}.png`);
    } finally {
      await context.close();
    }
  }
} finally {
  await browser.close();
}

Run it with:

node capture-locales.mjs

The Page screenshot API accepts a path and supports screenshot options; context locale is a browser emulation setting. See the [Playwright Page screenshot documentation](https://playwright.dev/docs/screenshots) and [Playwright emulation documentation](https://playwright.dev/docs/emulation).

Choose a readiness condition that represents the finished translation

domcontentloaded means the initial document has been parsed; it does not establish that client-rendered translations, images, or data are ready. The sample waits for a visible heading as a simple placeholder. Better signals may include a translated title, a locale-specific navigation item, or an application-ready marker that appears only after the localized content is rendered.

When the site has no stable selector, use a deliberate delay only as a last resort and choose it from the site’s behavior. A fixed delay can be too short on slow runs and waste time on fast ones. Network-idle conditions can also be a poor proxy for readiness on pages that maintain background requests.

4. Run large batches with Playwright Test

For a larger matrix, Playwright Test gives each capture a named test and lets you limit the number of workers. This makes failures attributable to a locale and page while keeping concurrency explicit. Install the test runner and Chromium if they are not already installed:

npm install --save-dev @playwright/test
npx playwright install chromium

Create playwright.config.js:

import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  workers: 2,
  reporter: 'list',
  use: {
    browserName: 'chromium',
  },
});

Create tests/localized-screenshots.spec.js:

import { test, expect } from '@playwright/test';
import { mkdir } from 'node:fs/promises';

const targets = [
  { locale: 'en-US', url: 'https://example.com/en', pageKey: 'home', expectedHeading: 'Welcome' },
  { locale: 'de-DE', url: 'https://example.com/de', pageKey: 'home', expectedHeading: 'Willkommen' },
  { locale: 'ja-JP', url: 'https://example.com/ja', pageKey: 'home', expectedHeading: 'ようこそ' },
];

for (const target of targets) {
  test(`capture ${target.pageKey} in ${target.locale}`, async ({ browser }) => {
    const context = await browser.newContext({ locale: target.locale });
    try {
      const page = await context.newPage();
      await page.goto(target.url, { waitUntil: 'domcontentloaded' });
      const heading = page.locator('h1');
      await expect(heading).toBeVisible();
      await expect(heading).toContainText(target.expectedHeading);

      const outputDir = `screenshots/${target.locale}`;
      await mkdir(outputDir, { recursive: true });
      await page.screenshot({
        path: `${outputDir}/${target.pageKey}.png`,
        fullPage: true,
        animations: 'disabled',
      });
    } finally {
      await context.close();
    }
  });
}

Run the batch with a chosen worker limit:

npx playwright test --workers=2

Start with a small worker count and increase it only when the machine and target site can handle the additional parallel navigation. Playwright Test workers run in separate processes and use isolated browser contexts. See [Projects](https://playwright.dev/docs/test-projects), [Parallelism](https://playwright.dev/docs/test-parallel), and the [CLI reference](https://playwright.dev/docs/test-cli).

When to use projects

Use projects when the same captures must run under different browser engines or shared configurations. A project can define browser or device settings, while each test case supplies locale and target URL. This adds coverage and runtime, so include additional engines only when they answer a real compatibility question. Playwright projects can target Chromium, Firefox, and WebKit; see the [browser documentation](https://playwright.dev/docs/browsers).

5. Pick screenshot scope and output naming

Choice Use it when Tradeoff
Viewport screenshot You need the visible fold, a responsive check, or a compact artifact. Content below the viewport is omitted.
Full-page screenshot You need a whole-page review or a complete localized page record. Long pages create larger images and may expose lazy-loading behavior that needs attention.
One browser engine You need a focused language batch. Results do not describe rendering in other engines.
Multiple browser projects You need cross-engine comparisons. More jobs require more runtime and environment management.

Playwright’s screenshot API takes a fullPage option. For visual regression, Playwright Test also provides screenshot assertions and reference-image comparisons. Organize files deterministically, for example screenshots/<locale>/<page-key>.png. If repeated runs need to be retained, add a run identifier or version directory so a later capture cannot silently replace an earlier artifact.

See the [screenshot options](https://playwright.dev/docs/screenshots) and [visual comparisons guide](https://playwright.dev/docs/test-snapshots). The guide notes that browser rendering can vary with host operating system, browser version, settings, hardware, power source, headless mode, and other factors. For reliable comparisons, keep the browser and host environment consistent.

6. cURL, Python, and Node.js alternatives

Playwright’s documented APIs are JavaScript/TypeScript, Python, Java, and .NET. cURL is not a browser automation client and cannot configure a browser locale or render a page with Playwright. Use the following only if you want to trigger a hosted screenshot capture instead of operating a local Playwright browser.

Python Playwright library

This runnable Python example follows the same pattern: locale per context, a site-specific readiness selector, and locale-specific output files.

from pathlib import Path
from playwright.async_api import async_playwright
import asyncio

TARGETS = [
    {"locale": "en-US", "url": "https://example.com/en", "page_key": "home"},
    {"locale": "de-DE", "url": "https://example.com/de", "page_key": "home"},
    {"locale": "ja-JP", "url": "https://example.com/ja", "page_key": "home"},
]

async def main():
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        try:
            for target in TARGETS:
                context = await browser.new_context(locale=target["locale"])
                try:
                    page = await context.new_page()
                    await page.goto(target["url"], wait_until="domcontentloaded")
                    await page.locator("h1").wait_for(state="visible")
                    output = Path("screenshots") / target["locale"]
                    output.mkdir(parents=True, exist_ok=True)
                    await page.screenshot(
                        path=str(output / f"{target['page_key']}.png"),
                        full_page=True,
                        animations="disabled",
                    )
                finally:
                    await context.close()
        finally:
            await browser.close()

asyncio.run(main())

Install the Python package and browser with pip install playwright and playwright install chromium. Consult the [Playwright Python documentation](https://playwright.dev/python/docs/intro) for version-specific setup.

cURL request for ScreenshotNeo

A cURL call cannot create a Playwright context. It can request a hosted screenshot from ScreenshotNeo, which is useful when you need screenshot files without maintaining browser installation and capture code. See the [ScreenshotNeo API docs](https://screenshotneo.com/docs/).

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP, or PDF. Its capture flow accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for AI agents and MCP clients.

For a localized target, pass the site’s explicit language URL. The ScreenshotNeo code below demonstrates the one-call capture pattern; it does not set Playwright locale emulation.

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

See the ScreenshotNeo documentation for API parameters. Cookie banners, popups, and chat widgets are removed before the shot; bot checks, blank pages, and failed loads are never billed; an MCP server lets AI agents take screenshots. The Free plan includes 1,000 screenshots a month with no card, and paid plans start at $5 for 3,000. Sign up for free and capture 1,000 screenshots a month without a card.

7. Troubleshooting

Symptom Likely cause Fix
Every screenshot shows the same language The site selects language from its route, saved preference, or account instead of browser locale, or it redirects to a default route. Use the site’s explicit localized URL where available. Check the final URL and assert a translated marker before capture.
Screenshot captures a loading or untranslated page Navigation completion was mistaken for application readiness. Wait for a stable localized selector or application-ready state. Avoid relying on a generic load event as proof that translations are rendered.
Timeout waiting for a selector The selector is wrong for that locale, content did not load, or the expected element is hidden. Inspect the page and route, use a selector shared across translations where possible, and check the locale-specific fallback behavior.
Files overwrite each other Parallel jobs write to the same path. Include locale, page key, and when needed project or run identifier in every output path.
Parallel batch overloads the host or site Too many browser workers or navigations are running together. Lower --workers or the config’s workers value, then raise gradually if resources and the target site permit.
Visual diffs change between runs without content changes Browser version, host OS, fonts, hardware, headless mode, or other rendering inputs differ. Keep capture environment and browser version stable for baseline generation and comparison.
Full-page image misses content loaded near the bottom The page lazy-loads content as it enters the viewport. Scroll through the page or use an application-specific load signal before the screenshot, then confirm the page has rendered the sections you need.

8. Performance, reliability, and cost

Sequential capture is easy to reason about and limits simultaneous load, but total elapsed time grows with the number of targets. A bounded worker count can shorten a large batch by running independent cases concurrently; the appropriate value depends on available CPU and memory, browser cost, and the target site’s tolerance. The sources provide no universal throughput figure, so measure your own workload rather than assuming a fixed captures-per-minute rate.

For repeatable output, keep locale, URL, readiness condition, browser engine, viewport, and output path explicit. Close each context after its capture and close the browser when the batch ends. For retries, retry only failed targets and preserve enough error context to identify locale and URL; avoid allowing retries to overwrite a successful artifact without recording which run produced it.

Local Playwright has no per-screenshot API fee, but you manage the machine, browser downloads, runtime, storage, and maintenance. ScreenshotNeo offers a hosted alternative with Free at 1,000 shots per month, Starter $5 for 3,000, Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000; yearly billing gives two months free. Every feature is on every plan. Choose based on whether browser control and locale emulation must happen in your own Playwright code or a hosted screenshot request better fits the job.

9. FAQ

Does setting locale translate the website?

No. It emulates browser locale behavior. The site decides which language to serve, so use a localized route or verify the rendered translation.

Can I use one context for several locales?

Create a separate context for each locale configuration. This makes each capture’s browser settings explicit and keeps cookies and storage isolated between contexts.

Should I capture full pages or just the viewport?

Use full-page output for whole-page localization review and viewport output for fold-level checks. Ensure lazy-loaded content has appeared before full-page capture.

How many workers should I use?

Start with a modest explicit limit, then adjust based on host resources and the target site’s response. There is no universally correct worker count.

Sources