@prestack/thread-ui 0.1.10 → 0.1.11

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.
Files changed (209) hide show
  1. package/CHANGELOG.md +187 -0
  2. package/dist/ui/attach.js +6 -7
  3. package/dist/ui/compositions.d.ts +0 -8
  4. package/dist/ui/compositions.js +3 -166
  5. package/dist/ui/generators/routes.js +1 -0
  6. package/package.json +2 -2
  7. package/dist/ui/animations/anm_appear.ts +0 -13
  8. package/dist/ui/animations/anm_blur_in.ts +0 -19
  9. package/dist/ui/animations/anm_blur_out.ts +0 -18
  10. package/dist/ui/animations/anm_dismiss.ts +0 -18
  11. package/dist/ui/animations/anm_drop.ts +0 -19
  12. package/dist/ui/animations/anm_fall.ts +0 -18
  13. package/dist/ui/animations/anm_flash.ts +0 -18
  14. package/dist/ui/animations/anm_glow.ts +0 -16
  15. package/dist/ui/animations/anm_lift.ts +0 -16
  16. package/dist/ui/animations/anm_nudge.ts +0 -16
  17. package/dist/ui/animations/anm_pop_in.ts +0 -23
  18. package/dist/ui/animations/anm_pop_out.ts +0 -22
  19. package/dist/ui/animations/anm_press.ts +0 -12
  20. package/dist/ui/animations/anm_pulse.ts +0 -13
  21. package/dist/ui/animations/anm_rise.ts +0 -19
  22. package/dist/ui/animations/anm_shake.ts +0 -13
  23. package/dist/ui/animations/anm_slide_in_left.ts +0 -19
  24. package/dist/ui/animations/anm_slide_in_right.ts +0 -19
  25. package/dist/ui/animations/anm_slide_out_left.ts +0 -18
  26. package/dist/ui/animations/anm_slide_out_right.ts +0 -18
  27. package/dist/ui/animations/anm_spin.ts +0 -12
  28. package/dist/ui/animations/anm_swell.ts +0 -16
  29. package/dist/ui/animations/anm_wobble.ts +0 -24
  30. package/dist/ui/animations/anm_zoom_in.ts +0 -19
  31. package/dist/ui/animations/anm_zoom_out.ts +0 -18
  32. package/dist/ui/animations.ts +0 -308
  33. package/dist/ui/attach.ts +0 -553
  34. package/dist/ui/charts.tsx +0 -69
  35. package/dist/ui/checked.ts +0 -32
  36. package/dist/ui/collector.ts +0 -140
  37. package/dist/ui/compositions.ts +0 -613
  38. package/dist/ui/consent.ts +0 -55
  39. package/dist/ui/content.tsx +0 -27
  40. package/dist/ui/contrast.ts +0 -27
  41. package/dist/ui/controls.tsx +0 -25
  42. package/dist/ui/disclosure.tsx +0 -20
  43. package/dist/ui/dismiss-layer.ts +0 -87
  44. package/dist/ui/door.ts +0 -140
  45. package/dist/ui/drag.ts +0 -316
  46. package/dist/ui/elements/accordion.tsx +0 -47
  47. package/dist/ui/elements/accordion.works.ts +0 -66
  48. package/dist/ui/elements/aspect-ratio.tsx +0 -31
  49. package/dist/ui/elements/aspect-ratio.works.ts +0 -5
  50. package/dist/ui/elements/avatar.tsx +0 -34
  51. package/dist/ui/elements/avatar.works.ts +0 -16
  52. package/dist/ui/elements/badge.tsx +0 -33
  53. package/dist/ui/elements/badge.works.ts +0 -9
  54. package/dist/ui/elements/bars.tsx +0 -50
  55. package/dist/ui/elements/bars.works.ts +0 -29
  56. package/dist/ui/elements/box.tsx +0 -36
  57. package/dist/ui/elements/breadcrumb.tsx +0 -38
  58. package/dist/ui/elements/breadcrumb.works.ts +0 -24
  59. package/dist/ui/elements/button.tsx +0 -46
  60. package/dist/ui/elements/button.works.tsx +0 -52
  61. package/dist/ui/elements/column.tsx +0 -29
  62. package/dist/ui/elements/consent.tsx +0 -71
  63. package/dist/ui/elements/consent.works.tsx +0 -132
  64. package/dist/ui/elements/donut.tsx +0 -45
  65. package/dist/ui/elements/donut.works.ts +0 -42
  66. package/dist/ui/elements/each.tsx +0 -34
  67. package/dist/ui/elements/each.works.tsx +0 -82
  68. package/dist/ui/elements/empty.tsx +0 -33
  69. package/dist/ui/elements/field.tsx +0 -45
  70. package/dist/ui/elements/field.works.ts +0 -65
  71. package/dist/ui/elements/funnel.tsx +0 -50
  72. package/dist/ui/elements/funnel.works.ts +0 -37
  73. package/dist/ui/elements/gauge.tsx +0 -40
  74. package/dist/ui/elements/gauge.works.ts +0 -42
  75. package/dist/ui/elements/glyph.tsx +0 -30
  76. package/dist/ui/elements/glyph.works.tsx +0 -42
  77. package/dist/ui/elements/grid.tsx +0 -29
  78. package/dist/ui/elements/heatmap.tsx +0 -48
  79. package/dist/ui/elements/heatmap.works.ts +0 -24
  80. package/dist/ui/elements/hit-box.tsx +0 -47
  81. package/dist/ui/elements/hit-box.works.tsx +0 -576
  82. package/dist/ui/elements/image.tsx +0 -29
  83. package/dist/ui/elements/line-chart.tsx +0 -45
  84. package/dist/ui/elements/line-chart.works.ts +0 -41
  85. package/dist/ui/elements/link.tsx +0 -38
  86. package/dist/ui/elements/link.works.ts +0 -25
  87. package/dist/ui/elements/media.tsx +0 -57
  88. package/dist/ui/elements/menu.tsx +0 -51
  89. package/dist/ui/elements/menu.works.tsx +0 -154
  90. package/dist/ui/elements/money.tsx +0 -36
  91. package/dist/ui/elements/money.works.ts +0 -9
  92. package/dist/ui/elements/pagination.tsx +0 -39
  93. package/dist/ui/elements/pagination.works.ts +0 -20
  94. package/dist/ui/elements/progress.tsx +0 -34
  95. package/dist/ui/elements/progress.works.ts +0 -8
  96. package/dist/ui/elements/radio-group.tsx +0 -50
  97. package/dist/ui/elements/radio-group.works.ts +0 -33
  98. package/dist/ui/elements/row.tsx +0 -29
  99. package/dist/ui/elements/scatter.tsx +0 -42
  100. package/dist/ui/elements/scatter.works.ts +0 -38
  101. package/dist/ui/elements/select.tsx +0 -43
  102. package/dist/ui/elements/select.works.ts +0 -5
  103. package/dist/ui/elements/separator.tsx +0 -31
  104. package/dist/ui/elements/skeleton.tsx +0 -31
  105. package/dist/ui/elements/slider.tsx +0 -34
  106. package/dist/ui/elements/slider.works.ts +0 -5
  107. package/dist/ui/elements/sparkline.tsx +0 -37
  108. package/dist/ui/elements/sparkline.works.ts +0 -32
  109. package/dist/ui/elements/spinner.tsx +0 -33
  110. package/dist/ui/elements/stack.tsx +0 -29
  111. package/dist/ui/elements/stat.tsx +0 -36
  112. package/dist/ui/elements/stat.works.ts +0 -14
  113. package/dist/ui/elements/surface.tsx +0 -36
  114. package/dist/ui/elements/surface.works.ts +0 -9
  115. package/dist/ui/elements/table.tsx +0 -62
  116. package/dist/ui/elements/table.works.ts +0 -30
  117. package/dist/ui/elements/tabs.tsx +0 -47
  118. package/dist/ui/elements/tabs.works.ts +0 -84
  119. package/dist/ui/elements/text.tsx +0 -33
  120. package/dist/ui/elements/textarea.tsx +0 -38
  121. package/dist/ui/elements/textarea.works.ts +0 -5
  122. package/dist/ui/elements/time.tsx +0 -40
  123. package/dist/ui/elements/time.works.ts +0 -29
  124. package/dist/ui/elements/toggle.tsx +0 -34
  125. package/dist/ui/elements/toggle.works.ts +0 -5
  126. package/dist/ui/elements/tooltip.tsx +0 -40
  127. package/dist/ui/elements/tooltip.works.ts +0 -77
  128. package/dist/ui/elements/value.tsx +0 -38
  129. package/dist/ui/elements/value.works.ts +0 -9
  130. package/dist/ui/elements.ts +0 -127
  131. package/dist/ui/focus-scope.ts +0 -121
  132. package/dist/ui/fonts.ts +0 -355
  133. package/dist/ui/generators/dev.d.ts +0 -1
  134. package/dist/ui/generators/dev.js +0 -62
  135. package/dist/ui/generators/dev.ts +0 -75
  136. package/dist/ui/generators/elements.ts +0 -796
  137. package/dist/ui/generators/i18n.ts +0 -283
  138. package/dist/ui/generators/icons.ts +0 -207
  139. package/dist/ui/generators/react.ts +0 -267
  140. package/dist/ui/generators/routes.ts +0 -847
  141. package/dist/ui/generators/show.ts +0 -261
  142. package/dist/ui/generators/styles.ts +0 -677
  143. package/dist/ui/generators/variants.ts +0 -196
  144. package/dist/ui/i18n/catalogs/en.ts +0 -191
  145. package/dist/ui/i18n/format.ts +0 -221
  146. package/dist/ui/i18n/locales.ts +0 -33
  147. package/dist/ui/i18n/parse.ts +0 -315
  148. package/dist/ui/i18n/translator.ts +0 -31
  149. package/dist/ui/i18n/types.ts +0 -96
  150. package/dist/ui/icons.ts +0 -331
  151. package/dist/ui/islands/map.ts +0 -58
  152. package/dist/ui/localizations.ts +0 -75
  153. package/dist/ui/material.ts +0 -236
  154. package/dist/ui/materials/mat_bordered.ts +0 -13
  155. package/dist/ui/materials/mat_ceramic.ts +0 -13
  156. package/dist/ui/materials/mat_clay.ts +0 -13
  157. package/dist/ui/materials/mat_elevated.ts +0 -24
  158. package/dist/ui/materials/mat_flat.ts +0 -34
  159. package/dist/ui/materials/mat_glass.ts +0 -20
  160. package/dist/ui/materials/mat_lit.ts +0 -13
  161. package/dist/ui/materials/mat_moulded.ts +0 -11
  162. package/dist/ui/materials.ts +0 -197
  163. package/dist/ui/modes.ts +0 -44
  164. package/dist/ui/navigation.tsx +0 -29
  165. package/dist/ui/positioning.ts +0 -90
  166. package/dist/ui/presented.tsx +0 -119
  167. package/dist/ui/provider.tsx +0 -184
  168. package/dist/ui/render-loop.ts +0 -133
  169. package/dist/ui/roving-focus.ts +0 -161
  170. package/dist/ui/screens.ts +0 -141
  171. package/dist/ui/scroll-lock.ts +0 -28
  172. package/dist/ui/series.ts +0 -97
  173. package/dist/ui/signals.ts +0 -386
  174. package/dist/ui/styleguide.ts +0 -231
  175. package/dist/ui/styleguides.ts +0 -374
  176. package/dist/ui/themes.ts +0 -22
  177. package/dist/ui/token.ts +0 -78
  178. package/dist/ui/tokens/shadow.ts +0 -25
  179. package/dist/ui/tokens.ts +0 -234
  180. package/dist/ui/validators.ts +0 -232
  181. package/dist/ui/variant.ts +0 -349
  182. package/dist/ui/variants/var_avatar-round.ts +0 -14
  183. package/dist/ui/variants/var_bubble-me.ts +0 -14
  184. package/dist/ui/variants/var_bubble-them.ts +0 -13
  185. package/dist/ui/variants/var_button-border.ts +0 -76
  186. package/dist/ui/variants/var_button-cta.ts +0 -76
  187. package/dist/ui/variants/var_button-destructive-ghost.ts +0 -72
  188. package/dist/ui/variants/var_button-destructive.ts +0 -76
  189. package/dist/ui/variants/var_button-fill.ts +0 -76
  190. package/dist/ui/variants/var_button-ghost.ts +0 -72
  191. package/dist/ui/variants/var_button-glass.ts +0 -55
  192. package/dist/ui/variants/var_button-normal.ts +0 -76
  193. package/dist/ui/variants/var_button-quiet.ts +0 -74
  194. package/dist/ui/variants/var_card.ts +0 -17
  195. package/dist/ui/variants/var_chip.ts +0 -13
  196. package/dist/ui/variants/var_choice.ts +0 -15
  197. package/dist/ui/variants/var_framed.ts +0 -10
  198. package/dist/ui/variants/var_glyph-plain.ts +0 -11
  199. package/dist/ui/variants/var_input.ts +0 -43
  200. package/dist/ui/variants/var_link-inline.ts +0 -29
  201. package/dist/ui/variants/var_list-row.ts +0 -34
  202. package/dist/ui/variants/var_rule.ts +0 -10
  203. package/dist/ui/variants/var_skeleton-block.ts +0 -12
  204. package/dist/ui/variants/var_spinner-turning.ts +0 -12
  205. package/dist/ui/variants/var_stat-tile.ts +0 -13
  206. package/dist/ui/variants/var_tab-bar.ts +0 -15
  207. package/dist/ui/variants/var_tap-target.ts +0 -25
  208. package/dist/ui/variants.ts +0 -64
  209. package/dist/ui/wrappers.tsx +0 -429
