be-intl 0.0.49 → 0.0.51
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 +52 -0
- package/be-intl.js +168 -148
- package/emc.json +13 -0
- package/package.json +3 -4
- package//360/237/214/220.json +13 -0
package/README.md
CHANGED
|
@@ -36,6 +36,58 @@ We can also employ more semantic syntax:
|
|
|
36
36
|
<data value=123456.789 lang="de-DE" be-intl-style=currency be-intl-currency=EUR></data>
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
## Locale resolution
|
|
40
|
+
|
|
41
|
+
The examples above put `lang` right on the formatted element, but that isn't required.
|
|
42
|
+
`be-intl` uses the element's **effective language**, resolved the same way the browser's
|
|
43
|
+
`:lang()` selector works:
|
|
44
|
+
|
|
45
|
+
1. the nearest ancestor with a `lang` (or `xml:lang`) attribute — crossing shadow-root
|
|
46
|
+
boundaries via the host element;
|
|
47
|
+
2. otherwise `<html lang>`;
|
|
48
|
+
3. otherwise the browser's own `navigator.language`.
|
|
49
|
+
|
|
50
|
+
So in practice you set `lang` once, high up:
|
|
51
|
+
|
|
52
|
+
```html
|
|
53
|
+
<html lang="de-DE">
|
|
54
|
+
...
|
|
55
|
+
<data value=123456.789 be-intl-style=currency be-intl-currency=EUR></data>
|
|
56
|
+
<!-- emits 123.456,79 € -->
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Changing the formatted element's **own** `lang` after it has been enhanced re-formats it only
|
|
60
|
+
if you opt in with `be-intl-observe-lang` (`🌐-observe-lang`); changes to an ancestor's `lang`
|
|
61
|
+
after enhancement are not tracked.
|
|
62
|
+
|
|
63
|
+
## Announcing updates to assistive technology
|
|
64
|
+
|
|
65
|
+
`be-intl` writes the formatted string into `textContent` on first render and again whenever the
|
|
66
|
+
bound value or the effective locale changes. By default it leaves ARIA untouched — most
|
|
67
|
+
formatted `<data>` / `<time>` elements are static readouts, and turning every one into a live
|
|
68
|
+
region (especially for a locale switch that only changes *presentation*, not the value) is
|
|
69
|
+
usually just screen-reader noise.
|
|
70
|
+
|
|
71
|
+
When a particular value *is* something the user is watching change, opt that element in with
|
|
72
|
+
`be-intl-announce` (`🌐-announce`):
|
|
73
|
+
|
|
74
|
+
```html
|
|
75
|
+
<output be-intl be-intl-announce></output>
|
|
76
|
+
<data be-intl be-intl-announce value="0" id="unread"></data>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
With the attribute set, `be-intl` — **after** the element's first render, so the initial value
|
|
80
|
+
isn't spoken on load — marks it as a polite live region:
|
|
81
|
+
|
|
82
|
+
- `aria-live="polite"` (skipped for `<output>`, which is already an implicit polite live
|
|
83
|
+
region), and
|
|
84
|
+
- `aria-atomic="true"`, so multi-token output such as a formatted date is announced as one
|
|
85
|
+
phrase rather than word-by-word.
|
|
86
|
+
|
|
87
|
+
Later re-formats then mutate an already-registered region and are announced. `assertive` is
|
|
88
|
+
intentionally not offered — interrupting the user to read out a reformatted number is almost
|
|
89
|
+
never the right call.
|
|
90
|
+
|
|
39
91
|
## Alternative names
|
|
40
92
|
|
|
41
93
|
The semantic example above involves a lot of keyboard tapping of the letters "be-intl". To avoid blisters on your itty bitty fingers, we provide an alternative base attribute you can use:
|
package/be-intl.js
CHANGED
|
@@ -1,148 +1,168 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
/** @import {Actions, PAP, AllProps, AP} from './types/be-intl/types' */;
|
|
3
|
-
/** @import {RoundaboutOptions} from './types/roundabout/types' */;
|
|
4
|
-
/** @import {ElementEnhancementGateway, SpawnContext} from './types/assign-gingerly/types' */;
|
|
5
|
-
/** @import {EMC} from './types/mount-observer/types' */;
|
|
6
|
-
/** @import {RAConfig} from './types/roundabout/types' */;
|
|
7
|
-
|
|
8
|
-
const defaultLocale = new Intl.NumberFormat().resolvedOptions().locale;
|
|
9
|
-
|
|
10
|
-
/** Semantic sugar attributes folded into `format`. */
|
|
11
|
-
const SEMANTIC_KEYS = ['style', 'currency', 'weekday', 'year', 'month', 'day'];
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* @implements {Actions}
|
|
15
|
-
*/
|
|
16
|
-
class BeIntl {
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* @this {AllProps & Actions}
|
|
20
|
-
* @param {Element & ElementEnhancementGateway} enhancedElement
|
|
21
|
-
* @param {SpawnContext} ctx
|
|
22
|
-
* @param {PAP} initVals
|
|
23
|
-
*/
|
|
24
|
-
constructor(enhancedElement, ctx, initVals){
|
|
25
|
-
this.init(this, enhancedElement, ctx, initVals);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* @param {AllProps} self
|
|
30
|
-
* @param {Element & ElementEnhancementGateway} enhancedElement
|
|
31
|
-
* @param {SpawnContext} ctx
|
|
32
|
-
* @param {PAP} initVals
|
|
33
|
-
*/
|
|
34
|
-
async init(self, enhancedElement, ctx, initVals){
|
|
35
|
-
const {customData} = /** @type {EMC<any, AllProps, Element, RAConfig<AllProps, Actions>>} */ (ctx.emc);
|
|
36
|
-
/**
|
|
37
|
-
* @type {RoundaboutOptions}
|
|
38
|
-
*/
|
|
39
|
-
const raOptions = {
|
|
40
|
-
...customData,
|
|
41
|
-
vm: self,
|
|
42
|
-
initialPropVals: {
|
|
43
|
-
enhancedElement,
|
|
44
|
-
...customData?.defaultPropVals,
|
|
45
|
-
...initVals
|
|
46
|
-
}
|
|
47
|
-
};
|
|
48
|
-
await (await import('roundabout-lib/roundabout.js')).roundabout(raOptions);
|
|
49
|
-
self.initialized = true;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Wire up live-update observation, resolve the locale, and seed the value.
|
|
54
|
-
* @param {AP} self
|
|
55
|
-
* @returns {Promise<PAP>}
|
|
56
|
-
*/
|
|
57
|
-
async hydrate(self){
|
|
58
|
-
const {enhancedElement} = self;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
*
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
* @
|
|
126
|
-
*/
|
|
127
|
-
formatDate(self){
|
|
128
|
-
const {enhancedElement, value, intlDateFormat} = self;
|
|
129
|
-
if(intlDateFormat === undefined || value === undefined || value === null) return;
|
|
130
|
-
enhancedElement.textContent = intlDateFormat.format(/** @type {Date} */ (value));
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
1
|
+
// @ts-check
|
|
2
|
+
/** @import {Actions, PAP, AllProps, AP} from './types/be-intl/types' */;
|
|
3
|
+
/** @import {RoundaboutOptions} from './types/roundabout/types' */;
|
|
4
|
+
/** @import {ElementEnhancementGateway, SpawnContext} from './types/assign-gingerly/types' */;
|
|
5
|
+
/** @import {EMC} from './types/mount-observer/types' */;
|
|
6
|
+
/** @import {RAConfig} from './types/roundabout/types' */;
|
|
7
|
+
|
|
8
|
+
const defaultLocale = new Intl.NumberFormat().resolvedOptions().locale;
|
|
9
|
+
|
|
10
|
+
/** Semantic sugar attributes folded into `format`. */
|
|
11
|
+
const SEMANTIC_KEYS = ['style', 'currency', 'weekday', 'year', 'month', 'day'];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @implements {Actions}
|
|
15
|
+
*/
|
|
16
|
+
class BeIntl {
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @this {AllProps & Actions}
|
|
20
|
+
* @param {Element & ElementEnhancementGateway} enhancedElement
|
|
21
|
+
* @param {SpawnContext} ctx
|
|
22
|
+
* @param {PAP} initVals
|
|
23
|
+
*/
|
|
24
|
+
constructor(enhancedElement, ctx, initVals){
|
|
25
|
+
this.init(this, enhancedElement, ctx, initVals);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {AllProps} self
|
|
30
|
+
* @param {Element & ElementEnhancementGateway} enhancedElement
|
|
31
|
+
* @param {SpawnContext} ctx
|
|
32
|
+
* @param {PAP} initVals
|
|
33
|
+
*/
|
|
34
|
+
async init(self, enhancedElement, ctx, initVals){
|
|
35
|
+
const {customData} = /** @type {EMC<any, AllProps, Element, RAConfig<AllProps, Actions>>} */ (ctx.emc);
|
|
36
|
+
/**
|
|
37
|
+
* @type {RoundaboutOptions}
|
|
38
|
+
*/
|
|
39
|
+
const raOptions = {
|
|
40
|
+
...customData,
|
|
41
|
+
vm: self,
|
|
42
|
+
initialPropVals: {
|
|
43
|
+
enhancedElement,
|
|
44
|
+
...customData?.defaultPropVals,
|
|
45
|
+
...initVals
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
await (await import('roundabout-lib/roundabout.js')).roundabout(raOptions);
|
|
49
|
+
self.initialized = true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Wire up live-update observation, resolve the locale, and seed the value.
|
|
54
|
+
* @param {AP} self
|
|
55
|
+
* @returns {Promise<PAP>}
|
|
56
|
+
*/
|
|
57
|
+
async hydrate(self){
|
|
58
|
+
const {enhancedElement} = self;
|
|
59
|
+
|
|
60
|
+
// Re-format whenever the element's underlying value property changes.
|
|
61
|
+
// <data>/<output> reflect through `value`, <time> through `dateTime`.
|
|
62
|
+
const valueProp = enhancedElement.localName === 'time' ? 'dateTime' : 'value';
|
|
63
|
+
const inference = await infer(enhancedElement);
|
|
64
|
+
const propagator = await inference.getPropagator();
|
|
65
|
+
propagator.addEventListener(valueProp, () => {
|
|
66
|
+
self.value = inference.value;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Opt-in: track lang changes on the element itself. Container-`lang`
|
|
70
|
+
// changes after mount aren't observed (rare); `inference.lang` still
|
|
71
|
+
// walks ancestors + shadow hosts on each read.
|
|
72
|
+
if(self.observeLang && enhancedElement instanceof HTMLElement){
|
|
73
|
+
const langObserver = new MutationObserver(() => {
|
|
74
|
+
self.locale = inference.lang || defaultLocale;
|
|
75
|
+
});
|
|
76
|
+
langObserver.observe(enhancedElement, {attributes: true, attributeFilter: ['lang']});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const locale = self.locale || inference.lang || defaultLocale;
|
|
80
|
+
return {locale, value: inference.value};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* (Re)build the Intl formatter. `value` is seeded by `hydrate` and kept
|
|
85
|
+
* fresh by the propagator, so it isn't read here.
|
|
86
|
+
* @param {AP} self
|
|
87
|
+
* @returns {PAP}
|
|
88
|
+
*/
|
|
89
|
+
onFormattingChange(self){
|
|
90
|
+
const {enhancedElement, locale} = self;
|
|
91
|
+
// Explicit JSON (`self.format`) wins; semantic sub-attributes fill the gaps.
|
|
92
|
+
const format = {.../** @type {Record<string, any>} */ (self.format || {})};
|
|
93
|
+
for(const key of SEMANTIC_KEYS){
|
|
94
|
+
const val = /** @type {Record<string, any>} */ (self)[key];
|
|
95
|
+
if(val !== undefined && format[key] === undefined) format[key] = val;
|
|
96
|
+
}
|
|
97
|
+
if(enhancedElement.localName === 'time'){
|
|
98
|
+
return {
|
|
99
|
+
intlDateFormat: new Intl.DateTimeFormat(locale, /** @type {Intl.DateTimeFormatOptions} */ (format)),
|
|
100
|
+
resolved: true,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
intlNumberFormat: new Intl.NumberFormat(locale, /** @type {Intl.NumberFormatOptions} */ (format)),
|
|
105
|
+
resolved: true,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @param {AP} self
|
|
111
|
+
* @returns {PAP | void}
|
|
112
|
+
*/
|
|
113
|
+
formatNumber(self){
|
|
114
|
+
const {enhancedElement, value, intlNumberFormat} = self;
|
|
115
|
+
if(intlNumberFormat === undefined || value === undefined || value === null){
|
|
116
|
+
enhancedElement.textContent = '';
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
enhancedElement.textContent = intlNumberFormat.format(/** @type {number} */ (value));
|
|
120
|
+
if(!self.rendered) return {rendered: true};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @param {AP} self
|
|
125
|
+
* @returns {PAP | void}
|
|
126
|
+
*/
|
|
127
|
+
formatDate(self){
|
|
128
|
+
const {enhancedElement, value, intlDateFormat} = self;
|
|
129
|
+
if(intlDateFormat === undefined || value === undefined || value === null) return;
|
|
130
|
+
enhancedElement.textContent = intlDateFormat.format(/** @type {Date} */ (value));
|
|
131
|
+
if(!self.rendered) return {rendered: true};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Opt-in via `be-intl-announce`. Once the value has rendered at least once,
|
|
136
|
+
* mark the element as a polite ARIA live region so subsequent re-formats
|
|
137
|
+
* (value or locale changes) are announced by assistive tech. Deliberately
|
|
138
|
+
* deferred past the first render so the initial value isn't spoken on load,
|
|
139
|
+
* and `aria-live` is skipped for `<output>` (already an implicit polite live
|
|
140
|
+
* region). `aria-atomic` keeps multi-token output (e.g. a formatted date)
|
|
141
|
+
* announcing as one unit.
|
|
142
|
+
* @param {AP} self
|
|
143
|
+
*/
|
|
144
|
+
armLiveRegion(self){
|
|
145
|
+
const {enhancedElement} = self;
|
|
146
|
+
if(!(enhancedElement instanceof HTMLElement)) return;
|
|
147
|
+
enhancedElement.setAttribute('aria-atomic', 'true');
|
|
148
|
+
if(enhancedElement.localName !== 'output'){
|
|
149
|
+
enhancedElement.setAttribute('aria-live', 'polite');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Resolve the `inferencer` enhancement instance for an element. Typed loosely
|
|
156
|
+
* (`Promise<any>`) so the class doesn't couple to the `Infer` shape; `.value`
|
|
157
|
+
* is a live, type-coerced read (`Date` for `<time>`, `number` for `<data>`),
|
|
158
|
+
* `getPropagator()` emits on value-property changes.
|
|
159
|
+
* @param {Element & ElementEnhancementGateway} from
|
|
160
|
+
* @returns {Promise<any>}
|
|
161
|
+
*/
|
|
162
|
+
async function infer(from){
|
|
163
|
+
return /** @type {any} */ (
|
|
164
|
+
from.enh.get((await import('assign-gingerly/inferencer/inferencer.js')).registryItem)
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export { BeIntl };
|
package/emc.json
CHANGED
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
"observeLang": "${base}-observe-lang",
|
|
19
19
|
"_observeLang": {
|
|
20
20
|
"instanceOf": "Boolean"
|
|
21
|
+
},
|
|
22
|
+
"announce": "${base}-announce",
|
|
23
|
+
"_announce": {
|
|
24
|
+
"instanceOf": "Boolean"
|
|
21
25
|
}
|
|
22
26
|
}
|
|
23
27
|
},
|
|
@@ -65,6 +69,15 @@
|
|
|
65
69
|
"value",
|
|
66
70
|
"intlDateFormat"
|
|
67
71
|
]
|
|
72
|
+
},
|
|
73
|
+
"armLiveRegion": {
|
|
74
|
+
"ifAllOf": [
|
|
75
|
+
"announce",
|
|
76
|
+
"rendered"
|
|
77
|
+
],
|
|
78
|
+
"ifKeyIn": [
|
|
79
|
+
"rendered"
|
|
80
|
+
]
|
|
68
81
|
}
|
|
69
82
|
}
|
|
70
83
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "be-intl",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.51",
|
|
4
4
|
"keywords": [
|
|
5
5
|
"web-components",
|
|
6
6
|
"web-component",
|
|
@@ -31,14 +31,13 @@
|
|
|
31
31
|
"update": "ncu -u && npm install"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"assign-gingerly": "0.0.
|
|
34
|
+
"assign-gingerly": "0.0.93",
|
|
35
35
|
"be-hive": "0.1.18",
|
|
36
|
-
"inferencer": "0.0.13",
|
|
37
36
|
"mount-observer": "0.1.53",
|
|
38
37
|
"roundabout-lib": "0.0.36"
|
|
39
38
|
},
|
|
40
39
|
"devDependencies": {
|
|
41
|
-
"spa-ssi": "0.0.
|
|
40
|
+
"spa-ssi": "0.0.28",
|
|
42
41
|
"@playwright/test": "1.62.1"
|
|
43
42
|
},
|
|
44
43
|
"author": "anderson.bruce.b@gmail.com",
|
package//360/237/214/220.json
CHANGED
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
"observeLang": "${base}-observe-lang",
|
|
19
19
|
"_observeLang": {
|
|
20
20
|
"instanceOf": "Boolean"
|
|
21
|
+
},
|
|
22
|
+
"announce": "${base}-announce",
|
|
23
|
+
"_announce": {
|
|
24
|
+
"instanceOf": "Boolean"
|
|
21
25
|
}
|
|
22
26
|
}
|
|
23
27
|
},
|
|
@@ -65,6 +69,15 @@
|
|
|
65
69
|
"value",
|
|
66
70
|
"intlDateFormat"
|
|
67
71
|
]
|
|
72
|
+
},
|
|
73
|
+
"armLiveRegion": {
|
|
74
|
+
"ifAllOf": [
|
|
75
|
+
"announce",
|
|
76
|
+
"rendered"
|
|
77
|
+
],
|
|
78
|
+
"ifKeyIn": [
|
|
79
|
+
"rendered"
|
|
80
|
+
]
|
|
68
81
|
}
|
|
69
82
|
}
|
|
70
83
|
}
|