@octanejs/aria 0.0.5 → 0.0.6

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 (120) hide show
  1. package/package.json +3 -3
  2. package/src/components/Table.ts +2618 -0
  3. package/src/components/Tree.ts +1450 -0
  4. package/src/components/TreeDropTargetDelegate.ts +363 -0
  5. package/src/components/index.ts +57 -0
  6. package/src/grid/GridKeyboardDelegate.ts +514 -0
  7. package/src/grid/useGrid.ts +263 -0
  8. package/src/grid/useGridCell.ts +346 -0
  9. package/src/grid/useGridRow.ts +108 -0
  10. package/src/grid/useGridRowGroup.ts +29 -0
  11. package/src/grid/utils.ts +24 -0
  12. package/src/index.ts +38 -0
  13. package/src/intl/autocomplete/index.ts +9 -2
  14. package/src/intl/breadcrumbs/index.ts +9 -2
  15. package/src/intl/combobox/index.ts +9 -2
  16. package/src/intl/compileMessages.ts +356 -0
  17. package/src/intl/grid/index.ts +9 -2
  18. package/src/intl/gridlist/index.ts +9 -2
  19. package/src/intl/menu/index.ts +9 -2
  20. package/src/intl/numberfield/index.ts +9 -2
  21. package/src/intl/overlays/index.ts +9 -2
  22. package/src/intl/searchfield/index.ts +9 -2
  23. package/src/intl/spinbutton/index.ts +9 -2
  24. package/src/intl/table/ar-AE.json +13 -0
  25. package/src/intl/table/bg-BG.json +13 -0
  26. package/src/intl/table/cs-CZ.json +13 -0
  27. package/src/intl/table/da-DK.json +13 -0
  28. package/src/intl/table/de-DE.json +13 -0
  29. package/src/intl/table/el-GR.json +13 -0
  30. package/src/intl/table/en-US.json +13 -0
  31. package/src/intl/table/es-ES.json +13 -0
  32. package/src/intl/table/et-EE.json +13 -0
  33. package/src/intl/table/fi-FI.json +13 -0
  34. package/src/intl/table/fr-FR.json +13 -0
  35. package/src/intl/table/he-IL.json +13 -0
  36. package/src/intl/table/hr-HR.json +13 -0
  37. package/src/intl/table/hu-HU.json +13 -0
  38. package/src/intl/table/index.ts +82 -0
  39. package/src/intl/table/it-IT.json +13 -0
  40. package/src/intl/table/ja-JP.json +13 -0
  41. package/src/intl/table/ko-KR.json +13 -0
  42. package/src/intl/table/lt-LT.json +13 -0
  43. package/src/intl/table/lv-LV.json +13 -0
  44. package/src/intl/table/nb-NO.json +13 -0
  45. package/src/intl/table/nl-NL.json +13 -0
  46. package/src/intl/table/pl-PL.json +13 -0
  47. package/src/intl/table/pt-BR.json +13 -0
  48. package/src/intl/table/pt-PT.json +13 -0
  49. package/src/intl/table/ro-RO.json +13 -0
  50. package/src/intl/table/ru-RU.json +13 -0
  51. package/src/intl/table/sk-SK.json +13 -0
  52. package/src/intl/table/sl-SI.json +13 -0
  53. package/src/intl/table/sr-SP.json +13 -0
  54. package/src/intl/table/sv-SE.json +13 -0
  55. package/src/intl/table/tr-TR.json +13 -0
  56. package/src/intl/table/uk-UA.json +13 -0
  57. package/src/intl/table/zh-CN.json +13 -0
  58. package/src/intl/table/zh-TW.json +13 -0
  59. package/src/intl/tag/index.ts +9 -2
  60. package/src/intl/tree/ar-AE.json +4 -0
  61. package/src/intl/tree/bg-BG.json +4 -0
  62. package/src/intl/tree/cs-CZ.json +4 -0
  63. package/src/intl/tree/da-DK.json +4 -0
  64. package/src/intl/tree/de-DE.json +4 -0
  65. package/src/intl/tree/el-GR.json +4 -0
  66. package/src/intl/tree/en-US.json +4 -0
  67. package/src/intl/tree/es-ES.json +4 -0
  68. package/src/intl/tree/et-EE.json +4 -0
  69. package/src/intl/tree/fi-FI.json +4 -0
  70. package/src/intl/tree/fr-FR.json +4 -0
  71. package/src/intl/tree/he-IL.json +4 -0
  72. package/src/intl/tree/hr-HR.json +4 -0
  73. package/src/intl/tree/hu-HU.json +4 -0
  74. package/src/intl/tree/index.ts +82 -0
  75. package/src/intl/tree/it-IT.json +4 -0
  76. package/src/intl/tree/ja-JP.json +4 -0
  77. package/src/intl/tree/ko-KR.json +4 -0
  78. package/src/intl/tree/lt-LT.json +4 -0
  79. package/src/intl/tree/lv-LV.json +4 -0
  80. package/src/intl/tree/nb-NO.json +4 -0
  81. package/src/intl/tree/nl-NL.json +4 -0
  82. package/src/intl/tree/pl-PL.json +4 -0
  83. package/src/intl/tree/pt-BR.json +4 -0
  84. package/src/intl/tree/pt-PT.json +4 -0
  85. package/src/intl/tree/ro-RO.json +4 -0
  86. package/src/intl/tree/ru-RU.json +4 -0
  87. package/src/intl/tree/sk-SK.json +4 -0
  88. package/src/intl/tree/sl-SI.json +4 -0
  89. package/src/intl/tree/sr-SP.json +4 -0
  90. package/src/intl/tree/sv-SE.json +4 -0
  91. package/src/intl/tree/tr-TR.json +4 -0
  92. package/src/intl/tree/uk-UA.json +4 -0
  93. package/src/intl/tree/zh-CN.json +4 -0
  94. package/src/intl/tree/zh-TW.json +4 -0
  95. package/src/stately/grid/GridCollection.ts +200 -0
  96. package/src/stately/grid/useGridState.ts +160 -0
  97. package/src/stately/index.ts +43 -0
  98. package/src/stately/table/Cell.ts +49 -0
  99. package/src/stately/table/Column.ts +121 -0
  100. package/src/stately/table/Row.ts +145 -0
  101. package/src/stately/table/TableBody.ts +66 -0
  102. package/src/stately/table/TableCollection.ts +390 -0
  103. package/src/stately/table/TableColumnLayout.ts +148 -0
  104. package/src/stately/table/TableHeader.ts +65 -0
  105. package/src/stately/table/TableUtils.ts +269 -0
  106. package/src/stately/table/useTableColumnResizeState.ts +202 -0
  107. package/src/stately/table/useTableState.ts +235 -0
  108. package/src/stately/table/useTreeGridState.ts +248 -0
  109. package/src/table/TableKeyboardDelegate.ts +205 -0
  110. package/src/table/useTable.ts +175 -0
  111. package/src/table/useTableCell.ts +81 -0
  112. package/src/table/useTableColumnHeader.ts +155 -0
  113. package/src/table/useTableColumnResize.ts +374 -0
  114. package/src/table/useTableHeaderRow.ts +57 -0
  115. package/src/table/useTableRow.ts +198 -0
  116. package/src/table/useTableRowGroup.ts +13 -0
  117. package/src/table/useTableSelectionCheckbox.ts +105 -0
  118. package/src/table/utils.ts +42 -0
  119. package/src/tree/useTree.ts +76 -0
  120. package/src/tree/useTreeItem.ts +109 -0
