@fusion.dev/helix-core 0.148.0 → 0.149.1
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/custom-elements.json +8518 -6050
- package/dist/controllers/aria-forward-controller/aria-forward-controller.js +1 -1
- package/dist/controllers/chart-pointer-controller/chart-pointer-controller.d.ts +2 -2
- package/dist/controllers/chart-pointer-controller/chart-pointer-controller.js +5 -4
- package/dist/controllers/machine-controller/machine-controller.d.ts +69 -0
- package/dist/controllers/machine-controller/machine-controller.js +78 -0
- package/dist/controllers/machine-controller/machine.d.ts +93 -0
- package/dist/controllers/machine-controller/machine.js +20 -0
- package/dist/controllers/reorder-controller/reorder-controller.d.ts +285 -0
- package/dist/controllers/reorder-controller/reorder-controller.js +497 -0
- package/dist/controllers/source-controller/source-controller.js +1 -1
- package/dist/controllers/virtualize-controller/heights.js +1 -1
- package/dist/controllers/virtualize-controller/virtualize-controller.d.ts +117 -3
- package/dist/controllers/virtualize-controller/virtualize-controller.js +134 -32
- package/dist/events/before-reorder.d.ts +14 -0
- package/dist/events/before-reorder.js +11 -0
- package/dist/events/details.d.ts +28 -0
- package/dist/events/events.d.ts +4 -2
- package/dist/events/events.js +3 -1
- package/dist/events/names.js +1 -1
- package/dist/events/reorder.d.ts +18 -12
- package/dist/events/reordered.d.ts +4 -4
- package/dist/helix-element/helix-element.js +2 -1
- package/dist/helix-element/react-audit-off.d.ts +11 -0
- package/dist/helix-element/react-audit-off.js +1 -0
- package/dist/helix-element/react-audit.d.ts +20 -0
- package/dist/helix-element/react-audit.js +56 -0
- package/dist/helix-element/react-bridge.d.ts +2 -22
- package/dist/helix-element/react-bridge.js +1 -56
- package/dist/index.d.ts +10 -4
- package/dist/index.js +10 -4
- package/dist/mixins/mixin-chart-source/mixin-chart-source.d.ts +2 -2
- package/dist/mixins/mixin-chart-source/mixin-chart-source.js +3 -0
- package/dist/mixins/mixin-collection/mixin-collection.js +4 -0
- package/dist/mixins/mixin-dismissible/mixin-dismissible.js +3 -0
- package/dist/mixins/mixin-field/mixin-field.js +1 -1
- package/dist/mixins/mixin-segmented-field/mixin-segmented-field.d.ts +12 -0
- package/dist/mixins/mixin-segmented-field/mixin-segmented-field.js +15 -2
- package/dist/mixins/mixin-tokens/mixin-tokens.d.ts +5 -0
- package/dist/mixins/mixin-tokens/mixin-tokens.js +13 -2
- package/dist/mixins/mixin-viewport/mixin-viewport.d.ts +17 -15
- package/dist/mixins/mixin-viewport/mixin-viewport.js +15 -34
- package/dist/utils/calendar/calendar.d.ts +27 -0
- package/dist/utils/calendar/calendar.js +17 -0
- package/dist/utils/count-label/count-label.d.ts +7 -4
- package/dist/utils/count-label/count-label.js +2 -5
- package/dist/utils/date-segments/date-segments.d.ts +2 -1
- package/dist/utils/date-segments/date-segments.js +10 -0
- package/dist/utils/locale-of/locale-of.d.ts +5 -0
- package/dist/utils/locale-of/locale-of.js +8 -2
- package/dist/utils/reorder/drag-preview.d.ts +46 -0
- package/dist/utils/reorder/drag-preview.js +58 -0
- package/dist/utils/reorder/reorder.d.ts +178 -0
- package/dist/utils/reorder/reorder.js +112 -0
- package/dist/utils/report-runtime/report-runtime.d.ts +7 -4
- package/dist/utils/slotted-words/slotted-words.d.ts +3 -2
- package/dist/utils/slotted-words/slotted-words.js +3 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/words/index.d.ts +3 -0
- package/dist/words/index.js +3 -0
- package/dist/words/message.d.ts +47 -0
- package/dist/words/message.js +152 -0
- package/dist/words/registry.d.ts +89 -0
- package/dist/words/registry.js +101 -0
- package/dist/words/words-locale.d.ts +34 -0
- package/dist/words/words-locale.js +30 -0
- package/dist/words/words.d.ts +52 -0
- package/dist/words/words.js +100 -0
- package/package.json +40 -25
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
export const IDLE = { phase: 'idle' };
|
|
2
|
+
export const reorderStep = (state, input) => {
|
|
3
|
+
switch (input.type) {
|
|
4
|
+
case 'press':
|
|
5
|
+
return state.phase === 'idle'
|
|
6
|
+
? {
|
|
7
|
+
from: input.from,
|
|
8
|
+
key: input.key,
|
|
9
|
+
phase: 'pressed',
|
|
10
|
+
pointer: input.pointer,
|
|
11
|
+
x: input.x,
|
|
12
|
+
y: input.y,
|
|
13
|
+
}
|
|
14
|
+
: null;
|
|
15
|
+
case 'drag':
|
|
16
|
+
if (state.phase === 'pressed') {
|
|
17
|
+
return Math.hypot(input.x - state.x, input.y - state.y) < input.slop
|
|
18
|
+
? null
|
|
19
|
+
: { by: 'pointer', from: state.from, key: state.key, phase: 'held', to: input.to };
|
|
20
|
+
}
|
|
21
|
+
return state.phase === 'held' && state.by === 'pointer' && state.to !== input.to
|
|
22
|
+
? { ...state, to: input.to }
|
|
23
|
+
: null;
|
|
24
|
+
case 'release':
|
|
25
|
+
if (state.phase === 'pressed') {
|
|
26
|
+
return IDLE;
|
|
27
|
+
}
|
|
28
|
+
return state.phase === 'held' && state.by === 'pointer'
|
|
29
|
+
? { ...state, phase: 'dropped' }
|
|
30
|
+
: null;
|
|
31
|
+
case 'pick':
|
|
32
|
+
if (state.phase === 'idle') {
|
|
33
|
+
return { by: 'keyboard', from: input.from, key: input.key, phase: 'held', to: input.from };
|
|
34
|
+
}
|
|
35
|
+
return state.phase === 'held' && state.by === 'keyboard' && state.key === input.key
|
|
36
|
+
? { ...state, phase: 'dropped' }
|
|
37
|
+
: null;
|
|
38
|
+
case 'step':
|
|
39
|
+
return state.phase === 'held' && state.by === 'keyboard' && state.to !== input.to
|
|
40
|
+
? { ...state, to: input.to }
|
|
41
|
+
: null;
|
|
42
|
+
case 'target':
|
|
43
|
+
return state.phase === 'held' && state.by === 'keyboard'
|
|
44
|
+
? { ...state, phase: 'dropped', to: input.to }
|
|
45
|
+
: null;
|
|
46
|
+
case 'drop':
|
|
47
|
+
return state.phase === 'held' ? { ...state, phase: 'dropped' } : null;
|
|
48
|
+
case 'cancel':
|
|
49
|
+
if (state.phase === 'pressed') {
|
|
50
|
+
return IDLE;
|
|
51
|
+
}
|
|
52
|
+
return state.phase === 'held' ? { by: state.by, key: state.key, phase: 'canceled' } : null;
|
|
53
|
+
case 'settle':
|
|
54
|
+
return state.phase === 'dropped' || state.phase === 'canceled' ? IDLE : null;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
export const passedCount = (centers, at, reversed = false) => centers.filter((center) => (reversed ? at < center : at > center)).length;
|
|
58
|
+
export const centersOf = (spans, reversed = false) => {
|
|
59
|
+
const drawn = spans.flatMap((span, index) => (span ? [index] : []));
|
|
60
|
+
const first = drawn[0] ?? spans.length;
|
|
61
|
+
const last = drawn[drawn.length - 1] ?? -1;
|
|
62
|
+
let before = reversed ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
|
|
63
|
+
return spans.map((span, index) => {
|
|
64
|
+
if (span) {
|
|
65
|
+
before = (span.start + span.end) / 2;
|
|
66
|
+
return before;
|
|
67
|
+
}
|
|
68
|
+
if (index < first) {
|
|
69
|
+
return reversed ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
|
|
70
|
+
}
|
|
71
|
+
return index > last ? (reversed ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY) : before;
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
export const reorderKey = (key, { axis, count, rtl, to }) => {
|
|
75
|
+
const last = Math.max(0, count - 1);
|
|
76
|
+
if (key === 'Home') {
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
if (key === 'End') {
|
|
80
|
+
return last;
|
|
81
|
+
}
|
|
82
|
+
const back = axis === 'block' ? 'ArrowUp' : rtl ? 'ArrowRight' : 'ArrowLeft';
|
|
83
|
+
const forward = axis === 'block' ? 'ArrowDown' : rtl ? 'ArrowLeft' : 'ArrowRight';
|
|
84
|
+
if (key !== back && key !== forward) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
return Math.min(last, Math.max(0, to + (key === forward ? 1 : -1)));
|
|
88
|
+
};
|
|
89
|
+
export const dropMark = (from, to) => to === from ? undefined : { edge: to < from ? 'before' : 'after', index: to };
|
|
90
|
+
export const placeKey = (all, shown, key, to) => {
|
|
91
|
+
const others = shown.filter((one) => one !== key);
|
|
92
|
+
const rest = all.filter((one) => one !== key);
|
|
93
|
+
const beside = others[to - 1] ?? others[0];
|
|
94
|
+
if (beside === undefined) {
|
|
95
|
+
return [...all];
|
|
96
|
+
}
|
|
97
|
+
rest.splice(rest.indexOf(beside) + (to > 0 ? 1 : 0), 0, key);
|
|
98
|
+
return rest;
|
|
99
|
+
};
|
|
100
|
+
export const edgeSpeed = (at, start, end, { most = 12, zone = 40 } = {}) => {
|
|
101
|
+
const reach = Math.min(zone, (end - start) / 2);
|
|
102
|
+
if (reach <= 0) {
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
if (at < start + reach) {
|
|
106
|
+
return -Math.round(most * Math.min(1, (start + reach - at) / reach)) || 0;
|
|
107
|
+
}
|
|
108
|
+
if (at > end - reach) {
|
|
109
|
+
return Math.round(most * Math.min(1, (at - (end - reach)) / reach));
|
|
110
|
+
}
|
|
111
|
+
return 0;
|
|
112
|
+
};
|
|
@@ -30,7 +30,7 @@ export type RuntimeKind =
|
|
|
30
30
|
| 'min-out-of-range'
|
|
31
31
|
/** A Stat was given one of Trend's directions, `up`, `down` or `neutral`, which lay nothing out. */
|
|
32
32
|
| 'misread-direction'
|
|
33
|
-
/** A React handler prop names an event the element never dispatches itself. */
|
|
33
|
+
/** A React handler prop names an event the element never dispatches itself. Development builds only. */
|
|
34
34
|
| 'misplaced-handler'
|
|
35
35
|
/** An Image has no `alt`, so a screen reader announces its file name. */
|
|
36
36
|
| 'missing-alt'
|
|
@@ -59,9 +59,12 @@ export type RuntimeKind =
|
|
|
59
59
|
| 'picture-in-picture-refused'
|
|
60
60
|
/** A Grid's `min` is no length it can write, so the grid lays out by its column count. */
|
|
61
61
|
| 'refused-length'
|
|
62
|
-
/** A React handler prop is a removed callback, which never ran from JSX. */
|
|
62
|
+
/** A React handler prop is a removed callback, which never ran from JSX. Development builds only. */
|
|
63
63
|
| 'removed-callback'
|
|
64
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* A React handler prop uses Solid's `on:`, which React reads as another event's name.
|
|
66
|
+
* Development builds only.
|
|
67
|
+
*/
|
|
65
68
|
| 'solid-handler-syntax'
|
|
66
69
|
/** A stencil's regions went unused: no named parts, or parts too small. */
|
|
67
70
|
| 'stencil-regions-unused'
|
|
@@ -73,7 +76,7 @@ export type RuntimeKind =
|
|
|
73
76
|
| 'two-default-operators'
|
|
74
77
|
/** A Quote's text starts or ends with a quotation mark, so the browser's marks double it. */
|
|
75
78
|
| 'typed-marks'
|
|
76
|
-
/** A React handler prop names an event nothing dispatches. */
|
|
79
|
+
/** A React handler prop names an event nothing dispatches. Development builds only. */
|
|
77
80
|
| 'unknown-event'
|
|
78
81
|
/** An Icon's `name` is in no icon set it can read, so it draws nothing. */
|
|
79
82
|
| 'unknown-icon'
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* For a component that says what the page put in it, which is the page's own
|
|
6
6
|
* text and not anything this library wrote: an empty state's heading and
|
|
7
7
|
* message, a banner's message. Text nodes count toward the default slot, as
|
|
8
|
-
* they are assigned to it
|
|
9
|
-
*
|
|
8
|
+
* they are assigned to it; comments, such as the markers a template writes
|
|
9
|
+
* around its text, are left out. Nothing on a server, whose element has no
|
|
10
|
+
* children to read.
|
|
10
11
|
*/
|
|
11
12
|
export declare const slottedWords: (host: Element, slots: readonly string[]) => string;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export const slottedWords = (host, slots) => [...(host.childNodes ?? [])]
|
|
2
|
-
.filter((node) =>
|
|
2
|
+
.filter((node) => node instanceof Element
|
|
3
|
+
? slots.includes(node.slot)
|
|
4
|
+
: node.nodeType === 3 && slots.includes(''))
|
|
3
5
|
.map((node) => node.textContent?.replace(/\s+/g, ' ').trim() ?? '')
|
|
4
6
|
.filter(Boolean)
|
|
5
7
|
.join(' ');
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.
|
|
1
|
+
export const VERSION = '0.149.1';
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { formatMessage, type Message, type MessageOptions, type MessageTable, type MessageValues, } from './message.js';
|
|
2
|
+
export { clearWords, type HelixWords, localeChain, refreshWords, registerWords, setDefaultWordsLocale, type Words, } from './registry.js';
|
|
3
|
+
export { handedLocale, wordsLocale } from './words-locale.js';
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A sentence a component says, written in a small subset of ICU
|
|
3
|
+
* MessageFormat, the syntax translation tools already read:
|
|
4
|
+
*
|
|
5
|
+
* - `{name}`, a value put in where the sentence says. A number is written in
|
|
6
|
+
* the element's locale.
|
|
7
|
+
* - `{n, number}`, `{n, number, percent}` and `{n, number, integer}`.
|
|
8
|
+
* - `{n, plural, =0 {...} one {# ...} other {...}}`, with `offset:`, and
|
|
9
|
+
* `{n, selectordinal, ...}` in the same shape. `#` is the number.
|
|
10
|
+
* - `{x, select, a {...} other {...}}`.
|
|
11
|
+
* - `''` for one apostrophe, and `'{'` for a brace written as text. A lone
|
|
12
|
+
* apostrophe is text, so "L'élément" needs no escaping.
|
|
13
|
+
*/
|
|
14
|
+
export type Message = string;
|
|
15
|
+
/** A word table, such as `color-words`. Its entries merge one by one. */
|
|
16
|
+
export type MessageTable = Readonly<Record<string, Message>>;
|
|
17
|
+
/** The values a message puts in, by name. */
|
|
18
|
+
export type MessageValues = Readonly<Record<string, string | number>>;
|
|
19
|
+
/** How a message is formatted. */
|
|
20
|
+
export interface MessageOptions {
|
|
21
|
+
/**
|
|
22
|
+
* Wraps each value in Unicode isolates (U+2068 and U+2069), so a number or
|
|
23
|
+
* a Latin name keeps its order inside a right-to-left sentence.
|
|
24
|
+
*/
|
|
25
|
+
isolate?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* The locale whose plural rules pick a form, where it is not the locale
|
|
28
|
+
* the numbers are written in. A message from the English fallback counts
|
|
29
|
+
* with English rules on a Polish page, because its forms are English.
|
|
30
|
+
*/
|
|
31
|
+
plurals?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A message with its values put in, its numbers written in `locale`, and its
|
|
35
|
+
* plural forms picked by the rules of `options.plurals`, or of `locale`.
|
|
36
|
+
*
|
|
37
|
+
* **A value that is missing leaves its placeholder as written**, so
|
|
38
|
+
* `{count}` shows on the page as the mistake it is. An unknown argument type
|
|
39
|
+
* writes the value as text, so a type added later changes only the messages
|
|
40
|
+
* that use it.
|
|
41
|
+
*
|
|
42
|
+
* ```js
|
|
43
|
+
* formatMessage('{count, plural, one {# wynik} few {# wyniki} many {# wyników} other {# wyniku}}', { count: 5 }, 'pl')
|
|
44
|
+
* // '5 wyników'
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export declare const formatMessage: (message: Message, values: MessageValues, locale: string, options?: MessageOptions) => string;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { formatNumber } from '../utils/number-format/number-format.js';
|
|
2
|
+
const parsed = new Map();
|
|
3
|
+
const WORD = /\s*([^\s,{}]+)\s*/y;
|
|
4
|
+
const SPACE = /\s*/y;
|
|
5
|
+
const parse = (source) => {
|
|
6
|
+
let at = 0;
|
|
7
|
+
const fail = () => {
|
|
8
|
+
throw at;
|
|
9
|
+
};
|
|
10
|
+
const match = (pattern) => {
|
|
11
|
+
pattern.lastIndex = at;
|
|
12
|
+
const found = pattern.exec(source) ?? fail();
|
|
13
|
+
at = pattern.lastIndex;
|
|
14
|
+
return found[1];
|
|
15
|
+
};
|
|
16
|
+
const take = (char) => {
|
|
17
|
+
if (source[at++] !== char) {
|
|
18
|
+
fail();
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const text = (plural) => {
|
|
22
|
+
const parts = [];
|
|
23
|
+
let written = '';
|
|
24
|
+
const flush = () => {
|
|
25
|
+
if (written) {
|
|
26
|
+
parts.push(written);
|
|
27
|
+
}
|
|
28
|
+
written = '';
|
|
29
|
+
};
|
|
30
|
+
for (let char = source[at]; char && char !== '}'; char = source[at]) {
|
|
31
|
+
const next = source[++at];
|
|
32
|
+
if (char === "'" && next === "'") {
|
|
33
|
+
written += char;
|
|
34
|
+
at++;
|
|
35
|
+
}
|
|
36
|
+
else if (char === "'" && (next === '{' || next === '}' || (plural && next === '#'))) {
|
|
37
|
+
while (at < source.length) {
|
|
38
|
+
const quoted = source[at++];
|
|
39
|
+
if (quoted === "'") {
|
|
40
|
+
if (source[at] !== "'") {
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
at++;
|
|
44
|
+
}
|
|
45
|
+
written += quoted;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else if (char === '#' && plural) {
|
|
49
|
+
flush();
|
|
50
|
+
parts.push(0);
|
|
51
|
+
}
|
|
52
|
+
else if (char === '{') {
|
|
53
|
+
flush();
|
|
54
|
+
parts.push(argument(plural));
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
written += char;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
flush();
|
|
61
|
+
return parts;
|
|
62
|
+
};
|
|
63
|
+
const argument = (plural) => {
|
|
64
|
+
const found = { n: match(WORD) };
|
|
65
|
+
if (source[at] !== '}') {
|
|
66
|
+
take(',');
|
|
67
|
+
const type = match(WORD);
|
|
68
|
+
found.t = type;
|
|
69
|
+
if (source[at] !== '}') {
|
|
70
|
+
take(',');
|
|
71
|
+
if (type !== 'plural' && type !== 'selectordinal' && type !== 'select') {
|
|
72
|
+
const end = source.indexOf('}', at);
|
|
73
|
+
found.s = source.slice(at, end < 0 ? fail() : end).trim();
|
|
74
|
+
at = end;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
const branches = {};
|
|
78
|
+
found.b = branches;
|
|
79
|
+
while (source[at] !== '}') {
|
|
80
|
+
let key = match(WORD);
|
|
81
|
+
if (key.startsWith('offset:')) {
|
|
82
|
+
found.o = Number(key.slice(7) || match(WORD));
|
|
83
|
+
key = match(WORD);
|
|
84
|
+
}
|
|
85
|
+
take('{');
|
|
86
|
+
branches[key] = text(type !== 'select' || plural);
|
|
87
|
+
take('}');
|
|
88
|
+
match(SPACE);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
at++;
|
|
94
|
+
return found;
|
|
95
|
+
};
|
|
96
|
+
try {
|
|
97
|
+
const parts = text(false);
|
|
98
|
+
return at < source.length ? fail() : parts;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return [source];
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const rules = new Map();
|
|
105
|
+
const STYLES = {
|
|
106
|
+
integer: { maximumFractionDigits: 0 },
|
|
107
|
+
percent: { style: 'percent' },
|
|
108
|
+
};
|
|
109
|
+
export const formatMessage = (message, values, locale, options = {}) => {
|
|
110
|
+
let parts = parsed.get(message);
|
|
111
|
+
if (!parts) {
|
|
112
|
+
parts = parse(message);
|
|
113
|
+
parsed.set(message, parts);
|
|
114
|
+
}
|
|
115
|
+
const plurals = options.plurals ?? locale;
|
|
116
|
+
const isolated = (text) => (options.isolate ? `\u2068${text}\u2069` : text);
|
|
117
|
+
const run = (from, count) => from
|
|
118
|
+
.map((part) => {
|
|
119
|
+
if (typeof part === 'string') {
|
|
120
|
+
return part;
|
|
121
|
+
}
|
|
122
|
+
if (part === 0) {
|
|
123
|
+
return formatNumber(count, locale);
|
|
124
|
+
}
|
|
125
|
+
const { b: branches, n: name, t: type } = part;
|
|
126
|
+
const value = values[name];
|
|
127
|
+
if (value === undefined) {
|
|
128
|
+
return `{${name}}`;
|
|
129
|
+
}
|
|
130
|
+
if (type === 'number' || (!type && typeof value === 'number')) {
|
|
131
|
+
return isolated(formatNumber(+value, locale, STYLES[part.s]));
|
|
132
|
+
}
|
|
133
|
+
if (!branches) {
|
|
134
|
+
return isolated(String(value));
|
|
135
|
+
}
|
|
136
|
+
if (type === 'select') {
|
|
137
|
+
return run(branches[value] ?? branches.other ?? [], count);
|
|
138
|
+
}
|
|
139
|
+
const shifted = +value - (part.o ?? 0);
|
|
140
|
+
const key = `${plurals}|${type}`;
|
|
141
|
+
let rule = rules.get(key);
|
|
142
|
+
if (!rule) {
|
|
143
|
+
rule = new Intl.PluralRules(plurals || undefined, {
|
|
144
|
+
type: type === 'plural' ? 'cardinal' : 'ordinal',
|
|
145
|
+
});
|
|
146
|
+
rules.set(key, rule);
|
|
147
|
+
}
|
|
148
|
+
return run(branches[`=${value}`] ?? branches[rule.select(shifted)] ?? branches.other ?? [], shifted);
|
|
149
|
+
})
|
|
150
|
+
.join('');
|
|
151
|
+
return run(parts, 0);
|
|
152
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { Message, MessageTable } from './message.js';
|
|
2
|
+
/**
|
|
3
|
+
* The words of every family, keyed by the family's styled tag, each with its
|
|
4
|
+
* word properties.
|
|
5
|
+
*
|
|
6
|
+
* Empty here. The styled and headless packages fill it from their manifests,
|
|
7
|
+
* in `types/words.d.ts`, and an element of your own adds its tag the same way:
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* declare module '@fusion.dev/helix-core' {
|
|
11
|
+
* interface HelixWords {
|
|
12
|
+
* 'my-uploader': { dropLabel: Message }
|
|
13
|
+
* }
|
|
14
|
+
* }
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export interface HelixWords {
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Words for one locale, by tag and property. A tag that `HelixWords` names is
|
|
21
|
+
* checked against it, and any other tag takes any words.
|
|
22
|
+
*/
|
|
23
|
+
export type Words = {
|
|
24
|
+
readonly [T in keyof HelixWords]?: Readonly<Partial<HelixWords[T]>>;
|
|
25
|
+
} & {
|
|
26
|
+
readonly [tag: string]: Readonly<Record<string, Message | MessageTable>> | undefined;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Adds words for a locale, merged over the words already registered for it.
|
|
30
|
+
* Elements on the page update once, in a microtask.
|
|
31
|
+
*
|
|
32
|
+
* `locale` is a BCP 47 tag. Name a pack by its full tag where a language has
|
|
33
|
+
* regional forms, such as `pt-BR`, `pt-PT` and `es-419`. A word table, such
|
|
34
|
+
* as Color Picker's `colorWords`, merges entry by entry.
|
|
35
|
+
*
|
|
36
|
+
* ```js
|
|
37
|
+
* registerWords('de', { 'helix-combobox': { emptyText: 'Keine Treffer' } })
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export declare const registerWords: (locale: string, ...words: Words[]) => void;
|
|
41
|
+
/** Removes a locale's words, or every locale's where no locale is given. */
|
|
42
|
+
export declare const clearWords: (locale?: string) => void;
|
|
43
|
+
/**
|
|
44
|
+
* Sets the locale an element speaks on a server, and in its first render
|
|
45
|
+
* while it hydrates, where neither its `locale` nor its own `lang` says.
|
|
46
|
+
* Defaults to `en`.
|
|
47
|
+
*
|
|
48
|
+
* **Process-wide**, so it suits a server that renders one language. A server
|
|
49
|
+
* that renders several sets `lang` or `locale` on the elements.
|
|
50
|
+
*/
|
|
51
|
+
export declare const setDefaultWordsLocale: (locale: string) => void;
|
|
52
|
+
/** The locale set with `setDefaultWordsLocale`. */
|
|
53
|
+
export declare const defaultWordsLocale: () => string;
|
|
54
|
+
/**
|
|
55
|
+
* Asks every element that reads words to update, after an ancestor's `lang`
|
|
56
|
+
* changes. A change to the document's own `lang` is watched for you.
|
|
57
|
+
*/
|
|
58
|
+
export declare const refreshWords: () => void;
|
|
59
|
+
/**
|
|
60
|
+
* The locales a word is looked for in, most specific first, ending in `en`.
|
|
61
|
+
*
|
|
62
|
+
* **The script comes before the bare language**, so Hong Kong (`zh-HK`)
|
|
63
|
+
* reaches a Traditional Chinese pack (`zh-Hant`) before a `zh` pack, which is
|
|
64
|
+
* usually Simplified. Unicode extensions are dropped, and a tag `Intl` refuses
|
|
65
|
+
* falls straight to `en`.
|
|
66
|
+
*
|
|
67
|
+
* `de-CH` tries `de-ch`, `de-latn-ch`, `de-latn`, `de`, then `en`.
|
|
68
|
+
*/
|
|
69
|
+
export declare const localeChain: (locale: string) => string[];
|
|
70
|
+
/** Whether any words are registered, for a read that can skip the lookup. */
|
|
71
|
+
export declare const wordsRegistered: () => boolean;
|
|
72
|
+
/**
|
|
73
|
+
* A registered word for a tag's property in a locale, with the locale it was
|
|
74
|
+
* found in, or `undefined`.
|
|
75
|
+
*
|
|
76
|
+
* **Per key, along the chain.** A `de-CH` pack that holds only the Swiss
|
|
77
|
+
* spellings falls through to `de` for every other word. A word table merges
|
|
78
|
+
* along the chain, the more specific entry winning.
|
|
79
|
+
*/
|
|
80
|
+
export declare const lookupWord: (locale: string, tag: string, name: string) => [Message | MessageTable, string] | undefined;
|
|
81
|
+
/**
|
|
82
|
+
* Calls `watcher` once in the microtask after words are registered or
|
|
83
|
+
* cleared, or the document's `lang` changes. Returns the function that stops
|
|
84
|
+
* it.
|
|
85
|
+
*
|
|
86
|
+
* The first watcher in a browser starts watching the document's `lang`, once
|
|
87
|
+
* for the page.
|
|
88
|
+
*/
|
|
89
|
+
export declare const watchWords: (watcher: () => void) => (() => void);
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
const held = (globalThis[Symbol.for('helix.words')] ??= {
|
|
2
|
+
fallback: 'en',
|
|
3
|
+
packs: new Map(),
|
|
4
|
+
watchers: new Set(),
|
|
5
|
+
});
|
|
6
|
+
const changed = () => {
|
|
7
|
+
if (!held.queued) {
|
|
8
|
+
held.queued = true;
|
|
9
|
+
queueMicrotask(() => {
|
|
10
|
+
held.queued = false;
|
|
11
|
+
for (const watcher of held.watchers) {
|
|
12
|
+
watcher();
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
export const registerWords = (locale, ...words) => {
|
|
18
|
+
const key = locale.toLowerCase();
|
|
19
|
+
const pack = held.packs.get(key) ?? {};
|
|
20
|
+
held.packs.set(key, pack);
|
|
21
|
+
for (const one of words) {
|
|
22
|
+
for (const tag in one) {
|
|
23
|
+
const into = (pack[tag] ??= {});
|
|
24
|
+
for (const [name, value] of Object.entries(one[tag] ?? {})) {
|
|
25
|
+
const before = into[name];
|
|
26
|
+
into[name] =
|
|
27
|
+
typeof value === 'object' && typeof before === 'object' ? { ...before, ...value } : value;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
changed();
|
|
32
|
+
};
|
|
33
|
+
export const clearWords = (locale) => {
|
|
34
|
+
if (locale === undefined) {
|
|
35
|
+
held.packs.clear();
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
held.packs.delete(locale.toLowerCase());
|
|
39
|
+
}
|
|
40
|
+
changed();
|
|
41
|
+
};
|
|
42
|
+
export const setDefaultWordsLocale = (locale) => {
|
|
43
|
+
held.fallback = locale;
|
|
44
|
+
changed();
|
|
45
|
+
};
|
|
46
|
+
export const defaultWordsLocale = () => held.fallback;
|
|
47
|
+
export const refreshWords = () => changed();
|
|
48
|
+
const chains = new Map();
|
|
49
|
+
export const localeChain = (locale) => {
|
|
50
|
+
let found = chains.get(locale);
|
|
51
|
+
if (found) {
|
|
52
|
+
return found;
|
|
53
|
+
}
|
|
54
|
+
let tags = [];
|
|
55
|
+
try {
|
|
56
|
+
const asked = new Intl.Locale(locale);
|
|
57
|
+
const full = asked.maximize?.();
|
|
58
|
+
const script = `${full?.language}-${full?.script}`;
|
|
59
|
+
tags = [
|
|
60
|
+
asked.baseName,
|
|
61
|
+
`${script}-${full?.region}`,
|
|
62
|
+
script,
|
|
63
|
+
`${asked.language}-${asked.region}`,
|
|
64
|
+
asked.language,
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
}
|
|
69
|
+
found = [
|
|
70
|
+
...new Set([...tags, 'en']
|
|
71
|
+
.filter((tag) => tag && !tag.includes('undefined'))
|
|
72
|
+
.map((tag) => tag.toLowerCase())),
|
|
73
|
+
];
|
|
74
|
+
chains.set(locale, found);
|
|
75
|
+
return found;
|
|
76
|
+
};
|
|
77
|
+
export const wordsRegistered = () => held.packs.size > 0;
|
|
78
|
+
export const lookupWord = (locale, tag, name) => {
|
|
79
|
+
let found;
|
|
80
|
+
for (const one of localeChain(locale)) {
|
|
81
|
+
const word = held.packs.get(one)?.[tag]?.[name];
|
|
82
|
+
if (word === undefined) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (typeof word === 'string') {
|
|
86
|
+
return found ?? [word, one];
|
|
87
|
+
}
|
|
88
|
+
found = found ? [{ ...word, ...found[0] }, found[1]] : [word, one];
|
|
89
|
+
}
|
|
90
|
+
return found;
|
|
91
|
+
};
|
|
92
|
+
export const watchWords = (watcher) => {
|
|
93
|
+
held.watchers.add(watcher);
|
|
94
|
+
if (!held.observed && typeof MutationObserver === 'function' && globalThis.document) {
|
|
95
|
+
held.observed = true;
|
|
96
|
+
new MutationObserver(changed).observe(document.documentElement, {
|
|
97
|
+
attributeFilter: ['lang'],
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return () => held.watchers.delete(watcher);
|
|
101
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The locale an element speaks: the one its words, its plural forms, and the
|
|
3
|
+
* numbers in its sentences are in.
|
|
4
|
+
*
|
|
5
|
+
* 1. The element's `locale` property, where it has one and it is set.
|
|
6
|
+
* 2. On a server, and in the first render while the element hydrates, its own
|
|
7
|
+
* `lang` attribute, then the locale set with `setDefaultWordsLocale`, which
|
|
8
|
+
* is `en` until you set it. A server knows the element's attributes and
|
|
9
|
+
* nothing around it, so a hydrating render reads the same inputs and draws
|
|
10
|
+
* what the server drew.
|
|
11
|
+
* 3. Otherwise, the nearest `lang`, read out through shadow roots, then the
|
|
12
|
+
* runtime's own locale.
|
|
13
|
+
*
|
|
14
|
+
* **One rule for every family**, so one sentence never mixes the words of one
|
|
15
|
+
* language with the plural rules or digits of another. A `lang` that names a
|
|
16
|
+
* bare language the runtime already speaks, such as `en` in a British
|
|
17
|
+
* browser, is the runtime's locale, as `localeOf` reads it for dates.
|
|
18
|
+
*
|
|
19
|
+
* Always a tag `Intl` takes.
|
|
20
|
+
*/
|
|
21
|
+
export declare const wordsLocale: (element: Element & {
|
|
22
|
+
locale?: string | undefined;
|
|
23
|
+
}) => string;
|
|
24
|
+
/**
|
|
25
|
+
* The locale an element hands a child that reads only its own `lang` or
|
|
26
|
+
* `locale`, such as the list inside a combobox: the element's `locale`, or
|
|
27
|
+
* the nearest `lang` where it names a locale the runtime does not already
|
|
28
|
+
* speak. Empty where nothing says, so the child keeps the runtime's locale and
|
|
29
|
+
* gains no attribute, and empty while the element hydrates, as a server could
|
|
30
|
+
* not have known it.
|
|
31
|
+
*/
|
|
32
|
+
export declare const handedLocale: (element: Element & {
|
|
33
|
+
locale?: string | undefined;
|
|
34
|
+
}) => string;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { localeOf, runtimeLocale } from '../utils/locale-of/locale-of.js';
|
|
2
|
+
import { defaultWordsLocale } from './registry.js';
|
|
3
|
+
const checked = new Map();
|
|
4
|
+
const tagOf = (written) => {
|
|
5
|
+
let found = checked.get(written);
|
|
6
|
+
if (found === undefined) {
|
|
7
|
+
found = '';
|
|
8
|
+
try {
|
|
9
|
+
found = Intl.getCanonicalLocales(written)[0] ?? '';
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
}
|
|
13
|
+
checked.set(written, found);
|
|
14
|
+
}
|
|
15
|
+
return found;
|
|
16
|
+
};
|
|
17
|
+
export const wordsLocale = (element) => {
|
|
18
|
+
const own = tagOf(element.locale || '');
|
|
19
|
+
if (own) {
|
|
20
|
+
return own;
|
|
21
|
+
}
|
|
22
|
+
if (typeof element.closest !== 'function' || element.hydrating) {
|
|
23
|
+
return tagOf(element.getAttribute?.('lang') ?? '') || defaultWordsLocale();
|
|
24
|
+
}
|
|
25
|
+
return localeOf(element) || runtimeLocale();
|
|
26
|
+
};
|
|
27
|
+
export const handedLocale = (element) => {
|
|
28
|
+
const own = typeof element.locale === 'string' ? element.locale : '';
|
|
29
|
+
return element.hydrating ? own : localeOf(element, own);
|
|
30
|
+
};
|