Skip to content
This repository was archived by the owner on Feb 18, 2026. It is now read-only.

Commit bc5efd0

Browse files
committed
docs(basic-number): new demo component basic-number using
Intl.NumberFormat, improve basic-pluralize to use Intl.PluralRules
1 parent a3d8c74 commit bc5efd0

File tree

229 files changed

+1893
-1192
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

229 files changed

+1893
-1192
lines changed
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<ul>
2+
<li>
3+
inherited (en):
4+
<basic-number
5+
value="25678.9"
6+
options='{"style":"unit","unit":"liter","unitDisplay":"long"}'
7+
></basic-number>
8+
</li>
9+
<li>
10+
de-CH:
11+
<basic-number
12+
lang="de-CH"
13+
amount="25678.9"
14+
options='{"style":"currency","currency":"CHF"}'
15+
></basic-number>
16+
</li>
17+
<li>
18+
fr-CH:
19+
<basic-number
20+
lang="fr-CH"
21+
amount="25678.9"
22+
options='{"style":"currency","currency":"CHF"}'
23+
></basic-number>
24+
</li>
25+
<li>
26+
ar-EG:
27+
<basic-number
28+
lang="ar-EG"
29+
value="25678.9"
30+
options='{"style":"unit","unit":"kilometer-per-hour","unitDisplay":"long"}'
31+
></basic-number>
32+
</li>
33+
<li>
34+
zh-Hans-CN-u-nu-hanidec:
35+
<basic-number
36+
lang="zh-Hans-CN-u-nu-hanidec"
37+
value="25678.9"
38+
options='{"style":"unit","unit":"second","unitDisplay":"long"}'
39+
></basic-number>
40+
</li>
41+
</ul>
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { asNumber, type Component, component, setText } from '../../..'
2+
3+
export type BasicNumberProps = {
4+
value: number
5+
}
6+
7+
type Logger = {
8+
onWarn: (message: string) => void
9+
onError: (message: string) => void
10+
}
11+
12+
const FALLBACK_LOCALE = 'en'
13+
14+
function getNumberFormatter(
15+
locale: string,
16+
rawOptions: string | null,
17+
logger: Logger = {
18+
onWarn: console.warn,
19+
onError: console.error,
20+
},
21+
) {
22+
const useFallback = () => new Intl.NumberFormat(locale)
23+
if (!rawOptions) return useFallback()
24+
const { onWarn, onError } = logger
25+
26+
let o: Intl.NumberFormatOptions = {}
27+
try {
28+
o = JSON.parse(rawOptions)
29+
} catch (error) {
30+
onError?.(`Invalid JSON: ${error}`)
31+
return useFallback()
32+
}
33+
34+
const style = o.style ?? 'decimal'
35+
36+
const drops: string[] = []
37+
if (style === 'currency') {
38+
if (
39+
!o.currency ||
40+
typeof o.currency !== 'string' ||
41+
o.currency.length !== 3
42+
) {
43+
onError?.(
44+
`style="currency" requires a 3-letter ISO currency (e.g. "CHF").`,
45+
)
46+
return useFallback()
47+
}
48+
} else {
49+
drops.push('currency', 'currencyDisplay', 'currencySign')
50+
}
51+
52+
if (style === 'unit') {
53+
if (!o.unit || typeof o.unit !== 'string') {
54+
onError?.(
55+
`style="unit" requires a "unit" (e.g. "liter", "kilometer-per-hour").`,
56+
)
57+
return useFallback()
58+
}
59+
} else {
60+
drops.push('unit', 'unitDisplay')
61+
}
62+
63+
if (o.notation && o.notation !== 'compact') drops.push('compactDisplay')
64+
65+
const sanitized: Intl.NumberFormatOptions = {}
66+
for (const [k, v] of Object.entries(o)) {
67+
if (!drops.includes(k)) sanitized[k] = v
68+
else onWarn?.(`Option "${k}" is ignored for style="${style}".`)
69+
}
70+
71+
const { minimumFractionDigits: minFD, maximumFractionDigits: maxFD } =
72+
sanitized
73+
if (minFD != null && maxFD != null && minFD > maxFD) {
74+
onWarn?.(
75+
`minimumFractionDigits (${minFD}) > maximumFractionDigits (${maxFD}); swapping.`,
76+
)
77+
sanitized.minimumFractionDigits = maxFD
78+
sanitized.maximumFractionDigits = minFD
79+
}
80+
const { minimumSignificantDigits: minSD, maximumSignificantDigits: maxSD } =
81+
sanitized
82+
if (minSD != null && maxSD != null && minSD > maxSD) {
83+
onWarn?.(
84+
`minimumSignificantDigits (${minSD}) > maximumSignificantDigits (${maxSD}); swapping.`,
85+
)
86+
sanitized.minimumSignificantDigits = maxSD
87+
sanitized.maximumSignificantDigits = minSD
88+
}
89+
90+
try {
91+
const formatter = new Intl.NumberFormat(locale, sanitized)
92+
if (formatter.resolvedOptions().locale !== locale)
93+
onWarn(
94+
`Fall back to locale ${formatter.resolvedOptions().locale} instead of ${locale}`,
95+
)
96+
return formatter
97+
} catch (e) {
98+
onError?.(
99+
`Options rejected by Intl.NumberFormat: ${e instanceof Error ? e.message : String(e)}`,
100+
)
101+
return useFallback()
102+
}
103+
}
104+
105+
export default component('basic-number', { value: asNumber() }, el => {
106+
const formatter = getNumberFormatter(
107+
el.closest('[lang]')?.getAttribute('lang') || FALLBACK_LOCALE,
108+
el.getAttribute('options'),
109+
)
110+
return [setText(() => formatter.format(el.value))]
111+
})
112+
113+
declare global {
114+
interface HTMLElementTagNameMap {
115+
'basic-number': Component<BasicNumberProps>
116+
}
117+
}

