react-x11 1.2.0 → 2.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.
Files changed (158) hide show
  1. package/README.md +330 -81
  2. package/package.json +132 -14
  3. package/src/ClickToComponent.js +9 -2
  4. package/src/DevToolsIntegration.js +440 -32
  5. package/src/Reconciler.js +463 -165
  6. package/src/a11y.js +1299 -0
  7. package/src/acceleratorhooks.js +86 -0
  8. package/src/accelerators.js +128 -0
  9. package/src/activate.js +168 -0
  10. package/src/anchor.js +360 -0
  11. package/src/appcontext.js +161 -0
  12. package/src/appearance.js +748 -0
  13. package/src/appearancehooks.js +96 -0
  14. package/src/apphooks.js +73 -0
  15. package/src/application.js +748 -0
  16. package/src/atspi.js +1953 -0
  17. package/src/bus.js +545 -0
  18. package/src/bushooks.js +136 -0
  19. package/src/clientmessage.js +140 -0
  20. package/src/clipboard.js +121 -0
  21. package/src/components/Button.js +148 -33
  22. package/src/components/Checkbox.js +66 -32
  23. package/src/components/Dialog.js +96 -29
  24. package/src/components/FileDialog.js +491 -0
  25. package/src/components/Icon.js +329 -0
  26. package/src/components/Menu.js +797 -133
  27. package/src/components/PasswordInput.js +434 -0
  28. package/src/components/ProgressBar.js +91 -12
  29. package/src/components/Radio.js +78 -35
  30. package/src/components/Select.js +255 -65
  31. package/src/components/Slider.js +102 -41
  32. package/src/components/SplitPane.js +206 -0
  33. package/src/components/Switch.js +100 -22
  34. package/src/components/Table.js +529 -0
  35. package/src/components/Tabs.js +211 -0
  36. package/src/components/Tooltip.js +528 -54
  37. package/src/components/anchor.js +154 -107
  38. package/src/components/change.js +34 -0
  39. package/src/components/dnd.js +103 -0
  40. package/src/components/index.js +16 -3
  41. package/src/components/keys.js +17 -20
  42. package/src/components/scribble.js +247 -0
  43. package/src/components/theme.js +350 -42
  44. package/src/components/typeahead.js +12 -2
  45. package/src/compose.js +868 -0
  46. package/src/compositing.js +224 -0
  47. package/src/dbusmenu.js +384 -0
  48. package/src/debug.d.ts +63 -0
  49. package/src/debug.js +642 -0
  50. package/src/decorations.js +486 -0
  51. package/src/desktopsettings.js +209 -0
  52. package/src/desktopsettingshooks.js +62 -0
  53. package/src/dnd.js +1725 -0
  54. package/src/editmenu.js +272 -0
  55. package/src/errors.js +98 -0
  56. package/src/events.js +1406 -162
  57. package/src/extensions.js +45 -0
  58. package/src/filedialog.js +375 -0
  59. package/src/filedialoghooks.js +132 -0
  60. package/src/fonthooks.js +64 -0
  61. package/src/fonts.js +301 -0
  62. package/src/foreignnodes.js +519 -0
  63. package/src/frame/child.js +33 -0
  64. package/src/frame/childmain.js +274 -0
  65. package/src/frame/env.js +140 -0
  66. package/src/frame/index.js +444 -0
  67. package/src/frame/lifecycle.js +67 -0
  68. package/src/frame/protocol.js +179 -0
  69. package/src/frames.js +73 -0
  70. package/src/glbackend.js +114 -0
  71. package/src/glnodes.js +125 -67
  72. package/src/globalmenu.js +683 -0
  73. package/src/host.d.ts +88 -0
  74. package/src/host.js +42 -0
  75. package/src/idle.js +490 -0
  76. package/src/idlehooks.js +100 -0
  77. package/src/imagesource.js +349 -0
  78. package/src/index.d.ts +475 -0
  79. package/src/index.js +83 -9
  80. package/src/inputtime.js +206 -0
  81. package/src/jsx-dev-runtime.d.ts +21 -0
  82. package/src/jsx-dev-runtime.js +2 -0
  83. package/src/jsx-runtime.d.ts +39 -0
  84. package/src/jsx-runtime.js +6 -0
  85. package/src/keyboard.js +256 -0
  86. package/src/keyboardstate.js +278 -0
  87. package/src/keyboardstatehooks.js +58 -0
  88. package/src/keysyms.d.ts +149 -0
  89. package/src/keysyms.js +270 -0
  90. package/src/locale.js +170 -0
  91. package/src/localehooks.js +47 -0
  92. package/src/menuitem.js +223 -0
  93. package/src/node.d.ts +597 -0
  94. package/src/node.js +44 -0
  95. package/src/nodes.js +9546 -690
  96. package/src/ntk.d.ts +44 -0
  97. package/src/ntk.js +25 -0
  98. package/src/paintcache.js +366 -0
  99. package/src/palette.js +380 -0
  100. package/src/pastestate.js +66 -0
  101. package/src/portal.js +461 -0
  102. package/src/priority.js +26 -0
  103. package/src/refresh/index.d.ts +40 -0
  104. package/src/refresh/index.js +122 -0
  105. package/src/refresh/loader.d.ts +37 -0
  106. package/src/refresh/loader.js +401 -0
  107. package/src/refresh/register.d.ts +5 -0
  108. package/src/refresh/register.js +13 -0
  109. package/src/registry.js +232 -0
  110. package/src/scale.js +626 -0
  111. package/src/scalehooks.js +27 -0
  112. package/src/screencolor.js +640 -0
  113. package/src/screencolorhooks.js +101 -0
  114. package/src/screens.js +754 -0
  115. package/src/screenshooks.js +137 -0
  116. package/src/startup.js +302 -0
  117. package/src/style.d.ts +126 -0
  118. package/src/style.js +33 -0
  119. package/src/styles.js +1482 -6
  120. package/src/svgnodes.js +306 -0
  121. package/src/testing/a11y.js +484 -0
  122. package/src/testing/components.js +414 -0
  123. package/src/testing/events.js +407 -0
  124. package/src/testing/harness.js +455 -0
  125. package/src/testing/index.d.ts +558 -0
  126. package/src/testing/index.js +85 -0
  127. package/src/testing/mock-app.js +463 -0
  128. package/src/testing/pixels.js +152 -0
  129. package/src/testing/queries.js +224 -0
  130. package/src/textrange.js +83 -0
  131. package/src/textselection.js +439 -0
  132. package/src/trace-registry.js +63 -0
  133. package/src/transfer.js +93 -0
  134. package/src/types/appearance.d.ts +84 -0
  135. package/src/types/application.d.ts +173 -0
  136. package/src/types/components.d.ts +820 -0
  137. package/src/types/dbus.d.ts +177 -0
  138. package/src/types/elements.d.ts +897 -0
  139. package/src/types/events.d.ts +549 -0
  140. package/src/types/filedialog.d.ts +260 -0
  141. package/src/types/fonts.d.ts +124 -0
  142. package/src/types/frame.d.ts +146 -0
  143. package/src/types/globalmenu.d.ts +42 -0
  144. package/src/types/nodes.d.ts +199 -0
  145. package/src/types/screencolor.d.ts +84 -0
  146. package/src/types/style.d.ts +434 -0
  147. package/src/types/system.d.ts +287 -0
  148. package/src/windowid.js +151 -0
  149. package/src/windowstate.js +393 -0
  150. package/src/xsettings.js +336 -0
  151. package/src/yoga.d.ts +55 -0
  152. package/src/yoga.js +135 -0
  153. package/src/components/Canvas3D.js +0 -28
  154. package/src/geometry3d.js +0 -223
  155. package/src/pointer3d.js +0 -158
  156. package/src/raycast3d.js +0 -146
  157. package/src/richnodes.js +0 -436
  158. package/src/scene3d.js +0 -683
