# Internationalization (/docs/intl)





Every `Intl` constructor takes a locale as its first argument and a `timeZone` as an option, and
both have a runtime default. `Intl.DateTimeFormat()` resolves to Node's `en-US` on a server and to
the visitor's locale in a browser; the host zone resolves to the server's zone — UTC on most
hosts — and to the visitor's.

Reading either implicitly makes what you render a property of *where the code ran*. On a
client-only app that is invisible. Under SSR it is a hydration mismatch waiting for the first
visitor configured differently from the build server.

So nothing in this kit reads them implicitly. Components resolve both through `useLocale()` and
`useTimeZone()`, and the value they get is either something you stated or a documented constant.

<Preview code="&#x22;<IntlProvider locale={locale} timeZone={timeZone}>\n  <Readout />\n</IntlProvider>&#x22;">
  <IntlDemo />
</Preview>

# Quick start [#quick-start]

Wrap the app once:

```tsx
import { IntlProvider } from '@42-central/ui-react';

<IntlProvider locale="fr-FR" timeZone="Europe/Paris">
  <App />
</IntlProvider>;
```

Both props are optional and **independent** — setting only `timeZone` leaves the locale alone, and
a nested provider inherits whichever value it doesn't set, so a section can override one without
restating the other.

Any single component can still override either one for itself:

```tsx
<Calendar timeZone="UTC" />
```

## Where the value comes from [#where-the-value-comes-from]

If your server knows the user's locale — from their account, their session, or the
`Accept-Language` header — pass it down. That is the only arrangement with no compromise at all:
the server renders the right thing, the client agrees with it, and nothing changes after paint.

```tsx
// app/layout.tsx
export default async function Layout({ children }: LayoutProps<'/'>) {
  const locale = await resolveUserLocale();

  return (
    <IntlProvider locale={locale}>
      <body>{children}</body>
    </IntlProvider>
  );
}
```

# Defaults [#defaults]

The two defaults are deliberately different, because the failure modes are different.

|            | Default   | Why                                                                                                                         |
| ---------- | --------- | --------------------------------------------------------------------------------------------------------------------------- |
| `locale`   | `"en-US"` | A wrong locale is an unfamiliar format — `09/17` instead of `17/09`. Harmless, so determinism wins and detection is opt-in. |
| `timeZone` | `"auto"`  | A wrong zone is wrong **data**, not a format. A meeting at 14:00 Paris shown as 12:00 is a bug, not a preference.           |