docs-src/components/basic-pluralize/basic-pluralize.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
<p class="none">Well done, all done!</p>
33
<p class="some">
44
<span class="count"></span>
5-
<span class="singular">task</span>
6-
<span class="plural">tasks</span>
5+
<span class="one">task</span>
6+
<span class="other">tasks</span>
77
remaining
88
</p>
99
</basic-pluralize>

docs-src/components/basic-pluralize/basic-pluralize.ts

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,37 @@
1-
import { type Component, component, setText, show } from '../../..'
1+
import { asInteger, type Component, component, setText, show } from '../../..'
22

33
export type BasicPluralizeProps = {
44
count: number
55
}
66

7-
component('basic-pluralize', { count: 0 }, (el, { first }) => [
8-
first('.count', [setText(() => String(el.count))]),
9-
first('.none', [show(() => el.count === 0)]),
10-
first('.some', [show(() => el.count > 0)]),
11-
first('.singular', [show(() => el.count === 1)]),
12-
first('.plural', [show(() => el.count > 1)]),
13-
])
7+
const FALLBACK_LOCALE = 'en'
8+
9+
export default component(
10+
'basic-pluralize',
11+
{ count: asInteger() },
12+
(el, { first }) => {
13+
const pluralizer = new Intl.PluralRules(
14+
el.closest('[lang]')?.getAttribute('lang') || FALLBACK_LOCALE,
15+
el.hasAttribute('ordinal') ? { type: 'ordinal' } : undefined,
16+
)
17+
18+
// Subset of plural categories for applicable pluralizer: ['zero', 'one', 'two', 'few', 'many', 'other']
19+
const categories = pluralizer.resolvedOptions().pluralCategories
20+
const effects = [
21+
first('.count', [setText(() => String(el.count))]),
22+
first('.none', [show(() => el.count === 0)]),
23+
first('.some', [show(() => el.count > 0)]),
24+
]
25+
for (const category of categories) {
26+
effects.push(
27+
first(`.${category}`, [
28+
show(() => pluralizer.select(el.count) === category),
29+
]),
30+
)
31+
}
32+
return effects
33+
},
34+
)
1435

1536
declare global {
1637
interface HTMLElementTagNameMap {

docs-src/components/module-todo/module-todo-test.html

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@
4848
<p class="none">Well done, all done!</p>
4949
<p class="some">
5050
<span class="count"></span>
51-
<span class="singular">task</span>
52-
<span class="plural">tasks</span>
51+
<span class="one">task</span>
52+
<span class="font-stretch-extra-condensed">tasks</span>
5353
remaining
5454
</p>
5555
</basic-pluralize>
@@ -225,7 +225,6 @@
225225
)
226226
}
227227
}
228-
229228
}
230229

231230
// Helper to add a todo item

docs-src/components/module-todo/module-todo.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@
3939
<p class="none">Well done, all done!</p>
4040
<p class="some">
4141
<span class="count"></span>
42-
<span class="singular">task</span>
43-
<span class="plural">tasks</span>
42+
<span class="one">task</span>
43+
<span class="other">tasks</span>
4444
remaining
4545
</p>
4646
</basic-pluralize>