package/src/keysyms.js ADDED
@@ -0,0 +1,270 @@
1
+ // X11 keysyms, as `react-x11/keysyms`.
2
+ //
3
+ // One vocabulary for app code and tests: the widget keyboard handlers read
4
+ // these, `fireEvent.key` takes them, and `onKeyDown` reports them on
5
+ // `ev.keysym`. The full X11 set is several thousand names; this is the part
6
+ // a GUI actually handles, plus the rule for everything else.
7
+ //
8
+ // Two facts make the long tail unnecessary:
9
+ //
10
+ // - **Latin-1 is identity.** For U+0020 to U+00FF the keysym *is* the code
11
+ // point, so `'a'` is `0x61` and `'é'` is `0xe9`. That is the whole ASCII
12
+ // and Latin-1 range, no table needed.
13
+ // - **Everything else is `0x01000000 + codePoint`.** That is the Unicode
14
+ // keysym rule, and `keysymOf` below applies both.
15
+
16
+ /** The keysym for a single character, by the two rules above. */
17
+ export function keysymOf(char) {
18
+ const code = String(char).codePointAt(0);
19
+ if (code == null) return 0;
20
+ if (code >= 0x20 && code <= 0xff) return code;
21
+ return 0x01000000 + code;
22
+ }
23
+
24
+ /** The character a keysym produces, or `''` for a non-printing key. */
25
+ export function charOf(keysym) {
26
+ if (keysym >= 0x20 && keysym <= 0xff) return String.fromCodePoint(keysym);
27
+ if (keysym >= 0x01000100 && keysym <= 0x0110ffff) {
28
+ return String.fromCodePoint(keysym - 0x01000000);
29
+ }
30
+ return '';
31
+ }
32
+
33
+ // --- editing and navigation ------------------------------------------------
34
+
35
+ export const XK_BACKSPACE = 0xff08;
36
+ export const XK_TAB = 0xff09;
37
+ export const XK_RETURN = 0xff0d;
38
+ export const XK_ESCAPE = 0xff1b;
39
+ export const XK_DELETE = 0xffff;
40
+ export const XK_INSERT = 0xff63;
41
+
42
+ export const XK_HOME = 0xff50;
43
+ export const XK_LEFT = 0xff51;
44
+ export const XK_UP = 0xff52;
45
+ export const XK_RIGHT = 0xff53;
46
+ export const XK_DOWN = 0xff54;
47
+ export const XK_PAGE_UP = 0xff55;
48
+ export const XK_PAGE_DOWN = 0xff56;
49
+ export const XK_END = 0xff57;
50
+
51
+ export const XK_KP_ENTER = 0xff8d;
52
+ export const XK_MENU = 0xff67;
53
+ export const XK_SPACE = 0x0020;
54
+
55
+ // --- composition -----------------------------------------------------------
56
+ //
57
+ // The keys that type nothing on their own. A dead key waits for the letter
58
+ // it decorates (`dead_acute` then `e` is `é`) and `Multi_key` — the Compose
59
+ // key — opens a sequence of them. They are ordinary keysyms that arrive on
60
+ // ordinary key events; what turns a run of them into a character is the
61
+ // state machine in `src/compose.js`, and `charOf` deliberately answers `''`
62
+ // for every one of them because on their own they produce no text.
63
+ //
64
+ // The whole `dead_*` block is here rather than the handful most layouts
65
+ // use: the names are the vocabulary an application matches on, and half a
66
+ // block is a table nobody can trust.
67
+
68
+ export const XK_MULTI_KEY = 0xff20;
69
+
70
+ export const XK_DEAD_GRAVE = 0xfe50;
71
+ export const XK_DEAD_ACUTE = 0xfe51;
72
+ export const XK_DEAD_CIRCUMFLEX = 0xfe52;
73
+ export const XK_DEAD_TILDE = 0xfe53;
74
+ export const XK_DEAD_MACRON = 0xfe54;
75
+ export const XK_DEAD_BREVE = 0xfe55;
76
+ export const XK_DEAD_ABOVEDOT = 0xfe56;
77
+ export const XK_DEAD_DIAERESIS = 0xfe57;
78
+ export const XK_DEAD_ABOVERING = 0xfe58;
79
+ export const XK_DEAD_DOUBLEACUTE = 0xfe59;
80
+ export const XK_DEAD_CARON = 0xfe5a;
81
+ export const XK_DEAD_CEDILLA = 0xfe5b;
82
+ export const XK_DEAD_OGONEK = 0xfe5c;
83
+ export const XK_DEAD_IOTA = 0xfe5d;
84
+ export const XK_DEAD_VOICED_SOUND = 0xfe5e;
85
+ export const XK_DEAD_SEMIVOICED_SOUND = 0xfe5f;
86
+ export const XK_DEAD_BELOWDOT = 0xfe60;
87
+ export const XK_DEAD_HOOK = 0xfe61;
88
+ export const XK_DEAD_HORN = 0xfe62;
89
+ export const XK_DEAD_STROKE = 0xfe63;
90
+ export const XK_DEAD_ABOVECOMMA = 0xfe64;
91
+ export const XK_DEAD_ABOVEREVERSEDCOMMA = 0xfe65;
92
+ export const XK_DEAD_DOUBLEGRAVE = 0xfe66;
93
+ export const XK_DEAD_BELOWRING = 0xfe67;
94
+ export const XK_DEAD_BELOWMACRON = 0xfe68;
95
+ export const XK_DEAD_BELOWCIRCUMFLEX = 0xfe69;
96
+ export const XK_DEAD_BELOWTILDE = 0xfe6a;
97
+ export const XK_DEAD_BELOWBREVE = 0xfe6b;
98
+ export const XK_DEAD_BELOWDIAERESIS = 0xfe6c;
99
+ export const XK_DEAD_INVERTEDBREVE = 0xfe6d;
100
+ export const XK_DEAD_BELOWCOMMA = 0xfe6e;
101
+ export const XK_DEAD_CURRENCY = 0xfe6f;
102
+
103
+ /** Whether a keysym is one of the `XK_dead_*` block. */
104
+ export function isDeadKeysym(keysym) {
105
+ return keysym >= XK_DEAD_GRAVE && keysym <= XK_DEAD_CURRENCY;
106
+ }
107
+
108
+ // --- modifiers -------------------------------------------------------------
109
+
110
+ export const XK_SHIFT_L = 0xffe1;
111
+ export const XK_SHIFT_R = 0xffe2;
112
+ export const XK_CONTROL_L = 0xffe3;
113
+ export const XK_CONTROL_R = 0xffe4;
114
+ export const XK_CAPS_LOCK = 0xffe5;
115
+ export const XK_ALT_L = 0xffe9;
116
+ export const XK_ALT_R = 0xffea;
117
+ export const XK_SUPER_L = 0xffeb;
118
+ export const XK_SUPER_R = 0xffec;
119
+
120
+ // --- function keys ---------------------------------------------------------
121
+
122
+ export const XK_F1 = 0xffbe;
123
+ export const XK_F2 = 0xffbf;
124
+ export const XK_F3 = 0xffc0;
125
+ export const XK_F4 = 0xffc1;
126
+ export const XK_F5 = 0xffc2;
127
+ export const XK_F6 = 0xffc3;
128
+ export const XK_F7 = 0xffc4;
129
+ export const XK_F8 = 0xffc5;
130
+ export const XK_F9 = 0xffc6;
131
+ export const XK_F10 = 0xffc7;
132
+ export const XK_F11 = 0xffc8;
133
+ export const XK_F12 = 0xffc9;
134
+
135
+ /**
136
+ * The letter of a Ctrl chord, independent of Shift. ntk derives `codepoint`
137
+ * from the *shifted* keysym, so Ctrl+Shift+Z arrives as `Z` while Ctrl+Z
138
+ * arrives as `z` — the keysym does not shift, so match on that and fall back
139
+ * to the codepoint when the keymap has not been read yet.
140
+ *
141
+ * Here rather than beside its first caller because both layers need it: the
142
+ * `<textinput>` node reads Ctrl+C/V/Z, and so does any widget that answers a
143
+ * chord of its own.
144
+ */
145
+ export function ctrlChordLetter(ev) {
146
+ const code = ev.keysym ?? ev.codepoint;
147
+ if (code == null) return null;
148
+ return code >= 0x41 && code <= 0x5a ? code + 0x20 : code;
149
+ }
150
+
151
+ /**
152
+ * The X11 modifier mask bits, as they arrive on `ev.nativeEvent.buttons`
153
+ * and as `fireEvent` takes them. Bit 3 (Mod1) is Alt and bit 6 (Mod4) is
154
+ * Super on virtually every layout; those four — Shift, Control, Mod1, Mod4
155
+ * — are the ones a synthetic event decodes, as `shiftKey`/`ctrlKey`/
156
+ * `altKey`/`metaKey`.
157
+ */
158
+ export const MOD = {
159
+ Shift: 1,
160
+ Lock: 2,
161
+ Control: 4,
162
+ Mod1: 8,
163
+ Alt: 8,
164
+ Mod2: 16,
165
+ Mod3: 32,
166
+ Mod4: 64,
167
+ Super: 64,
168
+ Mod5: 128,
169
+ };
170
+
171
+ // --- keysym names ----------------------------------------------------------
172
+ //
173
+ // X11 names the *key* rather than the character it types — `plus` and not
174
+ // `+`, `Return` and not `Enter` — and that is the vocabulary a menu
175
+ // `shortcut` speaks, because dbusmenu carries `gdk_keyval_name()` strings
176
+ // (`src/menuitem.js`). `keysymFromName` is the other direction from
177
+ // `charOf`: a name in, the keysym out, so a chord written for the panel is
178
+ // the same chord the key path matches against.
179
+
180
+ // The printable ASCII whose X11 name is a word. Everything else in that
181
+ // range is its own name — `a`, `7` — and goes through `keysymOf`.
182
+ // `quoteright` and `quoteleft` are the deprecated spellings of `apostrophe`
183
+ // and `grave`, still emitted by older importers.
184
+ const PUNCTUATION_NAMES = {
185
+ exclam: '!',
186
+ quotedbl: '"',
187
+ numbersign: '#',
188
+ dollar: '$',
189
+ percent: '%',
190
+ ampersand: '&',
191
+ apostrophe: "'",
192
+ quoteright: "'",
193
+ parenleft: '(',
194
+ parenright: ')',
195
+ asterisk: '*',
196
+ plus: '+',
197
+ comma: ',',
198
+ minus: '-',
199
+ period: '.',
200
+ slash: '/',
201
+ colon: ':',
202
+ semicolon: ';',
203
+ less: '<',
204
+ equal: '=',
205
+ greater: '>',
206
+ question: '?',
207
+ at: '@',
208
+ bracketleft: '[',
209
+ backslash: '\\',
210
+ bracketright: ']',
211
+ asciicircum: '^',
212
+ underscore: '_',
213
+ grave: '`',
214
+ quoteleft: '`',
215
+ braceleft: '{',
216
+ bar: '|',
217
+ braceright: '}',
218
+ asciitilde: '~',
219
+ space: ' ',
220
+ };
221
+
222
+ // The keys that are not characters at all. `Prior`/`Next` are the core
223
+ // protocol's names for the page keys and `Page_Up`/`Page_Down` the ones
224
+ // everything since has used; both arrive, so both resolve.
225
+ const NAMED_KEYSYMS = {
226
+ BackSpace: XK_BACKSPACE,
227
+ Tab: XK_TAB,
228
+ Return: XK_RETURN,
229
+ Escape: XK_ESCAPE,
230
+ Delete: XK_DELETE,
231
+ Insert: XK_INSERT,
232
+ Home: XK_HOME,
233
+ Left: XK_LEFT,
234
+ Up: XK_UP,
235
+ Right: XK_RIGHT,
236
+ Down: XK_DOWN,
237
+ Prior: XK_PAGE_UP,
238
+ Page_Up: XK_PAGE_UP,
239
+ Next: XK_PAGE_DOWN,
240
+ Page_Down: XK_PAGE_DOWN,
241
+ End: XK_END,
242
+ KP_Enter: XK_KP_ENTER,
243
+ Menu: XK_MENU,
244
+ Multi_key: XK_MULTI_KEY,
245
+ };
246
+
247
+ // `XK_F1` through `XK_F35` are one contiguous run, so the whole function row
248
+ // is arithmetic rather than 35 more table entries.
249
+ const FUNCTION_KEY = /^F([1-9]|[12]\d|3[0-5])$/;
250
+
251
+ /**
252
+ * The keysym an X11 key *name* stands for — `'Return'`, `'plus'`, `'F5'`,
253
+ * `'s'` — or `undefined` for a name nothing here knows.
254
+ *
255
+ * This is the vocabulary menu `shortcut` chords are written in, and the
256
+ * reason it is public: a shortcut can be moved into or out of a menu
257
+ * without being respelled. A one-character name is the character itself, by
258
+ * `keysymOf`'s two rules, which covers the letters, the digits and anyone
259
+ * who wrote `'+'` where X would have said `plus`.
260
+ */
261
+ export function keysymFromName(name) {
262
+ if (typeof name !== 'string' || name === '') return undefined;
263
+ if ([...name].length === 1) return keysymOf(name);
264
+ const named = NAMED_KEYSYMS[name];
265
+ if (named !== undefined) return named;
266
+ const punctuation = PUNCTUATION_NAMES[name];
267
+ if (punctuation !== undefined) return keysymOf(punctuation);
268
+ const fn = FUNCTION_KEY.exec(name);
269
+ return fn ? XK_F1 + Number(fn[1]) - 1 : undefined;
270
+ }
package/src/locale.js ADDED
@@ -0,0 +1,170 @@
1
+ // Which language and conventions this app was started in.
2
+ //
3
+ // ## Why the environment comes before `Intl`
4
+ //
5
+ // `Intl.DateTimeFormat().resolvedOptions().locale` is the obvious answer and
6
+ // it is the *second* one here, because node resolves it through ICU and ICU
7
+ // answers `en-US` for a great many environments that are not en-US — a
8
+ // small-ICU build has one locale compiled in, and a container with `LANG`
9
+ // set but no locale archive still tells you what the user asked for. POSIX
10
+ // `LC_ALL` / `LC_MESSAGES` / `LANG` is what the person who launched the app
11
+ // actually said, so it wins where it says anything.
12
+ //
13
+ // `C` and `POSIX` are not locales — they are the *absence* of one, spelled
14
+ // as a value — so they fall through rather than being converted to a tag.
15
+ //
16
+ // ## This does not change while an app runs
17
+ //
18
+ // A process's environment is fixed at exec, and `Intl`'s resolution with it.
19
+ // `org.freedesktop.locale1` can announce a change for *future* logins, and
20
+ // neither node nor ICU picks it up for this one, so publishing it here would
21
+ // be reporting a change that has not happened to anything the app can see.
22
+ //
23
+ // So `useLocale()` subscribes to nothing, the way `useSupports('shaders')`
24
+ // does — the hook shape is for composition and for the day a rung underneath
25
+ // it can genuinely change, not because there is a store behind it.
26
+
27
+ import { localeDirection } from './palette.js';
28
+
29
+ const ENV = typeof process === 'undefined' ? {} : (process.env ?? {});
30
+
31
+ /**
32
+ * A POSIX locale string → a BCP-47 tag, or null when it is not a locale.
33
+ *
34
+ * `ru_RU.UTF-8@euro` → `ru-RU`: the codeset after `.` and the modifier after
35
+ * `@` are POSIX's and mean nothing to `Intl`, and the territory separator is
36
+ * `_` where BCP-47 wants `-`.
37
+ *
38
+ * Pure, and exported for the test.
39
+ */
40
+ export function toLanguageTag(value) {
41
+ if (typeof value !== 'string') return null;
42
+ const bare = value.split('.')[0].split('@')[0].trim().replace(/_/g, '-');
43
+ if (!bare) return null;
44
+ // The two values that mean "no locale at all". Passing either to `Intl`
45
+ // gets a RangeError from `C` and, worse, a plausible-looking `posix` tag
46
+ // from the other.
47
+ if (/^(C|POSIX)$/i.test(bare)) return null;
48
+ try {
49
+ // Canonicalises case and rejects a malformed tag — a `LANG` of
50
+ // `en_US_POSIX` or a typo should fall through to ICU rather than reach a
51
+ // formatter and throw there.
52
+ return Intl.getCanonicalLocales(bare)[0] ?? null;
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Which day the week starts on for a locale: `0` Sunday … `6` Saturday.
60
+ *
61
+ * The runtime knows this — CLDR carries it — but only through
62
+ * `Intl.Locale#getWeekInfo`, which is recent enough that a small-ICU build or
63
+ * an older V8 has neither it nor the `weekInfo` property it was proposed as.
64
+ * So it is feature-detected, and what is left when it is missing is **Monday**:
65
+ * ISO 8601's answer, and the one most of the world uses.
66
+ */
67
+ export function localeWeekStart(locale) {
68
+ try {
69
+ const info = new Intl.Locale(locale ?? undefined);
70
+ const week = info.getWeekInfo?.() ?? info.weekInfo;
71
+ // CLDR counts 1 Monday … 7 Sunday; JS counts 0 Sunday … 6 Saturday
72
+ if (week?.firstDay) return week.firstDay % 7;
73
+ } catch {
74
+ // an invalid locale tag: the caller's formatters will complain about it
75
+ // more usefully than a week-start fallback could
76
+ }
77
+ return 1;
78
+ }
79
+
80
+ /**
81
+ * Which way a tag reads.
82
+ *
83
+ * CLDR knows, through `Intl.Locale#getTextInfo()`, and that is the answer
84
+ * where the runtime has it — it covers every script rather than the list of
85
+ * languages a hand-maintained set can hold. `localeDirection()` in
86
+ * `palette.js` is the fallback and the same set the default theme's direction
87
+ * is seeded from, so the two cannot disagree about a language both know.
88
+ */
89
+ export function directionOf(tag, env = ENV) {
90
+ try {
91
+ const info = new Intl.Locale(tag ?? undefined);
92
+ const text = info.getTextInfo?.() ?? info.textInfo;
93
+ if (text?.direction === 'rtl' || text?.direction === 'ltr') {
94
+ return text.direction;
95
+ }
96
+ } catch {
97
+ // no getTextInfo in this build, or a tag Intl.Locale will not take
98
+ }
99
+ return localeDirection(tag ? { LANG: tag } : env);
100
+ }
101
+
102
+ /**
103
+ * Resolve everything at once. Pure in `env`, so a test can state an
104
+ * environment rather than mutate the process's.
105
+ */
106
+ export function resolveLocale(env = ENV) {
107
+ const fromEnv =
108
+ toLanguageTag(env.LC_ALL) ??
109
+ toLanguageTag(env.LC_MESSAGES) ??
110
+ toLanguageTag(env.LANG);
111
+
112
+ let resolved = fromEnv;
113
+ let timeZone = null;
114
+ try {
115
+ const options = Intl.DateTimeFormat(
116
+ resolved ?? undefined,
117
+ ).resolvedOptions();
118
+ // ICU may not have the locale the environment named — a small build has
119
+ // one — in which case `resolvedOptions().locale` is the nearest it does
120
+ // have. The environment's answer still stands: it is what the user asked
121
+ // for, and every `Intl` call in the app will fall back the same way this
122
+ // one just did.
123
+ resolved ??= options.locale;
124
+ timeZone = options.timeZone ?? null;
125
+ } catch {
126
+ // no ICU at all
127
+ }
128
+
129
+ return Object.freeze({
130
+ locale: resolved ?? 'en-US',
131
+ direction: directionOf(resolved, env),
132
+ weekStartsOn: localeWeekStart(resolved),
133
+ timeZone,
134
+ source: fromEnv ? 'env' : resolved ? 'intl' : null,
135
+ });
136
+ }
137
+
138
+ /** Read once: see the note at the top about why there is nothing to watch. */
139
+ let snapshot = null;
140
+
141
+ /** The resolved locale, as one frozen object. Stable for the process. */
142
+ export function systemLocale() {
143
+ snapshot ??= resolveLocale();
144
+ return snapshot;
145
+ }
146
+
147
+ /**
148
+ * Test seam: state a locale without re-execing the process. `null` releases
149
+ * the pin and resolves from the environment again.
150
+ *
151
+ * Naming a `locale` re-derives `direction` and `weekStartsOn` from it rather
152
+ * than keeping the process's own — pinning `he-IL` and silently staying
153
+ * left-to-right is not a locale anybody has.
154
+ */
155
+ export function setLocaleForTests(values) {
156
+ if (values === null) {
157
+ snapshot = null;
158
+ return null;
159
+ }
160
+ const current = systemLocale();
161
+ const locale = values.locale ?? current.locale;
162
+ snapshot = Object.freeze({
163
+ locale,
164
+ direction: values.direction ?? directionOf(locale),
165
+ weekStartsOn: values.weekStartsOn ?? localeWeekStart(locale),
166
+ timeZone: values.timeZone ?? current.timeZone,
167
+ source: 'test',
168
+ });
169
+ return snapshot;
170
+ }
@@ -0,0 +1,47 @@
1
+ // `useLocale()` — the language and conventions the app was started in.
2
+ //
3
+ // The resolution lives in `locale.js`. There is no store and no subscription
4
+ // behind this: a process's locale is fixed at exec (see the note there), so
5
+ // the hook exists for composition and discoverability rather than because
6
+ // anything can change.
7
+
8
+ import { systemLocale } from './locale.js';
9
+
10
+ /**
11
+ * Which language and conventions this app was started in.
12
+ *
13
+ * ```jsx
14
+ * const { locale, weekStartsOn } = useLocale();
15
+ *
16
+ * <Calendar locale={locale} weekStartsOn={weekStartsOn} /> // components
17
+ * <text>{new Intl.NumberFormat(locale).format(total)}</text>
18
+ * ```
19
+ *
20
+ * | | |
21
+ * | --- | --- |
22
+ * | `locale` | a BCP-47 tag: `'en-GB'`, `'ru-RU'` |
23
+ * | `direction` | `'ltr'` or `'rtl'` — what the default theme is already seeded with |
24
+ * | `weekStartsOn` | `0` Sunday … `6` Saturday, from CLDR |
25
+ * | `timeZone` | `'Europe/London'`, or null where ICU could not say |
26
+ * | `source` | `'env'` when `LANG` and friends said, `'intl'` when ICU did |
27
+ *
28
+ * **`LC_ALL` / `LC_MESSAGES` / `LANG` win over `Intl`'s own answer**, because
29
+ * ICU resolves to what it has compiled in rather than to what the user asked
30
+ * for — a small-ICU node reports `en-US` for every environment there is. The
31
+ * tag here is what the person who launched the app said; `Intl` is the
32
+ * fallback for a process started without any of them.
33
+ *
34
+ * **This does not change while the app runs**, and there is deliberately no
35
+ * subscription: a process's environment is fixed at exec and ICU's resolution
36
+ * with it, so a desktop that switches language announces it for the *next*
37
+ * login and nothing this process can observe has moved.
38
+ *
39
+ * Text direction is here for completeness and is rarely what you want to read
40
+ * — the widget set already mirrors itself from the same value, and
41
+ * `useDirection()` is the one to ask inside a component, because it also
42
+ * honours a `<ThemeProvider direction>` and a `direction` style property the
43
+ * locale knows nothing about. See docs/styling.md.
44
+ */
45
+ export function useLocale() {
46
+ return systemLocale();
47
+ }