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

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.
@@ -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,24 @@
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
+ });
18
26
  }
19
- function defaultOnCancel() {
20
- p.cancel('Cancelled.');
27
+ function defaultOnCancel(locale) {
28
+ p.cancel(createCliTranslate(locale)('promptCatalog.common.cancelled'));
21
29
  exit(0);
22
30
  }
23
31
  function defaultOnMissingNonInteractive(message) {
@@ -121,7 +129,8 @@ export function isPromptBlockSkipped(def, values) {
121
129
  * If **`preset`** defines **`key`**, validate and set **`out[key]`**, return **`true`** (caller should `continue`).
122
130
  * No-op for non-input block types.
123
131
  */
124
- function tryApplyPreset(key, def, preset, out, hooks) {
132
+ function tryApplyPreset(key, def, preset, out, hooks, locale) {
133
+ const t = createCliTranslate(locale);
125
134
  if (!hasIvKey(preset, key)) {
126
135
  return false;
127
136
  }
@@ -134,7 +143,7 @@ function tryApplyPreset(key, def, preset, out, hooks) {
134
143
  case 'text': {
135
144
  const s = String(raw ?? '');
136
145
  if (def.required && isBlankText(s)) {
137
- hooks.onMissingNonInteractive(`"${key}" is required; set a non-empty values.${key} or omit it to prompt.`);
146
+ hooks.onMissingNonInteractive(t('promptCatalog.preset.required', { key }));
138
147
  }
139
148
  out[key] = s;
140
149
  return true;
@@ -147,7 +156,7 @@ function tryApplyPreset(key, def, preset, out, hooks) {
147
156
  const valueList = selectOptionValues(def.options);
148
157
  const s = String(raw ?? '');
149
158
  if (!valueList.includes(s)) {
150
- hooks.onMissingNonInteractive(`Invalid values.${key}: "${s}". Expected one of: ${valueList.join(', ')}`);
159
+ hooks.onMissingNonInteractive(t('promptCatalog.preset.invalidSelect', { key, value: s, options: valueList.join(', ') }));
151
160
  }
152
161
  out[key] = s;
153
162
  return true;
@@ -155,7 +164,7 @@ function tryApplyPreset(key, def, preset, out, hooks) {
155
164
  case 'password': {
156
165
  const s = String(raw ?? '');
157
166
  if (def.required && isBlankText(s)) {
158
- hooks.onMissingNonInteractive(`"${key}" is required; set a non-empty values.${key} or omit it to prompt.`);
167
+ hooks.onMissingNonInteractive(t('promptCatalog.preset.required', { key }));
159
168
  }
160
169
  out[key] = s;
161
170
  return true;
@@ -168,13 +177,13 @@ function tryApplyPreset(key, def, preset, out, hooks) {
168
177
  const s = String(raw ?? '').trim();
169
178
  if (s === '') {
170
179
  if (def.required) {
171
- hooks.onMissingNonInteractive(`"${key}" is required; set values.${key} or omit it to prompt.`);
180
+ hooks.onMissingNonInteractive(t('promptCatalog.preset.required', { key }));
172
181
  }
173
182
  out[key] = def.initialValue ?? 0;
174
183
  return true;
175
184
  }
176
185
  if (!/^-?\d+$/.test(s)) {
177
- hooks.onMissingNonInteractive(`Invalid values.${key}: must be an integer.`);
186
+ hooks.onMissingNonInteractive(t('promptCatalog.preset.invalidInteger', { key }));
178
187
  }
179
188
  out[key] = Number.parseInt(s, 10);
180
189
  return true;
@@ -200,12 +209,14 @@ function tryApplyPreset(key, def, preset, out, hooks) {
200
209
  * 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
210
  */
202
211
  export async function runPromptCatalog(catalog, options = {}) {
212
+ const locale = resolveCliLocale(options.locale);
213
+ const t = createCliTranslate(locale);
203
214
  const promptIv = options.initialValues ?? {};
204
215
  const yesIv = options.yesInitialValues ?? {};
205
216
  const resolveIv = options.yes ? { ...promptIv, ...yesIv } : promptIv;
206
217
  const useYesInitial = Boolean(options.yes);
207
218
  const hooks = {
208
- onCancel: options.hooks?.onCancel ?? defaultOnCancel,
219
+ onCancel: options.hooks?.onCancel ?? (() => defaultOnCancel(locale)),
209
220
  onMissingNonInteractive: options.hooks?.onMissingNonInteractive ?? defaultOnMissingNonInteractive,
210
221
  };
211
222
  const interactive = Boolean(stdinStream.isTTY && stdoutStream.isTTY && !options.yes);
@@ -213,7 +224,7 @@ export async function runPromptCatalog(catalog, options = {}) {
213
224
  const out = {};
214
225
  for (const [key, def] of Object.entries(catalog)) {
215
226
  // Apply `values` presets before `hidden` / `when` so CLI/env fixes still win without UI.
216
- if (tryApplyPreset(key, def, preset, out, hooks)) {
227
+ if (tryApplyPreset(key, def, preset, out, hooks, locale)) {
217
228
  const errV = await runPromptFieldValidate(def, out[key], out);
218
229
  if (errV) {
219
230
  hooks.onMissingNonInteractive(errV);
@@ -224,11 +235,11 @@ export async function runPromptCatalog(catalog, options = {}) {
224
235
  continue;
225
236
  }
226
237
  if (def.type === 'intro') {
227
- p.intro(def.title);
238
+ p.intro(resolvePromptText(def.title, locale));
228
239
  continue;
229
240
  }
230
241
  if (def.type === 'outro') {
231
- p.outro(def.message);
242
+ p.outro(resolvePromptText(def.message, locale));
232
243
  continue;
233
244
  }
234
245
  if (def.type === 'run') {
@@ -236,10 +247,14 @@ export async function runPromptCatalog(catalog, options = {}) {
236
247
  continue;
237
248
  }
238
249
  if (def.type === 'text') {
250
+ const message = resolvePromptText(def.message, locale, key);
251
+ const placeholder = def.placeholder !== undefined
252
+ ? resolvePromptText(def.placeholder, locale)
253
+ : undefined;
239
254
  if (!interactive) {
240
255
  const merged = mergedText(key, def, resolveIv, useYesInitial, out);
241
256
  if (def.required && isBlankText(merged)) {
242
- hooks.onMissingNonInteractive(`Non-interactive: "${key}" is required; set initialValues.${key}, yesInitialValues.${key}, yesInitialValue on the block, or initialValue.`);
257
+ hooks.onMissingNonInteractive(t('promptCatalog.nonInteractive.textRequired', { key }));
243
258
  }
244
259
  out[key] = merged;
245
260
  const errT = await runPromptFieldValidate(def, merged, { ...out, [key]: merged });
@@ -253,16 +268,16 @@ export async function runPromptCatalog(catalog, options = {}) {
253
268
  let last = merged;
254
269
  for (;;) {
255
270
  const raw = await p.text({
256
- message: def.message,
271
+ message,
257
272
  initialValue: last,
258
- ...(def.placeholder !== undefined ? { placeholder: def.placeholder } : {}),
273
+ ...(placeholder !== undefined ? { placeholder } : {}),
259
274
  });
260
275
  if (p.isCancel(raw)) {
261
276
  hooks.onCancel();
262
277
  }
263
278
  const s = typeof raw === 'string' ? raw : last;
264
279
  if (def.required && isBlankText(s)) {
265
- p.log.error('Required');
280
+ p.log.error(t('promptCatalog.common.required'));
266
281
  last = s;
267
282
  continue;
268
283
  }
@@ -278,10 +293,10 @@ export async function runPromptCatalog(catalog, options = {}) {
278
293
  continue;
279
294
  }
280
295
  const raw = await p.text({
281
- message: def.message,
296
+ message,
282
297
  initialValue: merged,
283
- ...(def.placeholder !== undefined ? { placeholder: def.placeholder } : {}),
284
- validate: def.required ? (value) => (isBlankText(value) ? 'Required' : undefined) : undefined,
298
+ ...(placeholder !== undefined ? { placeholder } : {}),
299
+ validate: def.required ? (value) => (isBlankText(value) ? t('promptCatalog.common.required') : undefined) : undefined,
285
300
  });
286
301
  if (p.isCancel(raw)) {
287
302
  hooks.onCancel();
@@ -290,6 +305,7 @@ export async function runPromptCatalog(catalog, options = {}) {
290
305
  continue;
291
306
  }
292
307
  if (def.type === 'boolean') {
308
+ const message = resolvePromptText(def.message, locale, key);
293
309
  if (!interactive) {
294
310
  const b = mergedBoolean(key, def, resolveIv, useYesInitial);
295
311
  out[key] = b;
@@ -303,7 +319,7 @@ export async function runPromptCatalog(catalog, options = {}) {
303
319
  if (def.validate) {
304
320
  for (;;) {
305
321
  const raw = await p.confirm({
306
- message: def.message,
322
+ message,
307
323
  initialValue: merged,
308
324
  });
309
325
  if (p.isCancel(raw)) {
@@ -321,7 +337,7 @@ export async function runPromptCatalog(catalog, options = {}) {
321
337
  continue;
322
338
  }
323
339
  const raw = await p.confirm({
324
- message: def.message,
340
+ message,
325
341
  initialValue: merged,
326
342
  });
327
343
  if (p.isCancel(raw)) {
@@ -331,9 +347,10 @@ export async function runPromptCatalog(catalog, options = {}) {
331
347
  continue;
332
348
  }
333
349
  if (def.type === 'select') {
350
+ const message = resolvePromptText(def.message, locale, key);
334
351
  const valueList = selectOptionValues(def.options);
335
352
  if (def.required && def.options.length === 0) {
336
- hooks.onMissingNonInteractive(`Select "${key}" is required but has no options.`);
353
+ hooks.onMissingNonInteractive(t('promptCatalog.nonInteractive.selectRequiredNoOptions', { key }));
337
354
  }
338
355
  if (!interactive) {
339
356
  const merged = mergedSelect(key, def, resolveIv, useYesInitial);
@@ -344,8 +361,8 @@ export async function runPromptCatalog(catalog, options = {}) {
344
361
  ? String(promptIv[key])
345
362
  : undefined;
346
363
  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.`);
364
+ ? t('promptCatalog.nonInteractive.selectInvalidValue', { key, value: bad, options: valueList.join(', ') })
365
+ : t('promptCatalog.nonInteractive.selectMissingDefault', { key }));
349
366
  }
350
367
  out[key] = merged;
351
368
  const errS = await runPromptFieldValidate(def, merged, { ...out, [key]: merged });
@@ -360,15 +377,15 @@ export async function runPromptCatalog(catalog, options = {}) {
360
377
  valueList[0];
361
378
  if (uiInitial === undefined || !valueList.includes(uiInitial)) {
362
379
  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.`;
380
+ ? t('promptCatalog.nonInteractive.selectRequiredInteractive', { key })
381
+ : t('promptCatalog.nonInteractive.selectMissingInteractiveDefault', { key });
365
382
  hooks.onMissingNonInteractive(hint);
366
383
  }
367
384
  if (def.validate) {
368
385
  for (;;) {
369
386
  const raw = await p.select({
370
- message: def.message,
371
- options: clackSelectOptions(def.options),
387
+ message,
388
+ options: clackSelectOptions(def.options, locale),
372
389
  initialValue: uiInitial,
373
390
  });
374
391
  if (p.isCancel(raw)) {
@@ -386,8 +403,8 @@ export async function runPromptCatalog(catalog, options = {}) {
386
403
  continue;
387
404
  }
388
405
  const raw = await p.select({
389
- message: def.message,
390
- options: clackSelectOptions(def.options),
406
+ message,
407
+ options: clackSelectOptions(def.options, locale),
391
408
  initialValue: uiInitial,
392
409
  });
393
410
  if (p.isCancel(raw)) {
@@ -397,11 +414,12 @@ export async function runPromptCatalog(catalog, options = {}) {
397
414
  continue;
398
415
  }
399
416
  if (def.type === 'password') {
417
+ const message = resolvePromptText(def.message, locale, key);
400
418
  if (!interactive) {
401
419
  const merged = mergedPassword(key, def, resolveIv, useYesInitial);
402
420
  if (merged === undefined) {
403
421
  if (def.required) {
404
- hooks.onMissingNonInteractive(`Non-interactive: "${key}" is required; set initialValues.${key}, yesInitialValues.${key}, or initialValue / yesInitialValue on the block.`);
422
+ hooks.onMissingNonInteractive(t('promptCatalog.nonInteractive.passwordRequired', { key }));
405
423
  }
406
424
  out[key] = '';
407
425
  const errPE = await runPromptFieldValidate(def, '', { ...out, [key]: '' });
@@ -411,7 +429,7 @@ export async function runPromptCatalog(catalog, options = {}) {
411
429
  continue;
412
430
  }
413
431
  if (def.required && isBlankText(merged)) {
414
- hooks.onMissingNonInteractive(`Non-interactive: "${key}" is required; set a non-empty initialValues / yesInitialValues / initialValue / yesInitialValue.`);
432
+ hooks.onMissingNonInteractive(t('promptCatalog.nonInteractive.passwordRequiredNonEmpty', { key }));
415
433
  }
416
434
  out[key] = merged;
417
435
  const errP = await runPromptFieldValidate(def, merged, { ...out, [key]: merged });
@@ -423,15 +441,15 @@ export async function runPromptCatalog(catalog, options = {}) {
423
441
  if (def.validate) {
424
442
  for (;;) {
425
443
  const raw = await p.password({
426
- message: def.message,
427
- validate: def.required ? (value) => (isBlankText(value) ? 'Required' : undefined) : undefined,
444
+ message,
445
+ validate: def.required ? (value) => (isBlankText(value) ? t('promptCatalog.common.required') : undefined) : undefined,
428
446
  });
429
447
  if (p.isCancel(raw)) {
430
448
  hooks.onCancel();
431
449
  }
432
450
  const s = typeof raw === 'string' ? raw : '';
433
451
  if (def.required && isBlankText(s)) {
434
- p.log.error('Required');
452
+ p.log.error(t('promptCatalog.common.required'));
435
453
  continue;
436
454
  }
437
455
  const errP = await runPromptFieldValidate(def, s, { ...out, [key]: s });
@@ -445,8 +463,8 @@ export async function runPromptCatalog(catalog, options = {}) {
445
463
  continue;
446
464
  }
447
465
  const raw = await p.password({
448
- message: def.message,
449
- validate: def.required ? (value) => (isBlankText(value) ? 'Required' : undefined) : undefined,
466
+ message,
467
+ validate: def.required ? (value) => (isBlankText(value) ? t('promptCatalog.common.required') : undefined) : undefined,
450
468
  });
451
469
  if (p.isCancel(raw)) {
452
470
  hooks.onCancel();
@@ -455,11 +473,15 @@ export async function runPromptCatalog(catalog, options = {}) {
455
473
  continue;
456
474
  }
457
475
  if (def.type === 'integer') {
476
+ const message = resolvePromptText(def.message, locale, key);
477
+ const placeholder = def.placeholder !== undefined
478
+ ? resolvePromptText(def.placeholder, locale)
479
+ : undefined;
458
480
  if (!interactive) {
459
481
  const merged = mergedInteger(key, def, resolveIv, useYesInitial);
460
482
  if (merged === undefined) {
461
483
  if (def.required) {
462
- hooks.onMissingNonInteractive(`Non-interactive: "${key}" is required; set initialValues.${key}, yesInitialValues.${key}, or initialValue / yesInitialValue on the block.`);
484
+ hooks.onMissingNonInteractive(t('promptCatalog.nonInteractive.integerRequired', { key }));
463
485
  }
464
486
  const z = def.initialValue ?? 0;
465
487
  out[key] = z;
@@ -482,16 +504,16 @@ export async function runPromptCatalog(catalog, options = {}) {
482
504
  let last = lineDefault;
483
505
  for (;;) {
484
506
  const raw = await p.text({
485
- message: def.message,
507
+ message,
486
508
  initialValue: last,
487
- ...(def.placeholder !== undefined ? { placeholder: def.placeholder } : {}),
509
+ ...(placeholder !== undefined ? { placeholder } : {}),
488
510
  validate: (value) => {
489
- const t = value.trim();
490
- if (t === '') {
491
- return def.required ? 'Required' : undefined;
511
+ const trimmed = value.trim();
512
+ if (trimmed === '') {
513
+ return def.required ? t('promptCatalog.common.required') : undefined;
492
514
  }
493
- if (!/^-?\d+$/.test(t)) {
494
- return 'Must be an integer';
515
+ if (!/^-?\d+$/.test(trimmed)) {
516
+ return t('promptCatalog.common.mustBeInteger');
495
517
  }
496
518
  return undefined;
497
519
  },
@@ -523,16 +545,16 @@ export async function runPromptCatalog(catalog, options = {}) {
523
545
  continue;
524
546
  }
525
547
  const raw = await p.text({
526
- message: def.message,
548
+ message,
527
549
  initialValue: lineDefault,
528
- ...(def.placeholder !== undefined ? { placeholder: def.placeholder } : {}),
550
+ ...(placeholder !== undefined ? { placeholder } : {}),
529
551
  validate: (value) => {
530
- const t = value.trim();
531
- if (t === '') {
532
- return def.required ? 'Required' : undefined;
552
+ const trimmed = value.trim();
553
+ if (trimmed === '') {
554
+ return def.required ? t('promptCatalog.common.required') : undefined;
533
555
  }
534
- if (!/^-?\d+$/.test(t)) {
535
- return 'Must be an integer';
556
+ if (!/^-?\d+$/.test(trimmed)) {
557
+ return t('promptCatalog.common.mustBeInteger');
536
558
  }
537
559
  return undefined;
538
560
  },
@@ -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
  }