@object-ui/i18n 17.7.0 → 18.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -196,7 +196,70 @@ A date-only string such as `'2026-09-01'` names a calendar day: every date
196
196
  helper reads it as that day in every viewer's time zone (`formatDateTime` shows
197
197
  its midnight, and `formatRelativeTime` counts to the start of it). A string
198
198
  with a time part is an instant, read in the viewer's zone. `formatDateSpec`
199
- applies its `timeZone` to an instant only.
199
+ applies its `timeZone` to an instant only. These helpers do not read the
200
+ display zone below; the field cells and measures format through
201
+ `@object-ui/core`'s date faces, which do.
202
+
203
+ ### Regional defaults (`LocalizationProvider`)
204
+
205
+ `LocalizationProvider` carries the workspace's resolved regional defaults to
206
+ every renderer below it, and `useLocalization()` reads them (`{}` outside a
207
+ provider). The console fills it from `GET /api/v1/auth/me/localization`.
208
+
209
+ ```tsx
210
+ import type { ReactNode } from 'react';
211
+ import { LocalizationProvider, useLocalization } from '@object-ui/i18n';
212
+
213
+ export function Regional({ children }: { children: ReactNode }) {
214
+ return (
215
+ <LocalizationProvider value={{ currency: 'EUR', locale: 'de-DE', timezone: 'Europe/Berlin' }}>
216
+ {children}
217
+ </LocalizationProvider>
218
+ );
219
+ }
220
+
221
+ export function ZoneLabel() {
222
+ const { timezone } = useLocalization();
223
+ return <span>{timezone ?? 'viewer zone'}</span>;
224
+ }
225
+ ```
226
+
227
+ - `currency` (ISO 4217) is the default a currency field without its own code
228
+ renders in.
229
+ - `locale` (BCP-47) outranks the UI language for number and date formatting;
230
+ read the effective tag with `useDisplayLocale()`.
231
+ - `timezone` (IANA) is the zone instants render in. The provider hands it to
232
+ `setDisplayTimeZone` in `@object-ui/core`, so every date and datetime face
233
+ takes it without any renderer passing a zone, and `useLocalization().timezone`
234
+ reads back the zone those faces use (`undefined` for a name the runtime does
235
+ not know). Without one, instants render in the viewer's zone. There is one
236
+ display zone per page, and unmounting the provider clears it.
237
+
238
+ #### The first day of the week
239
+
240
+ `firstDayOfWeek(locale)` answers the first day of the week for a BCP-47 tag,
241
+ numbered as `Date.prototype.getDay` numbers weekdays (0 is Sunday), which is
242
+ also what react-day-picker's `weekStartsOn` takes. It reads the engine's
243
+ `Intl.Locale` week info, and CLDR's region table where the engine has none, so
244
+ a week grid or a "this week" bound starts where the user's locale says rather
245
+ than on a fixed day. There is no separate week-start setting: the tag is the
246
+ one the caller already formats its dates with, such as `useDisplayLocale()`.
247
+
248
+ ```tsx
249
+ import { firstDayOfWeek, useDisplayLocale } from '@object-ui/i18n';
250
+
251
+ firstDayOfWeek('en-US'); // 0, Sunday
252
+ firstDayOfWeek('en-GB'); // 1, Monday
253
+ firstDayOfWeek('zh-CN'); // 1, Monday
254
+
255
+ function WeekHeader() {
256
+ const weekStart = firstDayOfWeek(useDisplayLocale());
257
+ // ...lay the week out from `weekStart`
258
+ }
259
+ ```
260
+
261
+ A tag `Intl` refuses (`en_US`) throws the same `RangeError` that formatting a
262
+ date with it would.
200
263
 
201
264
  ### Built-in locales — one is resident, nine are fetched on demand
202
265
 
@@ -7,18 +7,20 @@
7
7
  */
