@nocobase/cli 2.1.0-alpha.21 → 2.1.0-alpha.23

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.
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { spawn } from 'node:child_process';
10
10
  import net from 'node:net';
11
+ import { translateCli } from "./cli-locale.js";
11
12
  const API_BASE_URL_EXAMPLE = 'http://localhost:13000/api';
12
13
  const ENV_KEY_PATTERN = /^[A-Za-z0-9]+$/;
13
14
  const TCP_PORT_EXAMPLE = '13000';
@@ -21,10 +22,10 @@ export function validateApiBaseUrl(value) {
21
22
  url = new URL(raw);
22
23
  }
23
24
  catch {
24
- return `Enter a valid URL, for example ${API_BASE_URL_EXAMPLE}.`;
25
+ return translateCli('validators.apiBaseUrl.invalid', { example: API_BASE_URL_EXAMPLE });
25
26
  }
26
27
  if (url.protocol !== 'http:' && url.protocol !== 'https:') {
27
- return `URL must start with http:// or https://, for example ${API_BASE_URL_EXAMPLE}.`;
28
+ return translateCli('validators.apiBaseUrl.invalidProtocol', { example: API_BASE_URL_EXAMPLE });
28
29
  }
29
30
  return undefined;
30
31
  }
@@ -34,7 +35,7 @@ export function validateEnvKey(value) {
34
35
  return undefined;
35
36
  }
36
37
  if (!ENV_KEY_PATTERN.test(raw)) {
37
- return 'Use letters and numbers only.';
38
+ return translateCli('validators.envKey.invalid');
38
39
  }
39
40
  return undefined;
40
41
  }
@@ -59,7 +60,7 @@ export function validateTcpPort(value) {
59
60
  }
60
61
  const port = parseTcpPort(raw);
61
62
  if (port === undefined) {
62
- return `Enter a valid TCP port between 1 and 65535, for example ${TCP_PORT_EXAMPLE}.`;
63
+ return translateCli('validators.tcpPort.invalid', { example: TCP_PORT_EXAMPLE });
63
64
  }
64
65
  return undefined;
65
66
  }
@@ -138,7 +139,7 @@ async function allocateAvailableTcpPort() {
138
139
  : undefined;
139
140
  if (!port) {
140
141
  server.close(() => {
141
- reject(new Error('Failed to allocate an available TCP port.'));
142
+ reject(new Error(translateCli('validators.tcpPort.allocateFailed')));
142
143
  });
143
144
  return;
144
145
  }
@@ -160,7 +161,7 @@ export async function findAvailableTcpPort() {
160
161
  return candidate;
161
162
  }
162
163
  }
163
- throw new Error('Failed to allocate an available TCP port that is not already published by Docker.');
164
+ throw new Error(translateCli('validators.tcpPort.allocateNotDockerPublished'));
164
165
  }
