be-intl 0.0.48 → 0.0.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -36,6 +36,30 @@ 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
+
39
63
  ## Alternative names
40
64
 
41
65
  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:
@@ -45,30 +69,23 @@ The semantic example above involves a lot of keyboard tapping of the letters "be
45
69
  🌐-weekday=long 🌐-year=numeric 🌐-month=long 🌐-day=numeric></time>
46
70
  ```
47
71
 
48
- ## Viewing Locally
49
-
50
- Any web server than can serve static files will do, but...
72
+ ## Viewing Demos Locally
51
73
 
52
- 1. Install git.
53
- 2. Fork/clone this repo.
54
- 3. Install node.
55
- 4. Open command window to folder where you cloned this repo.
56
- 5. > npm install
57
- 6. > npm run serve
58
- 7. Open http://localhost:8000/demo in a modern browser.
74
+ 1. Install git
75
+ 2. Fork/clone this repo
76
+ 3. Install node.js
77
+ 4. Open command window to folder where you cloned this repo
78
+ 5. > git submodule add https://github.com/bahrus/types.git types
79
+ 6. > git submodule update --init --recursive
80
+ 7. > npm install
81
+ 8. > npm run build
82
+ 9. > npm run serve
83
+ 10. Open http://localhost:8000/demo/ in a modern browser
59
84
 
60
- ## Importing in ES Modules:
61
-
62
- ```JavaScript
63
- import 'be-intl/be-intl.js';
85
+ ## Running Tests
64
86
 