8
8
  /**
9
9
  * LocalizationContext — the tenant's resolved REGIONAL defaults (currency,
10
- * locale) for the current request, surfaced to every renderer.
10
+ * locale, time zone) for the current request, surfaced to every renderer.
11
11
  *
12
12
  * This is the client half of ADR-0053: the framework resolves a workspace
13
- * `localization.currency` / `locale` onto each request's ExecutionContext and
14
- * exposes them at `GET /api/v1/auth/me/localization`. A fetching wrapper (in
15
- * app-shell) loads them once and feeds this PURE context, so low-level field /
16
- * measure renderers can resolve a currency code down to the org default —
17
- * instead of hard-coding `$`/`¥`/`USD` or degrading to a bare number — without
18
- * any of them depending on app-shell or making their own fetch.
13
+ * `localization.currency` / `locale` / `timezone` onto each request's
14
+ * ExecutionContext and exposes them at `GET /api/v1/auth/me/localization`. A
15
+ * fetching wrapper (in app-shell) loads them once and feeds this PURE context,
16
+ * so low-level field / measure renderers can resolve a currency code down to
17
+ * the org default — instead of hard-coding `$`/`¥`/`USD` or degrading to a
18
+ * bare number — without any of them depending on app-shell or making their own
19
+ * fetch.
19
20
  *
20
21
  * Undefined values mean "no tenant default known" → consumers render a plain
21
- * number (the existing safe behavior).
22
+ * number (the existing safe behavior), and instants render in the viewer's
23
+ * own zone.
22
24
  */
23
25
  import * as React from 'react';
