# Test locales and timezones for hydration errors

> Use the matrix option to test locales and timezones for hydration errors, plus themes, viewports and browsers, in a small set of pairwise runs.

Source: https://hydration.jscrate.dev/docs/environment-matrix
Last updated: 2026-09-18

To test locales and timezones for hydration errors, list the values in the
`matrix` option. hydration-proof runs each scenario in combinations of them,
and of themes, viewports, browsers, network and CPU speeds, then says which
value a finding depends on. Pairwise selection keeps the run small: every pair
of values is tested at least once.

## Why test more than one environment?

Most hydration bugs are environment bugs: a date formatted in the browser's
[timezone](https://hydration.jscrate.dev/docs/causes/timezone) or [locale](https://hydration.jscrate.dev/docs/causes/locale), a
[theme](https://hydration.jscrate.dev/docs/causes/theme) read from `prefers-color-scheme`, or a layout that
[branches on screen size](https://hydration.jscrate.dev/docs/causes/media-query). The server renders one
version and the visitor's browser another.

Without a matrix, hydration-proof tests one scenario named `default`. It uses
this machine's locale and timezone and a light color scheme, so a server
started on the same machine renders with the same settings. That is the one
combination where these bugs never show up.

## How to test locales and timezones for hydration errors

Add a `matrix` to the config. Pick values that differ in ways your code cares
about:

```ts title="hydration-proof.config.ts"
import { defineConfig } from "hydration-proof";

export default defineConfig({
  matrix: {
    // A right-to-left locale, and timezones on both sides of UTC.
    locale: ["en-US", "de-DE", "ar-EG"],
    timezoneId: ["UTC", "Asia/Karachi", "America/Los_Angeles"],
    colorScheme: ["light", "dark"],
    viewport: ["desktop", "mobile"],
    browser: ["chromium", "firefox", "webkit"],
    // A returning visitor: scripts arrive in a different order.
    cache: ["cold", "warm"],
    // Chromium only; other browsers skip it with a note.
    cpu: [1, 4],

    // Anything the tool cannot guess. Each value is scenario settings.
    axes: {
      flags: {
        "new-checkout": { cookies: [{ name: "flag_checkout", value: "new" }] },
        "old-checkout": {},
      },
      currency: {
        eur: { localStorage: { currency: "EUR" } },
        jpy: { localStorage: { currency: "JPY" } },
      },
    },

    strategy: "pairwise",
    max: 16,
  },
});
```

Then run the tests as usual:

```bash
npx hydration-proof test
```

This config has 1,728 combinations. Pairwise testing covers every pair of
values with about a dozen of them (11 here), because pairs of values are what
usually breaks.

## Which axes can you vary?

| Axis            | Values                                                                                           |
| --------------- | ------------------------------------------------------------------------------------------------ |
| `locale`        | Locales such as `'de-DE'`. Also sets `Accept-Language`                                           |
| `timezoneId`    | IANA timezones such as `'Asia/Karachi'`                                                          |
| `colorScheme`   | `'light'`, `'dark'`, `'no-preference'`                                                           |
| `reducedMotion` | `'reduce'`, `'no-preference'`                                                                    |
| `viewport`      | `'mobile'`, `'tablet'`, `'desktop'`, or `{ width, height }`                                      |
| `browser`       | `'chromium'`, `'firefox'`, `'webkit'`                                                            |
| `network`       | `'fast'` (no throttling), `'fast-3g'`, `'slow-3g'`, or `{ downloadKbps, uploadKbps, latencyMs }` |
| `cpu`           | Slowdown factors such as `[1, 4]`, where `1` is no slowdown (Chromium only)                      |
| `cache`         | `'cold'`, and `'warm'`, which loads the page once before testing it                              |
| `axes`          | Custom axes: axis name, then value name, then scenario settings                                  |

`cache: ['cold', 'warm']` is a cheap axis worth adding: a warm cache changes
the order scripts arrive in, which changes when hydration starts.

### Custom axes

Use `axes` for anything the tool cannot know: feature flags, tenants, a
currency cookie, an A/B bucket. Each value applies scenario settings:

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `cookies` | `CookieConfig[]` | — | Cookies this axis value sets. |
| `headers` | `Record<string, string>` | — | Request headers this axis value sets. |
| `localStorage` | `Record<string, string>` | — | `localStorage` entries this axis value sets. |
| `sessionStorage` | `Record<string, string>` | — | `sessionStorage` entries this axis value sets. |
| `initScripts` | `string[]` | — | Scripts this axis value runs before page scripts. |
| `query` | `Record<string, string>` | — | Query parameters this axis value adds to every URL. |

## Choose a strategy

| `strategy`             | What it tests                                                                     |
| ---------------------- | --------------------------------------------------------------------------------- |
| `'pairwise'` (default) | Every pair of values at least once                                                |
| `'full'`               | Every combination. A matrix larger than `max` falls back to pairwise, with a note |
| `'sample'`             | A random subset of `max` environments, chosen from `seed` (default `1`)           |

`max` (default `16`) is the most environments per scenario. If pairwise needs
more than that, the first `max` are tested and a note tells you to raise it for
full pair coverage.

The first value of every axis is the baseline, and the combination of all
baselines is always tested when the browsers can run it. Put the value most of
your users have first.

`matrix.scenarios` limits the matrix to some scenarios; the others run as
configured. The login of a scenario runs once and is shared by all of its
environments (see [scenarios and sign-in](https://hydration.jscrate.dev/docs/scenarios)).

## Which combinations are skipped?

Some combinations cannot run. They are left out with a note instead of failing
the run:

- CPU slowdown needs Chromium.
- Firefox and WebKit have no network throttling. There, every request except
  the document is delayed by the latency instead, and a warm cache cannot be
  combined with a throttled network.
- Firefox has no mobile emulation, so `'mobile'` only sets the viewport size
  and touch.
- Mocks turn the HTTP cache off, so `'warm'` only repeats the visit.

## Read the results

Each environment is a scenario named after the values it varies, such as
`guest (de-DE, Asia/Karachi, firefox)`. Axes with a single value are left out
of the name, and custom axes appear as `name=value` (`flags=new-checkout`).
`--scenario guest` selects every environment of `guest`.

When a finding appears in some environments of a route and not in others, the
report names the values that separate them. The terminal lists these at the
end of the run:

```text
  Only in some environments
    HP1001 /checkout #total  Only found with locale de-DE.
```

With `--mode both`, the build is an axis too: "Only found with the production
build." The HTML report shows the other environments of the same route next to
each finding. To go from "only in de-DE" to a proven cause, add
[probes](https://hydration.jscrate.dev/docs/probes).

## Turn the matrix off with --no-matrix

A full matrix is slower than you want for a quick local check. `--no-matrix`
tests the scenarios as configured:

```bash
npx hydration-proof test --no-matrix
```

## Find flaky findings with --repeat

A finding in one environment out of twelve is usually a real bug that needs
that environment, not flakiness. `--repeat` tells the two apart: it loads every
page several times.

```bash
npx hydration-proof test --repeat 3
```

Findings that appear in only some runs are marked flaky ("Seen in 2 of 3 runs
of this page.") and still count, because an intermittent mismatch is a real
bug. Each page gets a flakiness score: the share of runs whose findings differ
from the most common result. The same option is `repeat: 3` in the config.

## Options

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `locale` | `string[]` | — | Locales to test, e.g. `['en-US', 'de-DE', 'ar-EG']`. |
| `timezoneId` | `string[]` | — | Timezones to test, e.g. `['UTC', 'Asia/Karachi', 'America/Los_Angeles']`. |
| `colorScheme` | `ColorScheme[]` | — | Color schemes to test, e.g. `['light', 'dark']`. |
| `reducedMotion` | `("reduce" \| "no-preference")[]` | — | Reduced-motion settings to test. |
| `viewport` | `ViewportOption[]` | — | Viewports to test: sizes or the presets `mobile`, `tablet`, `desktop`. |
| `browser` | `BrowserName[]` | — | Browsers to test: `chromium`, `firefox`, `webkit`. |
| `network` | `NetworkProfile[]` | — | Network profiles to test: `fast` (no throttling), `fast-3g`, `slow-3g` or custom. |
| `cpu` | `number[]` | — | CPU slowdown factors (Chromium only); `1` is no slowdown. |
| `cache` | `CacheState[]` | — | Cache states to test: `cold` and `warm`. |
| `axes` | `Record<string, Record<string, ScenarioVariant>>` | — | Custom axes: axis name → value name → scenario settings. `{ flags: { 'new-checkout': { cookies: [{ name: 'flag', value: 'on' }] }, 'old-checkout': {} } }` |
| `strategy` | `"pairwise" \| "full" \| "sample"` | — | `pairwise` (default) covers every pair of values, `full` every combination, `sample` a random subset. |
| `max` | `number` | 16 | Most environments per scenario. |
| `seed` | `number` | 1 | Seed for `sample`. |
| `scenarios` | `string[]` | all | Scenarios the matrix applies to. |

## Related

- [Scenarios and sign-in](https://hydration.jscrate.dev/docs/scenarios): the environments the matrix expands
- [Prove which factor causes a finding](https://hydration.jscrate.dev/docs/probes)
- [Timezone hydration mismatches](https://hydration.jscrate.dev/docs/causes/timezone)
- [Locale-dependent formatting](https://hydration.jscrate.dev/docs/causes/locale)
- [The `matrix` option](https://hydration.jscrate.dev/docs/configuration#matrix) in the configuration reference