65
87
  ```
66
-
67
- ## Using from CDN:
68
-
69
- ```html
70
- <script type=module crossorigin=anonymous>
71
- import 'https://esm.run/be-intl';
72
- </script>
88
+ > npm run test
73
89
  ```
74
90
 
91
+
package/be-intl.js CHANGED
@@ -1,87 +1,148 @@
1
1
  // @ts-check
2
- import { propInfo, rejected, resolved } from 'be-enhanced/cc.js';
3
- import { BeValueAdded } from 'be-value-added/be-value-added.js';
4
- /** @import {Actions, AP} from './ts-refs/be-intl/types' */
5
- /** @import {BEConfig, IEnhancement, BEAllProps} from './ts-refs/be-enhanced/types.d.ts' */
6
- /** @import {Positraction, PropLookup, PropInfo} from './ts-refs/trans-render/froop/types.d.ts' */
7
- /** @import {BVAActions, BVAAllProps, BVAP} from './ts-refs/be-value-added/types' */;
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'];
8
12
 
9
13
  /**
10
14
  * @implements {Actions}
11
15
  */
12
- class BeIntl extends BeValueAdded {
16
+ class BeIntl {
17
+
13
18
  /**
14
- * @type {BEConfig<AP & BEAllProps, Actions & IEnhancement>}
19
+ * @this {AllProps & Actions}
20
+ * @param {Element & ElementEnhancementGateway} enhancedElement
21
+ * @param {SpawnContext} ctx
22
+ * @param {PAP} initVals
15
23
  */
16
- static config = {
17
- propInfo: {
18
- ...propInfo,
19
- locale: {},
20
- format: {},
21
- intlDateFormat: {},
22
- intlNumberFormat: {},
23
- currency: {},
24
- },
25
- actions: {
26
-
27
- hydrate: {
28
- ifAllOf: ['attached'],
29
- },
30
- formatNumber: {
31
- ifKeyIn: ['value'],
32
- ifAllOf: ['intlNumberFormat']
33
- },
34
- formatDate: {
35
- ifKeyIn: ['value'],
36
- ifAllOf: ['intlDateFormat']
37
- },
38
- onFormattingChange: {
39
- ifAllOf: ['locale'],
40
- ifKeyIn: ['format', 'currency']
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
41
46
  }
42
- },
43
- positractions: [
44
- /** @type {Positraction<BVAP & BEAllProps>} */
45
- (resolved),
46
- /** @type {Positraction<BVAP & BEAllProps>} */
47
- (rejected),
48
- ]
49
- };
47
+ };
48
+ await (await import('roundabout-lib/roundabout.js')).roundabout(raOptions);
49
+ self.initialized = true;
50
+ }
51
+
50
52
  /**
51
- *
52
- * @param {AP & BEAllProps} self
53
- * @returns {void}
53
+ * Wire up live-update observation, resolve the locale, and seed the value.
54
+ * @param {AP} self
55
+ * @returns {Promise<PAP>}
54
56
  */
55
- formatNumber(self) {
56
- const { enhancedElement, value } = self;
57
- if (value === undefined) {
58
- enhancedElement.textContent = '';
59
- return;
57
+ async hydrate(self){
58
+ const {enhancedElement} = self;
59
+ if(enhancedElement instanceof HTMLElement){
60
+ enhancedElement.ariaLive = 'polite';
61
+ }
62
+
63
+ // Re-format whenever the element's underlying value property changes.
64
+ // <data>/<output> reflect through `value`, <time> through `dateTime`.
65
+ const valueProp = enhancedElement.localName === 'time' ? 'dateTime' : 'value';
66
+ const inference = await infer(enhancedElement);
67
+ const propagator = await inference.getPropagator();
68
+ propagator.addEventListener(valueProp, () => {
69
+ self.value = inference.value;
70
+ });
71
+
72
+ // Opt-in: track lang changes on the element itself. Container-`lang`
73
+ // changes after mount aren't observed (rare); `inference.lang` still
74
+ // walks ancestors + shadow hosts on each read.
75
+ if(self.observeLang && enhancedElement instanceof HTMLElement){
76
+ const langObserver = new MutationObserver(() => {
77
+ self.locale = inference.lang || defaultLocale;
78
+ });
79
+ langObserver.observe(enhancedElement, {attributes: true, attributeFilter: ['lang']});
60
80
  }
61
- const { intlNumberFormat } = self;
62
- enhancedElement.textContent = intlNumberFormat.format(value);
81
+
82
+ const locale = self.locale || inference.lang || defaultLocale;
83
+ return {locale, value: inference.value};
63
84
  }
64
- formatDate(self) {
65
- const { enhancedElement, value, intlNumberFormat } = self;
66
- enhancedElement.textContent = self.intlDateFormat.format(value);
85
+
86
+ /**
87
+ * (Re)build the Intl formatter. `value` is seeded by `hydrate` and kept
88
+ * fresh by the propagator, so it isn't read here.
89
+ * @param {AP} self
90
+ * @returns {PAP}
91
+ */
92
+ onFormattingChange(self){
93
+ const {enhancedElement, locale} = self;
94
+ // Explicit JSON (`self.format`) wins; semantic sub-attributes fill the gaps.
95
+ const format = {.../** @type {Record<string, any>} */ (self.format || {})};
96
+ for(const key of SEMANTIC_KEYS){
97
+ const val = /** @type {Record<string, any>} */ (self)[key];
98
+ if(val !== undefined && format[key] === undefined) format[key] = val;
99
+ }
100
+ if(enhancedElement.localName === 'time'){
101
+ return {
102
+ intlDateFormat: new Intl.DateTimeFormat(locale, /** @type {Intl.DateTimeFormatOptions} */ (format)),
103
+ resolved: true,
104
+ };
105
+ }
106
+ return {
107
+ intlNumberFormat: new Intl.NumberFormat(locale, /** @type {Intl.NumberFormatOptions} */ (format)),
108
+ resolved: true,
109
+ };
67
110
  }
68
- onFormattingChange(self) {
69
- const { enhancedElement, locale, format, currency } = self;
70
- switch (enhancedElement.localName) {
71
- case 'time':
72
- return {
73
- intlDateFormat: new Intl.DateTimeFormat(locale, format),
74
- resolved: true
75
- };
76
- default:
77
- if (currency !== undefined)
78
- format.currency = currency;
79
- return {
80
- intlNumberFormat: new Intl.NumberFormat(locale, format),
81
- resolved: true
82
- };
111
+
112
+ /**
113
+ * @param {AP} self
114
+ */
115
+ formatNumber(self){
116
+ const {enhancedElement, value, intlNumberFormat} = self;
117
+ if(intlNumberFormat === undefined || value === undefined || value === null){
118
+ enhancedElement.textContent = '';
119
+ return;
83
120
  }
121
+ enhancedElement.textContent = intlNumberFormat.format(/** @type {number} */ (value));
84
122
  }
123
+
124
+ /**
125
+ * @param {AP} self
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
+ * Resolve the `inferencer` enhancement instance for an element. Typed loosely
136
+ * (`Promise<any>`) so the class doesn't couple to the `Infer` shape; `.value`
137
+ * is a live, type-coerced read (`Date` for `<time>`, `number` for `<data>`),
138
+ * `getPropagator()` emits on value-property changes.
139
+ * @param {Element & ElementEnhancementGateway} from
140
+ * @returns {Promise<any>}
141
+ */
142
+ async function infer(from){
143
+ return /** @type {any} */ (
144
+ from.enh.get((await import('inferencer/inferencer.js')).registryItem)
145
+ );
85
146
  }
86
- await BeIntl.bootUp();
147
+
87
148
  export { BeIntl };
package/emc.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "enhConfig": {
3
+ "enhKey": "BeIntl",
4
+ "spawn": "be-intl/be-intl.js",
5
+ "withAttrs": {
6
+ "base": "be-intl",
7
+ "_base": {
8
+ "mapsTo": "format",
9
+ "instanceOf": "Object",
10
+ "valIfNull": {}
11
+ },
12
+ "style": "${base}-style",
13
+ "currency": "${base}-currency",
14
+ "weekday": "${base}-weekday",
15
+ "year": "${base}-year",
16
+ "month": "${base}-month",
17
+ "day": "${base}-day",
18
+ "observeLang": "${base}-observe-lang",
19
+ "_observeLang": {
20
+ "instanceOf": "Boolean"
21
+ }
22
+ }
23
+ },
24
+ "customData": {
25
+ "weakRef": {
26
+ "properties": [
27
+ "enhancedElement"
28
+ ]
29
+ },
30
+ "actions": {
31
+ "hydrate": {
32
+ "ifAllOf": [
33
+ "enhancedElement",
34
+ "initialized"
35
+ ],
36
+ "ifKeyIn": [
37
+ "initialized"
38
+ ]
39
+ },
40
+ "onFormattingChange": {
41
+ "ifAllOf": [
42
+ "locale",
43
+ "initialized"
44
+ ],
45
+ "ifKeyIn": [
46
+ "locale",
47
+ "format",
48
+ "initialized"
49
+ ]
50
+ },
51
+ "formatNumber": {
52
+ "ifAllOf": [
53
+ "intlNumberFormat"
54
+ ],
55
+ "ifKeyIn": [
56
+ "value",
57
+ "intlNumberFormat"
58
+ ]
59
+ },
60
+ "formatDate": {
61
+ "ifAllOf": [
62
+ "intlDateFormat"
63
+ ],
64
+ "ifKeyIn": [
65
+ "value",
66
+ "intlDateFormat"
67
+ ]
68
+ }
69
+ }
70
+ }
71
+ }
package/package.json CHANGED
@@ -1,43 +1,46 @@
1
- {
2
- "name": "be-intl",
3
- "version": "0.0.48",
4
- "keywords": [
5
- "web-components",
6
- "web-component",
7
- "custom-element",
8
- "custom-elements"
9
- ],
10
- "description": "Format numbers, dates automatically and semantically.",
11
- "main": "be-intl.js",
12
- "module": "be-intl.js",
13
- "exports": {
14
- ".": "./be-intl.js",
15
- "./be-intl.js": "./be-intl.js",
16
- "./emc.js": "./emc.js",
17
- "./🌐.js": "./🌐.js"
18
- },
19
- "files": [
20
- "*.js",
21
- "types.d.ts"
22
- ],
23
- "types": "./ts-refs/types.d.ts",
24
- "scripts": {
25
- "test": "playwright test",
26
- "serve": "python3 ./node_modules/ssi-server/ssi_server.py",
27
- "safari": "npx playwright wk http://localhost:8000",
28
- "update": "ncu -u && npm install"
29
- },
30
- "dependencies": {
31
- "be-enhanced": "0.0.163",
32
- "be-hive": "0.0.243",
33
- "be-propagating": "0.0.58",
34
- "be-value-added": "0.0.51",
35
- "trans-render": "0.0.877"
36
- },
37
- "devDependencies": {
38
- "ssi-server": "0.0.1",
39
- "@playwright/test": "1.49.1"
40
- },
41
- "author": "anderson.bruce.b@gmail.com",
42
- "license": "MIT"
43
- }
1
+ {
2
+ "name": "be-intl",
3
+ "version": "0.0.50",
4
+ "keywords": [
5
+ "web-components",
6
+ "web-component",
7
+ "custom-element",
8
+ "custom-elements"
9
+ ],
10
+ "description": "Format numbers, dates automatically and semantically.",
11
+ "main": "be-intl.js",
12
+ "module": "be-intl.js",
13
+ "exports": {
14
+ ".": "./be-intl.js",
15
+ "./be-intl.js": "./be-intl.js",
16
+ "./🌐.js": "./🌐.js",
17
+ "./emc.json": "./emc.json",
18
+ "./🌐.json": "./🌐.json"
19
+ },
20
+ "files": [
21
+ "*.js",
22
+ "*.json",
23
+ "types.d.ts"
24
+ ],
25
+ "types": "./types/be-intl/types.d.ts",
26
+ "scripts": {
27
+ "build": "node emc.mjs > emc.json && node 🌐.mjs > 🌐.json",
28
+ "serve": "node ./node_modules/spa-ssi/serve.js",
29
+ "test": "playwright test",
30
+ "safari": "npx playwright wk http://localhost:8000",
31
+ "update": "ncu -u && npm install"
32
+ },
33
+ "dependencies": {
34
+ "assign-gingerly": "0.0.93",
35
+ "be-hive": "0.1.18",
36
+ "inferencer": "0.0.13",
37
+ "mount-observer": "0.1.53",
38
+ "roundabout-lib": "0.0.36"
39
+ },
40
+ "devDependencies": {
41
+ "spa-ssi": "0.0.27",
42
+ "@playwright/test": "1.62.1"
43
+ },
44
+ "author": "anderson.bruce.b@gmail.com",
45
+ "license": "MIT"
46
+ }
@@ -0,0 +1,71 @@
1
+ {
2
+ "enhConfig": {
3
+ "enhKey": "🌐",
4
+ "spawn": "be-intl/be-intl.js",
5
+ "withAttrs": {
6
+ "base": "🌐",
7
+ "_base": {
8
+ "mapsTo": "format",
9
+ "instanceOf": "Object",
10
+ "valIfNull": {}
11
+ },
12
+ "style": "${base}-style",
13
+ "currency": "${base}-currency",
14
+ "weekday": "${base}-weekday",
15
+ "year": "${base}-year",
16
+ "month": "${base}-month",
17
+ "day": "${base}-day",
18
+ "observeLang": "${base}-observe-lang",
19
+ "_observeLang": {
20
+ "instanceOf": "Boolean"
21
+ }
22
+ }
23
+ },
24
+ "customData": {
25
+ "weakRef": {
26
+ "properties": [
27
+ "enhancedElement"
28
+ ]
29
+ },
30
+ "actions": {
31
+ "hydrate": {
32
+ "ifAllOf": [
33
+ "enhancedElement",
34
+ "initialized"
35
+ ],
36
+ "ifKeyIn": [
37
+ "initialized"
38
+ ]
39
+ },
40
+ "onFormattingChange": {
41
+ "ifAllOf": [
42
+ "locale",
43
+ "initialized"
44
+ ],
45
+ "ifKeyIn": [
46
+ "locale",
47
+ "format",
48
+ "initialized"
49
+ ]
50
+ },
51
+ "formatNumber": {
52
+ "ifAllOf": [
53
+ "intlNumberFormat"
54
+ ],
55
+ "ifKeyIn": [
56
+ "value",
57
+ "intlNumberFormat"
58
+ ]
59
+ },
60
+ "formatDate": {
61
+ "ifAllOf": [
62
+ "intlDateFormat"
63
+ ],
64
+ "ifKeyIn": [
65
+ "value",
66
+ "intlDateFormat"
67
+ ]
68
+ }
69
+ }
70
+ }
71
+ }
package/emc.js DELETED
@@ -1,60 +0,0 @@
1
- // @ts-check
2
- import { BeHive, MountObserver, seed } from 'be-hive/be-hive.js';
3
- const defaultLocale = new Intl.NumberFormat().resolvedOptions().locale;
4
- /** @import {EMC} from './ts-refs/trans-render/be/types' */
5
-
6
- /**
7
- * @type {EMC}
8
- */
9
- export const emc = {
10
- base: 'be-intl',
11
- branches: ['', 'style', 'currency', 'weekday', 'year', 'month', 'day'],
12
- map: {
13
- '0.0': {
14
- instanceOf: 'Object',
15
- mapsTo: 'format',
16
- valIfFalsy: {},
17
- },
18
- '1.0': {
19
- instanceOf: 'String',
20
- mapsTo: '?.format?.style'
21
- },
22
- '2.0': {
23
- instanceOf: 'String',
24
- mapsTo: '?.format?.currency'
25
- },
26
- '3.0': {
27
- instanceOf: 'String',
28
- mapsTo: '?.format?.weekday'
29
- },
30
- '4.0': {
31
- instanceOf: 'String',
32
- mapsTo: '?.format?.year'
33
- },
34
- '5.0': {
35
- instanceOf: 'String',
36
- mapsTo: '?.format?.month'
37
- },
38
- '6.0': {
39
- instanceOf: 'String',
40
- mapsTo: '?.format?.day'
41
- }
42
- },
43
- osotas: [
44
- {
45
- name: 'lang',
46
- valIfNull: defaultLocale,
47
- mapsTo: 'locale',
48
- }
49
- ],
50
- enhPropKey: 'beIntl',
51
- importEnh: async () => {
52
- const { BeIntl } =
53
- /** @type {{new(): IEnhancement<Element>}} */
54
- /** @type {any} */
55
- (await import('./be-intl.js'));
56
- return BeIntl;
57
- }
58
- };
59
- const mose = seed(emc);
60
- MountObserver.synthesize(document, BeHive, mose);
@@ -1,15 +0,0 @@
1
- // @ts-check
2
- import { MountObserver, seed, BeHive } from 'be-hive/be-hive.js';
3
- import { emc as baseEMC } from './emc.js';
4
- /** @import {EMC} from './ts-refs/trans-render/be/types.d.ts' */
5
-
6
- /**
7
- * @type {EMC}
8
- */
9
- export const emc = {
10
- ...baseEMC,
11
- base: '🌐',
12
- enhPropKey: '🌐',
13
- };
14
- const mose = seed(emc);
15
- MountObserver.synthesize(document, BeHive, mose);