@@ -0,0 +1,108 @@
1
+ // Ported from react-aria (source: .react-spectrum/packages/react-aria/src/grid/useGridRow.ts).
2
+ // octane adaptations:
3
+ // - `GridState`/`IGridCollection`/`GridNode` come from the ported stately grid hooks;
4
+ // `DOMAttributes` is a local structural prop-bag alias.
5
+ // - Public-hook slot threading (splitSlot/subSlot).
6
+ import { chain } from '../utils/chain';
7
+
8
+ import type { FocusableElement, RefObject } from '@react-types/shared';
9
+ import type { IGridCollection as GridCollection, GridNode } from '../stately/grid/GridCollection';
10
+ import { gridMap } from './utils';
11
+ import type { GridState } from '../stately/grid/useGridState';
12
+ import { SelectableItemStates, useSelectableItem } from '../selection/useSelectableItem';
13
+
14
+ import { S, splitSlot, subSlot } from '../internal';
15
+
16
+ // octane adaptation: minimal structural DOMAttributes (upstream's drags React's synthetic
17
+ // handler types along).
18
+ type DOMAttributes = Record<string, any>;
19
+
20
+ export interface GridRowProps<T> {
21
+ /**
22
+ * An object representing the grid row. Contains all the relevant information that makes up the
23
+ * grid row.
24
+ */
25
+ node: GridNode<T>;
26
+ /** Whether the grid row is contained in a virtual scroller. */
27
+ isVirtualized?: boolean;
28
+ /** Whether selection should occur on press up instead of press down. */
29
+ shouldSelectOnPressUp?: boolean;
30
+ /**
31
+ * Handler that is called when a user performs an action on the row.
32
+ * Please use onCellAction at the collection level instead.
33
+ *
34
+ * @deprecated
35
+ */
36
+ onAction?: () => void;
37
+ }
38
+
39
+ export interface GridRowAria extends SelectableItemStates {
40
+ /** Props for the grid row element. */
41
+ rowProps: DOMAttributes;
42
+ /** Whether the row is currently in a pressed state. */
43
+ isPressed: boolean;
44
+ }
45
+
46
+ /**
47
+ * Provides the behavior and accessibility implementation for a row in a grid.
48
+ *
49
+ * @param props - Props for the row.
50
+ * @param state - State of the parent grid, as returned by `useGridState`.
51
+ */
52
+ export function useGridRow<T, C extends GridCollection<T>, S extends GridState<T, C>>(
53
+ props: GridRowProps<T>,
54
+ state: S,
55
+ ref: RefObject<FocusableElement | null>,
56
+ ): GridRowAria;
57
+ // Slot-threading form: sibling ported hooks pass their derived sub-slot as the trailing arg.
58
+ export function useGridRow<T, C extends GridCollection<T>, S extends GridState<T, C>>(
59
+ props: GridRowProps<T>,
60
+ state: S,
61
+ ref: RefObject<FocusableElement | null>,
62
+ slot: symbol | undefined,
63
+ ): GridRowAria;
64
+ export function useGridRow(...args: any[]): GridRowAria {
65
+ const [user, slotArg] = splitSlot(args);
66
+ const slot = slotArg ?? S('useGridRow');
67
+ const props = user[0] as GridRowProps<any>;
68
+ const state = user[1] as GridState<any, GridCollection<any>>;
69
+ const ref = user[2] as RefObject<FocusableElement | null>;
70
+
71
+ let { node, isVirtualized, shouldSelectOnPressUp, onAction } = props;
72
+
73
+ let { actions, shouldSelectOnPressUp: gridShouldSelectOnPressUp } = gridMap.get(state)!;
74
+ let onRowAction = actions.onRowAction ? () => actions.onRowAction?.(node.key) : onAction;
75
+ let { itemProps, ...states } = useSelectableItem(
76
+ {
77
+ selectionManager: state.selectionManager,
78
+ key: node.key,
79
+ ref,
80
+ isVirtualized,
81
+ shouldSelectOnPressUp: gridShouldSelectOnPressUp || shouldSelectOnPressUp,
82
+ onAction:
83
+ onRowAction || node?.props?.onAction
84
+ ? chain(node?.props?.onAction, onRowAction)
85
+ : undefined,
86
+ isDisabled: state.collection.size === 0,
87
+ },
88
+ subSlot(slot, 'selectableItem'),
89
+ );
90
+
91
+ let isSelected = state.selectionManager.isSelected(node.key);
92
+
93
+ let rowProps: DOMAttributes = {
94
+ role: 'row',
95
+ 'aria-selected': state.selectionManager.selectionMode !== 'none' ? isSelected : undefined,
96
+ 'aria-disabled': states.isDisabled || undefined,
97
+ ...itemProps,
98
+ };
99
+
100
+ if (isVirtualized) {
101
+ rowProps['aria-rowindex'] = node.index + 1; // aria-rowindex is 1 based
102
+ }
103
+
104
+ return {
105
+ rowProps,
106
+ ...states,
107
+ };
108
+ }
@@ -0,0 +1,29 @@
1
+ // Ported from react-aria (source: .react-spectrum/packages/react-aria/src/grid/useGridRowGroup.ts).
2
+ // octane adaptations: `DOMAttributes` is a local structural prop-bag alias. The hook
3
+ // composes no base hooks, so the optional trailing slot is accepted (uniform calling
4
+ // convention) and unused.
5
+ import { splitSlot } from '../internal';
6
+
7
+ // octane adaptation: minimal structural DOMAttributes (upstream's drags React's synthetic
8
+ // handler types along).
9
+ type DOMAttributes = Record<string, any>;
10
+
11
+ export interface GridRowGroupAria {
12
+ /** Props for the row group element. */
13
+ rowGroupProps: DOMAttributes;
14
+ }
15
+
16
+ /**
17
+ * Provides the accessibility implementation for a row group in a grid.
18
+ */
19
+ export function useGridRowGroup(): GridRowGroupAria;
20
+ // Slot-threading form: sibling ported hooks pass their derived sub-slot as the trailing arg.
21
+ export function useGridRowGroup(slot: symbol | undefined): GridRowGroupAria;
22
+ export function useGridRowGroup(...args: any[]): GridRowGroupAria {
23
+ splitSlot(args);
24
+ return {
25
+ rowGroupProps: {
26
+ role: 'rowgroup',
27
+ },
28
+ };
29
+ }
@@ -0,0 +1,24 @@
1
+ // Ported from react-aria (source: .react-spectrum/packages/react-aria/src/grid/utils.ts).
2
+ // octane adaptations: `GridState`/`IGridCollection` types come from the ported stately
3
+ // grid hooks.
4
+ import type { IGridCollection as GridCollection } from '../stately/grid/GridCollection';
5
+
6
+ import type { GridState } from '../stately/grid/useGridState';
7
+ import type { Key, KeyboardDelegate } from '@react-types/shared';
8
+
9
+ interface GridMapShared {
10
+ keyboardDelegate: KeyboardDelegate;
11
+ actions: {
12
+ onRowAction?: (key: Key) => void;
13
+ onCellAction?: (key: Key) => void;
14
+ };
15
+ shouldSelectOnPressUp?: boolean;
16
+ }
17
+
18
+ // Used to share:
19
+ // keyboard delegate between useGrid and useGridCell
20
+ // onRowAction/onCellAction across hooks
21
+ export const gridMap: WeakMap<
22
+ GridState<unknown, GridCollection<unknown>>,
23
+ GridMapShared
24
+ > = new WeakMap<GridState<unknown, GridCollection<unknown>>, GridMapShared>();
package/src/index.ts CHANGED
@@ -318,3 +318,41 @@ export type {
318
318
  // combobox — mirrors the @react-aria/combobox shim surface.
319
319
  export { useComboBox } from './combobox/useComboBox';
320
320
  export type { AriaComboBoxProps, AriaComboBoxOptions, ComboBoxAria } from './combobox/useComboBox';
321
+
322
+ // tree — mirrors the @react-aria/tree shim surface.
323
+ export { useTree } from './tree/useTree';
324
+ export { useTreeItem } from './tree/useTreeItem';
325
+ export type { AriaTreeOptions, AriaTreeProps, TreeAria, TreeProps } from './tree/useTree';
326
+ export type { AriaTreeItemOptions, TreeItemAria } from './tree/useTreeItem';
327
+
328
+ // table — mirrors the @react-aria/table shim surface.
329
+ export { useTable } from './table/useTable';
330
+ export { useTableColumnHeader } from './table/useTableColumnHeader';
331
+ export { useTableRow } from './table/useTableRow';
332
+ export { useTableHeaderRow } from './table/useTableHeaderRow';
333
+ export { useTableCell } from './table/useTableCell';
334
+ export {
335
+ useTableSelectionCheckbox,
336
+ useTableSelectAllCheckbox,
337
+ } from './table/useTableSelectionCheckbox';
338
+ export { useTableColumnResize } from './table/useTableColumnResize';
339
+ export { useTableRowGroup } from './table/useTableRowGroup';
340
+ export type { AriaTableProps } from './table/useTable';
341
+ export type {
342
+ AriaTableColumnHeaderProps,
343
+ TableColumnHeaderAria,
344
+ } from './table/useTableColumnHeader';
345
+ export type { TableRowAria } from './table/useTableRow';
346
+ export type { TableHeaderRowAria } from './table/useTableHeaderRow';
347
+ export type { AriaTableCellProps, TableCellAria } from './table/useTableCell';
348
+ export type {
349
+ AriaTableSelectionCheckboxProps,
350
+ TableSelectionCheckboxAria,
351
+ TableSelectAllCheckboxAria,
352
+ } from './table/useTableSelectionCheckbox';
353
+ export type {
354
+ AriaTableColumnResizeProps,
355
+ TableColumnResizeAria,
356
+ } from './table/useTableColumnResize';
357
+ export type { GridAria, GridProps } from './grid/useGrid';
358
+ export type { GridRowAria, GridRowProps } from './grid/useGridRow';
@@ -2,6 +2,13 @@
2
2
  // upstream's Parcel glob import (`import intlMessages from '../../intl/autocomplete/*.json'`).
3
3
  // The JSON files are copied VERBATIM from .react-spectrum/packages/react-aria/intl/autocomplete/;
4
4
  // refresh them from the pinned checkout on version bumps, never hand-edit.
5
+ //
6
+ // Upstream's Parcel build additionally compiles each message through
7
+ // @internationalized/string-compiler, so `{variable}` placeholders and plural/select
8
+ // ICU arguments become message FUNCTIONS (LocalizedStringFormatter interpolates only
9
+ // function messages — plain strings are returned verbatim). `compileDictionaries`
10
+ // applies the equivalent compile step here at module init.
11
+ import { compileDictionaries } from '../compileMessages';
5
12
  import ar_AE from './ar-AE.json';
6
13
  import bg_BG from './bg-BG.json';
7
14
  import cs_CZ from './cs-CZ.json';
@@ -37,7 +44,7 @@ import uk_UA from './uk-UA.json';
37
44
  import zh_CN from './zh-CN.json';
38
45
  import zh_TW from './zh-TW.json';
39
46
 
40
- export default {
47
+ export default compileDictionaries({
41
48
  'ar-AE': ar_AE,
42
49
  'bg-BG': bg_BG,
43
50
  'cs-CZ': cs_CZ,
@@ -72,4 +79,4 @@ export default {
72
79
  'uk-UA': uk_UA,
73
80
  'zh-CN': zh_CN,
74
81
  'zh-TW': zh_TW,
75
- } as Record<string, Record<string, string>>;
82
+ });
@@ -2,6 +2,13 @@
2
2
  // upstream's Parcel glob import (`import intlMessages from '../../intl/breadcrumbs/*.json'`).
3
3
  // The JSON files are copied VERBATIM from .react-spectrum/packages/react-aria/intl/breadcrumbs/;
4
4
  // refresh them from the pinned checkout on version bumps, never hand-edit.
5
+ //
6
+ // Upstream's Parcel build additionally compiles each message through
7
+ // @internationalized/string-compiler, so `{variable}` placeholders and plural/select
8
+ // ICU arguments become message FUNCTIONS (LocalizedStringFormatter interpolates only
9
+ // function messages — plain strings are returned verbatim). `compileDictionaries`
10
+ // applies the equivalent compile step here at module init.
11
+ import { compileDictionaries } from '../compileMessages';
5
12
  import ar_AE from './ar-AE.json';
6
13
  import bg_BG from './bg-BG.json';
7
14
  import cs_CZ from './cs-CZ.json';
@@ -37,7 +44,7 @@ import uk_UA from './uk-UA.json';
37
44
  import zh_CN from './zh-CN.json';
38
45
  import zh_TW from './zh-TW.json';
39
46
 
40
- export default {
47
+ export default compileDictionaries({
41
48
  'ar-AE': ar_AE,
42
49
  'bg-BG': bg_BG,
43
50
  'cs-CZ': cs_CZ,
@@ -72,4 +79,4 @@ export default {
72
79
  'uk-UA': uk_UA,
73
80
  'zh-CN': zh_CN,
74
81
  'zh-TW': zh_TW,
75
- } as Record<string, Record<string, string>>;
82
+ });
@@ -2,6 +2,13 @@
2
2
  // upstream's Parcel glob import (`import intlMessages from '../../intl/combobox/*.json'`).
3
3
  // The JSON files are copied VERBATIM from .react-spectrum/packages/react-aria/intl/combobox/;
4
4
  // refresh them from the pinned checkout on version bumps, never hand-edit.
5
+ //
6
+ // Upstream's Parcel build additionally compiles each message through
7
+ // @internationalized/string-compiler, so `{variable}` placeholders and plural/select
8
+ // ICU arguments become message FUNCTIONS (LocalizedStringFormatter interpolates only
9
+ // function messages — plain strings are returned verbatim). `compileDictionaries`
10
+ // applies the equivalent compile step here at module init.
11
+ import { compileDictionaries } from '../compileMessages';
5
12
  import ar_AE from './ar-AE.json';
6
13
  import bg_BG from './bg-BG.json';
7
14
  import cs_CZ from './cs-CZ.json';
@@ -37,7 +44,7 @@ import uk_UA from './uk-UA.json';
37
44
  import zh_CN from './zh-CN.json';
38
45
  import zh_TW from './zh-TW.json';
39
46
 
40
- export default {
47
+ export default compileDictionaries({
41
48
  'ar-AE': ar_AE,
42
49
  'bg-BG': bg_BG,
43
50
  'cs-CZ': cs_CZ,
@@ -72,4 +79,4 @@ export default {
72
79
  'uk-UA': uk_UA,
73
80
  'zh-CN': zh_CN,
74
81
  'zh-TW': zh_TW,
75
- } as Record<string, Record<string, string>>;
82
+ });
@@ -0,0 +1,356 @@
1
+ // Shared runtime ICU message compiler for the generated src/intl/*/index.ts
2
+ // locale-dictionary indexes.
3
+ //
4
+ // Upstream react-aria loads its intl JSON through a Parcel transform that runs every
5
+ // message through @internationalized/string-compiler at BUILD time, turning any message
6
+ // with arguments into a function `(args, formatter) => string`.
7
+ // LocalizedStringFormatter.format only interpolates function messages — plain strings
8
+ // are returned verbatim — so exporting the raw JSON would render `Increase {fieldLabel}`
9
+ // and raw ICU plural source literally. This module is the runtime equivalent of that
10
+ // build step; each generated index applies it once at module init via
11
+ // `compileDictionaries`.
12
+ //
13
+ // It supports the ICU MessageFormat constructs the vendored dictionaries use: literal
14
+ // text (with ICU apostrophe quoting), `{argument}` interpolation, `{arg, number}`,
15
+ // `{arg, plural, …}` / `{arg, selectordinal, …}` (`offset:`, exact `=N` and category
16
+ // selectors, `#`), and `{arg, select, …}`, all arbitrarily nestable. Anything else
17
+ // (date/time arguments, number styles) throws at module init so a dictionary refresh
18
+ // that introduces a new construct fails loudly instead of silently rendering raw ICU
19
+ // source. Evaluation mirrors @internationalized/string-compiler's generated code
20
+ // exactly: `{argument}` coerces like a template literal, plural/select branches without
21
+ // arguments stay plain strings while branches with arguments become thunks over the
22
+ // current args, and `#` formats the nearest enclosing plural's value (select branches
23
+ // inherit it) through `formatter.number`.
24
+ import type {
25
+ LocalizedString,
26
+ LocalizedStringFormatter,
27
+ Variables,
28
+ } from '@internationalized/string';
29
+
30
+ // Structural view of LocalizedStringFormatter's plural/select/number helpers. They are
31
+ // `protected` in the published types, but compiled messages — upstream's and these —
32
+ // call them: format() passes the formatter itself as the message function's second
33
+ // argument for exactly this purpose.
34
+ interface MessageFormatter {
35
+ plural(
36
+ count: number,
37
+ options: Record<string, string | (() => string)>,
38
+ type?: Intl.PluralRuleType,
39
+ ): string;
40
+ select(options: Record<string, string | (() => string)>, value: string): string;
41
+ number(value: number): string;
42
+ }
43
+
44
+ type Part =
45
+ | { type: 'literal'; value: string }
46
+ | { type: 'argument'; name: string }
47
+ | { type: 'number'; name: string }
48
+ | { type: 'pound' }
49
+ | {
50
+ type: 'plural';
51
+ name: string;
52
+ offset: number;
53
+ pluralType: Intl.PluralRuleType;
54
+ options: Option[];
55
+ }
56
+ | { type: 'select'; name: string; options: Option[] };
57
+
58
+ // A plural/select branch. Literal-only branches pre-join to a plain string (`text`),
59
+ // matching upstream's compiled string-literal options; branches with arguments keep
60
+ // their parts and are rendered through a per-call thunk that captures the current args.
61
+ type Option = { key: string; text: string | null; parts: Part[] };
62
+
63
+ class MessageParser {
64
+ private pos = 0;
65
+
66
+ constructor(private message: string) {}
67
+
68
+ parse(): Part[] {
69
+ const parts = this.parseParts(false, false);
70
+ if (this.pos < this.message.length) {
71
+ throw this.error('unmatched `}`');
72
+ }
73
+ return parts;
74
+ }
75
+
76
+ private error(reason: string): Error {
77
+ return new Error(`unsupported ICU message (${reason} at offset ${this.pos}): ${this.message}`);
78
+ }
79
+
80
+ // Parses parts until end of input, or until an unconsumed `}` when `nested`.
81
+ private parseParts(nested: boolean, inPlural: boolean): Part[] {
82
+ const msg = this.message;
83
+ const parts: Part[] = [];
84
+ let literal = '';
85
+ const flush = () => {
86
+ if (literal !== '') {
87
+ parts.push({ type: 'literal', value: literal });
88
+ literal = '';
89
+ }
90
+ };
91
+ while (this.pos < msg.length) {
92
+ const ch = msg[this.pos];
93
+ if (ch === '}') {
94
+ if (nested) {
95
+ break;
96
+ }
97
+ throw this.error('unmatched `}`');
98
+ } else if (ch === '{') {
99
+ flush();
100
+ parts.push(this.parseArgument(inPlural));
101
+ } else if (ch === '#' && inPlural) {
102
+ flush();
103
+ parts.push({ type: 'pound' });
104
+ this.pos++;
105
+ } else if (ch === "'") {
106
+ literal += this.parseQuoted(inPlural);
107
+ } else {
108
+ literal += ch;
109
+ this.pos++;
110
+ }
111
+ }
112
+ flush();
113
+ return parts;
114
+ }
115
+
116
+ // ICU apostrophe quoting: `''` is a literal apostrophe; an apostrophe immediately
117
+ // before a syntax character (`{`, `}`, or `#` inside plural) quotes literal text up
118
+ // to the next lone apostrophe (an unterminated quote runs to end of message); any
119
+ // other apostrophe is plain text.
120
+ private parseQuoted(inPlural: boolean): string {
121
+ const msg = this.message;
122
+ const next = msg[this.pos + 1];
123
+ if (next === "'") {
124
+ this.pos += 2;
125
+ return "'";
126
+ }
127
+ if (next === '{' || next === '}' || (inPlural && next === '#')) {
128
+ this.pos += 2;
129
+ let out = next;
130
+ while (this.pos < msg.length) {
131
+ if (msg[this.pos] === "'") {
132
+ if (msg[this.pos + 1] === "'") {
133
+ out += "'";
134
+ this.pos += 2;
135
+ continue;
136
+ }
137
+ this.pos++;
138
+ return out;
139
+ }
140
+ out += msg[this.pos++];
141
+ }
142
+ return out;
143
+ }
144
+ this.pos++;
145
+ return "'";
146
+ }
147
+
148
+ private parseArgument(inPlural: boolean): Part {
149
+ this.pos++; // consume `{`
150
+ const name = this.parseToken();
151
+ if (!/^\w+$/.test(name)) {
152
+ throw this.error('invalid argument name');
153
+ }
154
+ if (this.consume('}')) {
155
+ return { type: 'argument', name };
156
+ }
157
+ this.expect(',');
158
+ const kind = this.parseToken();
159
+ switch (kind) {
160
+ case 'number':
161
+ if (this.consume('}')) {
162
+ return { type: 'number', name };
163
+ }
164
+ throw this.error('number format styles are unsupported');
165
+ case 'plural':
166
+ case 'selectordinal': {
167
+ this.expect(',');
168
+ this.skipWs();
169
+ let offset = 0;
170
+ if (this.message.startsWith('offset:', this.pos)) {
171
+ this.pos += 'offset:'.length;
172
+ this.skipWs();
173
+ let digits = '';
174
+ while (/\d/.test(this.message[this.pos] ?? '')) {
175
+ digits += this.message[this.pos++];
176
+ }
177
+ if (digits === '') {
178
+ throw this.error('invalid plural offset');
179
+ }
180
+ offset = parseInt(digits, 10);
181
+ }
182
+ // `#` binds to this plural inside every branch, including nested selects.
183
+ const options = this.parseOptions(true);
184
+ this.expect('}');
185
+ return {
186
+ type: 'plural',
187
+ name,
188
+ offset,
189
+ pluralType: kind === 'plural' ? 'cardinal' : 'ordinal',
190
+ options,
191
+ };
192
+ }
193
+ case 'select': {
194
+ this.expect(',');
195
+ // Select branches inherit the surrounding plural context for `#`.
196
+ const options = this.parseOptions(inPlural);
197
+ this.expect('}');
198
+ return { type: 'select', name, options };
199
+ }
200
+ default:
201
+ throw this.error(`unsupported argument type \`${kind}\``);
202
+ }
203
+ }
204
+
205
+ private parseOptions(branchInPlural: boolean): Option[] {
206
+ const options: Option[] = [];
207
+ for (;;) {
208
+ this.skipWs();
209
+ if (this.pos >= this.message.length || this.message[this.pos] === '}') {
210
+ break;
211
+ }
212
+ let key = '';
213
+ while (
214
+ this.pos < this.message.length &&
215
+ !'{}'.includes(this.message[this.pos]) &&
216
+ !/\s/.test(this.message[this.pos])
217
+ ) {
218
+ key += this.message[this.pos++];
219
+ }
220
+ if (key === '') {
221
+ throw this.error('invalid option selector');
222
+ }
223
+ this.skipWs();
224
+ this.expect('{');
225
+ const parts = this.parseParts(true, branchInPlural);
226
+ this.expect('}');
227
+ options.push(
228
+ parts.every((p) => p.type === 'literal')
229
+ ? { key, text: parts.map((p) => (p as { value: string }).value).join(''), parts }
230
+ : { key, text: null, parts },
231
+ );
232
+ }
233
+ if (options.length === 0) {
234
+ throw this.error('missing option selectors');
235
+ }
236
+ return options;
237
+ }
238
+
239
+ private parseToken(): string {
240
+ let out = '';
241
+ while (this.pos < this.message.length && !',}'.includes(this.message[this.pos])) {
242
+ out += this.message[this.pos++];
243
+ }
244
+ return out.trim();
245
+ }
246
+
247
+ private consume(ch: string): boolean {
248
+ if (this.message[this.pos] === ch) {
249
+ this.pos++;
250
+ return true;
251
+ }
252
+ return false;
253
+ }
254
+
255
+ private expect(ch: string): void {
256
+ if (!this.consume(ch)) {
257
+ throw this.error(`expected \`${ch}\``);
258
+ }
259
+ }
260
+
261
+ private skipWs(): void {
262
+ while (/\s/.test(this.message[this.pos] ?? '')) {
263
+ this.pos++;
264
+ }
265
+ }
266
+ }
267
+
268
+ function renderParts(
269
+ parts: Part[],
270
+ args: Variables,
271
+ formatter: MessageFormatter,
272
+ pluralValue: number | undefined,
273
+ ): string {
274
+ let res = '';
275
+ for (const part of parts) {
276
+ switch (part.type) {
277
+ case 'literal':
278
+ res += part.value;
279
+ break;
280
+ case 'argument':
281
+ // Matches the compiled `${args.name}` template hole (String coercion).
282
+ res += String(args?.[part.name]);
283
+ break;
284
+ case 'number':
285
+ res += formatter.number(args?.[part.name] as number);
286
+ break;
287
+ case 'pound':
288
+ res += formatter.number(pluralValue as number);
289
+ break;
290
+ case 'plural': {
291
+ const value =
292
+ part.offset === 0
293
+ ? (args?.[part.name] as number)
294
+ : (args?.[part.name] as number) - part.offset;
295
+ res += formatter.plural(
296
+ value,
297
+ buildOptions(part.options, args, formatter, value),
298
+ part.pluralType === 'cardinal' ? undefined : part.pluralType,
299
+ );
300
+ break;
301
+ }
302
+ case 'select':
303
+ res += formatter.select(
304
+ buildOptions(part.options, args, formatter, pluralValue),
305
+ args?.[part.name] as string,
306
+ );
307
+ break;
308
+ }
309
+ }
310
+ return res;
311
+ }
312
+
313
+ function buildOptions(
314
+ options: Option[],
315
+ args: Variables,
316
+ formatter: MessageFormatter,
317
+ pluralValue: number | undefined,
318
+ ): Record<string, string | (() => string)> {
319
+ const out: Record<string, string | (() => string)> = {};
320
+ for (const option of options) {
321
+ out[option.key] =
322
+ option.text !== null
323
+ ? option.text
324
+ : () => renderParts(option.parts, args, formatter, pluralValue);
325
+ }
326
+ return out;
327
+ }
328
+
329
+ export function compileString(message: string): LocalizedString {
330
+ const parts = new MessageParser(message).parse();
331
+ if (parts.every((p) => p.type === 'literal')) {
332
+ // No arguments: stays a plain (de-quoted) string, like upstream's compiled output.
333
+ return parts.map((p) => (p as { value: string }).value).join('');
334
+ }
335
+ return (args: Variables, formatter?: LocalizedStringFormatter<any, any>) =>
336
+ renderParts(parts, args, formatter as unknown as MessageFormatter, undefined);
337
+ }
338
+
339
+ export function compileDictionaries(
340
+ dictionaries: Record<string, Record<string, string>>,
341
+ ): Record<string, Record<string, LocalizedString>> {
342
+ const compiled: Record<string, Record<string, LocalizedString>> = {};
343
+ for (const locale in dictionaries) {
344
+ const strings = dictionaries[locale];
345
+ const out: Record<string, LocalizedString> = {};
346
+ for (const key in strings) {
347
+ try {
348
+ out[key] = compileString(strings[key]);
349
+ } catch (e) {
350
+ throw new Error(`Failed to compile intl message ${locale}/${key}: ${(e as Error).message}`);
351
+ }
352
+ }
353
+ compiled[locale] = out;
354
+ }
355
+ return compiled;
356
+ }
@@ -2,6 +2,13 @@
2
2
  // upstream's Parcel glob import (`import intlMessages from '../../intl/grid/*.json'`).
3
3
  // The JSON files are copied VERBATIM from .react-spectrum/packages/react-aria/intl/grid/;
4
4
  // refresh them from the pinned checkout on version bumps, never hand-edit.
5
+ //
6
+ // Upstream's Parcel build additionally compiles each message through
7
+ // @internationalized/string-compiler, so `{variable}` placeholders and plural/select
8
+ // ICU arguments become message FUNCTIONS (LocalizedStringFormatter interpolates only
9
+ // function messages — plain strings are returned verbatim). `compileDictionaries`
10
+ // applies the equivalent compile step here at module init.
11
+ import { compileDictionaries } from '../compileMessages';
5
12
  import ar_AE from './ar-AE.json';
6
13
  import bg_BG from './bg-BG.json';
7
14
  import cs_CZ from './cs-CZ.json';
@@ -37,7 +44,7 @@ import uk_UA from './uk-UA.json';
37
44
  import zh_CN from './zh-CN.json';
38
45
  import zh_TW from './zh-TW.json';
39
46
 
40
- export default {
47
+ export default compileDictionaries({
41
48
  'ar-AE': ar_AE,
42
49
  'bg-BG': bg_BG,
43
50
  'cs-CZ': cs_CZ,
@@ -72,4 +79,4 @@ export default {
72
79
  'uk-UA': uk_UA,
73
80
  'zh-CN': zh_CN,
74
81
  'zh-TW': zh_TW,
75
- } as Record<string, Record<string, string>>;
82
+ });