24
26
  export interface LocalizationValue {
@@ -26,8 +28,29 @@ export interface LocalizationValue {
26
28
  currency?: string;
27
29
  /** Tenant/display locale (BCP-47) for Intl formatting, or undefined. */
28
30
  locale?: string;
31
+ /**
32
+ * The zone instants render in (an IANA name), or undefined for the viewer's
33
+ * own (objectui#11693). The provider hands it to the central date faces in
34
+ * `@object-ui/core`; read back from {@link useLocalization}, it is the zone
35
+ * those faces render in, so a name the runtime does not know reads back
36
+ * `undefined`.
37
+ */
38
+ timezone?: string;
29
39
  }
30
- /** Pure provider — the fetching/loading concern lives in the app shell. */
40
+ /**
41
+ * Pure provider — the fetching/loading concern lives in the app shell.
42
+ *
43
+ * It is also the ONE place the display zone enters the date faces
44
+ * (objectui#11693): `value.timezone` is handed to `setDisplayTimeZone` in
45
+ * `@object-ui/core`, which every date and datetime face reads, so no renderer
46
+ * threads a zone of its own. The hand-off runs in a layout effect, and the
47
+ * zone the context carries is read back from core through
48
+ * `useSyncExternalStore`, so the context changes only AFTER the faces have the
49
+ * new zone: every consumer then re-renders in it, before the browser paints.
50
+ *
51
+ * The display zone is one per page, as the date faces are: a second provider
52
+ * with a different zone would race this one for it, and unmounting clears it.
53
+ */
31
54
  export declare function LocalizationProvider({ value, children, }: {
32
55
  value: LocalizationValue;
33
56
  children: React.ReactNode;
@@ -8,25 +8,47 @@ import { jsx as _jsx } from "react/jsx-runtime";
8
8
  */
9
9
  /**
10
10
  * LocalizationContext — the tenant's resolved REGIONAL defaults (currency,
11
- * locale) for the current request, surfaced to every renderer.
11
+ * locale, time zone) for the current request, surfaced to every renderer.
12
12
  *
13
13
  * This is the client half of ADR-0053: the framework resolves a workspace
14
- * `localization.currency` / `locale` onto each request's ExecutionContext and
15
- * exposes them at `GET /api/v1/auth/me/localization`. A fetching wrapper (in
16
- * app-shell) loads them once and feeds this PURE context, so low-level field /
17
- * measure renderers can resolve a currency code down to the org default —
18
- * instead of hard-coding `$`/`¥`/`USD` or degrading to a bare number — without
19
- * any of them depending on app-shell or making their own fetch.
14
+ * `localization.currency` / `locale` / `timezone` onto each request's
15
+ * ExecutionContext and exposes them at `GET /api/v1/auth/me/localization`. A
16
+ * fetching wrapper (in app-shell) loads them once and feeds this PURE context,
17
+ * so low-level field / measure renderers can resolve a currency code down to
18
+ * the org default — instead of hard-coding `$`/`¥`/`USD` or degrading to a
19
+ * bare number — without any of them depending on app-shell or making their own
20
+ * fetch.
20
21
  *
21
22
  * Undefined values mean "no tenant default known" → consumers render a plain
22
- * number (the existing safe behavior).
23
+ * number (the existing safe behavior), and instants render in the viewer's
24
+ * own zone.
23
25
  */
24
26
  import * as React from 'react';
27
+ import { getDisplayTimeZone, setDisplayTimeZone, subscribeDisplayTimeZone } from '@object-ui/core';
25
28
  const LocalizationCtx = React.createContext({});
26
- /** Pure provider — the fetching/loading concern lives in the app shell. */
29
+ /**
30
+ * Pure provider — the fetching/loading concern lives in the app shell.
31
+ *
32
+ * It is also the ONE place the display zone enters the date faces
33
+ * (objectui#11693): `value.timezone` is handed to `setDisplayTimeZone` in
34
+ * `@object-ui/core`, which every date and datetime face reads, so no renderer
35
+ * threads a zone of its own. The hand-off runs in a layout effect, and the
36
+ * zone the context carries is read back from core through
37
+ * `useSyncExternalStore`, so the context changes only AFTER the faces have the
38
+ * new zone: every consumer then re-renders in it, before the browser paints.
39
+ *
40
+ * The display zone is one per page, as the date faces are: a second provider
41
+ * with a different zone would race this one for it, and unmounting clears it.
42
+ */
27
43
  export function LocalizationProvider({ value, children, }) {
44
+ const { timezone } = value;
45
+ React.useLayoutEffect(() => {
46
+ setDisplayTimeZone(timezone);
47
+ return () => setDisplayTimeZone(undefined);
48
+ }, [timezone]);
49
+ const appliedZone = React.useSyncExternalStore(subscribeDisplayTimeZone, getDisplayTimeZone, getDisplayTimeZone);
28
50
  // Stable identity unless the resolved values actually change.
29
- const memo = React.useMemo(() => ({ currency: value.currency, locale: value.locale }), [value.currency, value.locale]);
51
+ const memo = React.useMemo(() => ({ currency: value.currency, locale: value.locale, timezone: appliedZone }), [value.currency, value.locale, appliedZone]);
30
52
  return _jsx(LocalizationCtx.Provider, { value: memo, children: children });
31
53
  }
32
54
  /**
package/dist/index.d.ts CHANGED
@@ -47,3 +47,4 @@ export { LocalizationProvider, useLocalization, type LocalizationValue } from '.
47
47
  export { resolveFieldCurrency } from './currency.js';
48
48
  export { useDisplayLocale } from './useDisplayLocale.js';
49
49
  export { formatDisplayNumber, shouldGroupDisplayNumber, type DisplayNumberFormatOptions, } from './utils/index.js';
50
+ export { firstDayOfWeek, type WeekdayIndex } from './utils/index.js';
package/dist/index.js CHANGED
@@ -98,3 +98,7 @@ export { resolveFieldCurrency } from './currency.js';
98
98
  // see `utils/number-display.ts` for the grouping policy and its interim status.
99
99
  export { useDisplayLocale } from './useDisplayLocale.js';
100
100
  export { formatDisplayNumber, shouldGroupDisplayNumber, } from './utils/index.js';
101
+ // objectui#11675 — the first day of the week for a locale tag. The calendar's
102
+ // grids and the timeline's week buckets read it from the tag they format with,
103
+ // so the week starts where the locale says and never on a hard-coded day.
104
+ export { firstDayOfWeek } from './utils/index.js';