@prestack/thread-ui 0.0.1 → 0.0.2
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/dist/ui/animations/anm_appear.ts +13 -0
- package/dist/ui/animations/anm_blur_in.ts +19 -0
- package/dist/ui/animations/anm_blur_out.ts +18 -0
- package/dist/ui/animations/anm_dismiss.ts +18 -0
- package/dist/ui/animations/anm_drop.ts +19 -0
- package/dist/ui/animations/anm_fall.ts +18 -0
- package/dist/ui/animations/anm_flash.ts +18 -0
- package/dist/ui/animations/anm_glow.ts +16 -0
- package/dist/ui/animations/anm_lift.ts +16 -0
- package/dist/ui/animations/anm_nudge.ts +16 -0
- package/dist/ui/animations/anm_pop_in.ts +23 -0
- package/dist/ui/animations/anm_pop_out.ts +22 -0
- package/dist/ui/animations/anm_press.ts +12 -0
- package/dist/ui/animations/anm_pulse.ts +13 -0
- package/dist/ui/animations/anm_rise.ts +19 -0
- package/dist/ui/animations/anm_shake.ts +13 -0
- package/dist/ui/animations/anm_slide_in_left.ts +19 -0
- package/dist/ui/animations/anm_slide_in_right.ts +19 -0
- package/dist/ui/animations/anm_slide_out_left.ts +18 -0
- package/dist/ui/animations/anm_slide_out_right.ts +18 -0
- package/dist/ui/animations/anm_spin.ts +12 -0
- package/dist/ui/animations/anm_swell.ts +16 -0
- package/dist/ui/animations/anm_wobble.ts +24 -0
- package/dist/ui/animations/anm_zoom_in.ts +19 -0
- package/dist/ui/animations/anm_zoom_out.ts +18 -0
- package/dist/ui/animations.ts +317 -0
- package/dist/ui/attach.js +13 -4
- package/dist/ui/attach.ts +613 -0
- package/dist/ui/charts.tsx +74 -0
- package/dist/ui/checked.d.ts +2 -2
- package/dist/ui/checked.ts +32 -0
- package/dist/ui/collector.ts +140 -0
- package/dist/ui/compositions.d.ts +10 -1
- package/dist/ui/compositions.js +262 -23
- package/dist/ui/compositions.ts +640 -0
- package/dist/ui/consent.ts +55 -0
- package/dist/ui/content.tsx +39 -0
- package/dist/ui/contrast.ts +27 -0
- package/dist/ui/controls.tsx +30 -0
- package/dist/ui/disclosure.tsx +22 -0
- package/dist/ui/dismiss-layer.ts +87 -0
- package/dist/ui/door.ts +140 -0
- package/dist/ui/drag.ts +316 -0
- package/dist/ui/elements/accordion.js +2 -2
- package/dist/ui/elements/accordion.tsx +47 -0
- package/dist/ui/elements/accordion.works.ts +66 -0
- package/dist/ui/elements/aspect-ratio.js +1 -1
- package/dist/ui/elements/aspect-ratio.tsx +30 -0
- package/dist/ui/elements/aspect-ratio.works.ts +5 -0
- package/dist/ui/elements/avatar.js +1 -1
- package/dist/ui/elements/avatar.tsx +33 -0
- package/dist/ui/elements/avatar.works.ts +16 -0
- package/dist/ui/elements/badge.js +1 -1
- package/dist/ui/elements/badge.tsx +32 -0
- package/dist/ui/elements/badge.works.ts +9 -0
- package/dist/ui/elements/bars.js +2 -2
- package/dist/ui/elements/bars.tsx +49 -0
- package/dist/ui/elements/bars.works.ts +29 -0
- package/dist/ui/elements/box.js +3 -2
- package/dist/ui/elements/box.tsx +35 -0
- package/dist/ui/elements/breadcrumb.js +2 -2
- package/dist/ui/elements/breadcrumb.tsx +37 -0
- package/dist/ui/elements/breadcrumb.works.ts +24 -0
- package/dist/ui/elements/button.js +1 -1
- package/dist/ui/elements/button.tsx +45 -0
- package/dist/ui/elements/button.works.tsx +52 -0
- package/dist/ui/elements/column.js +1 -1
- package/dist/ui/elements/column.tsx +28 -0
- package/dist/ui/elements/consent.js +2 -2
- package/dist/ui/elements/consent.tsx +70 -0
- package/dist/ui/elements/consent.works.tsx +132 -0
- package/dist/ui/elements/donut.js +2 -2
- package/dist/ui/elements/donut.tsx +44 -0
- package/dist/ui/elements/donut.works.ts +42 -0
- package/dist/ui/elements/each.js +1 -1
- package/dist/ui/elements/each.tsx +33 -0
- package/dist/ui/elements/each.works.tsx +82 -0
- package/dist/ui/elements/empty.js +1 -1
- package/dist/ui/elements/empty.tsx +32 -0
- package/dist/ui/elements/field.js +1 -1
- package/dist/ui/elements/field.tsx +44 -0
- package/dist/ui/elements/field.works.ts +65 -0
- package/dist/ui/elements/funnel.js +2 -2
- package/dist/ui/elements/funnel.tsx +49 -0
- package/dist/ui/elements/funnel.works.ts +37 -0
- package/dist/ui/elements/gauge.js +1 -1
- package/dist/ui/elements/gauge.tsx +39 -0
- package/dist/ui/elements/gauge.works.ts +42 -0
- package/dist/ui/elements/glyph.js +1 -1
- package/dist/ui/elements/glyph.tsx +29 -0
- package/dist/ui/elements/glyph.works.tsx +42 -0
- package/dist/ui/elements/grid.js +1 -1
- package/dist/ui/elements/grid.tsx +28 -0
- package/dist/ui/elements/heatmap.js +2 -2
- package/dist/ui/elements/heatmap.tsx +47 -0
- package/dist/ui/elements/heatmap.works.ts +24 -0
- package/dist/ui/elements/hit-box.js +1 -1
- package/dist/ui/elements/hit-box.tsx +46 -0
- package/dist/ui/elements/hit-box.works.tsx +576 -0
- package/dist/ui/elements/image.js +1 -1
- package/dist/ui/elements/image.tsx +28 -0
- package/dist/ui/elements/line-chart.js +2 -2
- package/dist/ui/elements/line-chart.tsx +44 -0
- package/dist/ui/elements/line-chart.works.ts +41 -0
- package/dist/ui/elements/link.js +1 -1
- package/dist/ui/elements/link.tsx +37 -0
- package/dist/ui/elements/link.works.ts +25 -0
- package/dist/ui/elements/media.js +1 -1
- package/dist/ui/elements/media.tsx +56 -0
- package/dist/ui/elements/menu.js +2 -2
- package/dist/ui/elements/menu.tsx +50 -0
- package/dist/ui/elements/menu.works.tsx +153 -0
- package/dist/ui/elements/money.js +1 -1
- package/dist/ui/elements/money.tsx +28 -0
- package/dist/ui/elements/money.works.ts +9 -0
- package/dist/ui/elements/pagination.js +2 -2
- package/dist/ui/elements/pagination.tsx +38 -0
- package/dist/ui/elements/pagination.works.ts +20 -0
- package/dist/ui/elements/progress.js +1 -1
- package/dist/ui/elements/progress.tsx +33 -0
- package/dist/ui/elements/progress.works.ts +8 -0
- package/dist/ui/elements/radio-group.js +2 -2
- package/dist/ui/elements/radio-group.tsx +49 -0
- package/dist/ui/elements/radio-group.works.ts +33 -0
- package/dist/ui/elements/row.js +1 -1
- package/dist/ui/elements/row.tsx +28 -0
- package/dist/ui/elements/scatter.js +1 -1
- package/dist/ui/elements/scatter.tsx +41 -0
- package/dist/ui/elements/scatter.works.ts +38 -0
- package/dist/ui/elements/select.js +2 -2
- package/dist/ui/elements/select.tsx +42 -0
- package/dist/ui/elements/select.works.ts +5 -0
- package/dist/ui/elements/separator.js +1 -1
- package/dist/ui/elements/separator.tsx +30 -0
- package/dist/ui/elements/skeleton.js +1 -1
- package/dist/ui/elements/skeleton.tsx +30 -0
- package/dist/ui/elements/slider.js +1 -1
- package/dist/ui/elements/slider.tsx +33 -0
- package/dist/ui/elements/slider.works.ts +5 -0
- package/dist/ui/elements/sparkline.js +1 -1
- package/dist/ui/elements/sparkline.tsx +36 -0
- package/dist/ui/elements/sparkline.works.ts +32 -0
- package/dist/ui/elements/spinner.js +1 -1
- package/dist/ui/elements/spinner.tsx +32 -0
- package/dist/ui/elements/stack.js +1 -1
- package/dist/ui/elements/stack.tsx +28 -0
- package/dist/ui/elements/stat.js +1 -1
- package/dist/ui/elements/stat.tsx +35 -0
- package/dist/ui/elements/stat.works.ts +14 -0
- package/dist/ui/elements/surface.js +1 -1
- package/dist/ui/elements/surface.tsx +35 -0
- package/dist/ui/elements/surface.works.ts +9 -0
- package/dist/ui/elements/table.js +2 -2
- package/dist/ui/elements/table.tsx +61 -0
- package/dist/ui/elements/table.works.ts +30 -0
- package/dist/ui/elements/tabs.js +2 -2
- package/dist/ui/elements/tabs.tsx +47 -0
- package/dist/ui/elements/tabs.works.ts +84 -0
- package/dist/ui/elements/text.js +1 -1
- package/dist/ui/elements/text.tsx +32 -0
- package/dist/ui/elements/textarea.js +1 -1
- package/dist/ui/elements/textarea.tsx +37 -0
- package/dist/ui/elements/textarea.works.ts +5 -0
- package/dist/ui/elements/time.js +1 -1
- package/dist/ui/elements/time.tsx +34 -0
- package/dist/ui/elements/time.works.ts +25 -0
- package/dist/ui/elements/toggle.js +1 -1
- package/dist/ui/elements/toggle.tsx +33 -0
- package/dist/ui/elements/toggle.works.ts +5 -0
- package/dist/ui/elements/tooltip.js +1 -1
- package/dist/ui/elements/tooltip.tsx +39 -0
- package/dist/ui/elements/tooltip.works.ts +77 -0
- package/dist/ui/elements/value.js +1 -1
- package/dist/ui/elements/value.tsx +30 -0
- package/dist/ui/elements/value.works.ts +9 -0
- package/dist/ui/elements.ts +127 -0
- package/dist/ui/focus-scope.ts +121 -0
- package/dist/ui/fonts.ts +355 -0
- package/dist/ui/generators/dev.ts +79 -0
- package/dist/ui/generators/elements.d.ts +1 -11
- package/dist/ui/generators/elements.js +43 -13
- package/dist/ui/generators/elements.ts +794 -0
- package/dist/ui/generators/i18n.ts +301 -0
- package/dist/ui/generators/icons.ts +214 -0
- package/dist/ui/generators/react.js +10 -3
- package/dist/ui/generators/react.ts +268 -0
- package/dist/ui/generators/routes.d.ts +1 -0
- package/dist/ui/generators/routes.js +89 -69
- package/dist/ui/generators/routes.ts +739 -0
- package/dist/ui/generators/show.ts +259 -0
- package/dist/ui/generators/styles.js +1 -1
- package/dist/ui/generators/styles.ts +670 -0
- package/dist/ui/generators/variants.ts +205 -0
- package/dist/ui/i18n/catalogs/en.ts +191 -0
- package/dist/ui/i18n/format.ts +221 -0
- package/dist/ui/i18n/locales.ts +33 -0
- package/dist/ui/i18n/parse.ts +315 -0
- package/dist/ui/i18n/translator.ts +31 -0
- package/dist/ui/i18n/types.ts +96 -0
- package/dist/ui/icons.ts +331 -0
- package/dist/ui/islands/map.ts +58 -0
- package/dist/ui/localizations.ts +75 -0
- package/dist/ui/makers/message.ts +292 -0
- package/dist/ui/material.ts +236 -0
- package/dist/ui/materials/mat_bordered.ts +13 -0
- package/dist/ui/materials/mat_ceramic.ts +13 -0
- package/dist/ui/materials/mat_clay.ts +13 -0
- package/dist/ui/materials/mat_elevated.ts +24 -0
- package/dist/ui/materials/mat_flat.ts +34 -0
- package/dist/ui/materials/mat_glass.ts +20 -0
- package/dist/ui/materials/mat_lit.ts +13 -0
- package/dist/ui/materials/mat_moulded.ts +11 -0
- package/dist/ui/materials.ts +206 -0
- package/dist/ui/modes.ts +44 -0
- package/dist/ui/navigation.tsx +32 -0
- package/dist/ui/positioning.ts +90 -0
- package/dist/ui/presented.tsx +119 -0
- package/dist/ui/provider.d.ts +2 -0
- package/dist/ui/provider.js +7 -1
- package/dist/ui/provider.tsx +182 -0
- package/dist/ui/render-loop.ts +133 -0
- package/dist/ui/roving-focus.ts +161 -0
- package/dist/ui/screens.js +17 -8
- package/dist/ui/screens.ts +142 -0
- package/dist/ui/scroll-lock.ts +28 -0
- package/dist/ui/series.ts +97 -0
- package/dist/ui/signals.ts +386 -0
- package/dist/ui/styleguide.ts +231 -0
- package/dist/ui/styleguides.ts +374 -0
- package/dist/ui/themes.ts +22 -0
- package/dist/ui/token.js +1 -1
- package/dist/ui/token.ts +78 -0
- package/dist/ui/tokens/shadow.ts +25 -0
- package/dist/ui/tokens.ts +235 -0
- package/dist/ui/validators.ts +232 -0
- package/dist/ui/variant.d.ts +1 -0
- package/dist/ui/variant.js +3 -0
- package/dist/ui/variant.ts +352 -0
- package/dist/ui/variants/var_avatar-round.ts +14 -0
- package/dist/ui/variants/var_bubble-me.ts +14 -0
- package/dist/ui/variants/var_bubble-them.ts +13 -0
- package/dist/ui/variants/var_button-border.ts +76 -0
- package/dist/ui/variants/var_button-cta.ts +76 -0
- package/dist/ui/variants/var_button-destructive-ghost.ts +72 -0
- package/dist/ui/variants/var_button-destructive.ts +76 -0
- package/dist/ui/variants/var_button-fill.ts +76 -0
- package/dist/ui/variants/var_button-ghost.ts +72 -0
- package/dist/ui/variants/var_button-glass.ts +55 -0
- package/dist/ui/variants/var_button-normal.ts +76 -0
- package/dist/ui/variants/var_button-quiet.ts +74 -0
- package/dist/ui/variants/var_card.ts +17 -0
- package/dist/ui/variants/var_chip.ts +13 -0
- package/dist/ui/variants/var_choice.ts +15 -0
- package/dist/ui/variants/var_framed.ts +10 -0
- package/dist/ui/variants/var_glyph-plain.ts +11 -0
- package/dist/ui/variants/var_input.ts +43 -0
- package/dist/ui/variants/var_link-inline.ts +29 -0
- package/dist/ui/variants/var_list-row.ts +34 -0
- package/dist/ui/variants/var_rule.ts +10 -0
- package/dist/ui/variants/var_skeleton-block.ts +12 -0
- package/dist/ui/variants/var_spinner-turning.ts +12 -0
- package/dist/ui/variants/var_stat-tile.ts +13 -0
- package/dist/ui/variants/var_tab-bar.ts +15 -0
- package/dist/ui/variants/var_tap-target.ts +25 -0
- package/dist/ui/variants.ts +64 -0
- package/dist/ui/wrappers.d.ts +4 -1
- package/dist/ui/wrappers.js +60 -20
- package/dist/ui/wrappers.tsx +426 -0
- package/package.json +8 -2
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
export interface Money {
|
|
4
|
+
readonly amount: number
|
|
5
|
+
|
|
6
|
+
readonly currency: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type Node =
|
|
10
|
+
| { readonly kind: 'text'; readonly value: string }
|
|
11
|
+
| { readonly kind: 'arg'; readonly name: string }
|
|
12
|
+
| { readonly kind: 'number'; readonly name: string; readonly style?: string }
|
|
13
|
+
| { readonly kind: 'date'; readonly name: string; readonly style?: string }
|
|
14
|
+
| { readonly kind: 'time'; readonly name: string; readonly style?: string }
|
|
15
|
+
| { readonly kind: 'money'; readonly name: string }
|
|
16
|
+
| {
|
|
17
|
+
readonly kind: 'plural'
|
|
18
|
+
readonly name: string
|
|
19
|
+
readonly ordinal: boolean
|
|
20
|
+
readonly offset: number
|
|
21
|
+
readonly options: ReadonlyMap<string, readonly Node[]>
|
|
22
|
+
}
|
|
23
|
+
| {
|
|
24
|
+
readonly kind: 'select'
|
|
25
|
+
readonly name: string
|
|
26
|
+
readonly options: ReadonlyMap<string, readonly Node[]>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
| { readonly kind: 'pound' }
|
|
30
|
+
|
|
31
|
+
export class MessageSyntaxError extends Error {
|
|
32
|
+
readonly source: string
|
|
33
|
+
readonly position: number
|
|
34
|
+
|
|
35
|
+
constructor(message: string, source: string, position: number) {
|
|
36
|
+
super(`${message} (at ${position} in ${JSON.stringify(source)})`)
|
|
37
|
+
this.name = 'MessageSyntaxError'
|
|
38
|
+
this.source = source
|
|
39
|
+
this.position = position
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const ARG_TYPES = new Set(['number', 'date', 'time', 'money', 'plural', 'selectordinal', 'select'])
|
|
44
|
+
|
|
45
|
+
class Parser {
|
|
46
|
+
readonly #src: string
|
|
47
|
+
#pos = 0
|
|
48
|
+
|
|
49
|
+
constructor(src: string) {
|
|
50
|
+
this.#src = src
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
parse(): readonly Node[] {
|
|
54
|
+
const nodes = this.#parseBody(false)
|
|
55
|
+
if (this.#pos < this.#src.length) {
|
|
56
|
+
throw new MessageSyntaxError('unexpected "}"', this.#src, this.#pos)
|
|
57
|
+
}
|
|
58
|
+
return nodes
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
#parseBody(inSub: boolean): readonly Node[] {
|
|
62
|
+
const nodes: Node[] = []
|
|
63
|
+
let text = ''
|
|
64
|
+
|
|
65
|
+
const flush = (): void => {
|
|
66
|
+
if (text !== '') {
|
|
67
|
+
nodes.push({ kind: 'text', value: text })
|
|
68
|
+
text = ''
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
while (this.#pos < this.#src.length) {
|
|
73
|
+
const ch = this.#src[this.#pos]
|
|
74
|
+
|
|
75
|
+
if (ch === "'") {
|
|
76
|
+
|
|
77
|
+
const next = this.#src[this.#pos + 1]
|
|
78
|
+
if (next === "'") {
|
|
79
|
+
text += "'"
|
|
80
|
+
this.#pos += 2
|
|
81
|
+
continue
|
|
82
|
+
}
|
|
83
|
+
if (next === '{' || next === '}' || next === '#') {
|
|
84
|
+
this.#pos += 1
|
|
85
|
+
while (this.#pos < this.#src.length) {
|
|
86
|
+
if (this.#src[this.#pos] === "'") {
|
|
87
|
+
if (this.#src[this.#pos + 1] === "'") {
|
|
88
|
+
text += "'"
|
|
89
|
+
this.#pos += 2
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
this.#pos += 1
|
|
93
|
+
break
|
|
94
|
+
}
|
|
95
|
+
text += this.#src[this.#pos]
|
|
96
|
+
this.#pos += 1
|
|
97
|
+
}
|
|
98
|
+
continue
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
text += "'"
|
|
102
|
+
this.#pos += 1
|
|
103
|
+
continue
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (ch === '#' && inSub) {
|
|
107
|
+
flush()
|
|
108
|
+
nodes.push({ kind: 'pound' })
|
|
109
|
+
this.#pos += 1
|
|
110
|
+
continue
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (ch === '}') {
|
|
114
|
+
if (inSub) break
|
|
115
|
+
throw new MessageSyntaxError('unexpected "}"', this.#src, this.#pos)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (ch === '{') {
|
|
119
|
+
flush()
|
|
120
|
+
nodes.push(this.#parseArg())
|
|
121
|
+
continue
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
text += ch
|
|
125
|
+
this.#pos += 1
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
flush()
|
|
129
|
+
return nodes
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
#parseArg(): Node {
|
|
133
|
+
const start = this.#pos
|
|
134
|
+
this.#pos += 1
|
|
135
|
+
this.#skipWs()
|
|
136
|
+
|
|
137
|
+
const name = this.#readIdent()
|
|
138
|
+
if (name === '') throw new MessageSyntaxError('expected an argument name', this.#src, start)
|
|
139
|
+
|
|
140
|
+
this.#skipWs()
|
|
141
|
+
|
|
142
|
+
if (this.#peek() === '}') {
|
|
143
|
+
this.#pos += 1
|
|
144
|
+
return { kind: 'arg', name }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (this.#peek() !== ',') {
|
|
148
|
+
throw new MessageSyntaxError(`expected "," or "}" after "${name}"`, this.#src, this.#pos)
|
|
149
|
+
}
|
|
150
|
+
this.#pos += 1
|
|
151
|
+
this.#skipWs()
|
|
152
|
+
|
|
153
|
+
const type = this.#readIdent()
|
|
154
|
+
if (!ARG_TYPES.has(type)) {
|
|
155
|
+
throw new MessageSyntaxError(`unknown argument type "${type}"`, this.#src, this.#pos)
|
|
156
|
+
}
|
|
157
|
+
this.#skipWs()
|
|
158
|
+
|
|
159
|
+
if (type === 'plural' || type === 'selectordinal') {
|
|
160
|
+
return this.#parsePlural(name, type === 'selectordinal')
|
|
161
|
+
}
|
|
162
|
+
if (type === 'select') {
|
|
163
|
+
return this.#parseSelect(name)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
let style: string | undefined
|
|
167
|
+
if (this.#peek() === ',') {
|
|
168
|
+
this.#pos += 1
|
|
169
|
+
this.#skipWs()
|
|
170
|
+
style = this.#readUntil('}').trim()
|
|
171
|
+
}
|
|
172
|
+
this.#expect('}')
|
|
173
|
+
|
|
174
|
+
if (type === 'money') {
|
|
175
|
+
if (style !== undefined && style !== '') {
|
|
176
|
+
throw new MessageSyntaxError('money takes no style — the currency travels with the value', this.#src, this.#pos)
|
|
177
|
+
}
|
|
178
|
+
return { kind: 'money', name }
|
|
179
|
+
}
|
|
180
|
+
if (type === 'number') return style ? { kind: 'number', name, style } : { kind: 'number', name }
|
|
181
|
+
if (type === 'date') return style ? { kind: 'date', name, style } : { kind: 'date', name }
|
|
182
|
+
return style ? { kind: 'time', name, style } : { kind: 'time', name }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
#parsePlural(name: string, ordinal: boolean): Node {
|
|
186
|
+
if (this.#peek() !== ',') {
|
|
187
|
+
throw new MessageSyntaxError('expected "," before plural options', this.#src, this.#pos)
|
|
188
|
+
}
|
|
189
|
+
this.#pos += 1
|
|
190
|
+
this.#skipWs()
|
|
191
|
+
|
|
192
|
+
let offset = 0
|
|
193
|
+
if (this.#src.startsWith('offset:', this.#pos)) {
|
|
194
|
+
this.#pos += 'offset:'.length
|
|
195
|
+
this.#skipWs()
|
|
196
|
+
const digits = this.#readWhile((c) => c >= '0' && c <= '9')
|
|
197
|
+
if (digits === '') throw new MessageSyntaxError('expected a number after "offset:"', this.#src, this.#pos)
|
|
198
|
+
offset = Number(digits)
|
|
199
|
+
this.#skipWs()
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const options = this.#parseOptions()
|
|
203
|
+
if (!options.has('other')) {
|
|
204
|
+
|
|
205
|
+
throw new MessageSyntaxError(`plural "${name}" is missing the required "other" branch`, this.#src, this.#pos)
|
|
206
|
+
}
|
|
207
|
+
return { kind: 'plural', name, ordinal, offset, options }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
#parseSelect(name: string): Node {
|
|
211
|
+
if (this.#peek() !== ',') {
|
|
212
|
+
throw new MessageSyntaxError('expected "," before select options', this.#src, this.#pos)
|
|
213
|
+
}
|
|
214
|
+
this.#pos += 1
|
|
215
|
+
this.#skipWs()
|
|
216
|
+
|
|
217
|
+
const options = this.#parseOptions()
|
|
218
|
+
if (!options.has('other')) {
|
|
219
|
+
throw new MessageSyntaxError(`select "${name}" is missing the required "other" branch`, this.#src, this.#pos)
|
|
220
|
+
}
|
|
221
|
+
return { kind: 'select', name, options }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
#parseOptions(): ReadonlyMap<string, readonly Node[]> {
|
|
225
|
+
const options = new Map<string, readonly Node[]>()
|
|
226
|
+
|
|
227
|
+
while (this.#pos < this.#src.length && this.#peek() !== '}') {
|
|
228
|
+
this.#skipWs()
|
|
229
|
+
if (this.#peek() === '}') break
|
|
230
|
+
|
|
231
|
+
const key = this.#readWhile((c) => !/\s/.test(c) && c !== '{')
|
|
232
|
+
if (key === '') throw new MessageSyntaxError('expected an option key', this.#src, this.#pos)
|
|
233
|
+
if (options.has(key)) {
|
|
234
|
+
throw new MessageSyntaxError(`duplicate option "${key}"`, this.#src, this.#pos)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
this.#skipWs()
|
|
238
|
+
this.#expect('{')
|
|
239
|
+
options.set(key, this.#parseBody(true))
|
|
240
|
+
this.#expect('}')
|
|
241
|
+
this.#skipWs()
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
this.#expect('}')
|
|
245
|
+
return options
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
#peek(): string | undefined {
|
|
249
|
+
return this.#src[this.#pos]
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
#expect(ch: string): void {
|
|
253
|
+
if (this.#src[this.#pos] !== ch) {
|
|
254
|
+
throw new MessageSyntaxError(`expected "${ch}"`, this.#src, this.#pos)
|
|
255
|
+
}
|
|
256
|
+
this.#pos += 1
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
#skipWs(): void {
|
|
260
|
+
while (this.#pos < this.#src.length && /\s/.test(this.#src[this.#pos] ?? '')) this.#pos += 1
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
#readWhile(pred: (c: string) => boolean): string {
|
|
264
|
+
let out = ''
|
|
265
|
+
while (this.#pos < this.#src.length) {
|
|
266
|
+
const c = this.#src[this.#pos]
|
|
267
|
+
if (c === undefined || !pred(c)) break
|
|
268
|
+
out += c
|
|
269
|
+
this.#pos += 1
|
|
270
|
+
}
|
|
271
|
+
return out
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
#readIdent(): string {
|
|
275
|
+
return this.#readWhile((c) => /[A-Za-z0-9_]/.test(c))
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
#readUntil(stop: string): string {
|
|
279
|
+
return this.#readWhile((c) => c !== stop)
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const cache = new Map<string, readonly Node[]>()
|
|
284
|
+
|
|
285
|
+
export function parseMessage(src: string): readonly Node[] {
|
|
286
|
+
const hit = cache.get(src)
|
|
287
|
+
if (hit) return hit
|
|
288
|
+
const parsed = new Parser(src).parse()
|
|
289
|
+
cache.set(src, parsed)
|
|
290
|
+
return parsed
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function messageArgs(src: string): ReadonlySet<string> {
|
|
294
|
+
const names = new Set<string>()
|
|
295
|
+
|
|
296
|
+
const pass = (nodes: readonly Node[]): void => {
|
|
297
|
+
for (const node of nodes) {
|
|
298
|
+
switch (node.kind) {
|
|
299
|
+
case 'text':
|
|
300
|
+
case 'pound':
|
|
301
|
+
break
|
|
302
|
+
case 'plural':
|
|
303
|
+
case 'select':
|
|
304
|
+
names.add(node.name)
|
|
305
|
+
for (const body of node.options.values()) pass(body)
|
|
306
|
+
break
|
|
307
|
+
default:
|
|
308
|
+
names.add(node.name)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
pass(parseMessage(src))
|
|
314
|
+
return names
|
|
315
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { formatMessage } from './format.ts'
|
|
2
|
+
import { type CatalogShape, type Locale, type Translator, entryText } from './types.ts'
|
|
3
|
+
|
|
4
|
+
export class UnknownMessageKeyError extends Error {
|
|
5
|
+
readonly key: string
|
|
6
|
+
constructor(key: string, locale: Locale) {
|
|
7
|
+
super(`no message "${key}" in locale "${locale}"`)
|
|
8
|
+
this.name = 'UnknownMessageKeyError'
|
|
9
|
+
this.key = key
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface TranslatorOptions {
|
|
14
|
+
readonly locale: Locale
|
|
15
|
+
readonly timeZone?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createTranslator<C extends CatalogShape>(
|
|
19
|
+
catalog: C,
|
|
20
|
+
options: TranslatorOptions,
|
|
21
|
+
): Translator<C> {
|
|
22
|
+
const translate = (key: string, params?: Readonly<Record<string, unknown>>): string => {
|
|
23
|
+
const entry = catalog[key]
|
|
24
|
+
if (entry === undefined) throw new UnknownMessageKeyError(key, options.locale)
|
|
25
|
+
return formatMessage(entryText(entry), params ?? {}, {
|
|
26
|
+
locale: options.locale,
|
|
27
|
+
timeZone: options.timeZone,
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
return translate as Translator<C>
|
|
31
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import type { Money } from './parse.ts'
|
|
4
|
+
|
|
5
|
+
export type { Money } from './parse.ts'
|
|
6
|
+
|
|
7
|
+
export type Locale = string
|
|
8
|
+
|
|
9
|
+
type Trim<S extends string> = S extends ` ${infer R}`
|
|
10
|
+
? Trim<R>
|
|
11
|
+
: S extends `${infer R} `
|
|
12
|
+
? Trim<R>
|
|
13
|
+
: S
|
|
14
|
+
|
|
15
|
+
type HasSubMessages<S extends string> = S extends `${string},plural${string}`
|
|
16
|
+
? true
|
|
17
|
+
: S extends `${string}, plural${string}`
|
|
18
|
+
? true
|
|
19
|
+
: S extends `${string},select${string}`
|
|
20
|
+
? true
|
|
21
|
+
: S extends `${string}, select${string}`
|
|
22
|
+
? true
|
|
23
|
+
: false
|
|
24
|
+
|
|
25
|
+
type ArgName<Inner extends string> = Inner extends `${infer N},${string}` ? Trim<N> : Trim<Inner>
|
|
26
|
+
|
|
27
|
+
type ArgValue<Inner extends string> = Inner extends `${string},${infer Rest}`
|
|
28
|
+
? Trim<Rest> extends `number${string}`
|
|
29
|
+
? number
|
|
30
|
+
: Trim<Rest> extends `date${string}`
|
|
31
|
+
? Date | number
|
|
32
|
+
: Trim<Rest> extends `time${string}`
|
|
33
|
+
? Date | number
|
|
34
|
+
: Trim<Rest> extends `money${string}`
|
|
35
|
+
? Money
|
|
36
|
+
: string | number
|
|
37
|
+
: string | number
|
|
38
|
+
|
|
39
|
+
type SimpleParams<S extends string> = S extends `${string}{${infer Inner}}${infer Rest}`
|
|
40
|
+
? { [K in ArgName<Inner>]: ArgValue<Inner> } & SimpleParams<Rest>
|
|
41
|
+
: unknown
|
|
42
|
+
|
|
43
|
+
type Prettify<T> = { [K in keyof T]: T[K] } & {}
|
|
44
|
+
|
|
45
|
+
export type MessageParams<S extends string> = Prettify<SimpleParams<S>>
|
|
46
|
+
|
|
47
|
+
declare const PARAMS: unique symbol
|
|
48
|
+
|
|
49
|
+
export interface DeclaredMessage<P extends Record<string, unknown>> {
|
|
50
|
+
readonly text: string
|
|
51
|
+
readonly [PARAMS]?: P
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function msg<P extends Record<string, unknown>>(text: string): DeclaredMessage<P> {
|
|
55
|
+
return { text }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type MessageEntry = string | DeclaredMessage<Record<string, unknown>>
|
|
59
|
+
|
|
60
|
+
export interface RequiresExplicitParams {
|
|
61
|
+
readonly __error: 'plural/select messages must declare params: use msg<{ ... }>(text)'
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type ValidEntry<E> = E extends string
|
|
65
|
+
? HasSubMessages<E> extends true
|
|
66
|
+
? RequiresExplicitParams
|
|
67
|
+
: E
|
|
68
|
+
: E
|
|
69
|
+
|
|
70
|
+
export type CatalogShape = Readonly<Record<string, MessageEntry>>
|
|
71
|
+
|
|
72
|
+
export type ParamsOf<E> =
|
|
73
|
+
E extends DeclaredMessage<infer P>
|
|
74
|
+
? P
|
|
75
|
+
: E extends string
|
|
76
|
+
? MessageParams<E>
|
|
77
|
+
: never
|
|
78
|
+
|
|
79
|
+
type HasNoKeys<T> = [keyof T] extends [never] ? true : false
|
|
80
|
+
|
|
81
|
+
export type Translator<C extends CatalogShape> = <K extends keyof C & string>(
|
|
82
|
+
key: K,
|
|
83
|
+
...args: HasNoKeys<ParamsOf<C[K]>> extends true
|
|
84
|
+
? [params?: Record<string, never>]
|
|
85
|
+
: [params: ParamsOf<C[K]>]
|
|
86
|
+
) => string
|
|
87
|
+
|
|
88
|
+
export function entryText(entry: MessageEntry): string {
|
|
89
|
+
return typeof entry === 'string' ? entry : entry.text
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function defineCatalog<const C extends Record<string, MessageEntry>>(catalog: {
|
|
93
|
+
[K in keyof C]: ValidEntry<C[K]>
|
|
94
|
+
}): C {
|
|
95
|
+
return catalog as unknown as C
|
|
96
|
+
}
|