165
166
  export async function validateAvailableTcpPort(value) {
166
167
  const raw = String(value ?? '').trim();
@@ -174,11 +175,11 @@ export async function validateAvailableTcpPort(value) {
174
175
  const port = parseTcpPort(raw);
175
176
  const available = await canListenOnTcpPort(port);
176
177
  if (!available) {
177
- return `Port ${port} is already in use. Choose another port.`;
178
+ return translateCli('validators.tcpPort.alreadyInUse', { port });
178
179
  }
179
180
  const dockerPorts = await getDockerPublishedTcpPorts();
180
181
  if (dockerPorts.has(port)) {
181
- return `Port ${port} is already in use by a Docker container. Choose another port.`;
182
+ return translateCli('validators.tcpPort.alreadyInUseByDocker', { port });
182
183
  }
183
184
  return undefined;
184
185
  }
@@ -8,7 +8,8 @@
8
8
  */
9
9
  import { spawn } from 'node:child_process';
10
10
  import { createServer } from 'node:http';
11
- import { isPromptBlockSkipped, runPromptFieldValidate, selectOptionValues, } from "./prompt-catalog.js";
11
+ import { createCliTranslate, resolveCliLocale, resolveLocalizedText, } from "./cli-locale.js";
12
+ import { isPromptBlockSkipped, runPromptFieldValidate, } from "./prompt-catalog.js";
12
13
  const DEFAULT_SUBMIT = '/__pwc_ui_submit';
13
14
  const DEFAULT_REFLOW = '/__pwc_ui_reflow';
14
15
  const DEFAULT_VALIDATE_STEP = '/__pwc_ui_validate_step';
@@ -19,6 +20,9 @@ export const PWC_FORM_META_STEP = '_pwcStep';
19
20
  export const PWC_FORM_META_FIELD = '_pwcField';
20
21
  const DEFAULT_TIMEOUT_MS = 30 * 60 * 1000;
21
22
  const DEFAULT_HOST = '127.0.0.1';
23
+ function resolveUiText(text, locale, fallback = '') {
24
+ return resolveLocalizedText(text, { locale, fallback });
25
+ }
22
26
  function hasValueKey(iv, key) {
23
27
  return (Object.prototype.hasOwnProperty.call(iv, key) && iv[key] !== undefined && iv[key] !== null);
24
28
  }
@@ -72,12 +76,17 @@ function defaultValueForInput(key, def, out) {
72
76
  case 'boolean':
73
77
  return def.initialValue !== undefined ? Boolean(def.initialValue) : true;
74
78
  case 'select': {
75
- const first = selectOptionValues(def.options)[0];
79
+ const first = def.options
80
+ .find((o) => typeof o === 'string' || o.disabled !== true);
81
+ const firstValue = typeof first === 'string' ? first : first?.value;
76
82
  const i = def.initialValue;
77
- if (i !== undefined && selectOptionValues(def.options).includes(i)) {
83
+ const enabledValues = def.options
84
+ .filter((o) => typeof o === 'string' || o.disabled !== true)
85
+ .map((o) => (typeof o === 'string' ? o : o.value));
86
+ if (i !== undefined && enabledValues.includes(i)) {
78
87
  return i;
79
88
  }
80
- return first ?? '';
89
+ return firstValue ?? '';
81
90
  }
82
91
  case 'password':
83
92
  return def.initialValue ?? '';
@@ -143,7 +152,9 @@ function normalizeWebRawForBlock(def, raw, key) {
143
152
  }
144
153
  case 'select': {
145
154
  const s = String(raw ?? '');
146
- const list = selectOptionValues(def.options);
155
+ const list = def.options
156
+ .filter((o) => typeof o === 'string' || o.disabled !== true)
157
+ .map((o) => (typeof o === 'string' ? o : o.value));
147
158
  if (list.includes(s)) {
148
159
  return s;
149
160
  }
@@ -171,6 +182,8 @@ function normalizeWebRawForBlock(def, raw, key) {
171
182
  * Runs per-field **`validate`** (including async) from the catalog (honoring {@link BuildWebPresetFromBodyOptions}).
172
183
  */
173
184
  export async function buildWebPresetFromBody(catalog, raw, userSeed = {}, buildOpts = {}) {
185
+ const locale = resolveCliLocale(buildOpts.locale);
186
+ const t = createCliTranslate(locale);
174
187
  const scope = buildOpts.scopeKeys ?? null;
175
188
  const inScope = (k) => scope === null || scope.has(k);
176
189
  const preset = {};
@@ -198,18 +211,18 @@ export async function buildWebPresetFromBody(catalog, raw, userSeed = {}, buildO
198
211
  val = defaultValueForInput(key, def, preset);
199
212
  }
200
213
  if (def.type === 'text' && def.required && val === '' && inScope(key)) {
201
- return { preset: {}, error: `Field "${key}" is required.`, fieldKey: key };
214
+ return { preset: {}, error: t('promptCatalog.web.fieldRequired', { key }), fieldKey: key };
202
215
  }
203
216
  if (def.type === 'password' && def.required && val === '' && inScope(key)) {
204
- return { preset: {}, error: `Field "${key}" is required.`, fieldKey: key };
217
+ return { preset: {}, error: t('promptCatalog.web.fieldRequired', { key }), fieldKey: key };
205
218
  }
206
219
  if (def.type === 'integer' && def.required && inScope(key)) {
207
- const t = String(raw[key] ?? '').trim();
208
- if (t === '') {
209
- return { preset: {}, error: `Field "${key}" is required.`, fieldKey: key };
220
+ const trimmed = String(raw[key] ?? '').trim();
221
+ if (trimmed === '') {
222
+ return { preset: {}, error: t('promptCatalog.web.fieldRequired', { key }), fieldKey: key };
210
223
  }
211
- if (!/^-?\d+$/.test(t)) {
212
- return { preset: {}, error: `Field "${key}" must be an integer.`, fieldKey: key };
224
+ if (!/^-?\d+$/.test(trimmed)) {
225
+ return { preset: {}, error: t('promptCatalog.web.fieldMustBeInteger', { key }), fieldKey: key };
213
226
  }
214
227
  }
215
228
  preset[key] = val;
@@ -229,7 +242,8 @@ function escapeHtml(s) {
229
242
  .replace(/"/g, '"')
230
243
  .replace(/'/g, ''');
231
244
  }
232
- function computePwcWizardSteps(options, merged) {
245
+ function computePwcWizardSteps(options, merged, locale) {
246
+ const t = createCliTranslate(locale);
233
247
  if (options.stages && options.stages.length > 0) {
234
248
  if (options.stages.length === 1) {
235
249
  const st0 = options.stages[0];
@@ -239,12 +253,12 @@ function computePwcWizardSteps(options, merged) {
239
253
  allKeys.push(key);
240
254
  }
241
255
  }
242
- const t = st0.sectionTitle?.trim();
243
- const d = st0.sectionDescription?.trim();
256
+ const title = resolveUiText(st0.sectionTitle, locale).trim();
257
+ const description = resolveUiText(st0.sectionDescription, locale).trim();
244
258
  return [
245
259
  {
246
- title: t && t.length > 0 ? t : 'Form',
247
- ...(d && d.length > 0 ? { description: d } : {}),
260
+ title: title && title.length > 0 ? title : t('promptCatalog.web.defaultFormTitle'),
261
+ ...(description && description.length > 0 ? { description } : {}),
248
262
  keys: allKeys,
249
263
  },
250
264
  ];
@@ -256,11 +270,11 @@ function computePwcWizardSteps(options, merged) {
256
270
  keys.push(key);
257
271
  }
258
272
  }
259
- const t = st.sectionTitle?.trim();
260
- const d = st.sectionDescription?.trim();
273
+ const title = resolveUiText(st.sectionTitle, locale).trim();
274
+ const description = resolveUiText(st.sectionDescription, locale).trim();
261
275
  return {
262
- title: t && t.length > 0 ? t : `Step ${i + 1}`,
263
- ...(d && d.length > 0 ? { description: d } : {}),
276
+ title: title && title.length > 0 ? title : t('promptCatalog.web.defaultStepTitle', { index: i + 1 }),
277
+ ...(description && description.length > 0 ? { description } : {}),
264
278
  keys,
265
279
  };
266
280
  });
@@ -275,23 +289,31 @@ function computePwcWizardSteps(options, merged) {
275
289
  return [];
276
290
  }
277
291
  /* Single `catalog` (no `stages`): one page, no side Steps nav — use `stages` for multi-page UI. */
278
- return [{ title: 'Form', keys: allKeys }];
292
+ return [{ title: t('promptCatalog.web.defaultFormTitle'), keys: allKeys }];
279
293
  }
280
294
  /** antd Form.Item-style explain line (per-field error / help). */
281
295
  const PWC_FORM_ITEM_EXPLAIN = '<div class="pwc-form-item-explain" data-pwc-explain="1" role="alert" hidden></div>';
282
296
  /** Suffix slot for status icon (e.g. fail) — filled by client script. */
283
297
  const PWC_FORM_ITEM_SUFFIX = '<span class="pwc-form-item-suffix" data-pwc-suffix="1" aria-hidden="true"></span>';
284
- function renderPwcRadioOptions(key, def, defaults, hidden) {
298
+ function renderPwcRadioOptions(key, def, defaults, hidden, locale) {
285
299
  return def.options
286
300
  .map((o, index) => {
287
301
  const val = typeof o === 'string' ? o : o.value;
288
- const lab = typeof o === 'string' ? o : o.label ?? o.value;
289
- const hint = typeof o === 'string' ? '' : o.hint ? `<div class="pwc-radio-option-hint">${escapeHtml(o.hint)}</div>` : '';
302
+ const lab = typeof o === 'string' ? o : resolveUiText(o.label, locale, o.value);
303
+ const hint = typeof o === 'string'
304
+ ? ''
305
+ : o.hint
306
+ ? `<div class="pwc-radio-option-hint">${escapeHtml(resolveUiText(o.hint, locale))}</div>`
307
+ : '';
308
+ const optionDisabled = typeof o !== 'string' && o.disabled === true;
290
309
  const checked = String(defaults[key] ?? '') === val ? ' checked' : '';
291
310
  const required = def.required && index === 0 ? ' required' : '';
292
- const disabled = hidden ? ' disabled' : '';
293
- return (`<label class="pwc-radio-option">` +
294
- `<input class="pwc-radio-input" name="${escapeHtml(key)}" type="radio" value="${escapeHtml(String(val))}"${checked}${required}${disabled} />` +
311
+ const disabled = hidden || optionDisabled ? ' disabled' : '';
312
+ const optionClass = optionDisabled ? 'pwc-radio-option pwc-radio-option--disabled' : 'pwc-radio-option';
313
+ const ariaDisabled = optionDisabled ? ' aria-disabled="true"' : '';
314
+ const staticDisabled = optionDisabled ? ' data-pwc-static-disabled="1"' : '';
315
+ return (`<label class="${optionClass}"${ariaDisabled}>` +
316
+ `<input class="pwc-radio-input" name="${escapeHtml(key)}" type="radio" value="${escapeHtml(String(val))}"${checked}${required}${disabled}${staticDisabled} />` +
295
317
  `<span class="pwc-radio-option-body">` +
296
318
  `<span class="pwc-radio-option-label">${escapeHtml(String(lab))}</span>` +
297
319
  hint +
@@ -300,18 +322,18 @@ function renderPwcRadioOptions(key, def, defaults, hidden) {
300
322
  })
301
323
  .join('');
302
324
  }
303
- function renderPwcFieldRow(key, def, defaults, show) {
325
+ function renderPwcFieldRow(key, def, defaults, show, locale) {
304
326
  if (!isInputBlock(def)) {
305
327
  return '';
306
328
  }
307
- const labelText = 'message' in def ? def.message : key;
329
+ const labelText = 'message' in def ? resolveUiText(def.message, locale, key) : key;
308
330
  const hidden = show[key] === false;
309
331
  const display = hidden ? 'none' : 'block';
310
332
  const itemOpen = (extraClass) => `<div class="pwc-form-item${extraClass ? ` ${extraClass}` : ''}" data-pwc-wrap="${escapeHtml(key)}" data-pwc-field="${escapeHtml(key)}" style="display:${display}">`;
311
333
  if (def.type === 'text') {
312
334
  const req = def.required ? ' required' : '';
313
335
  const disabled = hidden ? ' disabled' : '';
314
- const ph = def.placeholder ? ` placeholder="${escapeHtml(def.placeholder)}"` : '';
336
+ const ph = def.placeholder ? ` placeholder="${escapeHtml(resolveUiText(def.placeholder, locale))}"` : '';
315
337
  const v = escapeHtml(String(defaults[key] ?? ''));
316
338
  return (itemOpen('') +
317
339
  `<div class="pwc-form-item-label"><span class="pwc-l">${escapeHtml(labelText)}</span></div>` +
@@ -334,7 +356,7 @@ function renderPwcFieldRow(key, def, defaults, show) {
334
356
  }
335
357
  if (def.type === 'select') {
336
358
  if (def.variant === 'radio') {
337
- const opts = renderPwcRadioOptions(key, def, defaults, hidden);
359
+ const opts = renderPwcRadioOptions(key, def, defaults, hidden, locale);
338
360
  return (itemOpen('') +
339
361
  `<div class="pwc-form-item-label"><span class="pwc-l">${escapeHtml(labelText)}</span></div>` +
340
362
  `<div class="pwc-form-item-control">` +
@@ -348,9 +370,10 @@ function renderPwcFieldRow(key, def, defaults, show) {
348
370
  const opts = def.options
349
371
  .map((o) => {
350
372
  const val = typeof o === 'string' ? o : o.value;
351
- const lab = typeof o === 'string' ? o : o.label ?? o.value;
373
+ const lab = typeof o === 'string' ? o : resolveUiText(o.label, locale, o.value);
352
374
  const sel = String(defaults[key] ?? '') === val ? ' selected' : '';
353
- return `<option value="${escapeHtml(String(val))}"${sel}>${escapeHtml(String(lab))}</option>`;
375
+ const disabledOpt = typeof o !== 'string' && o.disabled === true ? ' disabled' : '';
376
+ return `<option value="${escapeHtml(String(val))}"${sel}${disabledOpt}>${escapeHtml(String(lab))}</option>`;
354
377
  })
355
378
  .join('');
356
379
  const req = def.required ? ' required' : '';
@@ -382,7 +405,7 @@ function renderPwcFieldRow(key, def, defaults, show) {
382
405
  `</div></div>`);
383
406
  }
384
407
  if (def.type === 'integer') {
385
- const ph = def.placeholder ? ` placeholder="${escapeHtml(def.placeholder)}"` : '';
408
+ const ph = def.placeholder ? ` placeholder="${escapeHtml(resolveUiText(def.placeholder, locale))}"` : '';
386
409
  const req = def.required ? ' required' : '';
387
410
  const disabled = hidden ? ' disabled' : '';
388
411
  const v = String(defaults[key] ?? 0);
@@ -412,7 +435,7 @@ const PWC_FORM_FAIL_ICON_SVG = `<svg class="pwc-form-fail__svg" viewBox="0 0 14
412
435
  * with CSS aligned to antd tokens (icon 32px, 14px title, `colorSplit` rail, `wait` / `process` / `finish`).
413
436
  * Primary in steps uses antd default blue in light / `#1668dc` in dark; form primary buttons still use the page accent.
414
437
  */
415
- function buildPwcAntdStyleStepsHeader(stepDefs, currentIndex, total, visibleStepIndices) {
438
+ function buildPwcAntdStyleStepsHeader(stepDefs, currentIndex, total, visibleStepIndices, uiText) {
416
439
  const parts = [];
417
440
  for (let i = 0; i < total; i += 1) {
418
441
  const st = i < stepDefs.length ? stepDefs[i] : { title: `Step ${i + 1}`, keys: [] };
@@ -427,7 +450,7 @@ function buildPwcAntdStyleStepsHeader(stepDefs, currentIndex, total, visibleStep
427
450
  const num = visiblePos + 1;
428
451
  const hiddenAttr = visiblePos === -1 ? ' hidden' : '';
429
452
  const iconHtml = state === 'finish'
430
- ? `<span class="pwc-ad-icon pwc-ad-icon--finish" aria-label="done">${PWC_AD_CHECK_SVG}</span>`
453
+ ? `<span class="pwc-ad-icon pwc-ad-icon--finish" aria-label="${escapeHtml(uiText.doneAriaLabel)}">${PWC_AD_CHECK_SVG}</span>`
431
454
  : `<span class="pwc-ad-icon" aria-label="${num}"><span class="pwc-ad-icon-num">${num}</span></span>`;
432
455
  const ariaCurrent = state === 'process' ? 'step' : 'false';
433
456
  const nextVisible = visibleStepIndices.indexOf(i + 1) !== -1;
@@ -447,7 +470,7 @@ function buildPwcAntdStyleStepsHeader(stepDefs, currentIndex, total, visibleStep
447
470
  `</div>` +
448
471
  `</div></li>`);
449
472
  }
450
- return (`<nav class="pwc-ad-steps pwc-ad-steps--vertical" id="pwcAntSteps" aria-label="Form steps" style="--pwc-ad-n:${total}">` +
473
+ return (`<nav class="pwc-ad-steps pwc-ad-steps--vertical" id="pwcAntSteps" aria-label="${escapeHtml(uiText.stepsAriaLabel)}" style="--pwc-ad-n:${total}">` +
451
474
  `<ol class="pwc-ad-steps-list pwc-ad-steps-list--vertical">${parts.join('')}</ol></nav>`);
452
475
  }
453
476
  function computeInitialVisibleStepIndices(catalog, stepDefs, show) {
@@ -467,7 +490,7 @@ function computeInitialVisibleStepIndices(catalog, stepDefs, show) {
467
490
  }
468
491
  return visible.length > 0 ? visible : [0];
469
492
  }
470
- function buildPwcFormHtml(catalog, defaults, show, stepDefs, initialStepIndex, totalSteps) {
493
+ function buildPwcFormHtml(catalog, defaults, show, stepDefs, initialStepIndex, totalSteps, locale, uiText) {
471
494
  if (stepDefs.length === 0) {
472
495
  return '';
473
496
  }
@@ -476,8 +499,8 @@ function buildPwcFormHtml(catalog, defaults, show, stepDefs, initialStepIndex, t
476
499
  const out = [];
477
500
  if (!oneStep) {
478
501
  out.push('<div class="pwc-wizard pwc-wizard--with-sidebar" id="pwcWizard">');
479
- out.push('<aside class="pwc-wizard-sidenav" id="pwcWizardSidenav" aria-label="Steps">');
480
- out.push(buildPwcAntdStyleStepsHeader(stepDefs, initialStepIndex, totalSteps, visibleStepIndices));
502
+ out.push(`<aside class="pwc-wizard-sidenav" id="pwcWizardSidenav" aria-label="${escapeHtml(uiText.stepsSidebarAriaLabel)}">`);
503
+ out.push(buildPwcAntdStyleStepsHeader(stepDefs, initialStepIndex, totalSteps, visibleStepIndices, uiText));
481
504
  out.push('</aside><div class="pwc-wizard-main">');
482
505
  }
483
506
  else {
@@ -494,23 +517,23 @@ function buildPwcFormHtml(catalog, defaults, show, stepDefs, initialStepIndex, t
494
517
  for (const key of sd.keys) {
495
518
  const def = catalog[key];
496
519
  if (def) {
497
- out.push(renderPwcFieldRow(key, def, defaults, show));
520
+ out.push(renderPwcFieldRow(key, def, defaults, show, locale));
498
521
  }
499
522
  }
500
523
  out.push('</section>');
501
524
  }
502
525
  if (!oneStep) {
503
- out.push(`<div class="pwc-wizard-ctl" id="pwcWizardCtl" role="group" aria-label="Step navigation and submit">` +
504
- `<button type="button" class="pwc-btn-pager" id="pwcBack" ${initialStepIndex === 0 ? 'hidden' : ''}>Back</button>` +
526
+ out.push(`<div class="pwc-wizard-ctl" id="pwcWizardCtl" role="group" aria-label="${escapeHtml(uiText.stepNavigationAriaLabel)}">` +
527
+ `<button type="button" class="pwc-btn-pager" id="pwcBack" ${initialStepIndex === 0 ? 'hidden' : ''}>${escapeHtml(uiText.back)}</button>` +
505
528
  `<div class="pwc-wizard-ctl-spacer" aria-hidden="true"></div>` +
506
- `<button type="button" class="pwc-btn-pager pwc-btn-pager--primary" id="pwcNext" ${initialStepIndex >= totalSteps - 1 ? 'hidden' : ''}>Next</button>` +
507
- `<button type="submit" class="pwc-btn-submit pwc-wizard-ctl__submit" id="pwcFormSubmit" ${initialStepIndex < totalSteps - 1 ? 'hidden' : ''}>Submit &amp; continue in terminal</button>` +
529
+ `<button type="button" class="pwc-btn-pager pwc-btn-pager--primary" id="pwcNext" ${initialStepIndex >= totalSteps - 1 ? 'hidden' : ''}>${escapeHtml(uiText.next)}</button>` +
530
+ `<button type="submit" class="pwc-btn-submit pwc-wizard-ctl__submit" id="pwcFormSubmit" ${initialStepIndex < totalSteps - 1 ? 'hidden' : ''}>${escapeHtml(uiText.submit)}</button>` +
508
531
  `</div>`);
509
532
  }
510
533
  else {
511
- out.push(`<div class="pwc-wizard-ctl" id="pwcWizardCtl" role="group" aria-label="Submit">` +
534
+ out.push(`<div class="pwc-wizard-ctl" id="pwcWizardCtl" role="group" aria-label="${escapeHtml(uiText.submitAriaLabel)}">` +
512
535
  `<div class="pwc-wizard-ctl-spacer" aria-hidden="true"></div>` +
513
- `<button type="submit" class="pwc-btn-submit pwc-wizard-ctl__submit" id="pwcFormSubmit">Submit &amp; continue in terminal</button>` +
536
+ `<button type="submit" class="pwc-btn-submit pwc-wizard-ctl__submit" id="pwcFormSubmit">${escapeHtml(uiText.submit)}</button>` +
514
537
  `</div>`);
515
538
  }
516
539
  /* Status / error Alert: under main form column only, not full-width under the side Steps. */
@@ -565,7 +588,7 @@ export function mergeWebUICatalogsFromStages(stages) {
565
588
  const sectionBeforeKey = new Map();
566
589
  const used = new Set();
567
590
  for (const stage of stages) {
568
- const title = stage.sectionTitle?.trim();
591
+ const title = typeof stage.sectionTitle === 'string' ? stage.sectionTitle.trim() : '';
569
592
  let needTitle = Boolean(title);
570
593
  for (const [key, def] of Object.entries(stage.catalog)) {
571
594
  if (isInputBlock(def) && needTitle && title) {
@@ -652,6 +675,8 @@ export function runPromptCatalogWebUI(catalogOrOptions, options) {
652
675
  return runPromptCatalogWebUIImpl({ catalog: catalogOrOptions });
653
676
  }
654
677
  function runPromptCatalogWebUIImpl(options) {
678
+ const locale = resolveCliLocale(options.locale);
679
+ const t = createCliTranslate(locale);
655
680
  const { merged } = resolveMergedCatalog(options);
656
681
  const userSeed = mergeValueSeedsFromOptions(options);
657
682
  const formDefaults = buildWebFormValuesFromCatalog(merged, userSeed);
@@ -660,15 +685,34 @@ function runPromptCatalogWebUIImpl(options) {
660
685
  const reflowPath = options.reflowPath ?? DEFAULT_REFLOW;
661
686
  const host = options.host ?? DEFAULT_HOST;
662
687
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
663
- const pageTitle = options.pageTitle ?? 'Prompt catalog (local UI)';
664
- const h1 = options.documentHeading ?? 'Configure parameters (localhost only)';
665
- const documentHint = options.documentHint ??
666
- 'This server is only bound to the loopback interface. After submit, the CLI continues in the same terminal session.';
688
+ const pageTitle = resolveUiText(options.pageTitle, locale, t('promptCatalog.web.pageTitle'));
689
+ const h1 = resolveUiText(options.documentHeading, locale, t('promptCatalog.web.documentHeading'));
690
+ const documentHint = resolveUiText(options.documentHint, locale, t('promptCatalog.web.documentHint'));
667
691
  const catalog = merged;
668
- const pwcStepDefs = computePwcWizardSteps(options, catalog);
692
+ const pwcStepDefs = computePwcWizardSteps(options, catalog, locale);
669
693
  const pwcNSteps = Math.max(1, pwcStepDefs.length);
670
694
  const resolveValidateStepPath = options.validateStepPath ?? DEFAULT_VALIDATE_STEP;
671
695
  const resolveValidateFieldPath = options.validateFieldPath ?? DEFAULT_VALIDATE_FIELD;
696
+ const uiText = {
697
+ stepsAriaLabel: t('promptCatalog.web.stepsAriaLabel'),
698
+ stepsSidebarAriaLabel: t('promptCatalog.web.stepsSidebarAriaLabel'),
699
+ stepNavigationAriaLabel: t('promptCatalog.web.stepNavigationAriaLabel'),
700
+ submitAriaLabel: t('promptCatalog.web.submitAriaLabel'),
701
+ back: t('promptCatalog.web.back'),
702
+ next: t('promptCatalog.web.next'),
703
+ submit: t('promptCatalog.web.submit'),
704
+ checking: t('promptCatalog.web.checking'),
705
+ sending: t('promptCatalog.web.sending'),
706
+ successTitle: t('promptCatalog.web.successTitle'),
707
+ errorTitle: t('promptCatalog.web.errorTitle'),
708
+ savedAndClosing: t('promptCatalog.web.savedAndClosing'),
709
+ savedCloseBlocked: t('promptCatalog.web.savedCloseBlocked'),
710
+ invalidValue: t('promptCatalog.web.invalidValue'),
711
+ invalidRequest: t('promptCatalog.web.invalidRequest'),
712
+ invalidStep: t('promptCatalog.web.invalidStep'),
713
+ invalidField: t('promptCatalog.web.invalidField'),
714
+ doneAriaLabel: 'done',
715
+ };
672
716
  let server;
673
717
  let timeoutId;
674
718
  return new Promise((resolve, reject) => {
@@ -686,15 +730,16 @@ function runPromptCatalogWebUIImpl(options) {
686
730
  };
687
731
  const servePage = (port) => {
688
732
  const base = `http://${host}:${port}`;
689
- const formInner = buildPwcFormHtml(catalog, formDefaults, initialShow, pwcStepDefs, 0, pwcNSteps);
733
+ const formInner = buildPwcFormHtml(catalog, formDefaults, initialShow, pwcStepDefs, 0, pwcNSteps, locale, uiText);
690
734
  const wizardClientJson = JSON.stringify({ n: pwcNSteps, stepDefs: pwcStepDefs });
691
735
  const pwcValStepUrl = pwcNSteps > 1 ? JSON.stringify(base + resolveValidateStepPath) : 'null';
692
736
  const pwcValFieldUrl = JSON.stringify(base + resolveValidateFieldPath);
737
+ const uiTextJson = JSON.stringify(uiText);
693
738
  const pwcShellClass = options.stages && options.stages.length > 0
694
739
  ? 'pwc-shell pwc-shell--stages'
695
740
  : 'pwc-shell';
696
741
  const page = `<!doctype html>
697
- <html lang="en">
742
+ <html lang="${escapeHtml(locale)}">
698
743
  <head>
699
744
  <meta charset="utf-8" />
700
745
  <meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -1099,6 +1144,15 @@ function runPromptCatalogWebUIImpl(options) {
1099
1144
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--pwc-ad-color-primary) 8%, transparent);
1100
1145
  background: color-mix(in srgb, var(--pwc-ad-color-primary) 2%, var(--pwc-ad-color-container));
1101
1146
  }
1147
+ .pwc-radio-option--disabled {
1148
+ cursor: not-allowed;
1149
+ background: var(--pwc-ad-color-fill-alter);
1150
+ }
1151
+ .pwc-radio-option--disabled:hover {
1152
+ border-color: var(--pwc-ad-color-border);
1153
+ box-shadow: none;
1154
+ background: var(--pwc-ad-color-fill-alter);
1155
+ }
1102
1156
  .pwc-radio-input {
1103
1157
  margin: 3px 0 0;
1104
1158
  width: 16px;
@@ -1107,6 +1161,9 @@ function runPromptCatalogWebUIImpl(options) {
1107
1161
  accent-color: var(--pwc-ad-color-primary);
1108
1162
  cursor: pointer;
1109
1163
  }
1164
+ .pwc-radio-option--disabled .pwc-radio-input {
1165
+ cursor: not-allowed;
1166
+ }
1110
1167
  .pwc-radio-option-body { min-width: 0; }
1111
1168
  .pwc-radio-option-label {
1112
1169
  display: block;
@@ -1114,12 +1171,18 @@ function runPromptCatalogWebUIImpl(options) {
1114
1171
  line-height: 1.5715;
1115
1172
  color: var(--pwc-ad-color-text);
1116
1173
  }
1174
+ .pwc-radio-option--disabled .pwc-radio-option-label {
1175
+ color: color-mix(in srgb, var(--pwc-ad-color-text) 55%, transparent);
1176
+ }
1117
1177
  .pwc-radio-option-hint {
1118
1178
  margin-top: 2px;
1119
1179
  font-size: 12px;
1120
1180
  line-height: 1.5;
1121
1181
  color: var(--pwc-ad-color-text-description);
1122
1182
  }
1183
+ .pwc-radio-option--disabled .pwc-radio-option-hint {
1184
+ color: color-mix(in srgb, var(--pwc-ad-color-text-description) 70%, transparent);
1185
+ }
1123
1186
  .pwc-radio-input:focus-visible {
1124
1187
  outline: none;
1125
1188
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--pwc-ad-color-primary) 20%, transparent);
@@ -1130,6 +1193,11 @@ function runPromptCatalogWebUIImpl(options) {
1130
1193
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--pwc-ad-color-primary) 12%, transparent);
1131
1194
  background: color-mix(in srgb, var(--pwc-ad-color-primary) 5%, var(--pwc-ad-color-container));
1132
1195
  }
1196
+ .pwc-radio-option--disabled:has(.pwc-radio-input:checked) {
1197
+ border-color: var(--pwc-ad-color-border);
1198
+ box-shadow: none;
1199
+ background: var(--pwc-ad-color-fill-alter);
1200
+ }
1133
1201
  .pwc-form-item-has-error .pwc-radio-option {
1134
1202
  border-color: var(--pwc-form-color-error);
1135
1203
  }
@@ -1328,6 +1396,7 @@ function runPromptCatalogWebUIImpl(options) {
1328
1396
  var pwcFieldMeta = ${JSON.stringify(PWC_FORM_META_FIELD)};
1329
1397
  var s = document.getElementById('pwcStatus');
1330
1398
  var wcfg = ${wizardClientJson};
1399
+ var uiText = ${uiTextJson};
1331
1400
  var pwcN = wcfg && typeof wcfg.n === 'number' ? wcfg.n : 1;
1332
1401
  var pwcSteps = (wcfg && wcfg.stepDefs) || [];
1333
1402
  var pwcCur = 0;
@@ -1367,7 +1436,7 @@ function runPromptCatalogWebUIImpl(options) {
1367
1436
  s.setAttribute('role', 'alert');
1368
1437
  s.innerHTML =
1369
1438
  '<span class="pwc-ad-alert__icon" aria-hidden="true">' + pwcErrIcon + '</span>' +
1370
- '<div class="pwc-ad-alert__inner"><div class="pwc-ad-alert__title">Error</div>' +
1439
+ '<div class="pwc-ad-alert__inner"><div class="pwc-ad-alert__title">' + pwcEsc(uiText.errorTitle) + '</div>' +
1371
1440
  '<div class="pwc-ad-alert__desc">' + pwcEsc(t) + '</div></div>';
1372
1441
  }
1373
1442
  function pwcSetStatusSuccess(message) {
@@ -1377,7 +1446,7 @@ function runPromptCatalogWebUIImpl(options) {
1377
1446
  s.setAttribute('role', 'status');
1378
1447
  s.innerHTML =
1379
1448
  '<span class="pwc-ad-alert__icon" aria-hidden="true">' + pwcOkIcon + '</span>' +
1380
- '<div class="pwc-ad-alert__inner"><div class="pwc-ad-alert__title">Success</div>' +
1449
+ '<div class="pwc-ad-alert__inner"><div class="pwc-ad-alert__title">' + pwcEsc(uiText.successTitle) + '</div>' +
1381
1450
  '<div class="pwc-ad-alert__desc">' + pwcEsc(t) + '</div></div>';
1382
1451
  }
1383
1452
  function pwcScheduleWindowClose() {
@@ -1387,9 +1456,9 @@ function runPromptCatalogWebUIImpl(options) {
1387
1456
  window.close();
1388
1457
  pwcCloseProbeTimer = setTimeout(function () {
1389
1458
  if (!s || document.visibilityState !== 'visible') { return; }
1390
- pwcSetStatusSuccess('Saved. Automatic close was blocked by the browser. You can close this tab now.');
1459
+ pwcSetStatusSuccess(uiText.savedCloseBlocked);
1391
1460
  }, 600);
1392
- }, 2000);
1461
+ }, 5000);
1393
1462
  }
1394
1463
  function pwcSetFieldError(key, message) {
1395
1464
  if (!form || !key) { return; }
@@ -1536,7 +1605,7 @@ function runPromptCatalogWebUIImpl(options) {
1536
1605
  w.style.display = hidden ? 'none' : 'block';
1537
1606
  var ctrls = w.querySelectorAll('input, select, textarea');
1538
1607
  for (var i = 0; i < ctrls.length; i++) {
1539
- ctrls[i].disabled = hidden;
1608
+ ctrls[i].disabled = hidden || ctrls[i].getAttribute('data-pwc-static-disabled') === '1';
1540
1609
  }
1541
1610
  if (!hidden && k && !pwcIsFieldDirty(k) && Object.prototype.hasOwnProperty.call(vals, k)) {
1542
1611
  setControlValue(form, k, vals[k]);
@@ -1701,8 +1770,8 @@ function runPromptCatalogWebUIImpl(options) {
1701
1770
  var inp = getControl(form, k);
1702
1771
  if (inp == null) { continue; }
1703
1772
  if (typeof inp.checkValidity === 'function' && !inp.checkValidity()) {
1704
- var vm = (typeof inp.validationMessage === 'string' && inp.validationMessage) ? inp.validationMessage : 'Invalid value';
1705
- pwcSetFieldError(k, vm);
1773
+ var vm = (typeof inp.validationMessage === 'string' && inp.validationMessage) ? inp.validationMessage : uiText.invalidValue;
1774
+ pwcSetFieldError(k, vm);
1706
1775
  if (typeof inp.focus === 'function') { inp.focus(); }
1707
1776
  return false;
1708
1777
  }
@@ -1760,7 +1829,7 @@ function runPromptCatalogWebUIImpl(options) {
1760
1829
  if (pwcValStep == null) {
1761
1830
  return reflow().then(function () { pwcSetStep(pwcVisibleNext(pwcCur)); });
1762
1831
  }
1763
- pwcSetStatusHint('Checking…');
1832
+ pwcSetStatusHint(uiText.checking);
1764
1833
  return fetch(pwcValStep, {
1765
1834
  method: 'POST',
1766
1835
  headers: { 'Content-Type': 'application/json' },
@@ -1828,7 +1897,7 @@ function runPromptCatalogWebUIImpl(options) {
1828
1897
  form.addEventListener('submit', function (e) {
1829
1898
  e.preventDefault();
1830
1899
  if (pwcN > 1 && pwcVisibleStepPosition(pwcCur) < pwcVisibleSteps.length - 1) { return; }
1831
- pwcSetStatusHint('Sending…');
1900
+ pwcSetStatusHint(uiText.sending);
1832
1901
  pwcClearAllFieldErrors();
1833
1902
  fetch(sub, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(collect()) })
1834
1903
  .then(function (r) {
@@ -1843,7 +1912,7 @@ function runPromptCatalogWebUIImpl(options) {
1843
1912
  return r.json();
1844
1913
  })
1845
1914
  .then(function () {
1846
- pwcSetStatusSuccess('Saved. This tab will close automatically in 2 seconds.');
1915
+ pwcSetStatusSuccess(uiText.savedAndClosing);
1847
1916
  pwcScheduleWindowClose();
1848
1917
  })
1849
1918
  .catch(function (err) {
@@ -1913,10 +1982,10 @@ function runPromptCatalogWebUIImpl(options) {
1913
1982
  : NaN;
1914
1983
  if (!Number.isInteger(step) || step < 0 || step >= pwcStepDefs.length) {
1915
1984
  res.writeHead(400, { 'Content-Type': 'application/json' });
1916
- res.end(JSON.stringify({ ok: false, error: 'Invalid or missing wizard step.' }));
1985
+ res.end(JSON.stringify({ ok: false, error: t('promptCatalog.web.invalidStep') }));
1917
1986
  return;
1918
1987
  }
1919
- const { error, fieldKey } = await buildWebPresetFromBody(catalog, readFormFromClient(readFormFromClientStrippingPwcMeta(raw)), userSeed, { scopeKeys: new Set(pwcStepDefs[step].keys) });
1988
+ const { error, fieldKey } = await buildWebPresetFromBody(catalog, readFormFromClient(readFormFromClientStrippingPwcMeta(raw)), userSeed, { scopeKeys: new Set(pwcStepDefs[step].keys), locale });
1920
1989
  if (error) {
1921
1990
  res.writeHead(400, { 'Content-Type': 'application/json' });
1922
1991
  res.end(JSON.stringify(fieldKey ? { ok: false, error, fieldKey } : { ok: false, error }));
@@ -1927,7 +1996,7 @@ function runPromptCatalogWebUIImpl(options) {
1927
1996
  }
1928
1997
  catch {
1929
1998
  res.writeHead(400, { 'Content-Type': 'application/json' });
1930
- res.end(JSON.stringify({ ok: false, error: 'Invalid request' }));
1999
+ res.end(JSON.stringify({ ok: false, error: t('promptCatalog.web.invalidRequest') }));
1931
2000
  }
1932
2001
  })();
1933
2002
  });
@@ -1946,10 +2015,10 @@ function runPromptCatalogWebUIImpl(options) {
1946
2015
  : '';
1947
2016
  if (!fieldKey || !Object.prototype.hasOwnProperty.call(catalog, fieldKey)) {
1948
2017
  res.writeHead(400, { 'Content-Type': 'application/json' });
1949
- res.end(JSON.stringify({ ok: false, error: 'Invalid or missing field key.' }));
2018
+ res.end(JSON.stringify({ ok: false, error: t('promptCatalog.web.invalidField') }));
1950
2019
  return;
1951
2020
  }
1952
- const { error } = await buildWebPresetFromBody(catalog, readFormFromClient(readFormFromClientStrippingPwcMeta(raw)), userSeed, { scopeKeys: new Set([fieldKey]) });
2021
+ const { error } = await buildWebPresetFromBody(catalog, readFormFromClient(readFormFromClientStrippingPwcMeta(raw)), userSeed, { scopeKeys: new Set([fieldKey]), locale });
1953
2022
  if (error) {
1954
2023
  res.writeHead(400, { 'Content-Type': 'application/json' });
1955
2024
  res.end(JSON.stringify({ ok: false, error, fieldKey }));
@@ -1960,7 +2029,7 @@ function runPromptCatalogWebUIImpl(options) {
1960
2029
  }
1961
2030
  catch {
1962
2031
  res.writeHead(400, { 'Content-Type': 'application/json' });
1963
- res.end(JSON.stringify({ ok: false, error: 'Invalid request' }));
2032
+ res.end(JSON.stringify({ ok: false, error: t('promptCatalog.web.invalidRequest') }));
1964
2033
  }
1965
2034
  })();
1966
2035
  });
@@ -1973,7 +2042,7 @@ function runPromptCatalogWebUIImpl(options) {
1973
2042
  void (async () => {
1974
2043
  try {
1975
2044
  const raw = JSON.parse(Buffer.concat(chunks).toString('utf8'));
1976
- const { preset, error, fieldKey } = await buildWebPresetFromBody(catalog, readFormFromClient(raw), userSeed);
2045
+ const { preset, error, fieldKey } = await buildWebPresetFromBody(catalog, readFormFromClient(raw), userSeed, { locale });
1977
2046
  if (error) {
1978
2047
  res.writeHead(400, { 'Content-Type': 'application/json' });
1979
2048
  res.end(JSON.stringify(fieldKey ? { ok: false, error, fieldKey } : { ok: false, error }));
@@ -1994,7 +2063,7 @@ function runPromptCatalogWebUIImpl(options) {
1994
2063
  }
1995
2064
  catch (e) {
1996
2065
  res.writeHead(400, { 'Content-Type': 'text/plain' });
1997
- res.end('Invalid request');
2066
+ res.end(t('promptCatalog.web.invalidRequest'));
1998
2067
  }
1999
2068
  })();
2000
2069
  });