@prestack/thread-ui 0.0.2 → 0.1.1
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/CHANGELOG.md +376 -0
- package/dist/ui/animations.js +2 -3
- package/dist/ui/animations.ts +1 -2
- package/dist/ui/attach.js +0 -6
- package/dist/ui/attach.ts +0 -6
- package/dist/ui/compositions.d.ts +1 -1
- package/dist/ui/compositions.js +29 -44
- package/dist/ui/compositions.ts +31 -38
- package/dist/ui/generators/dev.js +2 -1
- package/dist/ui/generators/dev.ts +2 -1
- package/dist/ui/generators/elements.js +1 -1
- package/dist/ui/generators/elements.ts +1 -1
- package/dist/ui/generators/i18n.js +2 -3
- package/dist/ui/generators/i18n.ts +1 -2
- package/dist/ui/generators/icons.js +3 -12
- package/dist/ui/generators/icons.ts +3 -3
- package/dist/ui/generators/react.js +3 -2
- package/dist/ui/generators/react.ts +3 -2
- package/dist/ui/generators/routes.js +10 -14
- package/dist/ui/generators/routes.ts +10 -6
- package/dist/ui/generators/show.js +4 -12
- package/dist/ui/generators/show.ts +4 -3
- package/dist/ui/generators/styles.js +2 -3
- package/dist/ui/generators/styles.ts +1 -2
- package/dist/ui/generators/variants.js +2 -3
- package/dist/ui/generators/variants.ts +1 -2
- package/dist/ui/materials.js +2 -3
- package/dist/ui/materials.ts +1 -2
- package/dist/ui/screens.js +7 -16
- package/dist/ui/screens.ts +6 -7
- package/dist/ui/tokens.js +1 -2
- package/dist/ui/tokens.ts +1 -2
- package/package.json +2 -2
- package/dist/ui/makers/message.d.ts +0 -1
- package/dist/ui/makers/message.js +0 -230
- package/dist/ui/makers/message.ts +0 -292
|
@@ -1,292 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
-
import { join } from 'node:path'
|
|
3
|
-
import { argv, exit, stdout } from 'node:process'
|
|
4
|
-
import { pathToFileURL } from 'node:url'
|
|
5
|
-
|
|
6
|
-
import { ELSEWHERE, RECEIPT, answer, answering, asking, given, guidance, issue, marked, named, needed, only, prompted, refuse, spend, within, wordsIn, wrapped, type Asked } from '@prestack/thread/core/asking'
|
|
7
|
-
import { named as packageOf } from '@prestack/thread/core/scope'
|
|
8
|
-
import { findTree, inTree } from '@prestack/thread/core/tree'
|
|
9
|
-
|
|
10
|
-
import { keyProps, keysIn, readWording, walk, wordingFile } from '../generators/i18n.ts'
|
|
11
|
-
import { fileFor, languagesIn, sourceIn } from '../localizations.ts'
|
|
12
|
-
|
|
13
|
-
const TREE = findTree()
|
|
14
|
-
|
|
15
|
-
const DOTTED = /^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/
|
|
16
|
-
|
|
17
|
-
const KEY: Asked = { at: 'key', guide: 'lowercase words joined by dots — `checkout.confirm`, `errors.db.not_found.title`' }
|
|
18
|
-
|
|
19
|
-
interface Row {
|
|
20
|
-
readonly id: string
|
|
21
|
-
readonly is?: string
|
|
22
|
-
readonly sets?: Readonly<Record<string, string>>
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async function languages(): Promise<{
|
|
26
|
-
readonly held: readonly { readonly id: string; readonly name: string; readonly flag: string }[]
|
|
27
|
-
readonly source: string
|
|
28
|
-
}> {
|
|
29
|
-
const path = inTree(TREE, `localizations.${'ts'}`)
|
|
30
|
-
let sheet: { LOCALIZATION?: { readonly holds?: readonly Row[]; readonly declares?: readonly Row[] } } = {}
|
|
31
|
-
try {
|
|
32
|
-
sheet = (await import(pathToFileURL(path).href)) as typeof sheet
|
|
33
|
-
} catch {
|
|
34
|
-
sheet = {}
|
|
35
|
-
}
|
|
36
|
-
if (sheet.LOCALIZATION === undefined) {
|
|
37
|
-
refuse(
|
|
38
|
-
`there is no ${within(TREE.at, path)} declaring a \`LOCALIZATION\` here, so this app is in no ` +
|
|
39
|
-
`language and a message has nowhere to go. Run \`npx thread attach ${packageOf('ui')}\`.`,
|
|
40
|
-
)
|
|
41
|
-
}
|
|
42
|
-
const rows = [...(sheet.LOCALIZATION.holds ?? []), ...(sheet.LOCALIZATION.declares ?? [])]
|
|
43
|
-
const held = languagesIn(rows)
|
|
44
|
-
const source = sourceIn(rows)
|
|
45
|
-
if (held.length === 0 || source === undefined) {
|
|
46
|
-
refuse('no language is declared, so there is nothing to write a message in. Add one with `npx thread create language`.')
|
|
47
|
-
}
|
|
48
|
-
return { held, source }
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function usage(props: readonly string[]): ReadonlyMap<string, readonly string[]> {
|
|
52
|
-
const found = new Map<string, string[]>()
|
|
53
|
-
for (const file of walk(TREE.at)) {
|
|
54
|
-
const text = readFileSync(file, 'utf8')
|
|
55
|
-
for (const key of new Set(keysIn(text, props))) {
|
|
56
|
-
const held = found.get(key) ?? []
|
|
57
|
-
held.push(within(TREE.at, file))
|
|
58
|
-
found.set(key, held)
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return found
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const mark = (key: string, said: string): string => marked([key, said])
|
|
65
|
-
|
|
66
|
-
function write(at: string, language: string, held: Readonly<Record<string, string>>): void {
|
|
67
|
-
writeFileSync(join(at, fileFor(language)), wordingFile(held), 'utf8')
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async function run(): Promise<number> {
|
|
71
|
-
const { held: spoken, source } = await languages()
|
|
72
|
-
const props = keyProps(TREE.at)
|
|
73
|
-
if (props.length === 0) {
|
|
74
|
-
refuse(
|
|
75
|
-
`nothing in the installed \`${packageOf('ui')}\` declares a prop of type \`MessageKey\`, so no key could be ` +
|
|
76
|
-
'recognised anywhere. That is a broken install rather than an app with no wording.',
|
|
77
|
-
)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const LANGUAGES: readonly Asked[] = spoken.map((one) => ({ at: one.id, guide: `what it says in ${one.name}` }))
|
|
81
|
-
|
|
82
|
-
const NEW: readonly Asked[] = [KEY, ...LANGUAGES]
|
|
83
|
-
|
|
84
|
-
const wording = new Map<string, Record<string, string>>()
|
|
85
|
-
for (const one of spoken) wording.set(one.id, { ...readWording(TREE.at, one.id) })
|
|
86
|
-
|
|
87
|
-
const written = wording.get(source) ?? {}
|
|
88
|
-
const keys = Object.keys(written).sort()
|
|
89
|
-
const used = usage(props)
|
|
90
|
-
|
|
91
|
-
const blank = (key: string): readonly string[] =>
|
|
92
|
-
spoken.filter((one) => ((wording.get(one.id) ?? {})[key] ?? '') === '').map((one) => one.id)
|
|
93
|
-
|
|
94
|
-
const listing = [
|
|
95
|
-
`${String(keys.length)} message(s), in ${spoken.map((one) => one.id).join(', ')} — written in ${source}:`,
|
|
96
|
-
'',
|
|
97
|
-
|
|
98
|
-
...keys.map((key) => {
|
|
99
|
-
const missing = blank(key)
|
|
100
|
-
const said = (written[key] ?? '') || '(nothing yet)'
|
|
101
|
-
return (
|
|
102
|
-
` [${mark(key, written[key] ?? '')}] ${key.padEnd(38)} ${said.length > 46 ? `${said.slice(0, 45)}…` : said}` +
|
|
103
|
-
`${missing.length === 0 ? '' : ` · still to write: ${missing.join(', ')}`}` +
|
|
104
|
-
`${(used.get(key) ?? []).length === 0 ? ' · named by nothing here' : ''}`
|
|
105
|
-
)
|
|
106
|
-
}),
|
|
107
|
-
].join('\n')
|
|
108
|
-
|
|
109
|
-
const words = wordsIn(['new'])
|
|
110
|
-
const verb = words[0]
|
|
111
|
-
|
|
112
|
-
const flagged = argv.indexOf(RECEIPT)
|
|
113
|
-
if (flagged === -1) {
|
|
114
|
-
stdout.write(`${listing}\n`)
|
|
115
|
-
const nonce = issue(TREE.at, listing)
|
|
116
|
-
stdout.write(
|
|
117
|
-
[
|
|
118
|
-
'',
|
|
119
|
-
...answering('message'),
|
|
120
|
-
'',
|
|
121
|
-
...guidance(NEW),
|
|
122
|
-
'',
|
|
123
|
-
...wrapped(
|
|
124
|
-
'Every language is asked for and none may be left blank. A key that exists in one language and ' +
|
|
125
|
-
'not another is what makes a screen come out half translated, and it is refused here rather ' +
|
|
126
|
-
'than found by whoever is reading the app in the other one.',
|
|
127
|
-
' ',
|
|
128
|
-
),
|
|
129
|
-
'',
|
|
130
|
-
` npx thread create message new ${RECEIPT} ${nonce}`,
|
|
131
|
-
'',
|
|
132
|
-
...ELSEWHERE,
|
|
133
|
-
'',
|
|
134
|
-
...wrapped(
|
|
135
|
-
'A rename moves the wording in every language and rewrites every file in the interface that ' +
|
|
136
|
-
'names it, in one run.',
|
|
137
|
-
' ',
|
|
138
|
-
),
|
|
139
|
-
'',
|
|
140
|
-
].join('\n'),
|
|
141
|
-
)
|
|
142
|
-
stdout.write(`create: receipt ${nonce} — spent on use, and stale the moment the wording changes.\n`)
|
|
143
|
-
return 0
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
spend(TREE.at, listing, argv[flagged + 1], `the wording`)
|
|
147
|
-
|
|
148
|
-
const all = given()
|
|
149
|
-
const { piped, ask, shut } = asking(all)
|
|
150
|
-
|
|
151
|
-
const ASKS: Readonly<Record<string, readonly string[]>> = {
|
|
152
|
-
new: ['do', ...named(NEW)],
|
|
153
|
-
edit: ['do', 'mark', ...named(NEW)],
|
|
154
|
-
remove: ['do', 'mark'],
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const doing = verb ?? (await ask('do', 'new, edit or remove: '))
|
|
158
|
-
const allowed = ASKS[doing]
|
|
159
|
-
if (allowed === undefined) {
|
|
160
|
-
shut()
|
|
161
|
-
refuse(`there is nothing called "${doing}" to do here — say new, edit, remove, or read, which needs no receipt`)
|
|
162
|
-
}
|
|
163
|
-
if (piped) only(all, allowed, `\`${doing}\``)
|
|
164
|
-
|
|
165
|
-
if (doing === 'remove' || doing === 'edit') {
|
|
166
|
-
const which = words[1] ?? (piped ? needed(all, 'mark', `\`${doing}\``) : await ask('mark', 'mark: '))
|
|
167
|
-
const one = keys.find((key) => mark(key, written[key] ?? '') === which)
|
|
168
|
-
if (one === undefined) {
|
|
169
|
-
shut()
|
|
170
|
-
refuse(`\`${which}\` is not a mark this sheet issued — they are printed in brackets beside each message.`)
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (doing === 'remove') {
|
|
174
|
-
const where = used.get(one) ?? []
|
|
175
|
-
if (where.length > 0) {
|
|
176
|
-
shut()
|
|
177
|
-
refuse(
|
|
178
|
-
`\`${one}\` is named by ${where.slice(0, 6).join(', ')}` +
|
|
179
|
-
`${where.length > 6 ? ` and ${String(where.length - 6)} more` : ''}. ` +
|
|
180
|
-
'Removing it would leave those asking for wording that does not exist, which throws the moment ' +
|
|
181
|
-
'the screen renders. Take it out of the interface first.',
|
|
182
|
-
)
|
|
183
|
-
}
|
|
184
|
-
shut()
|
|
185
|
-
for (const language of spoken) {
|
|
186
|
-
const made = { ...(wording.get(language.id) ?? {}) }
|
|
187
|
-
delete made[one]
|
|
188
|
-
write(TREE.at, language.id, made)
|
|
189
|
-
}
|
|
190
|
-
stdout.write(`create: removed ${one} from ${String(spoken.length)} language file(s)\n`)
|
|
191
|
-
return 0
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const renamed = piped ? (answer(all, KEY.at) ?? '') : await ask(KEY.at, `key (${one}, blank to keep) `)
|
|
195
|
-
if (renamed !== '' && !DOTTED.test(renamed)) {
|
|
196
|
-
shut()
|
|
197
|
-
refuse(`\`${renamed}\` is not a key — lowercase words joined by dots.`)
|
|
198
|
-
}
|
|
199
|
-
if (renamed !== '' && written[renamed] !== undefined) {
|
|
200
|
-
shut()
|
|
201
|
-
refuse(`\`${renamed}\` is already a message. Two keys with one name is one key, and the other's wording is lost.`)
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const said = new Map<string, string>()
|
|
205
|
-
for (const language of spoken) {
|
|
206
|
-
const now = (wording.get(language.id) ?? {})[one] ?? ''
|
|
207
|
-
const next = piped
|
|
208
|
-
? (answer(all, language.id) ?? '')
|
|
209
|
-
: await ask(language.id, `${language.name} (${now || 'nothing yet'}) `)
|
|
210
|
-
said.set(language.id, next === '' ? now : next)
|
|
211
|
-
}
|
|
212
|
-
shut()
|
|
213
|
-
|
|
214
|
-
const name = renamed === '' ? one : renamed
|
|
215
|
-
for (const language of spoken) {
|
|
216
|
-
const made = { ...(wording.get(language.id) ?? {}) }
|
|
217
|
-
delete made[one]
|
|
218
|
-
made[name] = said.get(language.id) ?? ''
|
|
219
|
-
write(TREE.at, language.id, made)
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
let moved = 0
|
|
223
|
-
if (renamed !== '') {
|
|
224
|
-
for (const file of walk(TREE.at)) {
|
|
225
|
-
const text = readFileSync(file, 'utf8')
|
|
226
|
-
if (!keysIn(text, props).includes(one)) continue
|
|
227
|
-
writeFileSync(
|
|
228
|
-
file,
|
|
229
|
-
text.replace(
|
|
230
|
-
new RegExp(`(\\b(?:${props.join('|')})\\s*=\\s*")${one.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(")`, 'g'),
|
|
231
|
-
`$1${name}$2`,
|
|
232
|
-
),
|
|
233
|
-
'utf8',
|
|
234
|
-
)
|
|
235
|
-
moved += 1
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
stdout.write(
|
|
240
|
-
`create: ${one}${renamed === '' ? '' : ` is now ${name}`} — rewritten in ${String(spoken.length)} language file(s)` +
|
|
241
|
-
`${moved === 0 ? '' : ` and ${String(moved)} file(s) of the interface`}\n`,
|
|
242
|
-
)
|
|
243
|
-
const still = blankIn(said, spoken)
|
|
244
|
-
if (still.length > 0) {
|
|
245
|
-
stdout.write(`create: still nothing in ${still.join(', ')} — the gate refuses while that holds.\n`)
|
|
246
|
-
}
|
|
247
|
-
return 0
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const key = piped ? needed(all, KEY.at, '`new`') : await ask(KEY.at, prompted(KEY))
|
|
251
|
-
if (!DOTTED.test(key)) {
|
|
252
|
-
shut()
|
|
253
|
-
refuse(`\`${key}\` is not a key — lowercase words joined by dots, like \`checkout.confirm\`.`)
|
|
254
|
-
}
|
|
255
|
-
if (written[key] !== undefined) {
|
|
256
|
-
shut()
|
|
257
|
-
refuse(`\`${key}\` is already a message. Edit it by its mark, or choose another key.`)
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const said = new Map<string, string>()
|
|
261
|
-
for (const language of spoken) {
|
|
262
|
-
const next = piped
|
|
263
|
-
? (answer(all, language.id) ?? '')
|
|
264
|
-
: await ask(language.id, `what it says in ${language.name}: `)
|
|
265
|
-
if (next === '') {
|
|
266
|
-
shut()
|
|
267
|
-
refuse(
|
|
268
|
-
`\`${key}\` has nothing to say in ${language.name}. Every language is answered at the moment a key ` +
|
|
269
|
-
'is made, because a key that exists in one language and not another is a screen that comes out ' +
|
|
270
|
-
'half translated. There is no way past this on purpose.',
|
|
271
|
-
)
|
|
272
|
-
}
|
|
273
|
-
said.set(language.id, next)
|
|
274
|
-
}
|
|
275
|
-
shut()
|
|
276
|
-
|
|
277
|
-
for (const language of spoken) {
|
|
278
|
-
write(TREE.at, language.id, { ...(wording.get(language.id) ?? {}), [key]: said.get(language.id) ?? '' })
|
|
279
|
-
}
|
|
280
|
-
stdout.write(`create: wrote ${key} in ${spoken.map((one) => one.id).join(', ')}\n`)
|
|
281
|
-
stdout.write(`create: name it in the interface with one of: ${props.join(', ')} — for example \`label="${key}"\`.\n`)
|
|
282
|
-
return 0
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function blankIn(
|
|
286
|
-
said: ReadonlyMap<string, string>,
|
|
287
|
-
spoken: readonly { readonly id: string }[],
|
|
288
|
-
): readonly string[] {
|
|
289
|
-
return spoken.filter((one) => (said.get(one.id) ?? '') === '').map((one) => one.id)
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if (argv[1] !== undefined) exit(await run())
|