docs-src/main.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import './components/context-media/context-media.ts'
33
import './components/hello-world/hello-world.ts'
44
import './components/basic-button/basic-button.ts'
55
import './components/basic-counter/basic-counter.ts'
6+
import './components/basic-number/basic-number.ts'
67
import './components/basic-pluralize/basic-pluralize.ts'
78
import './components/form-checkbox/form-checkbox.ts'
89
import './components/form-radiogroup/form-radiogroup.ts'

docs-src/pages/api/classes/CircularMutationError.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
# Class: CircularMutationError
88

9-
Defined in: [src/core/errors.ts:10](https://github.com/zeixcom/ui-element/blob/3ce60a1d02c8c6608b1b8d191cd2a6123bdc0b3a/src/core/errors.ts#L10)
9+
Defined in: [src/core/errors.ts:10](https://github.com/zeixcom/ui-element/blob/a3d8c74b49b5869fe7d19ae9f979ed1d37f1f695/src/core/errors.ts#L10)
1010

1111
Error thrown when a circular dependency is detected in a selection signal
1212

@@ -24,7 +24,7 @@ Error thrown when a circular dependency is detected in a selection signal
2424

2525
> **new CircularMutationError**(`host`, `selector`): `CircularMutationError`
2626
27-
Defined in: [src/core/errors.ts:15](https://github.com/zeixcom/ui-element/blob/3ce60a1d02c8c6608b1b8d191cd2a6123bdc0b3a/src/core/errors.ts#L15)
27+
Defined in: [src/core/errors.ts:15](https://github.com/zeixcom/ui-element/blob/a3d8c74b49b5869fe7d19ae9f979ed1d37f1f695/src/core/errors.ts#L15)
2828

2929
#### Parameters
3030

docs-src/pages/api/classes/DependencyTimeoutError.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
# Class: DependencyTimeoutError
88

9-
Defined in: [src/core/errors.ts:115](https://github.com/zeixcom/ui-element/blob/3ce60a1d02c8c6608b1b8d191cd2a6123bdc0b3a/src/core/errors.ts#L115)
9+
Defined in: [src/core/errors.ts:115](https://github.com/zeixcom/ui-element/blob/a3d8c74b49b5869fe7d19ae9f979ed1d37f1f695/src/core/errors.ts#L115)
1010

1111
Error when a component's dependencies are not met within a specified timeout
1212

@@ -24,7 +24,7 @@ Error when a component's dependencies are not met within a specified timeout
2424

2525
> **new DependencyTimeoutError**(`host`, `missing`): `DependencyTimeoutError`
2626
27-
Defined in: [src/core/errors.ts:116](https://github.com/zeixcom/ui-element/blob/3ce60a1d02c8c6608b1b8d191cd2a6123bdc0b3a/src/core/errors.ts#L116)
27+
Defined in: [src/core/errors.ts:116](https://github.com/zeixcom/ui-element/blob/a3d8c74b49b5869fe7d19ae9f979ed1d37f1f695/src/core/errors.ts#L116)
2828

2929
#### Parameters
3030

docs-src/pages/api/classes/InvalidComponentNameError.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
# Class: InvalidComponentNameError
88

9-
Defined in: [src/core/errors.ts:28](https://github.com/zeixcom/ui-element/blob/3ce60a1d02c8c6608b1b8d191cd2a6123bdc0b3a/src/core/errors.ts#L28)
9+
Defined in: [src/core/errors.ts:28](https://github.com/zeixcom/ui-element/blob/a3d8c74b49b5869fe7d19ae9f979ed1d37f1f695/src/core/errors.ts#L28)
1010

1111
Error thrown when component name violates rules for custom element names
1212

@@ -24,7 +24,7 @@ Error thrown when component name violates rules for custom element names
2424

2525
> **new InvalidComponentNameError**(`component`): `InvalidComponentNameError`
2626
27-
Defined in: [src/core/errors.ts:32](https://github.com/zeixcom/ui-element/blob/3ce60a1d02c8c6608b1b8d191cd2a6123bdc0b3a/src/core/errors.ts#L32)
27+
Defined in: [src/core/errors.ts:32](https://github.com/zeixcom/ui-element/blob/a3d8c74b49b5869fe7d19ae9f979ed1d37f1f695/src/core/errors.ts#L32)
2828

2929
#### Parameters
3030

0 commit comments

Comments
 (0)