@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.
@@ -1,5 +1,27 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
1
9
  import { promises as fs } from 'node:fs';
2
10
  import { resolveServerRequestTarget } from './env-auth.js';
11
+ const CLI_REQUEST_SOURCE_HEADER = 'x-request-source';
12
+ const CLI_REQUEST_SOURCE_VALUE = 'cli';
13
+ function stripUtf8Bom(text) {
14
+ return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
15
+ }
16
+ function parseJsonInput(raw, flagName) {
17
+ const content = stripUtf8Bom(raw);
18
+ try {
19
+ return JSON.parse(content);
20
+ }
21
+ catch (error) {
22
+ throw new Error(`Invalid JSON for --${flagName}: ${error?.message ?? 'parse failed'}`);
23
+ }
24
+ }
3
25
  function normalizeBaseUrl(baseUrl) {
4
26
  return baseUrl.replace(/\/+$/, '');
5
27
  }
@@ -59,6 +81,28 @@ function listProvidedBodyFlags(flags, parameters) {
59
81
  .filter((parameter) => hasParameterValue(flags, parameter))
60
82
  .map((parameter) => `--${parameter.flagName}`);
61
83
  }
84
+ function parseBodyFieldValue(rawValue, parameter) {
85
+ if (rawValue === undefined) {
86
+ return undefined;
87
+ }
88
+ if (parameter.isArray && !parameter.jsonEncoded) {
89
+ return Array.isArray(rawValue) ? rawValue : rawValue ? [rawValue] : undefined;
90
+ }
91
+ if (parameter.jsonEncoded || parameter.type === 'object' || parameter.type === 'array') {
92
+ if (typeof rawValue !== 'string') {
93
+ return rawValue;
94
+ }
95
+ const parsed = parseJsonInput(rawValue, parameter.flagName);
96
+ if (parameter.type === 'array' && !Array.isArray(parsed)) {
97
+ throw new Error(`--${parameter.flagName} must be a JSON array`);
98
+ }
99
+ if (parameter.type === 'object' && (parsed === null || Array.isArray(parsed) || typeof parsed !== 'object')) {
100
+ throw new Error(`--${parameter.flagName} must be a JSON object`);
101
+ }
102
+ return parsed;
103
+ }
104
+ return parseScalarValue(rawValue, parameter.type);
105
+ }
62
106
  export async function parseBody(flags, operation) {
63
107
  const inlineBody = flags.body;
64
108
  const bodyFile = flags['body-file'];
@@ -70,10 +114,10 @@ export async function parseBody(flags, operation) {
70
114
  throw new Error(`Conflicting request body inputs: received ${rawBodyInput} together with body field flags (${providedBodyFlags.join(', ')}). Use either body field flags or --body/--body-file.`);
71
115
  }
72
116
  if (inlineBody) {
73
- return JSON.parse(inlineBody);
117
+ return parseJsonInput(inlineBody, 'body');
74
118
  }
75
119
  if (bodyFile) {
76
- return fs.readFile(bodyFile, 'utf8').then((content) => JSON.parse(content));
120
+ return fs.readFile(bodyFile, 'utf8').then((content) => parseJsonInput(content, 'body-file'));
77
121
  }
78
122
  if (!bodyParameters.length) {
79
123
  return undefined;
@@ -81,9 +125,7 @@ export async function parseBody(flags, operation) {
81
125
  const body = {};
82
126
  for (const parameter of bodyParameters) {
83
127
  const rawValue = flags[parameter.flagName];
84
- const value = parameter.isArray && !parameter.jsonEncoded
85
- ? (Array.isArray(rawValue) ? rawValue : rawValue ? [rawValue] : undefined)
86
- : parseScalarValue(rawValue, parameter.type);
128
+ const value = parseBodyFieldValue(rawValue, parameter);
87
129
  if (parameter.required && (value === undefined || value === '')) {
88
130
  throw new Error(`Missing required body field --${parameter.flagName}`);
89
131
  }
@@ -103,6 +145,7 @@ export async function parseBody(flags, operation) {
103
145
  export async function executeApiRequest(options) {
104
146
  const { baseUrl, token } = await resolveServerRequestTarget(options);
105
147
  const headers = new Headers();
148
+ headers.set(CLI_REQUEST_SOURCE_HEADER, CLI_REQUEST_SOURCE_VALUE);
106
149
  if (token) {
107
150
  headers.set('authorization', `Bearer ${token}`);
108
151
  }
@@ -162,6 +205,7 @@ export async function executeApiRequest(options) {
162
205
  export async function executeRawApiRequest(options) {
163
206
  const { baseUrl, token } = await resolveServerRequestTarget(options);
164
207
  const headers = new Headers();
208
+ headers.set(CLI_REQUEST_SOURCE_HEADER, CLI_REQUEST_SOURCE_VALUE);
165
209
  if (token) {
166
210
  headers.set('authorization', `Bearer ${token}`);
167
211
  }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ import { readFileSync } from 'node:fs';
10
+ export const SUPPORTED_CLI_LOCALES = ['en-US', 'zh-CN'];
11
+ export const CLI_LOCALE_FLAG_OPTIONS = [...SUPPORTED_CLI_LOCALES];
12
+ export const CLI_LOCALE_FLAG_DESCRIPTION = 'Language for CLI prompts and the local setup UI.';
13
+ const DEFAULT_CLI_LOCALE = 'en-US';
14
+ const localeCache = {};
15
+ function normalizeCliLocale(value) {
16
+ const raw = String(value ?? '').trim();
17
+ if (!raw) {
18
+ return undefined;
19
+ }
20
+ const normalized = raw.replace(/\..*$/, '').replace(/_/g, '-').toLowerCase();
21
+ if (normalized === 'zh' || normalized.startsWith('zh-')) {
22
+ return 'zh-CN';
23
+ }
24
+ if (normalized === 'en' || normalized.startsWith('en-')) {
25
+ return 'en-US';
26
+ }
27
+ return undefined;
28
+ }
29
+ function loadLocaleMessages(locale) {
30
+ if (localeCache[locale]) {
31
+ return localeCache[locale];
32
+ }
33
+ const fileUrl = new URL(`../locale/${locale}.json`, import.meta.url);
34
+ const parsed = JSON.parse(readFileSync(fileUrl, 'utf8'));
35
+ localeCache[locale] = parsed;
36
+ return parsed;
37
+ }
38
+ function getPathValue(input, path) {
39
+ let current = input;
40
+ for (const part of path.split('.')) {
41
+ if (!current || typeof current !== 'object' || !Object.prototype.hasOwnProperty.call(current, part)) {
42
+ return undefined;
43
+ }
44
+ current = current[part];
45
+ }
46
+ return current;
47
+ }
48
+ function interpolateTemplate(template, values) {
49
+ return template.replace(/{{\s*([\w.]+)\s*}}/g, (_match, key) => {
50
+ const value = getPathValue(values, key);
51
+ return value === undefined || value === null ? '' : String(value);
52
+ });
53
+ }
54
+ export function detectCliLocale() {
55
+ const candidates = [
56
+ process.env.NB_LOCALE,
57
+ process.env.LC_ALL,
58
+ process.env.LC_MESSAGES,
59
+ process.env.LANG,
60
+ Intl.DateTimeFormat().resolvedOptions().locale,
61
+ ];
62
+ for (const candidate of candidates) {
63
+ const locale = normalizeCliLocale(candidate);
64
+ if (locale) {
65
+ return locale;
66
+ }
67
+ }
68
+ return DEFAULT_CLI_LOCALE;
69
+ }
70
+ export function resolveCliLocale(preferred) {
71
+ return normalizeCliLocale(preferred) ?? detectCliLocale();
72
+ }
73
+ export function applyCliLocale(preferred) {
74
+ const locale = resolveCliLocale(preferred);
75
+ process.env.NB_LOCALE = locale;
76
+ return locale;
77
+ }
78
+ export function createCliTranslate(preferred) {
79
+ const locale = resolveCliLocale(preferred);
80
+ return (key, values, fallback) => {
81
+ const messages = loadLocaleMessages(locale);
82
+ const template = getPathValue(messages, key);
83
+ if (typeof template !== 'string') {
84
+ return interpolateTemplate(fallback ?? key, values);
85
+ }
86
+ return interpolateTemplate(template, values);
87
+ };
88
+ }
89
+ export function translateCli(key, values, options) {
90
+ return createCliTranslate(options?.locale)(key, values, options?.fallback);
91
+ }
92
+ export function localeText(key, values, fallback) {
93
+ return {
94
+ key,
95
+ ...(values ? { values } : {}),
96
+ ...(fallback ? { fallback } : {}),
97
+ };
98
+ }
99
+ export function isLocalizedTextDef(value) {
100
+ return Boolean(value
101
+ && typeof value === 'object'
102
+ && typeof value.key === 'string');
103
+ }
104
+ export function resolveLocalizedText(text, options) {
105
+ if (text === undefined) {
106
+ return options?.fallback ?? '';
107
+ }
108
+ if (typeof text === 'string') {
109
+ return text;
110
+ }
111
+ return translateCli(text.key, text.values, {
112
+ locale: options?.locale,
113
+ fallback: text.fallback ?? options?.fallback,
114
+ });
115
+ }
@@ -483,7 +483,7 @@ export function buildOauthCompletionHtml() {
483
483
  statusMark: '✓',
484
484
  heading: 'Authentication complete',
485
485
  description: 'Your sign-in finished successfully. You can return to the terminal and continue there.',
486
- tip: 'This page will try to close automatically in a moment.',
486
+ tip: 'This page will close automatically in 10 seconds.',
487
487
  footer: 'You can close this page after returning to the terminal.',
488
488
  extraScriptHtml: ` <script>
489
489
  setTimeout(function () {
@@ -494,7 +494,7 @@ export function buildOauthCompletionHtml() {
494
494
  el.textContent = 'If this tab stays open, you can close it manually.';
495
495
  }
496
496
  }, 400);
497
- }, 1000);
497
+ }, 10000);
498
498
  </script>`,
499
499
  });
500
500
  }
@@ -8,16 +8,30 @@
8
8
  */
9
9
  import * as p from '@clack/prompts';
10
10
  import { exit, stdin as stdinStream, stdout as stdoutStream } from 'node:process';
11
+ import { createCliTranslate, resolveCliLocale, resolveLocalizedText, } from "./cli-locale.js";
11
12
  export function selectOptionValues(options) {
12
13
  return options.map((o) => (typeof o === 'string' ? o : o.value));
13
14
  }
14
- function clackSelectOptions(options) {
15
+ function resolvePromptText(text, locale, fallback = '') {
16
+ return resolveLocalizedText(text, { locale, fallback });
17
+ }
18
+ function clackSelectOptions(options, locale) {
15
19
  return options.map((o) => typeof o === 'string'
16
20
  ? { value: o, label: o }
17
- : { value: o.value, label: o.label ?? o.value, ...(o.hint !== undefined ? { hint: o.hint } : {}) });
21
+ : {
22
+ value: o.value,
23
+ label: resolvePromptText(o.label, locale, o.value),
24
+ ...(o.hint !== undefined ? { hint: resolvePromptText(o.hint, locale) } : {}),
25
+ ...(o.disabled !== undefined ? { disabled: o.disabled } : {}),
26
+ });
27
+ }
28
+ function enabledSelectOptionValues(options) {
29
+ return options
30
+ .filter((o) => typeof o === 'string' || o.disabled !== true)
31
+ .map((o) => (typeof o === 'string' ? o : o.value));
18
32
  }
19
- function defaultOnCancel() {
20
- p.cancel('Cancelled.');
33
+ function defaultOnCancel(locale) {
34
+ p.cancel(createCliTranslate(locale)('promptCatalog.common.cancelled'));
21
35
  exit(0);
22
36
  }
23
37
  function defaultOnMissingNonInteractive(message) {
@@ -55,21 +69,22 @@ function mergedBoolean(key, def, iv, useYesInitial) {
55
69
  }
56
70
  function mergedSelect(key, def, iv, useYesInitial) {
57
71
  const valueList = selectOptionValues(def.options);
72
+ const enabledValueList = enabledSelectOptionValues(def.options);
58
73
  if (hasIvKey(iv, key)) {
59
74
  const s = String(iv[key]);
60
- if (valueList.includes(s)) {
75
+ if (enabledValueList.includes(s)) {
61
76
  return s;
62
77
  }
63
78
  return undefined;
64
79
  }
65
- if (useYesInitial && def.yesInitialValue !== undefined && valueList.includes(def.yesInitialValue)) {
80
+ if (useYesInitial && def.yesInitialValue !== undefined && enabledValueList.includes(def.yesInitialValue)) {
66
81
  return def.yesInitialValue;
67
82
  }
68
83
  const d = def.initialValue;
69
- if (d !== undefined && valueList.includes(d)) {
84
+ if (d !== undefined && enabledValueList.includes(d)) {
70
85
  return d;
71
86
  }
72
- return valueList[0];
87
+ return enabledValueList[0];
73
88
  }
74
89
  function mergedInteger(key, def, iv, useYesInitial) {
75
90
  if (hasIvKey(iv, key)) {
@@ -121,7 +136,8 @@ export function isPromptBlockSkipped(def, values) {
121
136
  * If **`preset`** defines **`key`**, validate and set **`out[key]`**, return **`true`** (caller should `continue`).
122
137
  * No-op for non-input block types.
123
138
  */
124
- function tryApplyPreset(key, def, preset, out, hooks) {
139
+ function tryApplyPreset(key, def, preset, out, hooks, locale) {
140
+ const t = createCliTranslate(locale);
125
141
  if (!hasIvKey(preset, key)) {
126
142
  return false;
127
143
  }
@@ -134,7 +150,7 @@ function tryApplyPreset(key, def, preset, out, hooks) {
134
150
  case 'text': {
135
151
  const s = String(raw ?? '');
136
152
  if (def.required && isBlankText(s)) {
137
- hooks.onMissingNonInteractive(`"${key}" is required; set a non-empty values.${key} or omit it to prompt.`);
153
+ hooks.onMissingNonInteractive(t('promptCatalog.preset.required', { key }));
138
154
  }
139
155
  out[key] = s;
140
156
  return true;
@@ -147,7 +163,7 @@ function tryApplyPreset(key, def, preset, out, hooks) {
147
163
  const valueList = selectOptionValues(def.options);
148
164
  const s = String(raw ?? '');
149
165
  if (!valueList.includes(s)) {
150
- hooks.onMissingNonInteractive(`Invalid values.${key}: "${s}". Expected one of: ${valueList.join(', ')}`);
166
+ hooks.onMissingNonInteractive(t('promptCatalog.preset.invalidSelect', { key, value: s, options: valueList.join(', ') }));
151
167
  }
152
168
  out[key] = s;
153
169
  return true;
@@ -155,7 +171,7 @@ function tryApplyPreset(key, def, preset, out, hooks) {
155
171
  case 'password': {
156
172
  const s = String(raw ?? '');
157
173
  if (def.required && isBlankText(s)) {
158
- hooks.onMissingNonInteractive(`"${key}" is required; set a non-empty values.${key} or omit it to prompt.`);
174
+ hooks.onMissingNonInteractive(t('promptCatalog.preset.required', { key }));
159
175
  }
160
176
  out[key] = s;
161
177
  return true;
@@ -168,13 +184,13 @@ function tryApplyPreset(key, def, preset, out, hooks) {
168
184
  const s = String(raw ?? '').trim();
169
185
  if (s === '') {
170
186
  if (def.required) {
171
- hooks.onMissingNonInteractive(`"${key}" is required; set values.${key} or omit it to prompt.`);
187
+ hooks.onMissingNonInteractive(t('promptCatalog.preset.required', { key }));
172
188
  }
173
189
  out[key] = def.initialValue ?? 0;
174
190
  return true;
175
191
  }
176
192
  if (!/^-?\d+$/.test(s)) {
177
- hooks.onMissingNonInteractive(`Invalid values.${key}: must be an integer.`);
193
+ hooks.onMissingNonInteractive(t('promptCatalog.preset.invalidInteger', { key }));
178
194
  }
179
195
  out[key] = Number.parseInt(s, 10);
180
196
  return true;
@@ -200,12 +216,14 @@ function tryApplyPreset(key, def, preset, out, hooks) {
200
216
  * Input blocks may set **`validate(value, values)`** (sync or async): return a string to fail; used after required/type checks, and by the local web form on submit. When **`validate`** is set, interactive TTY steps re-ask on failure (`log.error` + retry) except for simple fields without a custom `validate` (fast path).
201
217
  */
202
218
  export async function runPromptCatalog(catalog, options = {}) {
219
+ const locale = resolveCliLocale(options.locale);
220
+ const t = createCliTranslate(locale);
203
221
  const promptIv = options.initialValues ?? {};
204
222
  const yesIv = options.yesInitialValues ?? {};
205
223
  const resolveIv = options.yes ? { ...promptIv, ...yesIv } : promptIv;
206
224
  const useYesInitial = Boolean(options.yes);
207
225
  const hooks = {
208
- onCancel: options.hooks?.onCancel ?? defaultOnCancel,
226
+ onCancel: options.hooks?.onCancel ?? (() => defaultOnCancel(locale)),
209
227
  onMissingNonInteractive: options.hooks?.onMissingNonInteractive ?? defaultOnMissingNonInteractive,
210
228
  };
211
229
  const interactive = Boolean(stdinStream.isTTY && stdoutStream.isTTY && !options.yes);
@@ -213,7 +231,7 @@ export async function runPromptCatalog(catalog, options = {}) {
213
231
  const out = {};
214
232
  for (const [key, def] of Object.entries(catalog)) {
215
233
  // Apply `values` presets before `hidden` / `when` so CLI/env fixes still win without UI.
216
- if (tryApplyPreset(key, def, preset, out, hooks)) {
234
+ if (tryApplyPreset(key, def, preset, out, hooks, locale)) {
217
235
  const errV = await runPromptFieldValidate(def, out[key], out);
218
236
  if (errV) {
219
237
  hooks.onMissingNonInteractive(errV);
@@ -224,11 +242,11 @@ export async function runPromptCatalog(catalog, options = {}) {
224
242
  continue;
225
243
  }
226
244
  if (def.type === 'intro') {
227
- p.intro(def.title);
245
+ p.intro(resolvePromptText(def.title, locale));
228
246
  continue;
229
247
  }
230
248
  if (def.type === 'outro') {
231
- p.outro(def.message);
249
+ p.outro(resolvePromptText(def.message, locale));
232
250
  continue;
233
251
  }
234
252
  if (def.type === 'run') {
@@ -236,10 +254,14 @@ export async function runPromptCatalog(catalog, options = {}) {
236
254
  continue;
237
255
  }
238
256
  if (def.type === 'text') {
257
+ const message = resolvePromptText(def.message, locale, key);
258
+ const placeholder = def.placeholder !== undefined
259
+ ? resolvePromptText(def.placeholder, locale)
260
+ : undefined;
239
261
  if (!interactive) {
240
262
  const merged = mergedText(key, def, resolveIv, useYesInitial, out);
241
263
  if (def.required && isBlankText(merged)) {
242
- hooks.onMissingNonInteractive(`Non-interactive: "${key}" is required; set initialValues.${key}, yesInitialValues.${key}, yesInitialValue on the block, or initialValue.`);
264
+ hooks.onMissingNonInteractive(t('promptCatalog.nonInteractive.textRequired', { key }));
243
265
  }
244
266
  out[key] = merged;
245
267
  const errT = await runPromptFieldValidate(def, merged, { ...out, [key]: merged });
@@ -253,16 +275,16 @@ export async function runPromptCatalog(catalog, options = {}) {
253
275
  let last = merged;
254
276
  for (;;) {
255
277
  const raw = await p.text({
256
- message: def.message,
278
+ message,
257
279
  initialValue: last,
258
- ...(def.placeholder !== undefined ? { placeholder: def.placeholder } : {}),
280
+ ...(placeholder !== undefined ? { placeholder } : {}),
259
281
  });
260
282
  if (p.isCancel(raw)) {
261
283
  hooks.onCancel();
262
284
  }
263
285
  const s = typeof raw === 'string' ? raw : last;
264
286
  if (def.required && isBlankText(s)) {
265
- p.log.error('Required');
287
+ p.log.error(t('promptCatalog.common.required'));
266
288
  last = s;
267
289
  continue;
268
290
  }
@@ -278,10 +300,10 @@ export async function runPromptCatalog(catalog, options = {}) {
278
300
  continue;
279
301
  }
280
302
  const raw = await p.text({
281
- message: def.message,
303
+ message,
282
304
  initialValue: merged,
283
- ...(def.placeholder !== undefined ? { placeholder: def.placeholder } : {}),
284
- validate: def.required ? (value) => (isBlankText(value) ? 'Required' : undefined) : undefined,
305
+ ...(placeholder !== undefined ? { placeholder } : {}),
306
+ validate: def.required ? (value) => (isBlankText(value) ? t('promptCatalog.common.required') : undefined) : undefined,
285
307
  });
286
308
  if (p.isCancel(raw)) {
287
309
  hooks.onCancel();
@@ -290,6 +312,7 @@ export async function runPromptCatalog(catalog, options = {}) {
290
312
  continue;
291
313
  }
292
314
  if (def.type === 'boolean') {
315
+ const message = resolvePromptText(def.message, locale, key);
293
316
  if (!interactive) {
294
317
  const b = mergedBoolean(key, def, resolveIv, useYesInitial);
295
318
  out[key] = b;
@@ -303,7 +326,7 @@ export async function runPromptCatalog(catalog, options = {}) {
303
326
  if (def.validate) {
304
327
  for (;;) {
305
328
  const raw = await p.confirm({
306
- message: def.message,
329
+ message,
307
330
  initialValue: merged,
308
331
  });
309
332
  if (p.isCancel(raw)) {
@@ -321,7 +344,7 @@ export async function runPromptCatalog(catalog, options = {}) {
321
344
  continue;
322
345
  }
323
346
  const raw = await p.confirm({
324
- message: def.message,
347
+ message,
325
348
  initialValue: merged,
326
349
  });
327
350
  if (p.isCancel(raw)) {
@@ -331,9 +354,10 @@ export async function runPromptCatalog(catalog, options = {}) {
331
354
  continue;
332
355
  }
333
356
  if (def.type === 'select') {
357
+ const message = resolvePromptText(def.message, locale, key);
334
358
  const valueList = selectOptionValues(def.options);
335
359
  if (def.required && def.options.length === 0) {
336
- hooks.onMissingNonInteractive(`Select "${key}" is required but has no options.`);
360
+ hooks.onMissingNonInteractive(t('promptCatalog.nonInteractive.selectRequiredNoOptions', { key }));
337
361
  }
338
362
  if (!interactive) {
339
363
  const merged = mergedSelect(key, def, resolveIv, useYesInitial);
@@ -344,8 +368,8 @@ export async function runPromptCatalog(catalog, options = {}) {
344
368
  ? String(promptIv[key])
345
369
  : undefined;
346
370
  hooks.onMissingNonInteractive(bad !== undefined
347
- ? `Invalid value for ${key}: ${bad}. Expected one of: ${valueList.join(', ')}`
348
- : `Non-interactive: set initialValues.${key}, yesInitialValues.${key}, or select.initialValue / yesInitialValue / options on the catalog block.`);
371
+ ? t('promptCatalog.nonInteractive.selectInvalidValue', { key, value: bad, options: valueList.join(', ') })
372
+ : t('promptCatalog.nonInteractive.selectMissingDefault', { key }));
349
373
  }
350
374
  out[key] = merged;
351
375
  const errS = await runPromptFieldValidate(def, merged, { ...out, [key]: merged });
@@ -360,15 +384,15 @@ export async function runPromptCatalog(catalog, options = {}) {
360
384
  valueList[0];
361
385
  if (uiInitial === undefined || !valueList.includes(uiInitial)) {
362
386
  const hint = def.required
363
- ? `Select "${key}" is required; set initialValues.${key} or select.initialValue / options on the catalog block.`
364
- : `Select "${key}" has no valid default; set initialValues.${key} or options on the catalog block.`;
387
+ ? t('promptCatalog.nonInteractive.selectRequiredInteractive', { key })
388
+ : t('promptCatalog.nonInteractive.selectMissingInteractiveDefault', { key });
365
389
  hooks.onMissingNonInteractive(hint);
366
390
  }
367
391
  if (def.validate) {
368
392
  for (;;) {
369
393
  const raw = await p.select({
370
- message: def.message,
371
- options: clackSelectOptions(def.options),
394
+ message,
395
+ options: clackSelectOptions(def.options, locale),
372
396
  initialValue: uiInitial,
373
397
  });
374
398
  if (p.isCancel(raw)) {
@@ -386,8 +410,8 @@ export async function runPromptCatalog(catalog, options = {}) {
386
410
  continue;
387
411
  }
388
412
  const raw = await p.select({
389
- message: def.message,
390
- options: clackSelectOptions(def.options),
413
+ message,
414
+ options: clackSelectOptions(def.options, locale),
391
415
  initialValue: uiInitial,
392
416
  });
393
417
  if (p.isCancel(raw)) {
@@ -397,11 +421,12 @@ export async function runPromptCatalog(catalog, options = {}) {
397
421
  continue;
398
422
  }
399
423
  if (def.type === 'password') {
424
+ const message = resolvePromptText(def.message, locale, key);
400
425
  if (!interactive) {
401
426
  const merged = mergedPassword(key, def, resolveIv, useYesInitial);
402
427
  if (merged === undefined) {
403
428
  if (def.required) {
404
- hooks.onMissingNonInteractive(`Non-interactive: "${key}" is required; set initialValues.${key}, yesInitialValues.${key}, or initialValue / yesInitialValue on the block.`);
429
+ hooks.onMissingNonInteractive(t('promptCatalog.nonInteractive.passwordRequired', { key }));
405
430
  }
406
431
  out[key] = '';
407
432
  const errPE = await runPromptFieldValidate(def, '', { ...out, [key]: '' });
@@ -411,7 +436,7 @@ export async function runPromptCatalog(catalog, options = {}) {
411
436
  continue;
412
437
  }
413
438
  if (def.required && isBlankText(merged)) {
414
- hooks.onMissingNonInteractive(`Non-interactive: "${key}" is required; set a non-empty initialValues / yesInitialValues / initialValue / yesInitialValue.`);
439
+ hooks.onMissingNonInteractive(t('promptCatalog.nonInteractive.passwordRequiredNonEmpty', { key }));
415
440
  }
416
441
  out[key] = merged;
417
442
  const errP = await runPromptFieldValidate(def, merged, { ...out, [key]: merged });
@@ -423,15 +448,15 @@ export async function runPromptCatalog(catalog, options = {}) {
423
448
  if (def.validate) {
424
449
  for (;;) {
425
450
  const raw = await p.password({
426
- message: def.message,
427
- validate: def.required ? (value) => (isBlankText(value) ? 'Required' : undefined) : undefined,
451
+ message,
452
+ validate: def.required ? (value) => (isBlankText(value) ? t('promptCatalog.common.required') : undefined) : undefined,
428
453
  });
429
454
  if (p.isCancel(raw)) {
430
455
  hooks.onCancel();
431
456
  }
432
457
  const s = typeof raw === 'string' ? raw : '';
433
458
  if (def.required && isBlankText(s)) {
434
- p.log.error('Required');
459
+ p.log.error(t('promptCatalog.common.required'));
435
460
  continue;
436
461
  }
437
462
  const errP = await runPromptFieldValidate(def, s, { ...out, [key]: s });
@@ -445,8 +470,8 @@ export async function runPromptCatalog(catalog, options = {}) {
445
470
  continue;
446
471
  }
447
472
  const raw = await p.password({
448
- message: def.message,
449
- validate: def.required ? (value) => (isBlankText(value) ? 'Required' : undefined) : undefined,
473
+ message,
474
+ validate: def.required ? (value) => (isBlankText(value) ? t('promptCatalog.common.required') : undefined) : undefined,
450
475
  });
451
476
  if (p.isCancel(raw)) {
452
477
  hooks.onCancel();
@@ -455,11 +480,15 @@ export async function runPromptCatalog(catalog, options = {}) {
455
480
  continue;
456
481
  }
457
482
  if (def.type === 'integer') {
483
+ const message = resolvePromptText(def.message, locale, key);
484
+ const placeholder = def.placeholder !== undefined
485
+ ? resolvePromptText(def.placeholder, locale)
486
+ : undefined;
458
487
  if (!interactive) {
459
488
  const merged = mergedInteger(key, def, resolveIv, useYesInitial);
460
489
  if (merged === undefined) {
461
490
  if (def.required) {
462
- hooks.onMissingNonInteractive(`Non-interactive: "${key}" is required; set initialValues.${key}, yesInitialValues.${key}, or initialValue / yesInitialValue on the block.`);
491
+ hooks.onMissingNonInteractive(t('promptCatalog.nonInteractive.integerRequired', { key }));
463
492
  }
464
493
  const z = def.initialValue ?? 0;
465
494
  out[key] = z;
@@ -482,16 +511,16 @@ export async function runPromptCatalog(catalog, options = {}) {
482
511
  let last = lineDefault;
483
512
  for (;;) {
484
513
  const raw = await p.text({
485
- message: def.message,
514
+ message,
486
515
  initialValue: last,
487
- ...(def.placeholder !== undefined ? { placeholder: def.placeholder } : {}),
516
+ ...(placeholder !== undefined ? { placeholder } : {}),
488
517
  validate: (value) => {
489
- const t = value.trim();
490
- if (t === '') {
491
- return def.required ? 'Required' : undefined;
518
+ const trimmed = value.trim();
519
+ if (trimmed === '') {
520
+ return def.required ? t('promptCatalog.common.required') : undefined;
492
521
  }
493
- if (!/^-?\d+$/.test(t)) {
494
- return 'Must be an integer';
522
+ if (!/^-?\d+$/.test(trimmed)) {
523
+ return t('promptCatalog.common.mustBeInteger');
495
524
  }
496
525
  return undefined;
497
526
  },
@@ -523,16 +552,16 @@ export async function runPromptCatalog(catalog, options = {}) {
523
552
  continue;
524
553
  }
525
554
  const raw = await p.text({
526
- message: def.message,
555
+ message,
527
556
  initialValue: lineDefault,
528
- ...(def.placeholder !== undefined ? { placeholder: def.placeholder } : {}),
557
+ ...(placeholder !== undefined ? { placeholder } : {}),
529
558
  validate: (value) => {
530
- const t = value.trim();
531
- if (t === '') {
532
- return def.required ? 'Required' : undefined;
559
+ const trimmed = value.trim();
560
+ if (trimmed === '') {
561
+ return def.required ? t('promptCatalog.common.required') : undefined;
533
562
  }
534
- if (!/^-?\d+$/.test(t)) {
535
- return 'Must be an integer';
563
+ if (!/^-?\d+$/.test(trimmed)) {
564
+ return t('promptCatalog.common.mustBeInteger');
536
565
  }
537
566
  return undefined;
538
567
  },