@prestack/thread-ui 0.0.2 → 0.1.0

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