Localized website screenshots

To capture a site the way a visitor in another country sees it, you set up to three browser signals: the language (browser.locale), the clock (browser.timezone) and the position (browser.geolocation). Each one reaches a different part of the site. This guide shows which part, so you can send only what the target site reads. More guides are on the guides hub.

RenderScreenshot does not translate pages. These options ask the site for the version it already has.

Last verified on 2026-10-15 against the renderer source and a local Puppeteer 24.43.1 (Chrome 148) replay of the same browser calls.

What each option changes

We replayed the renderer's calls against a local test page and read the results from the server and from page scripts:

Signal the site might read locale: "fr-FR" timezone: "Asia/Tokyo" geolocation
Accept-Language request header fr-FR (exactly this value) unchanged unchanged
navigator.language / navigator.languages fr-FR / ["fr-FR"] unchanged unchanged
Intl.DateTimeFormat().resolvedOptions().locale unchanged (en-US in our replay) unchanged unchanged
(1234567.891).toLocaleString() unchanged (1,234,567.891) unchanged unchanged
Time zone of Date and Intl unchanged Asia/Tokyo unchanged
navigator.geolocation.getCurrentPosition() unchanged unchanged Your coordinates, no permission prompt
Client IP address unchanged unchanged unchanged

Two rows need attention:

  • Number and date formats. locale changes the header and navigator.language, but not the default locale of Intl. A page that calls toLocaleString() with no argument still formats numbers the US way. A page that passes navigator.language to Intl gets French formats. Server-rendered pages that format with the Accept-Language header also get French formats.
  • IP address. The browser runs on Cloudflare's network, and Cloudflare documents that Browser Run does not rotate IP addresses (Browser Run FAQ, checked 2026-10-15). A site that redirects by IP sends the version for wherever the request leaves Cloudflare.

Choose the signal the site uses

The site picks its version by Send
URL path or subdomain (/de/, de.example.com) That URL. This is the most reliable method.
Accept-Language header browser.locale
navigator.language in JavaScript browser.locale
Visitor's clock (opening hours, "today" labels, event times) browser.timezone
Browser location API (store finders, maps) browser.geolocation
IP address The site's country URL or country selector. No option changes the IP.

The minimal request

GET supports locale and timezone:

curl -sS --fail -G \
  -H "Authorization: Bearer $RS_API_KEY" \
  --data-urlencode "url=https://example.com" \
  --data-urlencode "locale=de-DE" \
  --data-urlencode "timezone=Europe/Berlin" \
  https://api.renderscreenshot.com/v1/screenshot \
  -o de-DE.png

geolocation is POST only. A full German request with a Berlin position:

curl

curl -sS --fail -X POST https://api.renderscreenshot.com/v1/screenshot \
  -H "Authorization: Bearer $RS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
      "url": "https://example.com",
      "browser": {
        "locale": "de-DE",
        "timezone": "Europe/Berlin",
        "geolocation": {
          "latitude": 52.52,
          "longitude": 13.405,
          "accuracy": 100
        }
      }
    }' \
  -o de-DE.png

Node.js

import { writeFile } from 'node:fs/promises';

const body = {
  "url": "https://example.com",
  "browser": {
    "locale": "de-DE",
    "timezone": "Europe/Berlin",
    "geolocation": {
      "latitude": 52.52,
      "longitude": 13.405,
      "accuracy": 100
    }
  }
};

const res = await fetch('https://api.renderscreenshot.com/v1/screenshot', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.RS_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(body),
});

if (!res.ok) {
  console.error(res.status, await res.text());
  process.exit(1);
}

await writeFile('de-DE.png', Buffer.from(await res.arrayBuffer()));

Python

import json
import os
import sys
import urllib.error
import urllib.request

body = {
    'url': 'https://example.com',
    'browser': {
        'locale': 'de-DE',
        'timezone': 'Europe/Berlin',
        'geolocation': {
            'latitude': 52.52,
            'longitude': 13.405,
            'accuracy': 100,
        },
    },
}

req = urllib.request.Request(
    'https://api.renderscreenshot.com/v1/screenshot',
    data=json.dumps(body).encode(),
    headers={
        'Authorization': f"Bearer {os.environ['RS_API_KEY']}",
        'Content-Type': 'application/json',
    },
    method='POST',
)

try:
    with urllib.request.urlopen(req, timeout=90) as res:
        with open('de-DE.png', 'wb') as f:
            f.write(res.read())
except urllib.error.HTTPError as err:
    sys.exit(f'{err.code} {err.read().decode()}')

Ruby

require 'json'
require 'net/http'

body = {
  url: 'https://example.com',
  browser: {
    locale: 'de-DE',
    timezone: 'Europe/Berlin',
    geolocation: {
      latitude: 52.52,
      longitude: 13.405,
      accuracy: 100
    }
  }
}

uri = URI('https://api.renderscreenshot.com/v1/screenshot')
request = Net::HTTP::Post.new(uri)
request['Authorization'] = "Bearer #{ENV.fetch('RS_API_KEY')}"
request['Content-Type'] = 'application/json'
request.body = body.to_json

response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https', read_timeout: 90) do |http|
  http.request(request)