`"en-US"` is the same tag [Ark UI](https://ark-ui.com) defaults its own locale context to, so the
two halves of a `Calendar` — Ark's state machine and the kit's formatting around it — cannot
disagree.

## `"auto"` [#auto]

`"auto"` follows the host runtime: the browser's language, or the machine's time zone. It is
accepted anywhere a value is — on the provider, or on a single component.

```tsx
<IntlProvider locale="auto" timeZone="auto">
```

It is resolved through `useSyncExternalStore` with a fixed server snapshot, so **it never causes a
hydration mismatch**: the server and the first client render are both handed the defaults above,
and React swaps in the real values immediately after.

<Callout type="warn">
  React doesn't guarantee that swap lands before the browser paints, and in practice it doesn't
  reliably. Under SSR, `"auto"` can therefore show the default for one frame — an `en-US` date
  flicking to `fr-FR`. That's the cost of opting in, and it's exactly why `locale` doesn't default
  to it. A client-only app pays nothing: the server snapshot never runs there, so the first render
  is already correct.
</Callout>

# Server-side rendering [#server-side-rendering]

Three rules cover everything.

1. **State the locale if you can.** A locale your server already knows, passed as a prop, is
   correct on the first paint with no correction. `"auto"` is the fallback for when it genuinely
   isn't knowable until the browser runs.
2. **Don't read the clock during render.** A component given no value anchors itself to `new
   Date()`, which differs between the build/render and the hydration. Pass a fixed `now` (or
   `defaultValue`) to `Schedule`, `Calendar` and `DatePicker` when the markup is server-rendered,
   the same way this site's own demos pin a fixed instant.
3. **Prefer deterministic format options.** `hour: '2-digit'` is spec-guaranteed; `hour: 'numeric'`
   leaves zero-padding to each runtime's bundled ICU data, which Node and browsers don't always
   agree on. If you format a *range*, `.normalize('NFKC')` the result — the two ICUs disagree on
   which space glyph separates the endpoints.

# Text direction [#text-direction]

Direction comes from the **locale**. `IntlProvider` renders Ark's own locale provider underneath,
so an RTL locale sets `dir` for every Ark-backed part: `Tabs`, `SegmentGroup`, `Menu`, `Tooltip`,
`HoverCard` and the rest flip their layout, placement *and* arrow-key navigation with no extra
configuration.

```tsx
<IntlProvider locale="ar-EG">
```

<Callout type="warn">
  `<html dir="rtl">` on its own isn't enough. Without an RTL locale, Ark resolves `dir="ltr"` and
  writes it onto each of its parts, so a `Schedule` inside an RTL page ends up with an RTL grid
  and LTR chips and view switcher. The kit can't just let those parts inherit from the DOM: their
  keyboard handling needs the direction in JavaScript, and the server can't read the DOM. In
  development, the console warns once when the page and the locale disagree.
</Callout>

Set both from the same place, so they can't drift apart:

```tsx
// app/layout.tsx
export default async function Layout({ children }: LayoutProps<'/'>) {
  const { locale, dir } = await resolveUserLocale(); // e.g. { locale: 'ar-EG', dir: 'rtl' }

  return (
    <html lang={locale} dir={dir}>
      <IntlProvider locale={locale}>
        <body>{children}</body>
      </IntlProvider>
    </html>
  );
}
```

# This is not a translation system [#this-is-not-a-translation-system]

`IntlProvider` carries the inputs to `Intl`, and nothing else. Translated microcopy stays on each
component's own `translations` prop, which is deliberately prop-only with no context — see
[Calendar](/docs/calendar), [DatePicker](/docs/date-picker) or [Schedule](/docs/schedule).

The split is that anything derivable from the locale *is* derived and never becomes a translation
key: weekday and month names, AM/PM, unit abbreviations. `translations` exists only for the strings
`Intl` can't produce.

# API [#api]

## IntlProvider [#intlprovider]

<TypeTable
  type="{
  locale: {
    type: 'string',
    description: 'A BCP-47 tag (&#x22;fr-FR&#x22;), or &#x22;auto&#x22; to follow the browser. Inherited from an outer IntlProvider when omitted, and &#x22;en-US&#x22; when there is none.',
  },
  timeZone: {
    type: 'string',
    default: '&#x22;auto&#x22;',
    description: 'An IANA zone (&#x22;Europe/Paris&#x22;), or &#x22;auto&#x22; to follow the host. Inherited from an outer IntlProvider when omitted.',
  },
}"
/>

## Hooks [#hooks]

[`useLocale`](/docs/hooks/use-locale) and [`useTimeZone`](/docs/hooks/use-time-zone) read the
resolved values — for your own formatting, or to build a component that behaves like the kit's.

```tsx
const locale = useLocale(props.locale);
const timeZone = useTimeZone(props.timeZone);
```

Both return a concrete value, never `"auto"` and never `undefined`.

## Constants [#constants]

<TypeTable
  type="{
  DEFAULT_LOCALE: {
    type: '&#x22;en-US&#x22;',
    description: 'The locale used when nothing configures one, and what locale=&#x22;auto&#x22; reports during SSR and hydration.',
  },
  DEFAULT_TIME_ZONE: {
    type: '&#x22;UTC&#x22;',
    description: 'What timeZone=&#x22;auto&#x22; reports during SSR and hydration. Not the default for the timeZone prop itself — that is &#x22;auto&#x22;.',
  },
}"
/>
