|Kit

Internationalization

State the locale and time zone your app formats in, once — instead of inheriting whatever the runtime happened to default to.

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.

Thursday, September 17, 2026 at 9:30 PM

Code

<IntlProvider locale={locale} timeZone={timeZone}>
  <Readout />
</IntlProvider>

Quick start

Wrap the app once:

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:

<Calendar timeZone="UTC" />

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.

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

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

Defaults

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

DefaultWhy
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 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" 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.

<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.

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.

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

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.

<IntlProvider locale="ar-EG">

<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.

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

// 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

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, DatePicker or 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

IntlProvider

Prop

Type

Hooks

useLocale and useTimeZone read the resolved values — for your own formatting, or to build a component that behaves like the kit's.

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

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

Constants

Prop

Type

Changelog

v0.16.0
  • Warn in development when the page direction contradicts the locale

    #247
v0.12.0
  • New IntlProvider, with useLocale and useTimeZone: one app-wide locale and time zone

    #187

On this page