@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,205 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { basename, dirname, join, resolve } from 'node:path'
|
|
3
|
+
import { argv, stdout } from 'node:process'
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
5
|
+
|
|
6
|
+
import { exit, invoked } from '@prestack/thread/core/invoked'
|
|
7
|
+
import type { Threaded } from '@prestack/thread/core/thread'
|
|
8
|
+
import { findTree, inTree, strayFiles } from '@prestack/thread/core/tree'
|
|
9
|
+
|
|
10
|
+
export function nameOf(id: string): string {
|
|
11
|
+
return id.replace(/-/g, '_').toUpperCase()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function starter(row: Threaded): string {
|
|
15
|
+
return `import { variant } from '../variant.ts'
|
|
16
|
+
|
|
17
|
+
export const ${nameOf(row.id)} = variant({
|
|
18
|
+
id: '${row.id}',
|
|
19
|
+
sets: [],
|
|
20
|
+
reacts: [],
|
|
21
|
+
} as const)
|
|
22
|
+
`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function seeds(
|
|
26
|
+
rows: readonly Threaded[],
|
|
27
|
+
has: (at: string) => boolean,
|
|
28
|
+
): readonly { readonly path: string; readonly content: string }[] {
|
|
29
|
+
return rows.filter((row) => !has(row.at)).map((row) => ({ path: row.at, content: starter(row) }))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function mismatched(
|
|
33
|
+
rows: readonly Threaded[],
|
|
34
|
+
loader: (row: Threaded) => Promise<{ readonly id?: string } | undefined>,
|
|
35
|
+
): Promise<readonly string[]> {
|
|
36
|
+
const found: string[] = []
|
|
37
|
+
for (const row of rows) {
|
|
38
|
+
const variant = await loader(row)
|
|
39
|
+
if (variant === undefined) {
|
|
40
|
+
found.push(
|
|
41
|
+
`\`${row.id}\` is declared at \`${row.at}\`, and that file exports no \`${nameOf(row.id)}\`. ` +
|
|
42
|
+
'A row names a file, and the file is the thing.',
|
|
43
|
+
)
|
|
44
|
+
continue
|
|
45
|
+
}
|
|
46
|
+
if (variant.id !== row.id) {
|
|
47
|
+
found.push(
|
|
48
|
+
`\`${row.at}\` declares itself \`${String(variant.id)}\`, and the row that names it calls it \`${row.id}\`. ` +
|
|
49
|
+
"A variant file and the row that names it have to be the same variant — make the file's `id` match the row, or fix the row.",
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return found
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function extensionOf(path: string): string {
|
|
57
|
+
const name = basename(path)
|
|
58
|
+
const at = name.lastIndexOf('.')
|
|
59
|
+
return at === -1 ? '' : name.slice(at)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const VAR_PREFIX = /^var_/
|
|
63
|
+
|
|
64
|
+
export function orphaned(rows: readonly Threaded[], strays: readonly string[]): readonly string[] {
|
|
65
|
+
const named = new Set(rows.map((row) => row.at))
|
|
66
|
+
return strays
|
|
67
|
+
.filter((one) => !named.has(one))
|
|
68
|
+
.map(
|
|
69
|
+
(one) =>
|
|
70
|
+
`\`${one}\` is under variants and no row names it — not a variant, a file somebody wrote. ` +
|
|
71
|
+
'Add a row for it, or delete the file.',
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const GUARDED = /=\s*variant\s*\(/
|
|
76
|
+
|
|
77
|
+
const TYPED = /import type \{ Variant \} from '([^']*)'/
|
|
78
|
+
|
|
79
|
+
const OPENS = /export const ([A-Z0-9_]+) = \{/
|
|
80
|
+
|
|
81
|
+
const CLOSES = /\} as const satisfies Variant/
|
|
82
|
+
|
|
83
|
+
export function unguarded(rows: readonly Threaded[], read: (at: string) => string): readonly string[] {
|
|
84
|
+
return rows
|
|
85
|
+
.filter((row) => !GUARDED.test(read(row.at)))
|
|
86
|
+
.map(
|
|
87
|
+
(row) =>
|
|
88
|
+
`\`${row.at}\` declares \`${row.id}\` with \`satisfies Variant\`, which accepts a value no ` +
|
|
89
|
+
'row holds — `satisfies` checks against `string`, so `8px` and `var(--x)` compile. Run ' +
|
|
90
|
+
'`thread variants` and it rewrites the two lines that wrap the declaration, leaving what is ' +
|
|
91
|
+
'between them alone.',
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function guarding(source: string): string | undefined {
|
|
96
|
+
if (GUARDED.test(source)) return undefined
|
|
97
|
+
const typed = TYPED.exec(source)
|
|
98
|
+
const opens = OPENS.exec(source)
|
|
99
|
+
if (typed === null || opens === null || !CLOSES.test(source)) return undefined
|
|
100
|
+
return source
|
|
101
|
+
.replace(typed[0], `import { variant } from '${typed[1] ?? ''}'`)
|
|
102
|
+
.replace(opens[0], `export const ${opens[1] ?? ''} = variant({`)
|
|
103
|
+
.replace(CLOSES, '} as const)')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const SHEET = `variants.${'ts'}`
|
|
107
|
+
|
|
108
|
+
const MINE = `variants.${'ts'}`
|
|
109
|
+
|
|
110
|
+
async function run(): Promise<number> {
|
|
111
|
+
const own = argv.includes('--own')
|
|
112
|
+
|
|
113
|
+
const held = own
|
|
114
|
+
? ((await import(pathToFileURL(resolve(fileURLToPath(import.meta.url), '../..', MINE)).href)) as {
|
|
115
|
+
VARIANTS?: { readonly declares?: readonly Threaded[] }
|
|
116
|
+
})
|
|
117
|
+
: undefined
|
|
118
|
+
|
|
119
|
+
const tree = own ? undefined : findTree()
|
|
120
|
+
const root = tree === undefined ? resolve(fileURLToPath(import.meta.url), '../../../../..') : tree.at
|
|
121
|
+
if (tree !== undefined && !existsSync(inTree(tree, SHEET))) {
|
|
122
|
+
stdout.write(`thread: this tree has no ${tree.name}/${SHEET}, so it declares no variant and there is nothing to seed or check.\n`)
|
|
123
|
+
return 0
|
|
124
|
+
}
|
|
125
|
+
const sheet =
|
|
126
|
+
tree === undefined
|
|
127
|
+
? { VARIANT: held?.VARIANTS }
|
|
128
|
+
: ((await import(pathToFileURL(inTree(tree, SHEET)).href)) as {
|
|
129
|
+
VARIANT?: { readonly declares?: readonly Threaded[] }
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
if (sheet.VARIANT === undefined) {
|
|
133
|
+
stdout.write(
|
|
134
|
+
own
|
|
135
|
+
? `variants: this package's own ${MINE} exports no \`VARIANTS\`.\n`
|
|
136
|
+
: `thread: ${String(tree?.name)}/${SHEET} exports no \`VARIANT\`, so nothing declares a variant here.\n`,
|
|
137
|
+
)
|
|
138
|
+
return 1
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const rows = sheet.VARIANT.declares ?? []
|
|
142
|
+
const checking = argv.includes('--check')
|
|
143
|
+
const exists = (where: string): boolean => existsSync(join(root, where))
|
|
144
|
+
const made = seeds(rows, exists)
|
|
145
|
+
|
|
146
|
+
if (checking) {
|
|
147
|
+
const unseeded = rows.find((row) => !exists(row.at))
|
|
148
|
+
if (unseeded !== undefined) {
|
|
149
|
+
stdout.write(
|
|
150
|
+
`thread: \`${unseeded.id}\` is declared at \`${unseeded.at}\`, and the file is not there yet. ` +
|
|
151
|
+
'A row names a file, and `--check` never writes one — run `thread variants` and commit what it seeds.\n',
|
|
152
|
+
)
|
|
153
|
+
return 1
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
for (const seed of made) {
|
|
157
|
+
mkdirSync(dirname(join(root, seed.path)), { recursive: true })
|
|
158
|
+
writeFileSync(join(root, seed.path), seed.content, 'utf8')
|
|
159
|
+
stdout.write(`thread: seeded ${seed.path}\n`)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const wrong = (
|
|
164
|
+
await mismatched(rows, async (row) => {
|
|
165
|
+
const module = (await import(pathToFileURL(join(root, row.at)).href)) as Record<string, unknown>
|
|
166
|
+
return module[nameOf(row.id)] as { readonly id?: string } | undefined
|
|
167
|
+
})
|
|
168
|
+
)[0]
|
|
169
|
+
if (wrong !== undefined) {
|
|
170
|
+
stdout.write(`thread: ${wrong}\n`)
|
|
171
|
+
return 1
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const read = (at: string): string => readFileSync(join(root, at), 'utf8')
|
|
175
|
+
if (checking) {
|
|
176
|
+
const loose = unguarded(rows, read)[0]
|
|
177
|
+
if (loose !== undefined) {
|
|
178
|
+
stdout.write(`thread: ${loose}\n`)
|
|
179
|
+
return 1
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
for (const row of rows) {
|
|
183
|
+
const written = guarding(read(row.at))
|
|
184
|
+
if (written === undefined) continue
|
|
185
|
+
writeFileSync(join(root, row.at), written, 'utf8')
|
|
186
|
+
stdout.write(`thread: ${row.at} now declares through \`variant(...)\`\n`)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const stray = orphaned(rows, strayFiles(root, 'var_', '.' + 'ts'))[0]
|
|
191
|
+
if (stray !== undefined) {
|
|
192
|
+
stdout.write(`thread: ${stray}\n`)
|
|
193
|
+
return 1
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
stdout.write(
|
|
197
|
+
checking
|
|
198
|
+
? `thread: ${String(rows.length)} variant(s) declared, all seeded\n`
|
|
199
|
+
: `thread: ${String(rows.length)} variant(s) declared, ${String(made.length)} seeded, ` +
|
|
200
|
+
`${String(rows.length - made.length)} already written\n`,
|
|
201
|
+
)
|
|
202
|
+
return 0
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (invoked(import.meta.url)) exit(await run())
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import { defineCatalog, msg } from '../types.ts'
|
|
4
|
+
|
|
5
|
+
export const en = defineCatalog({
|
|
6
|
+
|
|
7
|
+
'errors.auth.unauthenticated.title': 'You need to sign in to do this',
|
|
8
|
+
'errors.auth.invalid_credentials.title': 'That email or password did not match',
|
|
9
|
+
'errors.auth.token_expired.title': 'Your session expired — sign in again',
|
|
10
|
+
'errors.auth.token_reused.title': 'This sign-in link was already used, so we signed you out everywhere',
|
|
11
|
+
'errors.auth.session_revoked.title': 'This session was signed out',
|
|
12
|
+
'errors.auth.denied.title': 'You do not have permission to do this',
|
|
13
|
+
|
|
14
|
+
'errors.quota.exceeded.title': 'You have used everything included in your plan',
|
|
15
|
+
'errors.quota.reservation_expired.title': 'That request took too long and was released',
|
|
16
|
+
|
|
17
|
+
'errors.rate_limit.exceeded.title': 'Too many attempts — wait a moment and try again',
|
|
18
|
+
|
|
19
|
+
'errors.feature.not_in_plan.title': 'Your plan does not include this feature',
|
|
20
|
+
|
|
21
|
+
'errors.db.not_found.title': 'We could not find that',
|
|
22
|
+
'errors.db.constraint_violation.title': 'That change would break a rule on this data',
|
|
23
|
+
'errors.db.already_exists.title': 'That already exists',
|
|
24
|
+
'errors.db.conflict.title': 'Someone else changed this first — reload and try again',
|
|
25
|
+
'errors.db.migration_checksum_mismatch.title': 'A migration that already ran has been edited',
|
|
26
|
+
'errors.db.destructive_without_dry_run.title': 'This change can delete data, so it needs a test run first',
|
|
27
|
+
'errors.db.round_trip_budget_exceeded.title': 'This node asked the database for more than it was built to need',
|
|
28
|
+
'errors.db.cursor_invalid.title': 'That link to the next page does not belong to this list',
|
|
29
|
+
'errors.db.cursor_expired.title': 'That link to the next page is too old — start again from the first page',
|
|
30
|
+
|
|
31
|
+
'errors.workflow.node_failed.title': 'A node in this process failed',
|
|
32
|
+
'errors.workflow.invalid_transition.title': 'This process cannot move to that state from here',
|
|
33
|
+
'errors.workflow.compensation_failed.title': 'We could not undo a failed process — someone needs to look at this',
|
|
34
|
+
'errors.workflow.already_running.title': 'This is already running — wait for it to finish before starting another',
|
|
35
|
+
|
|
36
|
+
'errors.queue.job_failed.title': 'A background job failed',
|
|
37
|
+
'errors.queue.dlq_exhausted.title': 'A background job failed too many times and was set aside',
|
|
38
|
+
|
|
39
|
+
'errors.ai.provider_unavailable.title': 'The AI service is not responding',
|
|
40
|
+
'errors.ai.schema_validation_failed.title': 'The AI returned something we could not use',
|
|
41
|
+
'errors.ai.tool_not_in_mask.title': 'The AI tried to use a tool it is not allowed to use',
|
|
42
|
+
'errors.ai.node_cap_exceeded.title': 'The AI took too many turns and was stopped',
|
|
43
|
+
|
|
44
|
+
'errors.search.index_failed.title': 'We could not add that to the search index',
|
|
45
|
+
'errors.search.query_invalid.title': 'That search is not valid',
|
|
46
|
+
|
|
47
|
+
'errors.comms.channel_kind_undeclared.title': 'A way of messaging was added without a name',
|
|
48
|
+
'errors.comms.channel_kind_undescribed.title': 'A way of messaging was added without a description',
|
|
49
|
+
'errors.comms.channel_kind_unlabelled.title': 'A way of messaging has no name people can be shown',
|
|
50
|
+
'errors.comms.channel_kind_undocumented.title': 'A way of messaging was added with no instructions for using it',
|
|
51
|
+
'errors.comms.channel_kind_duplicate.title': 'Two ways of messaging were added under the same name',
|
|
52
|
+
|
|
53
|
+
'errors.config.invalid.title': 'The configuration is not valid',
|
|
54
|
+
'errors.config.missing_env.title': 'A required setting is missing',
|
|
55
|
+
'errors.config.secret_in_public_var.title': 'A secret is named so the browser would see it',
|
|
56
|
+
'errors.config.credential_invalid.title': 'That credential was not accepted',
|
|
57
|
+
'errors.config.scope_missing.title': 'That credential does not have the access this needs',
|
|
58
|
+
'errors.config.resource_not_found.title': 'That credential works, but the resource it points to was not found',
|
|
59
|
+
'errors.config.destination_incompatible.title': 'The destination cannot receive this — check its plan',
|
|
60
|
+
'errors.config.blocks_exit.title': 'This was set up in a way that blocks moving it out — contact support',
|
|
61
|
+
|
|
62
|
+
'errors.net.timeout.title': 'That took too long',
|
|
63
|
+
'errors.net.upstream_unavailable.title': 'A service we depend on is down',
|
|
64
|
+
'errors.net.destination_refused.title': 'That address cannot receive webhooks',
|
|
65
|
+
|
|
66
|
+
'errors.internal.unexpected.title': 'Something went wrong on our side',
|
|
67
|
+
'errors.internal.not_implemented.title': 'This is not built yet',
|
|
68
|
+
'errors.internal.invalid_argument.title': 'Some of the information sent was not valid',
|
|
69
|
+
|
|
70
|
+
'quota.exceeded.detail': msg<{ used: number; allowance: number; meter: string }>(
|
|
71
|
+
'{used, number} of {allowance, number} {meter} used.',
|
|
72
|
+
),
|
|
73
|
+
'rate_limit.retry_after': msg<{ seconds: number }>(
|
|
74
|
+
'Try again in {seconds, plural, one {# second} other {# seconds}}.',
|
|
75
|
+
),
|
|
76
|
+
|
|
77
|
+
'migration.impact.rows': msg<{ rows: number; table: string }>(
|
|
78
|
+
'{rows, plural, =0 {No rows} one {# row} other {# rows}} in {table} will be affected.',
|
|
79
|
+
),
|
|
80
|
+
'migration.dry_run.required': 'Run this on a test copy before applying it.',
|
|
81
|
+
'sandbox.created': 'Test environment ready.',
|
|
82
|
+
'sandbox.torn_down': 'Test environment removed.',
|
|
83
|
+
|
|
84
|
+
'platform.event.deploy_reverted.version_id': 'Restored version',
|
|
85
|
+
'platform.event.deploy_reverted.description': 'A prior version was restored by a new forward deployment.',
|
|
86
|
+
|
|
87
|
+
'billing.invoice.total': msg<{ total: { amount: number; currency: string } }>(
|
|
88
|
+
'Total: {total, money}.',
|
|
89
|
+
),
|
|
90
|
+
'billing.invoice.due': msg<{ due: Date | number }>('Due {due, date, medium}.'),
|
|
91
|
+
'billing.usage.summary': msg<{ cost: { amount: number; currency: string }; calls: number }>(
|
|
92
|
+
'{calls, plural, one {# call} other {# calls}} costing {cost, money}.',
|
|
93
|
+
),
|
|
94
|
+
|
|
95
|
+
'common.empty': 'Nothing here yet.',
|
|
96
|
+
'common.loading': 'Loading…',
|
|
97
|
+
'common.error': 'Something went wrong.',
|
|
98
|
+
'common.retry': 'Try again',
|
|
99
|
+
'common.cancel': 'Cancel',
|
|
100
|
+
'common.confirm': 'Confirm',
|
|
101
|
+
'common.saved': 'Saved.',
|
|
102
|
+
|
|
103
|
+
'comms.kind.email.label': 'Email',
|
|
104
|
+
'comms.kind.internal.label': 'Internal message',
|
|
105
|
+
'comms.kind.push.label': 'Push notification',
|
|
106
|
+
'comms.kind.widget.label': 'Live chat',
|
|
107
|
+
|
|
108
|
+
'auth.ui.signIn.title': 'Sign in',
|
|
109
|
+
'auth.ui.signIn.submit': 'Sign in',
|
|
110
|
+
'auth.ui.signUp.title': 'Create your account',
|
|
111
|
+
'auth.ui.signUp.submit': 'Create account',
|
|
112
|
+
'auth.ui.field.email': 'Email',
|
|
113
|
+
'auth.ui.field.password': 'Password',
|
|
114
|
+
'auth.ui.field.name': 'Name',
|
|
115
|
+
'auth.ui.field.code': 'Code',
|
|
116
|
+
'auth.ui.field.optional': 'optional',
|
|
117
|
+
'auth.ui.magicLink.submit': 'Email me a sign-in link',
|
|
118
|
+
'auth.ui.magicLink.sent': 'If that address can be used, we have sent it a sign-in link.',
|
|
119
|
+
'auth.ui.otp.submit': 'Email me a code',
|
|
120
|
+
'auth.ui.otp.confirm': 'Sign in with the code',
|
|
121
|
+
'auth.ui.otp.sent': 'If that address can be used, we have sent it a code.',
|
|
122
|
+
'auth.ui.reset.submit': 'Email me a reset link',
|
|
123
|
+
'auth.ui.reset.sent': 'If that address can be used, we have sent it a reset link.',
|
|
124
|
+
'auth.ui.oauth.continueWith': msg<{ provider: string }>('Continue with {provider}'),
|
|
125
|
+
'auth.ui.passkey.signIn': 'Use a passkey',
|
|
126
|
+
'auth.ui.passkey.register': 'Add a passkey',
|
|
127
|
+
'auth.ui.passkey.added': 'Passkey added. You can use it to sign in from now on.',
|
|
128
|
+
'auth.ui.passkey.unsupported': 'This browser cannot use passkeys.',
|
|
129
|
+
|
|
130
|
+
'auth.ui.passkey.cancelled': 'Passkey sign-in was cancelled.',
|
|
131
|
+
'auth.ui.passkey.failed': 'That passkey could not be used.',
|
|
132
|
+
|
|
133
|
+
'consent.telemetry.label': 'Product analytics',
|
|
134
|
+
'consent.telemetry.describe':
|
|
135
|
+
'Which screens you visit and which buttons you press, so we can see where this app is confusing. Never what you type — passwords, messages and form contents are not recorded.',
|
|
136
|
+
'consent.title': 'Choose what you share',
|
|
137
|
+
'consent.accept': 'Accept selected',
|
|
138
|
+
'consent.acceptAll': 'Accept all',
|
|
139
|
+
'consent.decline': 'Decline all',
|
|
140
|
+
'consent.withdraw': 'Withdraw consent',
|
|
141
|
+
'consent.required': 'Always on',
|
|
142
|
+
'auth.ui.switch.toSignUp': 'Need an account? Create one',
|
|
143
|
+
'auth.ui.switch.toSignIn': 'Already have an account? Sign in',
|
|
144
|
+
'auth.ui.switch.toReset': 'Forgotten your password?',
|
|
145
|
+
'auth.ui.switch.back': 'Back',
|
|
146
|
+
'auth.ui.working': 'Working…',
|
|
147
|
+
'auth.ui.separator': 'or',
|
|
148
|
+
|
|
149
|
+
'auth.ui.error.generic': 'That did not work. Check what you entered and try again.',
|
|
150
|
+
'auth.ui.error.offline': 'We could not reach the server. Check your connection and try again.',
|
|
151
|
+
'auth.ui.signedIn': 'You are signed in.',
|
|
152
|
+
|
|
153
|
+
'auth.provider.google': 'Google',
|
|
154
|
+
'auth.provider.apple': 'Apple',
|
|
155
|
+
'auth.provider.github': 'GitHub',
|
|
156
|
+
'auth.provider.microsoft': 'Microsoft',
|
|
157
|
+
'validation.required': 'This is required.',
|
|
158
|
+
'validation.text.invalid': 'Enter some text.',
|
|
159
|
+
'validation.text.too_long': 'This is too long.',
|
|
160
|
+
'validation.richtext.invalid': 'This content could not be read.',
|
|
161
|
+
'validation.richtext.too_many_blocks': 'This content is too long.',
|
|
162
|
+
'validation.number.invalid': 'Enter a number.',
|
|
163
|
+
'validation.number.below_min': 'This is below the lowest value allowed.',
|
|
164
|
+
'validation.number.above_max': 'This is above the highest value allowed.',
|
|
165
|
+
'validation.integer.invalid': 'Enter a whole number.',
|
|
166
|
+
'validation.money.invalid': 'Enter an amount.',
|
|
167
|
+
|
|
168
|
+
'validation.money.bare_number': 'An amount needs a currency. Choose one and enter the amount again.',
|
|
169
|
+
'validation.percent.range': 'Enter a number between 0 and 100.',
|
|
170
|
+
'validation.duration.negative': 'A length of time cannot be negative.',
|
|
171
|
+
'validation.bool.invalid': 'Choose yes or no.',
|
|
172
|
+
'validation.date.format': 'Enter a date as YYYY-MM-DD.',
|
|
173
|
+
'validation.date.invalid': 'That date does not exist.',
|
|
174
|
+
'validation.datetime.format': 'Enter a date and time.',
|
|
175
|
+
'validation.phone.invalid': 'Enter a phone number with 6 to 15 digits.',
|
|
176
|
+
'validation.email.invalid': 'Enter an email address.',
|
|
177
|
+
|
|
178
|
+
'validation.password.tooShort': 'Use at least 8 characters.',
|
|
179
|
+
'validation.otp.invalid': 'Enter the 6-digit code.',
|
|
180
|
+
'validation.url.invalid': 'Enter a web address.',
|
|
181
|
+
'validation.url.scheme': 'Only web, email and phone links are allowed.',
|
|
182
|
+
'validation.ref.invalid': 'That file could not be found.',
|
|
183
|
+
'validation.geo.invalid': 'Enter a location.',
|
|
184
|
+
'validation.rating.range': 'Choose a rating.',
|
|
185
|
+
'validation.select.undeclared': 'Choose one of the options.',
|
|
186
|
+
'validation.json.too_deep': 'This data is nested too deeply.',
|
|
187
|
+
'validation.json.too_large': 'This data is too large.',
|
|
188
|
+
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
export type EnCatalog = typeof en
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import { type Money, type Node, parseMessage } from './parse.ts'
|
|
4
|
+
import type { Locale } from './types.ts'
|
|
5
|
+
|
|
6
|
+
export class MissingParamError extends Error {
|
|
7
|
+
readonly param: string
|
|
8
|
+
constructor(param: string, message: string) {
|
|
9
|
+
super(`missing parameter "${param}" for message ${JSON.stringify(message)}`)
|
|
10
|
+
this.name = 'MissingParamError'
|
|
11
|
+
this.param = param
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class ParamTypeError extends Error {
|
|
16
|
+
constructor(param: string, expected: string, received: unknown) {
|
|
17
|
+
super(`parameter "${param}" must be ${expected}, received ${typeof received}`)
|
|
18
|
+
this.name = 'ParamTypeError'
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface FormatOptions {
|
|
23
|
+
readonly locale: Locale
|
|
24
|
+
|
|
25
|
+
readonly timeZone?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const numberFormats = new Map<string, Intl.NumberFormat>()
|
|
29
|
+
const dateFormats = new Map<string, Intl.DateTimeFormat>()
|
|
30
|
+
const pluralRules = new Map<string, Intl.PluralRules>()
|
|
31
|
+
|
|
32
|
+
function numberFormat(locale: Locale, style: string | undefined): Intl.NumberFormat {
|
|
33
|
+
const key = `${locale}|${style ?? ''}`
|
|
34
|
+
let fmt = numberFormats.get(key)
|
|
35
|
+
if (!fmt) {
|
|
36
|
+
const options: Intl.NumberFormatOptions =
|
|
37
|
+
style === 'percent'
|
|
38
|
+
? { style: 'percent' }
|
|
39
|
+
: style === 'integer'
|
|
40
|
+
? { maximumFractionDigits: 0 }
|
|
41
|
+
: {}
|
|
42
|
+
fmt = new Intl.NumberFormat(locale, options)
|
|
43
|
+
numberFormats.set(key, fmt)
|
|
44
|
+
}
|
|
45
|
+
return fmt
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function moneyFormat(locale: Locale, currency: string): Intl.NumberFormat {
|
|
49
|
+
const key = `${locale}|money|${currency}`
|
|
50
|
+
let fmt = numberFormats.get(key)
|
|
51
|
+
if (!fmt) {
|
|
52
|
+
fmt = new Intl.NumberFormat(locale, { style: 'currency', currency })
|
|
53
|
+
numberFormats.set(key, fmt)
|
|
54
|
+
}
|
|
55
|
+
return fmt
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
type DateStyle = 'full' | 'long' | 'medium' | 'short'
|
|
59
|
+
const DATE_STYLES = new Set<string>(['full', 'long', 'medium', 'short'])
|
|
60
|
+
|
|
61
|
+
function dateFormat(
|
|
62
|
+
locale: Locale,
|
|
63
|
+
kind: 'date' | 'time',
|
|
64
|
+
style: string | undefined,
|
|
65
|
+
timeZone: string | undefined,
|
|
66
|
+
): Intl.DateTimeFormat {
|
|
67
|
+
const resolved: DateStyle = DATE_STYLES.has(style ?? '') ? (style as DateStyle) : 'medium'
|
|
68
|
+
const key = `${locale}|${kind}|${resolved}|${timeZone ?? ''}`
|
|
69
|
+
let fmt = dateFormats.get(key)
|
|
70
|
+
if (!fmt) {
|
|
71
|
+
const options: Intl.DateTimeFormatOptions =
|
|
72
|
+
kind === 'date' ? { dateStyle: resolved } : { timeStyle: resolved }
|
|
73
|
+
if (timeZone !== undefined) options.timeZone = timeZone
|
|
74
|
+
fmt = new Intl.DateTimeFormat(locale, options)
|
|
75
|
+
dateFormats.set(key, fmt)
|
|
76
|
+
}
|
|
77
|
+
return fmt
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function plural(locale: Locale, ordinal: boolean): Intl.PluralRules {
|
|
81
|
+
const key = `${locale}|${ordinal ? 'ordinal' : 'cardinal'}`
|
|
82
|
+
let rules = pluralRules.get(key)
|
|
83
|
+
if (!rules) {
|
|
84
|
+
rules = new Intl.PluralRules(locale, { type: ordinal ? 'ordinal' : 'cardinal' })
|
|
85
|
+
pluralRules.set(key, rules)
|
|
86
|
+
}
|
|
87
|
+
return rules
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function requireParam(
|
|
91
|
+
params: Readonly<Record<string, unknown>>,
|
|
92
|
+
name: string,
|
|
93
|
+
source: string,
|
|
94
|
+
): unknown {
|
|
95
|
+
if (!(name in params) || params[name] === undefined) {
|
|
96
|
+
throw new MissingParamError(name, source)
|
|
97
|
+
}
|
|
98
|
+
return params[name]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function asNumber(value: unknown, name: string): number {
|
|
102
|
+
if (typeof value !== 'number' || Number.isNaN(value)) {
|
|
103
|
+
throw new ParamTypeError(name, 'a number', value)
|
|
104
|
+
}
|
|
105
|
+
return value
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function asMoney(value: unknown, name: string): Money {
|
|
109
|
+
if (
|
|
110
|
+
typeof value !== 'object' ||
|
|
111
|
+
value === null ||
|
|
112
|
+
typeof (value as Money).amount !== 'number' ||
|
|
113
|
+
typeof (value as Money).currency !== 'string' ||
|
|
114
|
+
(value as Money).currency === ''
|
|
115
|
+
) {
|
|
116
|
+
|
|
117
|
+
throw new ParamTypeError(name, 'a { amount, currency } pair', value)
|
|
118
|
+
}
|
|
119
|
+
return value as Money
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function asDate(value: unknown, name: string): Date | number {
|
|
123
|
+
if (value instanceof Date) return value
|
|
124
|
+
if (typeof value === 'number') return value
|
|
125
|
+
throw new ParamTypeError(name, 'a Date or a timestamp', value)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
interface RenderContext {
|
|
129
|
+
readonly locale: Locale
|
|
130
|
+
readonly timeZone: string | undefined
|
|
131
|
+
readonly params: Readonly<Record<string, unknown>>
|
|
132
|
+
readonly source: string
|
|
133
|
+
|
|
134
|
+
readonly poundValue: number | undefined
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function render(nodes: readonly Node[], ctx: RenderContext): string {
|
|
138
|
+
let out = ''
|
|
139
|
+
|
|
140
|
+
for (const node of nodes) {
|
|
141
|
+
switch (node.kind) {
|
|
142
|
+
case 'text':
|
|
143
|
+
out += node.value
|
|
144
|
+
break
|
|
145
|
+
|
|
146
|
+
case 'pound':
|
|
147
|
+
out +=
|
|
148
|
+
ctx.poundValue === undefined
|
|
149
|
+
? '#'
|
|
150
|
+
: numberFormat(ctx.locale, undefined).format(ctx.poundValue)
|
|
151
|
+
break
|
|
152
|
+
|
|
153
|
+
case 'arg': {
|
|
154
|
+
const value = requireParam(ctx.params, node.name, ctx.source)
|
|
155
|
+
out += typeof value === 'number' ? numberFormat(ctx.locale, undefined).format(value) : String(value)
|
|
156
|
+
break
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
case 'number': {
|
|
160
|
+
const value = asNumber(requireParam(ctx.params, node.name, ctx.source), node.name)
|
|
161
|
+
out += numberFormat(ctx.locale, node.style).format(value)
|
|
162
|
+
break
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
case 'money': {
|
|
166
|
+
const value = asMoney(requireParam(ctx.params, node.name, ctx.source), node.name)
|
|
167
|
+
out += moneyFormat(ctx.locale, value.currency).format(value.amount)
|
|
168
|
+
break
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
case 'date': {
|
|
172
|
+
const value = asDate(requireParam(ctx.params, node.name, ctx.source), node.name)
|
|
173
|
+
out += dateFormat(ctx.locale, 'date', node.style, ctx.timeZone).format(value)
|
|
174
|
+
break
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
case 'time': {
|
|
178
|
+
const value = asDate(requireParam(ctx.params, node.name, ctx.source), node.name)
|
|
179
|
+
out += dateFormat(ctx.locale, 'time', node.style, ctx.timeZone).format(value)
|
|
180
|
+
break
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
case 'plural': {
|
|
184
|
+
const value = asNumber(requireParam(ctx.params, node.name, ctx.source), node.name)
|
|
185
|
+
const adjusted = value - node.offset
|
|
186
|
+
|
|
187
|
+
let body = node.options.get(`=${value}`)
|
|
188
|
+
if (!body) {
|
|
189
|
+
const category = plural(ctx.locale, node.ordinal).select(adjusted)
|
|
190
|
+
body = node.options.get(category) ?? node.options.get('other')
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
out += render(body ?? [], { ...ctx, poundValue: adjusted })
|
|
194
|
+
break
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
case 'select': {
|
|
198
|
+
const value = requireParam(ctx.params, node.name, ctx.source)
|
|
199
|
+
const body = node.options.get(String(value)) ?? node.options.get('other')
|
|
200
|
+
out += render(body ?? [], ctx)
|
|
201
|
+
break
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return out
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function formatMessage(
|
|
210
|
+
source: string,
|
|
211
|
+
params: Readonly<Record<string, unknown>>,
|
|
212
|
+
opts: FormatOptions,
|
|
213
|
+
): string {
|
|
214
|
+
return render(parseMessage(source), {
|
|
215
|
+
locale: opts.locale,
|
|
216
|
+
timeZone: opts.timeZone,
|
|
217
|
+
params,
|
|
218
|
+
source,
|
|
219
|
+
poundValue: undefined,
|
|
220
|
+
})
|
|
221
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
export const REFERENCE_LOCALE = 'en'
|
|
4
|
+
|
|
5
|
+
export interface LocaleDecl {
|
|
6
|
+
|
|
7
|
+
readonly tag: string
|
|
8
|
+
|
|
9
|
+
readonly endonym: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const REGISTRY = new Map<string, LocaleDecl>([[REFERENCE_LOCALE, { tag: 'en', endonym: 'English' }]])
|
|
13
|
+
|
|
14
|
+
export function defineLocale(locale: LocaleDecl): void {
|
|
15
|
+
if (!/^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$/.test(locale.tag)) {
|
|
16
|
+
throw new Error(`"${locale.tag}" is not a BCP 47 tag — write \`pt-BR\`, \`zh-Hans\`, \`en\``)
|
|
17
|
+
}
|
|
18
|
+
if (REGISTRY.has(locale.tag)) throw new Error(`locale "${locale.tag}" is already declared`)
|
|
19
|
+
REGISTRY.set(locale.tag, locale)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function locales(): readonly LocaleDecl[] {
|
|
23
|
+
return [...REGISTRY.values()].sort((a, b) => (a.tag < b.tag ? -1 : a.tag > b.tag ? 1 : 0))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function locale(tag: string): LocaleDecl | undefined {
|
|
27
|
+
return REGISTRY.get(tag)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function resetLocales(): void {
|
|
31
|
+
REGISTRY.clear()
|
|
32
|
+
REGISTRY.set(REFERENCE_LOCALE, { tag: 'en', endonym: 'English' })
|
|
33
|
+
}
|