@@ -1,196 +0,0 @@
1
- import { existsSync, readFileSync, writeFileSync } from '@prestack/thread/integrations/disk'
2
- import { 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
- export function orphaned(rows: readonly Threaded[], strays: readonly string[]): readonly string[] {
57
- const named = new Set(rows.map((row) => row.at))
58
- return strays
59
- .filter((one) => !named.has(one))
60
- .map(
61
- (one) =>
62
- `\`${one}\` is under variants and no row names it — not a variant, a file somebody wrote. ` +
63
- 'Add a row for it, or delete the file.',
64
- )
65
- }
66
-
67
- const GUARDED = /=\s*variant\s*\(/
68
-
69
- const TYPED = /import type \{ Variant \} from '([^']*)'/
70
-
71
- const OPENS = /export const ([A-Z0-9_]+) = \{/
72
-
73
- const CLOSES = /\} as const satisfies Variant/
74
-
75
- export function unguarded(rows: readonly Threaded[], read: (at: string) => string): readonly string[] {
76
- return rows
77
- .filter((row) => !GUARDED.test(read(row.at)))
78
- .map(
79
- (row) =>
80
- `\`${row.at}\` declares \`${row.id}\` with \`satisfies Variant\`, which accepts a value no ` +
81
- 'row holds — `satisfies` checks against `string`, so `8px` and `var(--x)` compile. Run ' +
82
- '`thread variants` and it rewrites the two lines that wrap the declaration, leaving what is ' +
83
- 'between them alone.',
84
- )
85
- }
86
-
87
- export function guarding(source: string): string | undefined {
88
- if (GUARDED.test(source)) return undefined
89
- const typed = TYPED.exec(source)
90
- const opens = OPENS.exec(source)
91
- if (typed === null || opens === null || !CLOSES.test(source)) return undefined
92
- return source
93
- .replace(typed[0], `import { variant } from '${typed[1] ?? ''}'`)
94
- .replace(opens[0], `export const ${opens[1] ?? ''} = variant({`)
95
- .replace(CLOSES, '} as const)')
96
- }
97
-
98
- const SHEET = `variants.${'ts'}`
99
-
100
- const MINE = `variants.${'ts'}`
101
-
102
- async function run(): Promise<number> {
103
- const own = argv.includes('--own')
104
-
105
- const held = own
106
- ? ((await import(pathToFileURL(resolve(fileURLToPath(import.meta.url), '../..', MINE)).href)) as {
107
- VARIANTS?: { readonly declares?: readonly Threaded[] }
108
- })
109
- : undefined
110
-
111
- const tree = own ? undefined : findTree()
112
- const root = tree === undefined ? resolve(fileURLToPath(import.meta.url), '../../../../..') : tree.at
113
- if (tree !== undefined && !existsSync(inTree(tree, SHEET))) {
114
- stdout.write(`thread: this tree has no ${tree.name}/${SHEET}, so it declares no variant and there is nothing to seed or check.\n`)
115
- return 0
116
- }
117
- const sheet =
118
- tree === undefined
119
- ? { VARIANT: held?.VARIANTS }
120
- : ((await import(pathToFileURL(inTree(tree, SHEET)).href)) as {
121
- VARIANT?: { readonly declares?: readonly Threaded[] }
122
- })
123
-
124
- if (sheet.VARIANT === undefined) {
125
- stdout.write(
126
- own
127
- ? `variants: this package's own ${MINE} exports no \`VARIANTS\`.\n`
128
- : `thread: ${String(tree?.name)}/${SHEET} exports no \`VARIANT\`, so nothing declares a variant here.\n`,
129
- )
130
- return 1
131
- }
132
-
133
- const rows = sheet.VARIANT.declares ?? []
134
- const checking = argv.includes('--check')
135
- const exists = (where: string): boolean => existsSync(join(root, where))
136
- const made = seeds(rows, exists)
137
-
138
- if (checking) {
139
- const unseeded = rows.find((row) => !exists(row.at))
140
- if (unseeded !== undefined) {
141
- stdout.write(
142
- `thread: \`${unseeded.id}\` is declared at \`${unseeded.at}\`, and the file is not there yet. ` +
143
- 'A row names a file, and `--check` never writes one — run `thread variants` and commit what it seeds.\n',
144
- )
145
- return 1
146
- }
147
- } else {
148
- for (const seed of made) {
149
- writeFileSync(join(root, seed.path), seed.content, 'utf8')
150
- stdout.write(`thread: seeded ${seed.path}\n`)
151
- }
152
- }
153
-
154
- const wrong = (
155
- await mismatched(rows, async (row) => {
156
- const module = (await import(pathToFileURL(join(root, row.at)).href)) as Record<string, unknown>
157
- return module[nameOf(row.id)] as { readonly id?: string } | undefined
158
- })
159
- )[0]
160
- if (wrong !== undefined) {
161
- stdout.write(`thread: ${wrong}\n`)
162
- return 1
163
- }
164
-
165
- const read = (at: string): string => readFileSync(join(root, at), 'utf8')
166
- if (checking) {
167
- const loose = unguarded(rows, read)[0]
168
- if (loose !== undefined) {
169
- stdout.write(`thread: ${loose}\n`)
170
- return 1
171
- }
172
- } else {
173
- for (const row of rows) {
174
- const written = guarding(read(row.at))
175
- if (written === undefined) continue
176
- writeFileSync(join(root, row.at), written, 'utf8')
177
- stdout.write(`thread: ${row.at} now declares through \`variant(...)\`\n`)
178
- }
179
- }
180
-
181
- const stray = orphaned(rows, strayFiles(root, 'var_', '.' + 'ts'))[0]
182
- if (stray !== undefined) {
183
- stdout.write(`thread: ${stray}\n`)
184
- return 1
185
- }
186
-
187
- stdout.write(
188
- checking
189
- ? `thread: ${String(rows.length)} variant(s) declared, all seeded\n`
190
- : `thread: ${String(rows.length)} variant(s) declared, ${String(made.length)} seeded, ` +
191
- `${String(rows.length - made.length)} already written\n`,
192
- )
193
- return 0
194
- }
195
-
196
- if (invoked(import.meta.url)) exit(await run())
@@ -1,191 +0,0 @@
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
@@ -1,221 +0,0 @@
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
- }
@@ -1,33 +0,0 @@
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
- }