end
abort("#{response.code} #{response.body}") unless response.is_a?(Net::HTTPSuccess)

File.binwrite('de-DE.png', response.body)

PHP

<?php

$body = [
    'url' => 'https://example.com',
    'browser' => [
        'locale' => 'de-DE',
        'timezone' => 'Europe/Berlin',
        'geolocation' => [
            'latitude' => 52.52,
            'longitude' => 13.405,
            'accuracy' => 100,
        ],
    ],
];

$ch = curl_init('https://api.renderscreenshot.com/v1/screenshot');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('RS_API_KEY'),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode($body, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 90,
]);

$image = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($image === false || $status !== 200) {
    fwrite(STDERR, $status . ' ' . ($image === false ? curl_error($ch) : $image) . "\n");
    exit(1);
}

file_put_contents('de-DE.png', $image);

Go

package main

import (
    "bytes"
    "io"
    "log"
    "net/http"
    "os"
)

const body = `{
  "url": "https://example.com",
  "browser": {
    "locale": "de-DE",
    "timezone": "Europe/Berlin",
    "geolocation": {
      "latitude": 52.52,
      "longitude": 13.405,
      "accuracy": 100
    }
  }
}`

func main() {
    req, err := http.NewRequest(http.MethodPost, "https://api.renderscreenshot.com/v1/screenshot", bytes.NewBufferString(body))
    if err != nil {
        log.Fatal(err)
    }
    req.Header.Set("Authorization", "Bearer "+os.Getenv("RS_API_KEY"))
    req.Header.Set("Content-Type", "application/json")

    res, err := http.DefaultClient.Do(req)
    if err != nil {
        log.Fatal(err)
    }
    defer res.Body.Close()

    data, err := io.ReadAll(res.Body)
    if err != nil {
        log.Fatal(err)
    }
    if res.StatusCode != http.StatusOK {
        log.Fatalf("%d %s", res.StatusCode, data)
    }

    if err := os.WriteFile("de-DE.png", data, 0o644); err != nil {
        log.Fatal(err)
    }
}

Java

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;

public class Capture {
    public static void main(String[] args) throws Exception {
        String body = """
            {
              "url": "https://example.com",
              "browser": {
                "locale": "de-DE",
                "timezone": "Europe/Berlin",
                "geolocation": {
                  "latitude": 52.52,
                  "longitude": 13.405,
                  "accuracy": 100
                }
              }
            }
            """;

        HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.renderscreenshot.com/v1/screenshot"))
            .header("Authorization", "Bearer " + System.getenv("RS_API_KEY"))
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();

        HttpResponse<byte[]> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofByteArray());
        if (response.statusCode() != 200) {
            System.err.println(response.statusCode() + " " + new String(response.body()));
            System.exit(1);
        }

        Files.write(Path.of("de-DE.png"), response.body());
    }
}

The renderer grants the location permission for the target page before it loads, so the site gets the position without a prompt. In our replay, getCurrentPosition() returned latitude 52.52, longitude 13.405 and accuracy 100.

One page in several locales

Each locale is a separate render and a separate cache entry. A shell loop over the GET form:

set -euo pipefail
for pair in en-US:America/New_York de-DE:Europe/Berlin ja-JP:Asia/Tokyo; do
  locale="${pair%%:*}"
  tz="${pair#*:}"
  curl -sS --fail -G \
    -H "Authorization: Bearer $RS_API_KEY" \
    --data-urlencode "url=https://example.com" \
    --data-urlencode "locale=$locale" \
    --data-urlencode "timezone=$tz" \
    https://api.renderscreenshot.com/v1/screenshot \
    -o "home-$locale.png"
done

Non-Latin scripts render without extra fonts

Browser Run ships fonts for Chinese, Japanese and Korean (Noto CJK, IPAfont Gothic, WenQuanYi Zen Hei), Arabic (KACST), Thai (TLWG) and several Indic scripts, according to Cloudflare's supported fonts list (checked 2026-10-15). A Japanese page that declares a web font still uses that font. Right-to-left layout comes from the page's own dir attribute. There is nothing to set in the request.

Parameters that matter

Parameter Format GET name Validation
browser.locale BCP 47, for example de-DE locale Rejected with invalid_locale if not BCP 47
browser.timezone IANA, for example Europe/Berlin timezone Rejected with invalid_timezone if unknown
browser.geolocation.latitude number POST only -90 to 90
browser.geolocation.longitude number POST only -180 to 180
browser.geolocation.accuracy meters POST only Default 100

Troubleshooting

Symptom Cause Fix
Page stays in English with locale: "de-DE" The site picks the language by URL or IP Request the German URL.
German text, US number formats The page formats with the default Intl locale Expected. Use the site's localized URL if it formats on the server.
Wrong country version, right language Geo-IP redirect Use the country URL. The IP cannot be changed with these options.
Times shown in UTC No timezone sent Add browser.timezone.
Store finder shows "location unavailable" geolocation was sent with GET Send it in a POST body.
Empty boxes instead of characters The page asks for a font that failed to load, with no fallback Check the page's font URLs. The renderer's built-in CJK fonts cover the fallback.

Related in this topic

Was this page helpful?