@datamoc/mw_games 0.15.0 → 0.17.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.
- package/CHANGELOG.md +154 -1
- package/README.md +21 -1
- package/dist/ai/lua.d.ts.map +1 -1
- package/dist/ai/lua.js +1 -0
- package/dist/ai/lua.js.map +1 -1
- package/dist/core/ActionJournal.d.ts +5 -0
- package/dist/core/ActionJournal.d.ts.map +1 -1
- package/dist/core/ActionJournal.js +12 -1
- package/dist/core/ActionJournal.js.map +1 -1
- package/dist/core/Clone.d.ts +31 -0
- package/dist/core/Clone.d.ts.map +1 -0
- package/dist/core/Clone.js +84 -0
- package/dist/core/Clone.js.map +1 -0
- package/dist/core/Collection.d.ts +1 -0
- package/dist/core/Collection.d.ts.map +1 -1
- package/dist/core/Collection.js +6 -2
- package/dist/core/Collection.js.map +1 -1
- package/dist/core/HttpTransport.d.ts +17 -0
- package/dist/core/HttpTransport.d.ts.map +1 -1
- package/dist/core/HttpTransport.js +61 -0
- package/dist/core/HttpTransport.js.map +1 -1
- package/dist/core/Multiplayer.d.ts +9 -0
- package/dist/core/Multiplayer.d.ts.map +1 -1
- package/dist/core/Multiplayer.js +48 -4
- package/dist/core/Multiplayer.js.map +1 -1
- package/dist/core/News.d.ts.map +1 -1
- package/dist/core/News.js +1 -2
- package/dist/core/News.js.map +1 -1
- package/dist/core/Sanitize.d.ts +19 -0
- package/dist/core/Sanitize.d.ts.map +1 -1
- package/dist/core/Sanitize.js +37 -0
- package/dist/core/Sanitize.js.map +1 -1
- package/dist/core/Save.d.ts +18 -9
- package/dist/core/Save.d.ts.map +1 -1
- package/dist/core/Save.js +37 -21
- package/dist/core/Save.js.map +1 -1
- package/dist/core/SaveSync.js +4 -4
- package/dist/core/SaveSync.js.map +1 -1
- package/dist/core/Settings.d.ts.map +1 -1
- package/dist/core/Settings.js +2 -1
- package/dist/core/Settings.js.map +1 -1
- package/dist/core/State.d.ts.map +1 -1
- package/dist/core/State.js +8 -7
- package/dist/core/State.js.map +1 -1
- package/dist/core/StoredValue.d.ts +1 -1
- package/dist/core/StoredValue.d.ts.map +1 -1
- package/dist/core/StoredValue.js +3 -2
- package/dist/core/StoredValue.js.map +1 -1
- package/dist/core/Telemetry.d.ts +19 -1
- package/dist/core/Telemetry.d.ts.map +1 -1
- package/dist/core/Telemetry.js +26 -1
- package/dist/core/Telemetry.js.map +1 -1
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +2 -1
- package/dist/core/index.js.map +1 -1
- package/dist/mw_games.global.js +107 -107
- package/dist/mwl/MapFile.js +11 -1
- package/dist/mwl/MapFile.js.map +1 -1
- package/dist/mwl/expression.js +38 -5
- package/dist/mwl/expression.js.map +1 -1
- package/dist/mwl/fengari.d.ts +8 -0
- package/dist/mwl/fengari.d.ts.map +1 -1
- package/dist/mwl/fengari.js +72 -2
- package/dist/mwl/fengari.js.map +1 -1
- package/dist/mwl/persistence.d.ts.map +1 -1
- package/dist/mwl/persistence.js +2 -1
- package/dist/mwl/persistence.js.map +1 -1
- package/dist/mwl/runtime.d.ts.map +1 -1
- package/dist/mwl/runtime.js +6 -1
- package/dist/mwl/runtime.js.map +1 -1
- package/dist/rpg/Marshal.d.ts.map +1 -1
- package/dist/rpg/Marshal.js +13 -0
- package/dist/rpg/Marshal.js.map +1 -1
- package/dist/simulation/Campaign.d.ts.map +1 -1
- package/dist/simulation/Campaign.js +9 -5
- package/dist/simulation/Campaign.js.map +1 -1
- package/dist/simulation/Harness.d.ts.map +1 -1
- package/dist/simulation/Harness.js +2 -1
- package/dist/simulation/Harness.js.map +1 -1
- package/dist/simulation/Runtime.d.ts +10 -0
- package/dist/simulation/Runtime.d.ts.map +1 -1
- package/dist/simulation/Runtime.js +25 -4
- package/dist/simulation/Runtime.js.map +1 -1
- package/dist/testing/index.d.ts +105 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +134 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/two-d/render/RpgmAutotile.d.ts +217 -0
- package/dist/two-d/render/RpgmAutotile.d.ts.map +1 -1
- package/dist/two-d/render/RpgmAutotile.js +756 -0
- package/dist/two-d/render/RpgmAutotile.js.map +1 -1
- package/dist/two-d/render/TileMap.d.ts +90 -0
- package/dist/two-d/render/TileMap.d.ts.map +1 -1
- package/dist/two-d/render/TileMap.js +319 -31
- package/dist/two-d/render/TileMap.js.map +1 -1
- package/dist/two-d/render/index.d.ts +3 -3
- package/dist/two-d/render/index.d.ts.map +1 -1
- package/dist/two-d/render/index.js +1 -1
- package/dist/two-d/render/index.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +90 -25
- package/sbom.cdx.json +5589 -0
- package/tools/benchmark-browser.d.mts +33 -0
- package/tools/benchmark-browser.mjs +163 -0
- package/tools/benchmark-history.d.mts +8 -0
- package/tools/benchmark-history.mjs +31 -0
- package/tools/browser-smoke.d.mts +30 -0
- package/tools/browser-smoke.mjs +167 -0
- package/tools/bundle-size.d.mts +11 -0
- package/tools/bundle-size.mjs +138 -0
- package/tools/csp.d.mts +7 -0
- package/tools/csp.mjs +56 -0
- package/tools/emit-page.d.mts +44 -0
- package/tools/emit-page.mjs +188 -0
- package/tools/extract-rgssad.d.mts +5 -0
- package/tools/extract-rgssad.mjs +64 -0
- package/tools/find-chrome.d.mts +7 -0
- package/tools/find-chrome.mjs +104 -0
- package/tools/i18n-edit.d.mts +29 -0
- package/tools/i18n-edit.mjs +748 -0
- package/tools/make-example-assets.d.mts +2 -0
- package/tools/make-example-assets.mjs +727 -0
- package/tools/multiplayer-server.d.mts +27 -0
- package/tools/multiplayer-server.mjs +195 -0
- package/tools/mwl.mjs +1 -0
- package/tools/sbom.d.mts +83 -0
- package/tools/sbom.mjs +330 -0
- package/tools/vendored/rgssad-wasm/LICENSE-MIT +21 -0
- package/tools/vendored/rgssad-wasm/README.md +15 -0
- package/tools/vendored/rgssad-wasm/package.json +17 -0
- package/tools/vendored/rgssad-wasm/rgssad_wasm.d.ts +41 -0
- package/tools/vendored/rgssad-wasm/rgssad_wasm.js +6 -0
- package/tools/vendored/rgssad-wasm/rgssad_wasm_bg.js +399 -0
- package/tools/vendored/rgssad-wasm/rgssad_wasm_bg.wasm +0 -0
- package/tools/vendored/rgssad-wasm/rgssad_wasm_bg.wasm.d.ts +17 -0
- package/tools/vite.d.mts +17 -0
- package/tools/vite.mjs +83 -0
|
@@ -0,0 +1,748 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { execFile } from 'node:child_process';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
7
|
+
import { basename, dirname, resolve } from 'node:path';
|
|
8
|
+
import readline from 'node:readline';
|
|
9
|
+
|
|
10
|
+
//the framework's own modules: the TypeScript sources inside this repository, the built `dist`
|
|
11
|
+
//inside an installed package, which ships no `src`
|
|
12
|
+
const fromSource = existsSync(new URL('../src/i18n/index.ts', import.meta.url));
|
|
13
|
+
const {
|
|
14
|
+
parseFTL,
|
|
15
|
+
createEditSession,
|
|
16
|
+
cueKeyFor,
|
|
17
|
+
diffPlaceholders,
|
|
18
|
+
isAudioKey,
|
|
19
|
+
sessionRows,
|
|
20
|
+
sessionCompleteness,
|
|
21
|
+
setTargetText,
|
|
22
|
+
setTargetSound,
|
|
23
|
+
copyFromBase,
|
|
24
|
+
deleteTargetKey,
|
|
25
|
+
swapSession,
|
|
26
|
+
tokenizeMessage,
|
|
27
|
+
validateCatalog,
|
|
28
|
+
validateMessageAudio,
|
|
29
|
+
messageText,
|
|
30
|
+
} = await import(fromSource ? '../src/i18n/index.ts' : '../dist/i18n/index.js');
|
|
31
|
+
const { parseMarkdown } = await import(fromSource ? '../src/two-d/ui/markdown.ts' : '../dist/two-d/ui/markdown.js');
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A split-screen translation editor for `mwg/i18n` catalogs, in the shape of a small
|
|
35
|
+
* terminal editor (ne-style): the reference language on the left, the translation being
|
|
36
|
+
* worked on on the right. Either file may be any language - the "main" side is whichever
|
|
37
|
+
* path is passed first, and `x` swaps the two at any time.
|
|
38
|
+
*
|
|
39
|
+
* A sound cue is an ordinary `<key>.audio` entry holding a sound path (read by the
|
|
40
|
+
* semantic formatter's `audio` channel, played through `mwg/audio`'s `Sound`), so
|
|
41
|
+
* associating one with a string is just setting one more key (`s`), never translating a
|
|
42
|
+
* file path.
|
|
43
|
+
*
|
|
44
|
+
* Catalog files are auto-detected: JSON (`{ locale, direction, messages }`) or FTL
|
|
45
|
+
* (`key = value` lines, parsed by `parseFTL`). Saving follows the target path's own
|
|
46
|
+
* extension; an FTL target flattens plural/select entries to plain text with a warning,
|
|
47
|
+
* so JSON targets are recommended wherever plurals matter.
|
|
48
|
+
*
|
|
49
|
+
* usage:
|
|
50
|
+
* node tools/i18n-edit.mjs <base> <target> [--create] [--assets <dir>]
|
|
51
|
+
* node tools/i18n-edit.mjs <base> <target> --check [--assets <dir>]
|
|
52
|
+
*
|
|
53
|
+
* The panes render markdown (`**bold**` as bold, `*italic*` as italic) and flag
|
|
54
|
+
* placeholder mismatches (`{dmg:03d}` dropped or reshaped in the translation) - the
|
|
55
|
+
* same `parseMarkdown`/`diffPlaceholders` the runtime uses, so the editor can never
|
|
56
|
+
* disagree with the game about either.
|
|
57
|
+
*
|
|
58
|
+
* `Ctrl+P` previews the current row's cue through the operating system's own player
|
|
59
|
+
* (`afplay`, `aplay`/`paplay`/`ffplay`, Windows WAV via SoundPlayer), since a
|
|
60
|
+
* dependency-free tool cannot bundle audio playback of its own; `MWG_SFX_PLAYER` names
|
|
61
|
+
* a replacement command when none of those fit.
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
const ANSI = {
|
|
65
|
+
reset: '\x1b[0m',
|
|
66
|
+
reverse: '\x1b[7m',
|
|
67
|
+
dim: '\x1b[2m',
|
|
68
|
+
red: '\x1b[31m',
|
|
69
|
+
green: '\x1b[32m',
|
|
70
|
+
yellow: '\x1b[33m',
|
|
71
|
+
cyan: '\x1b[36m',
|
|
72
|
+
altOn: '\x1b[?1049h',
|
|
73
|
+
altOff: '\x1b[?1049l',
|
|
74
|
+
hideCursor: '\x1b[?25l',
|
|
75
|
+
showCursor: '\x1b[?25h',
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
function usageError(message) {
|
|
79
|
+
console.error(`i18n-edit: ${message}`);
|
|
80
|
+
console.error('usage: node tools/i18n-edit.mjs <base> <target> [--create] [--check] [--assets <dir>]');
|
|
81
|
+
process.exit(2);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function parseArgs(argv) {
|
|
85
|
+
const positional = [];
|
|
86
|
+
let create = false;
|
|
87
|
+
let check = false;
|
|
88
|
+
let assetsDir = null;
|
|
89
|
+
for (let i = 0; i < argv.length; i++) {
|
|
90
|
+
const arg = argv[i];
|
|
91
|
+
if (arg === '--create') create = true;
|
|
92
|
+
else if (arg === '--check') check = true;
|
|
93
|
+
else if (arg === '--assets') {
|
|
94
|
+
assetsDir = argv[++i];
|
|
95
|
+
if (!assetsDir) usageError('--assets needs a directory');
|
|
96
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
97
|
+
console.log(
|
|
98
|
+
'Split-screen translation editor for mwg/i18n catalogs.\n\n' +
|
|
99
|
+
' node tools/i18n-edit.mjs <base> <target> [--create] [--assets <dir>]\n' +
|
|
100
|
+
' node tools/i18n-edit.mjs <base> <target> --check [--assets <dir>]\n\n' +
|
|
101
|
+
'Keys: j/k or arrows move, Enter/e edit, s sound cue, Ctrl+P preview cue,\n' +
|
|
102
|
+
' r copy from base, x swap sides, m missing-only, / search,\n' +
|
|
103
|
+
' v markdown preview, w save, q quit, ? help.',
|
|
104
|
+
);
|
|
105
|
+
process.exit(0);
|
|
106
|
+
} else if (arg.startsWith('-')) {
|
|
107
|
+
usageError(`unknown option ${arg}`);
|
|
108
|
+
} else {
|
|
109
|
+
positional.push(arg);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (positional.length !== 2) usageError('needs exactly a <base> and a <target> file');
|
|
113
|
+
return { basePath: positional[0], targetPath: positional[1], create, check, assetsDir };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function guessLocale(path) {
|
|
117
|
+
const name = basename(path).replace(/\.(json|ftl)$/i, '');
|
|
118
|
+
return name || 'und';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function guessDirection(locale) {
|
|
122
|
+
return /^(ar|he|fa|ur)(?:-|$)/i.test(locale) ? 'rtl' : 'ltr';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function isProbablyFTL(text) {
|
|
126
|
+
return /^[A-Za-z0-9_-]+\s*=/m.test(text);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** reads one catalog file, detecting JSON vs FTL; returns { catalog, format, typography } */
|
|
130
|
+
async function loadCatalogFile(path) {
|
|
131
|
+
const text = await readFile(path, 'utf8');
|
|
132
|
+
if (!path.toLowerCase().endsWith('.ftl')) {
|
|
133
|
+
try {
|
|
134
|
+
const data = JSON.parse(text);
|
|
135
|
+
if (data && typeof data === 'object' && data.messages && typeof data.messages === 'object') {
|
|
136
|
+
return {
|
|
137
|
+
catalog: {
|
|
138
|
+
locale: typeof data.locale === 'string' ? data.locale : guessLocale(path),
|
|
139
|
+
direction: data.direction === 'rtl' ? 'rtl' : 'ltr',
|
|
140
|
+
messages: data.messages,
|
|
141
|
+
...(data.typography === false ? { typography: false } : {}),
|
|
142
|
+
},
|
|
143
|
+
format: 'json',
|
|
144
|
+
typography: data.typography,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
} catch {
|
|
148
|
+
// not JSON - fall through to the FTL attempt below
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (path.toLowerCase().endsWith('.ftl') || isProbablyFTL(text)) {
|
|
152
|
+
return { catalog: parseFTL(guessLocale(path), text), format: 'ftl', typography: undefined };
|
|
153
|
+
}
|
|
154
|
+
throw new Error(`${path}: neither a JSON catalog ({ locale, direction, messages }) nor FTL`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function toJSONValue(value, flattened) {
|
|
158
|
+
if (typeof value === 'string') return value;
|
|
159
|
+
if (value !== null && typeof value === 'object' && 'format' in value) {
|
|
160
|
+
flattened.push(true);
|
|
161
|
+
return messageText(value);
|
|
162
|
+
}
|
|
163
|
+
return value;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function serializeTarget(targetPath, catalog) {
|
|
167
|
+
if (targetPath.toLowerCase().endsWith('.ftl')) {
|
|
168
|
+
const lines = [
|
|
169
|
+
`# generated by mwg/tools/i18n-edit, do not edit by hand unless flattened plurals are acceptable`,
|
|
170
|
+
];
|
|
171
|
+
let flattened = 0;
|
|
172
|
+
for (const key of Object.keys(catalog.messages).sort()) {
|
|
173
|
+
const value = catalog.messages[key];
|
|
174
|
+
if (typeof value === 'string') {
|
|
175
|
+
lines.push(`${key} = ${value}`);
|
|
176
|
+
} else {
|
|
177
|
+
flattened++;
|
|
178
|
+
lines.push(`${key} = ${messageText(value)}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return { text: lines.join('\n') + '\n', flattened };
|
|
182
|
+
}
|
|
183
|
+
const flattened = [];
|
|
184
|
+
const messages = {};
|
|
185
|
+
for (const key of Object.keys(catalog.messages).sort()) {
|
|
186
|
+
messages[key] = toJSONValue(catalog.messages[key], flattened);
|
|
187
|
+
}
|
|
188
|
+
const data = { locale: catalog.locale, direction: catalog.direction, messages };
|
|
189
|
+
if (catalog.typography === false) data.typography = false;
|
|
190
|
+
return { text: JSON.stringify(data, null, 2) + '\n', flattened: flattened.length };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** non-interactive report for CI; returns an exit code */
|
|
194
|
+
function runCheck(session, basePath, targetPath, assetsDir) {
|
|
195
|
+
const rows = sessionRows(session);
|
|
196
|
+
const missing = rows.filter((row) => row.status === 'missing').map((row) => row.key);
|
|
197
|
+
const extra = rows.filter((row) => row.status === 'extra').map((row) => row.key);
|
|
198
|
+
//audio entries are reported by validateMessageAudio below, not here
|
|
199
|
+
const issues = validateCatalog(session.target).filter((issue) => !isAudioKey(issue.key));
|
|
200
|
+
const audioIssues = [...validateMessageAudio(session.base), ...validateMessageAudio(session.target)];
|
|
201
|
+
const completeness = sessionCompleteness(session);
|
|
202
|
+
const root = assetsDir ? resolve(assetsDir) : process.cwd();
|
|
203
|
+
const missingFiles = [];
|
|
204
|
+
const placeholderProblems = [];
|
|
205
|
+
for (const row of rows) {
|
|
206
|
+
if (row.sound && row.soundKey && !isRemotePath(row.sound) && !existsSync(resolve(root, row.sound))) {
|
|
207
|
+
missingFiles.push(`${row.soundKey} -> ${row.sound}`);
|
|
208
|
+
}
|
|
209
|
+
const summary = placeholderSummary(row.baseText, row.targetText);
|
|
210
|
+
if (summary) placeholderProblems.push(`${row.key}: ${summary}`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
console.log(`base: ${basePath} (${session.base.locale})`);
|
|
214
|
+
console.log(`target: ${targetPath} (${session.target.locale})`);
|
|
215
|
+
console.log(`keys: ${rows.length}, completeness: ${(completeness * 100).toFixed(1)}%`);
|
|
216
|
+
for (const key of missing) console.log(`missing: ${key}`);
|
|
217
|
+
for (const key of extra) console.log(`extra: ${key}`);
|
|
218
|
+
for (const issue of issues) console.log(`issue: ${issue.key} [${issue.kind}] ${issue.detail}`);
|
|
219
|
+
for (const issue of audioIssues) console.log(`audio: ${issue.key} [${issue.kind}] ${issue.detail}`);
|
|
220
|
+
for (const entry of placeholderProblems) console.log(`placeholders: ${entry}`);
|
|
221
|
+
for (const entry of missingFiles) console.log(`cue file not found: ${entry}`);
|
|
222
|
+
|
|
223
|
+
const failed = missing.length > 0 || issues.length > 0 || audioIssues.length > 0 || placeholderProblems.length > 0;
|
|
224
|
+
return failed ? 1 : 0;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function isRemotePath(path) {
|
|
228
|
+
return /^(data:|https?:)/i.test(path);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ---------------------------------------------------------------- TUI
|
|
232
|
+
|
|
233
|
+
function truncate(text, width) {
|
|
234
|
+
const clean = text.replace(/\s+/g, ' ');
|
|
235
|
+
if (clean.length <= width) return clean;
|
|
236
|
+
return clean.slice(0, Math.max(0, width - 1)) + '…';
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const ANSI_BOLD = '\x1b[1m';
|
|
240
|
+
const ANSI_BOLD_OFF = '\x1b[22m';
|
|
241
|
+
const ANSI_ITALIC = '\x1b[3m';
|
|
242
|
+
const ANSI_ITALIC_OFF = '\x1b[23m';
|
|
243
|
+
|
|
244
|
+
/** styled spans to terminal escapes: bold as bold, italic as italic, markers consumed */
|
|
245
|
+
export function spansToAnsi(spans) {
|
|
246
|
+
return spans
|
|
247
|
+
.map((span) => {
|
|
248
|
+
let text = span.text;
|
|
249
|
+
if (span.italic) text = `${ANSI_ITALIC}${text}${ANSI_ITALIC_OFF}`;
|
|
250
|
+
if (span.bold) text = `${ANSI_BOLD}${text}${ANSI_BOLD_OFF}`;
|
|
251
|
+
return text;
|
|
252
|
+
})
|
|
253
|
+
.join('');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function visibleLength(ansi) {
|
|
257
|
+
return ansi.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Cuts styled text to a visible width, counting characters on screen rather than escape
|
|
262
|
+
* codes. A cut inside a styled run closes bold/italic afterwards, so no styling leaks
|
|
263
|
+
* into the next column (the closers touch only those two attributes, never the row's
|
|
264
|
+
* own reverse-video highlight).
|
|
265
|
+
*/
|
|
266
|
+
export function truncateAnsi(ansi, width) {
|
|
267
|
+
let out = '';
|
|
268
|
+
let visible = 0;
|
|
269
|
+
let cut = false;
|
|
270
|
+
const token = /(\x1b\[[0-9;]*m)|([^\x1b]+)/g;
|
|
271
|
+
let match;
|
|
272
|
+
while ((match = token.exec(ansi)) !== null && visible < width) {
|
|
273
|
+
if (match[1]) {
|
|
274
|
+
out += match[1];
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
const take = Math.min(match[2].length, width - visible);
|
|
278
|
+
out += match[2].slice(0, take);
|
|
279
|
+
visible += take;
|
|
280
|
+
if (take < match[2].length) cut = true;
|
|
281
|
+
}
|
|
282
|
+
if (cut) out += `${ANSI_BOLD_OFF}${ANSI_ITALIC_OFF}`;
|
|
283
|
+
return out;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function padAnsi(ansi, width) {
|
|
287
|
+
const missing = width - visibleLength(ansi);
|
|
288
|
+
return missing > 0 ? ansi + ' '.repeat(missing) : ansi;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** one-line summary of a row's placeholder drift, or '' when base and target agree */
|
|
292
|
+
export function placeholderSummary(baseText, targetText) {
|
|
293
|
+
if (!baseText || !targetText) return '';
|
|
294
|
+
const diff = diffPlaceholders(baseText, targetText);
|
|
295
|
+
const parts = [];
|
|
296
|
+
for (const token of diff.missing) parts.push(`missing {${token}}`);
|
|
297
|
+
for (const token of diff.extra) parts.push(`extra {${token}}`);
|
|
298
|
+
for (const { token, base, target } of diff.changed) {
|
|
299
|
+
parts.push(`{${token}}: '${base || '(plain)'}' vs '${target || '(plain)'}'`);
|
|
300
|
+
}
|
|
301
|
+
return parts.join(', ');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** placeholder tokens a translation must supply, for the edit prompt */
|
|
305
|
+
export function expectedPlaceholders(baseText) {
|
|
306
|
+
return tokenizeMessage(baseText)
|
|
307
|
+
.filter((part) => typeof part !== 'string')
|
|
308
|
+
.map(
|
|
309
|
+
(part) =>
|
|
310
|
+
`{${part.token}${part.debug ? '=' : ''}${part.conv ? `!${part.conv}` : ''}${part.spec === undefined ? '' : `:${part.spec}`}}`,
|
|
311
|
+
)
|
|
312
|
+
.join(' ');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const execFileAsync = promisify(execFile);
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* How to play one cue file, ordered by preference. A dependency-free tool cannot bundle
|
|
319
|
+
* audio playback, so it delegates to whatever the OS already has: `afplay` on macOS,
|
|
320
|
+
* `SoundPlayer` (WAV only) on Windows, and format-capable `aplay`/`paplay` before the
|
|
321
|
+
* general `ffplay` on everything else. `MWG_SFX_PLAYER` replaces the whole list with one
|
|
322
|
+
* command (the cue path is appended) when none of those fit. `platform` is injectable
|
|
323
|
+
* so the selection itself is unit-testable without spawning anything.
|
|
324
|
+
*/
|
|
325
|
+
export function playerCandidates(cuePath, platform = process.platform) {
|
|
326
|
+
if (process.env.MWG_SFX_PLAYER) return [{ cmd: process.env.MWG_SFX_PLAYER, args: [cuePath], shell: true }];
|
|
327
|
+
if (platform === 'darwin') return [{ cmd: 'afplay', args: [cuePath] }];
|
|
328
|
+
if (platform === 'win32') {
|
|
329
|
+
return /\.wav$/i.test(cuePath)
|
|
330
|
+
? [
|
|
331
|
+
{
|
|
332
|
+
cmd: 'powershell',
|
|
333
|
+
args: [
|
|
334
|
+
'-NoProfile',
|
|
335
|
+
'-Command',
|
|
336
|
+
`(New-Object Media.SoundPlayer '${cuePath.replace(/'/g, "''")}').PlaySync()`,
|
|
337
|
+
],
|
|
338
|
+
},
|
|
339
|
+
]
|
|
340
|
+
: [];
|
|
341
|
+
}
|
|
342
|
+
const ffplay = { cmd: 'ffplay', args: ['-nodisp', '-autoexit', '-loglevel', 'quiet', cuePath] };
|
|
343
|
+
if (/\.wav$/i.test(cuePath)) return [{ cmd: 'aplay', args: [cuePath] }, { cmd: 'paplay', args: [cuePath] }, ffplay];
|
|
344
|
+
if (/\.mp3$/i.test(cuePath)) return [ffplay, { cmd: 'mpg123', args: ['-q', cuePath] }];
|
|
345
|
+
return [{ cmd: 'paplay', args: [cuePath] }, ffplay];
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Runs the first candidate that exists, falling through only when the command itself
|
|
350
|
+
* is missing (`ENOENT`) - a player that runs and rejects the file reports its own
|
|
351
|
+
* error instead. Resolves with null on success, so the caller reports one outcome.
|
|
352
|
+
*/
|
|
353
|
+
async function playFirstAvailable(candidates) {
|
|
354
|
+
let missing = null;
|
|
355
|
+
for (const { cmd, args, shell } of candidates) {
|
|
356
|
+
try {
|
|
357
|
+
await execFileAsync(cmd, args, shell ? { shell: true } : {});
|
|
358
|
+
return null;
|
|
359
|
+
} catch (error) {
|
|
360
|
+
if (error?.code === 'ENOENT') {
|
|
361
|
+
missing = error;
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
return error;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return missing ?? new Error('no audio player found (set MWG_SFX_PLAYER to name one)');
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
371
|
+
const { basePath, targetPath, create, check, assetsDir } = parseArgs(argv);
|
|
372
|
+
|
|
373
|
+
let baseFile;
|
|
374
|
+
try {
|
|
375
|
+
baseFile = await loadCatalogFile(basePath);
|
|
376
|
+
} catch (error) {
|
|
377
|
+
console.error(`i18n-edit: ${error.message}`);
|
|
378
|
+
process.exit(1);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
let targetFile = null;
|
|
382
|
+
try {
|
|
383
|
+
targetFile = await loadCatalogFile(targetPath);
|
|
384
|
+
} catch (error) {
|
|
385
|
+
if (!create || !/ENOENT/.test(error.message)) {
|
|
386
|
+
console.error(`i18n-edit: ${error.message}${!create ? ' (pass --create to start a new target)' : ''}`);
|
|
387
|
+
process.exit(1);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
let session = targetFile
|
|
392
|
+
? createEditSession(baseFile.catalog, targetFile.catalog)
|
|
393
|
+
: createEditSession(baseFile.catalog, {
|
|
394
|
+
locale: guessLocale(targetPath),
|
|
395
|
+
direction: guessDirection(guessLocale(targetPath)),
|
|
396
|
+
messages: {},
|
|
397
|
+
});
|
|
398
|
+
const targetIsNew = !targetFile;
|
|
399
|
+
|
|
400
|
+
if (check) {
|
|
401
|
+
process.exit(runCheck(session, basePath, targetPath, assetsDir));
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
405
|
+
console.error('i18n-edit: the editor needs a terminal; use --check for piped output');
|
|
406
|
+
process.exit(2);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const editor = createEditor({ session, basePath, targetPath, targetIsNew, assetsDir });
|
|
410
|
+
await editor.run();
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function createEditor({ session, basePath, targetPath, targetIsNew, assetsDir }) {
|
|
414
|
+
let index = 0;
|
|
415
|
+
let scroll = 0;
|
|
416
|
+
let missingOnly = false;
|
|
417
|
+
let query = '';
|
|
418
|
+
let dirty = targetIsNew;
|
|
419
|
+
let status = '';
|
|
420
|
+
let showHelp = false;
|
|
421
|
+
let quit = false;
|
|
422
|
+
let prompting = false;
|
|
423
|
+
let renderMd = true;
|
|
424
|
+
|
|
425
|
+
const rows = () => sessionRows(session, { missingOnly, query });
|
|
426
|
+
|
|
427
|
+
function renderCell(raw, width) {
|
|
428
|
+
const flat = raw.replace(/\s+/g, ' ');
|
|
429
|
+
if (!renderMd) return truncate(flat, width).padEnd(width);
|
|
430
|
+
return padAnsi(truncateAnsi(spansToAnsi(parseMarkdown(flat)), width), width);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function clamp() {
|
|
434
|
+
const list = rows();
|
|
435
|
+
index = Math.max(0, Math.min(index, Math.max(0, list.length - 1)));
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function draw() {
|
|
439
|
+
const width = process.stdout.columns || 100;
|
|
440
|
+
const height = process.stdout.rows || 30;
|
|
441
|
+
const list = rows();
|
|
442
|
+
clamp();
|
|
443
|
+
const bodyHeight = height - 5;
|
|
444
|
+
if (index < scroll) scroll = index;
|
|
445
|
+
if (index >= scroll + bodyHeight) scroll = index - bodyHeight + 1;
|
|
446
|
+
|
|
447
|
+
const completeness = (sessionCompleteness(session) * 100).toFixed(1);
|
|
448
|
+
const keyW = Math.min(28, Math.max(12, Math.floor(width * 0.22)));
|
|
449
|
+
const rest = width - keyW - 4;
|
|
450
|
+
const baseW = Math.floor(rest / 2);
|
|
451
|
+
const targetW = rest - baseW;
|
|
452
|
+
|
|
453
|
+
let out = ANSI.hideCursor;
|
|
454
|
+
out += `${ANSI.cyan}${truncate(`${basename(basePath)} (${session.base.locale})`, keyW + baseW)}${ANSI.reset} `;
|
|
455
|
+
out += `${ANSI.green}${truncate(`-> ${basename(targetPath)} (${session.target.locale}) ${completeness}%${dirty ? ' [+]' : ''}`, targetW)}${ANSI.reset}\n`;
|
|
456
|
+
out += `${ANSI.dim}${'key'.padEnd(keyW)} ${'base'.padEnd(baseW)} target${missingOnly ? ' [missing-only]' : ''}${query ? ` [/${query}]` : ''}${ANSI.reset}\n`;
|
|
457
|
+
|
|
458
|
+
for (let i = scroll; i < Math.min(list.length, scroll + bodyHeight); i++) {
|
|
459
|
+
const row = list[i];
|
|
460
|
+
const marker = row.status === 'missing' ? ANSI.red + '!' : row.status === 'extra' ? ANSI.yellow + '+' : ' ';
|
|
461
|
+
const soundMark = row.sound ? (row.soundInherited ? `${ANSI.dim}~` : `${ANSI.cyan}*`) : ' ';
|
|
462
|
+
const phMark = placeholderSummary(row.baseText, row.targetText) ? `${ANSI.yellow}≠` : ' ';
|
|
463
|
+
const targetCell = row.targetText === '' ? `${ANSI.dim}<missing>` : renderCell(row.targetText, targetW);
|
|
464
|
+
const line =
|
|
465
|
+
`${marker}${soundMark}${phMark}${ANSI.reset} ` +
|
|
466
|
+
`${truncate(row.key, keyW - 4).padEnd(keyW - 4)} ` +
|
|
467
|
+
`${renderCell(row.baseText, baseW)} ` +
|
|
468
|
+
`${targetCell}`;
|
|
469
|
+
out += (i === index ? ANSI.reverse + line + ANSI.reset : line) + '\n';
|
|
470
|
+
}
|
|
471
|
+
const selected = list[index];
|
|
472
|
+
const soundLine = selected?.sound
|
|
473
|
+
? `cue ${selected.soundKey}: ${selected.sound}${selected.soundInherited ? ' (from base)' : ''}`
|
|
474
|
+
: 'no cue (s to set)';
|
|
475
|
+
const phLine = selected ? placeholderSummary(selected.baseText, selected.targetText) : '';
|
|
476
|
+
const infoLine = status || (phLine ? `placeholders differ: ${phLine}` : soundLine);
|
|
477
|
+
out += `\n${ANSI.dim}${truncate(infoLine, width)}${ANSI.reset}\n`;
|
|
478
|
+
out += `${ANSI.dim}j/k move Enter edit s cue r copy x swap m missing / find v render w save q quit ? help${ANSI.reset}`;
|
|
479
|
+
|
|
480
|
+
if (showHelp) {
|
|
481
|
+
out += `\n${ANSI.reverse} ${'i18n-edit help (any key closes)'.padEnd(width - 2)} ${ANSI.reset}\n`;
|
|
482
|
+
for (const line of [
|
|
483
|
+
'j/k, Up/Down move between keys g/G first/last key',
|
|
484
|
+
'Enter/e edit target text s set/clear <key>.audio cue',
|
|
485
|
+
'Ctrl+P preview the row cue r copy base text over',
|
|
486
|
+
'd delete a target-only key',
|
|
487
|
+
'x swap reference/target v toggle markdown preview',
|
|
488
|
+
'm missing-only filter / search, Esc clears',
|
|
489
|
+
'w save target file q quit (asks when unsaved)',
|
|
490
|
+
'* means the key has its own cue, ~ means it inherits the base cue,',
|
|
491
|
+
'≠ marks a placeholder mismatch against the reference text.',
|
|
492
|
+
]) {
|
|
493
|
+
out += truncate(line, width) + '\n';
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
process.stdout.write('\x1b[H\x1b[2J' + out);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function prompt(question, initial = '') {
|
|
501
|
+
prompting = true;
|
|
502
|
+
return new Promise((resolvePromise) => {
|
|
503
|
+
process.stdin.setRawMode(false);
|
|
504
|
+
process.stdout.write(`${ANSI.showCursor}\n${question}`);
|
|
505
|
+
if (initial) process.stdout.write(`${ANSI.dim}[${truncate(initial, 60)}]${ANSI.reset}`);
|
|
506
|
+
process.stdout.write('\n> ');
|
|
507
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
508
|
+
rl.question('', (answer) => {
|
|
509
|
+
rl.close();
|
|
510
|
+
process.stdin.setRawMode(true);
|
|
511
|
+
prompting = false;
|
|
512
|
+
resolvePromise(answer);
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
async function save() {
|
|
518
|
+
const { text, flattened } = serializeTarget(targetPath, session.target);
|
|
519
|
+
await writeFile(targetPath, text, 'utf8');
|
|
520
|
+
dirty = false;
|
|
521
|
+
status =
|
|
522
|
+
flattened > 0
|
|
523
|
+
? `saved ${targetPath} (warning: ${flattened} select/plural flattened)`
|
|
524
|
+
: `saved ${targetPath}`;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function checkCueFile(path) {
|
|
528
|
+
if (path === '' || isRemotePath(path)) return '';
|
|
529
|
+
const root = assetsDir ? resolve(assetsDir) : process.cwd();
|
|
530
|
+
return existsSync(resolve(root, path)) ? '' : ` (file not found under ${root})`;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function previewCue(displayName, absolutePath) {
|
|
534
|
+
status = `playing ${displayName} ...`;
|
|
535
|
+
playFirstAvailable(playerCandidates(absolutePath)).then((error) => {
|
|
536
|
+
if (quit) return;
|
|
537
|
+
status = error
|
|
538
|
+
? `could not play: ${String(error?.message ?? error).split('\n')[0]}`
|
|
539
|
+
: `played ${displayName}`;
|
|
540
|
+
draw();
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
async function run() {
|
|
545
|
+
process.stdout.write(ANSI.altOn);
|
|
546
|
+
const restore = () => {
|
|
547
|
+
process.stdout.write(ANSI.altOff + ANSI.showCursor);
|
|
548
|
+
};
|
|
549
|
+
process.on('SIGINT', () => {
|
|
550
|
+
restore();
|
|
551
|
+
process.exit(130);
|
|
552
|
+
});
|
|
553
|
+
readline.emitKeypressEvents(process.stdin);
|
|
554
|
+
process.stdin.setRawMode(true);
|
|
555
|
+
process.stdin.resume();
|
|
556
|
+
draw();
|
|
557
|
+
|
|
558
|
+
const onResize = () => {
|
|
559
|
+
if (!prompting) draw();
|
|
560
|
+
};
|
|
561
|
+
process.stdout.on('resize', onResize);
|
|
562
|
+
|
|
563
|
+
const done = new Promise((resolvePromise) => {
|
|
564
|
+
const keyHandler = async (str, key) => {
|
|
565
|
+
if (prompting) return;
|
|
566
|
+
try {
|
|
567
|
+
if (showHelp) {
|
|
568
|
+
showHelp = false;
|
|
569
|
+
draw();
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
const list = rows();
|
|
573
|
+
const selected = list[index];
|
|
574
|
+
switch (key.name) {
|
|
575
|
+
case 'up':
|
|
576
|
+
index = Math.max(0, index - 1);
|
|
577
|
+
break;
|
|
578
|
+
case 'k':
|
|
579
|
+
if (key.ctrl || key.meta) return;
|
|
580
|
+
index = Math.max(0, index - 1);
|
|
581
|
+
break;
|
|
582
|
+
case 'down':
|
|
583
|
+
index = Math.min(Math.max(0, list.length - 1), index + 1);
|
|
584
|
+
break;
|
|
585
|
+
case 'j':
|
|
586
|
+
if (key.ctrl || key.meta) return;
|
|
587
|
+
index = Math.min(Math.max(0, list.length - 1), index + 1);
|
|
588
|
+
break;
|
|
589
|
+
case 'pageup':
|
|
590
|
+
index = Math.max(0, index - 10);
|
|
591
|
+
break;
|
|
592
|
+
case 'pagedown':
|
|
593
|
+
index = Math.min(Math.max(0, list.length - 1), index + 10);
|
|
594
|
+
break;
|
|
595
|
+
case 'g':
|
|
596
|
+
index = key.shift ? Math.max(0, list.length - 1) : 0;
|
|
597
|
+
break;
|
|
598
|
+
case 'return':
|
|
599
|
+
case 'e':
|
|
600
|
+
if (selected) {
|
|
601
|
+
const needed = expectedPlaceholders(selected.baseText);
|
|
602
|
+
const answer = await prompt(
|
|
603
|
+
`edit [${selected.key}]${needed ? ` (needs ${needed})` : ''} (empty cancels)`,
|
|
604
|
+
selected.targetText || selected.baseText,
|
|
605
|
+
);
|
|
606
|
+
if (answer !== '') {
|
|
607
|
+
session = setTargetText(session, selected.key, answer);
|
|
608
|
+
dirty = true;
|
|
609
|
+
status = `updated ${selected.key}`;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
break;
|
|
613
|
+
case 's':
|
|
614
|
+
if (selected) {
|
|
615
|
+
const audioKey = cueKeyFor(session, selected.key);
|
|
616
|
+
const current = session.target.messages[audioKey];
|
|
617
|
+
const own = typeof current === 'string' ? current : '';
|
|
618
|
+
const answer = (
|
|
619
|
+
await prompt(
|
|
620
|
+
`cue path for [${selected.key}] -> ${audioKey} (empty clears)`,
|
|
621
|
+
own || selected.sound || '',
|
|
622
|
+
)
|
|
623
|
+
).trim();
|
|
624
|
+
if (answer !== '' || own !== '') {
|
|
625
|
+
session = setTargetSound(session, selected.key, answer);
|
|
626
|
+
dirty = true;
|
|
627
|
+
status =
|
|
628
|
+
answer === ''
|
|
629
|
+
? `cleared cue ${audioKey}`
|
|
630
|
+
: `cue ${audioKey} = ${answer}${checkCueFile(answer)}`;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
break;
|
|
634
|
+
case 'p':
|
|
635
|
+
//plain `p` is deliberately unbound: previews start audio out of nowhere,
|
|
636
|
+
//so the chord is the only way to ask for one
|
|
637
|
+
if ((key.ctrl || key.meta) && selected) {
|
|
638
|
+
if (!selected.sound) {
|
|
639
|
+
status = 'no cue on this key (s to set)';
|
|
640
|
+
} else if (isRemotePath(selected.sound)) {
|
|
641
|
+
status = 'remote cues cannot be previewed';
|
|
642
|
+
} else {
|
|
643
|
+
const root = assetsDir ? resolve(assetsDir) : process.cwd();
|
|
644
|
+
const absolute = resolve(root, selected.sound);
|
|
645
|
+
if (!existsSync(absolute)) {
|
|
646
|
+
status = `cue file not found: ${selected.sound}`;
|
|
647
|
+
} else {
|
|
648
|
+
previewCue(selected.sound, absolute);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
break;
|
|
653
|
+
case 'r':
|
|
654
|
+
if (selected && selected.status === 'missing') {
|
|
655
|
+
session = copyFromBase(session, selected.key);
|
|
656
|
+
dirty = true;
|
|
657
|
+
status = `copied base text for ${selected.key}`;
|
|
658
|
+
} else {
|
|
659
|
+
status = 'r copies only keys missing from the target';
|
|
660
|
+
}
|
|
661
|
+
break;
|
|
662
|
+
case 'd':
|
|
663
|
+
if (selected && selected.status === 'extra') {
|
|
664
|
+
session = deleteTargetKey(session, selected.key);
|
|
665
|
+
dirty = true;
|
|
666
|
+
status = `deleted extra key ${selected.key}`;
|
|
667
|
+
} else {
|
|
668
|
+
status = 'd deletes only keys absent from the base';
|
|
669
|
+
}
|
|
670
|
+
break;
|
|
671
|
+
case 'x':
|
|
672
|
+
session = swapSession(session);
|
|
673
|
+
index = 0;
|
|
674
|
+
scroll = 0;
|
|
675
|
+
status = `swapped: ${session.base.locale} is now the reference`;
|
|
676
|
+
break;
|
|
677
|
+
case 'v':
|
|
678
|
+
renderMd = !renderMd;
|
|
679
|
+
status = renderMd ? 'markdown preview on' : 'raw markdown source';
|
|
680
|
+
break;
|
|
681
|
+
case 'm':
|
|
682
|
+
missingOnly = !missingOnly;
|
|
683
|
+
index = 0;
|
|
684
|
+
scroll = 0;
|
|
685
|
+
break;
|
|
686
|
+
case 'w':
|
|
687
|
+
await save();
|
|
688
|
+
break;
|
|
689
|
+
case 'q':
|
|
690
|
+
if (dirty) {
|
|
691
|
+
const answer = await prompt('unsaved changes - quit anyway? (y/N)');
|
|
692
|
+
if (!/^y(es)?$/i.test(answer.trim())) break;
|
|
693
|
+
}
|
|
694
|
+
quit = true;
|
|
695
|
+
break;
|
|
696
|
+
case '?':
|
|
697
|
+
showHelp = true;
|
|
698
|
+
break;
|
|
699
|
+
case '/': {
|
|
700
|
+
const answer = await prompt('search (empty clears)');
|
|
701
|
+
query = answer.trim();
|
|
702
|
+
index = 0;
|
|
703
|
+
scroll = 0;
|
|
704
|
+
break;
|
|
705
|
+
}
|
|
706
|
+
case 'escape':
|
|
707
|
+
if (query !== '') {
|
|
708
|
+
query = '';
|
|
709
|
+
index = 0;
|
|
710
|
+
scroll = 0;
|
|
711
|
+
}
|
|
712
|
+
break;
|
|
713
|
+
default:
|
|
714
|
+
if (str === '?') showHelp = true;
|
|
715
|
+
break;
|
|
716
|
+
}
|
|
717
|
+
} finally {
|
|
718
|
+
draw();
|
|
719
|
+
if (quit) {
|
|
720
|
+
process.stdin.removeListener('keypress', keyHandler);
|
|
721
|
+
process.stdout.removeListener('resize', onResize);
|
|
722
|
+
resolvePromise();
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
process.stdin.on('keypress', keyHandler);
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
await done;
|
|
730
|
+
process.stdin.setRawMode(false);
|
|
731
|
+
process.stdin.pause();
|
|
732
|
+
restore();
|
|
733
|
+
console.log(dirty ? 'quit with unsaved changes.' : 'done.');
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
return { run };
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
//realpath, since an npm `bin` shim reaches this file through a symlink
|
|
740
|
+
const isMain = process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
741
|
+
if (isMain) {
|
|
742
|
+
main().catch((error) => {
|
|
743
|
+
console.error(`i18n-edit: ${error?.stack ?? error}`);
|
|
744
|
+
process.exit(1);
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
export { dirname, loadCatalogFile, serializeTarget };
|