@gunshi/plugin-i18n 0.26.3 → 0.27.0-alpha.10

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.
Files changed (4) hide show
  1. package/README.md +212 -8
  2. package/lib/index.d.ts +595 -70
  3. package/lib/index.js +68 -21
  4. package/package.json +10 -10
package/README.md CHANGED
@@ -50,7 +50,6 @@ const greetCommand = defineI18n({
50
50
  // Define resource fetcher for translations
51
51
  resource: async ctx => ({
52
52
  description: 'Greet someone in their language',
53
- examples: '$ your-cli --name yourname',
54
53
  'arg:name': "The person's name",
55
54
  greeting: 'Hello, {$name}!'
56
55
  }),
@@ -86,7 +85,7 @@ await cli(process.argv.slice(2), greetCommand, {
86
85
  - Default: `{}`
87
86
  - Description:
88
87
  - Built-in resource translations for different locales. Used for translating gunshi built-in level messages like "USAGE", "OPTIONS", "COMMANDS", etc.
89
- - for details, see [Resource Key Naming Conventions](#resource-key-naming-conventions)
88
+ - for details, see [Resource Key Naming Conventions](#-resource-key-naming-conventions)
90
89
 
91
90
  ### `translationAdapterFactory`
92
91
 
@@ -185,6 +184,88 @@ const i18nCommand = withI18nResource(basicCommand, async ctx => {
185
184
  })
186
185
  ```
187
186
 
187
+ ### Key Resolution Helpers
188
+
189
+ The i18n plugin exports key resolution helper functions that handle the internal key structure, so you don't need to manually prefix your keys:
190
+
191
+ - `resolveKey(key: string, ctx: CommandContext): string` - Resolves a custom key with command namespace if applicable
192
+ - `resolveArgKey(key: string, ctx: CommandContext): string` - Resolves an argument key with `arg:` prefix and namespace
193
+ - `resolveBuiltInKey(key: string): string` - Resolves a built-in key with `_:` prefix
194
+
195
+ ```ts
196
+ import { resolveKey, resolveArgKey, resolveBuiltInKey } from '@gunshi/plugin-i18n'
197
+
198
+ // These helpers automatically add the correct prefixes
199
+ resolveKey('description', ctx) // Returns namespaced key for description
200
+ resolveArgKey('verbose', ctx) // Returns 'arg:verbose' or 'command:arg:verbose' based on context
201
+ resolveBuiltInKey('USAGE') // Returns '_:USAGE'
202
+ ```
203
+
204
+ #### Why Use Key Resolution Helpers?
205
+
206
+ <!-- eslint-disable markdown/no-missing-label-refs -->
207
+
208
+ > [!NOTE]
209
+ > See also the [Resource Key Naming Conventions](#-resource-key-naming-conventions) section for details on how keys should be structured in your resource definitions.
210
+
211
+ <!-- eslint-enable markdown/no-missing-label-refs -->
212
+
213
+ The i18n plugin internally uses different key prefixes to organize translations:
214
+
215
+ - Built-in keys use `_:` prefix (e.g., `_:USAGE`, `_:OPTIONS`)
216
+ - Argument keys use `arg:` prefix (e.g., `arg:file`, `arg:verbose`)
217
+ - Command-specific keys can be namespaced with command name
218
+
219
+ While the `translate` function accepts keys without prefixes in most cases, using these helpers ensures:
220
+
221
+ - Consistent key resolution across your application
222
+ - Proper namespacing when using sub-commands
223
+ - Compatibility with future plugin updates
224
+ - Type-safe key generation
225
+
226
+ #### Example Usage
227
+
228
+ ```ts
229
+ import { defineI18n, resolveKey, resolveArgKey, resolveBuiltInKey } from '@gunshi/plugin-i18n'
230
+
231
+ const command = defineI18n({
232
+ name: 'deploy',
233
+ args: {
234
+ environment: { type: 'string', short: 'e' },
235
+ force: { type: 'boolean', short: 'f' }
236
+ },
237
+ resource: async ctx => ({
238
+ description: 'Deploy application',
239
+ 'arg:environment': 'Target environment',
240
+ 'arg:force': 'Force deployment',
241
+ deploy_start: 'Starting deployment...',
242
+ deploy_success: 'Deployment completed successfully!'
243
+ }),
244
+ run: async ctx => {
245
+ const { translate } = ctx.extensions['g:i18n']
246
+
247
+ // Direct usage (need to prefix with command name)
248
+ console.log(translate('deploy:deploy_start'))
249
+
250
+ // Using helpers for explicit control
251
+ const envKey = resolveArgKey('environment', ctx)
252
+ console.log(translate(envKey)) // Same as translate('arg:environment')
253
+
254
+ // Useful when building dynamic keys
255
+ const args = ['environment', 'force']
256
+ for (const arg of args) {
257
+ const key = resolveArgKey(arg, ctx)
258
+ const description = translate(key)
259
+ console.log(`${arg}: ${description}`)
260
+ }
261
+
262
+ // For built-in messages
263
+ const usageKey = resolveBuiltInKey('USAGE')
264
+ console.log(translate(usageKey)) // Translates the "USAGE" header
265
+ }
266
+ })
267
+ ```
268
+
188
269
  ## 🧩 Context Extensions
189
270
 
190
271
  When using the i18n plugin, your command context is extended via `ctx.extensions['g:i18n']`.
@@ -200,9 +281,17 @@ Available extensions:
200
281
 
201
282
  - `locale: Intl.Locale`: The current locale
202
283
  - `translate<T>(key: T, values?: Record<string, unknown>): string`: Translation function
284
+ - `loadResource(locale: string | Intl.Locale, ctx: CommandContext, command: Command): Promise<boolean>`: Manually load resources for a specific locale and command
203
285
 
204
286
  ## 📝 Resource Key Naming Conventions
205
287
 
288
+ <!-- eslint-disable markdown/no-missing-label-refs -->
289
+
290
+ > [!TIP]
291
+ > The plugin provides [Key Resolution Helpers](#key-resolution-helpers) (`resolveKey`, `resolveArgKey`, `resolveBuiltInKey`) to automatically handle these naming conventions programmatically.
292
+
293
+ <!-- eslint-enable markdown/no-missing-label-refs -->
294
+
206
295
  When defining your localization resources (either directly in the `resource` function or in separate files), there are specific naming conventions to follow for the keys:
207
296
 
208
297
  - **Command Description**: Use the key `description` for the main description of the command.
@@ -225,7 +314,7 @@ When defining your localization resources (either directly in the `resource` fun
225
314
  - `help` - Description for the help option ("Display this help message")
226
315
  - `version` - Description for the version option ("Display this version")
227
316
 
228
- Internally, these keys are prefixed with `_:` (e.g., `_:USAGE`, `_:OPTIONS`), but you don't need to use this prefix directly. When overriding built-in translations in your resources, use the key names without the prefix (e.g., providing your own translation for `NEGATABLE`, not `_:NEGATABLE`).
317
+ Internally, these keys are prefixed with `_:` (e.g., `_:USAGE`, `_:OPTIONS`), but you don't need to use this prefix directly. When overriding built-in translations in your resources, use the key names without the prefix (e.g., providing your own translation for `NEGATABLE`, not `_:NEGATABLE`). Additionally, custom keys are automatically namespaced with the command name in sub-command contexts (e.g., `deploy:informal_greeting` for a 'deploy' command).
229
318
 
230
319
  Here's an example illustrating the convention:
231
320
 
@@ -242,7 +331,6 @@ const command = defineI18n({
242
331
  // Example for 'en-US' locale
243
332
  return {
244
333
  description: 'This is my command.', // No prefix
245
- examples: '$ my-command --target file.txt', // No prefix
246
334
  'arg:target': 'The target file to process.', // 'arg:' prefix
247
335
  'arg:verbose': 'Enable verbose output.', // 'arg:' prefix
248
336
  'arg:no-verbose': 'Disable verbose logging specifically.', // Optional custom translation for the negatable option
@@ -255,6 +343,13 @@ const command = defineI18n({
255
343
  })
256
344
  ```
257
345
 
346
+ <!-- eslint-disable markdown/no-missing-label-refs -->
347
+
348
+ > [!IMPORTANT]
349
+ > When defining resources, argument and option descriptions **must** be prefixed with `arg:` (e.g., `arg:target`, `arg:verbose`). All other keys like `description`, `examples`, and custom keys do not require prefixes.
350
+
351
+ <!-- eslint-enable markdown/no-missing-label-refs -->
352
+
258
353
  Adhering to these conventions ensures that Gunshi correctly identifies and uses your translations for descriptions, help messages, and within your command's logic via `ctx.extensions['g:i18n'].translate()`.
259
354
 
260
355
  <!-- eslint-disable markdown/no-missing-label-refs -->
@@ -284,6 +379,78 @@ Bad Nested structure (won't work with `ctx.extensions['g:i18n'].translate('messa
284
379
  }
285
380
  ```
286
381
 
382
+ ## 🔧 Implementation Details
383
+
384
+ This section covers important implementation details that can help you better understand and use the i18n plugin.
385
+
386
+ ### Translation Return Values
387
+
388
+ The `translate` function has different behaviors depending on the key type:
389
+
390
+ - **Custom keys**: Returns an empty string (`''`) if the key is not found
391
+ - **Built-in keys** (internally prefixed with `_:`): Returns the key itself if not found
392
+
393
+ This difference is important for error handling and fallback displays:
394
+
395
+ ```ts
396
+ const { translate } = ctx.extensions['g:i18n']
397
+
398
+ // Custom key not found
399
+ translate('nonexistent_key') // Returns: ''
400
+
401
+ // Built-in key not found (if not overridden)
402
+ translate('USAGE') // Returns: 'USAGE'
403
+ ```
404
+
405
+ ### Resource Loading Behavior
406
+
407
+ When loading command resources, the plugin automatically:
408
+
409
+ 1. Extracts option descriptions from command args definitions
410
+ 2. Creates default resources in English
411
+ 3. Merges built-in resources with command-specific resources
412
+ 4. Handles errors gracefully (logs to console.error but continues execution)
413
+
414
+ ### Extending DefaultTranslation
415
+
416
+ The default translation adapter is exported and can be extended:
417
+
418
+ ```ts
419
+ import { DefaultTranslation } from '@gunshi/plugin-i18n'
420
+
421
+ class MyCustomTranslation extends DefaultTranslation {
422
+ translate(locale: string, key: string, values?: Record<string, unknown>): string | undefined {
423
+ const result = super.translate(locale, key, values)
424
+ // Add custom post-processing
425
+ return result?.toUpperCase()
426
+ }
427
+ }
428
+
429
+ // Use in plugin options
430
+ await cli(args, command, {
431
+ plugins: [
432
+ i18n({
433
+ translationAdapterFactory: options => new MyCustomTranslation(options)
434
+ })
435
+ ]
436
+ })
437
+ ```
438
+
439
+ ### Internal Key Prefixing
440
+
441
+ <!-- eslint-disable markdown/no-missing-label-refs -->
442
+
443
+ > [!IMPORTANT]
444
+ > Understanding these prefixes is essential for using the [Key Resolution Helpers](#key-resolution-helpers) effectively and following the [Resource Key Naming Conventions](#-resource-key-naming-conventions).
445
+
446
+ <!-- eslint-enable markdown/no-missing-label-refs -->
447
+
448
+ - Built-in resource keys are internally prefixed with `_:` (e.g., `_:USAGE`, `_:OPTIONS`)
449
+ - This prefix is handled automatically - you don't need to use it when overriding built-in translations
450
+ - Argument keys use the `arg:` prefix as documented
451
+ - Custom keys can be namespaced with the command name when in sub-command context (e.g., `deploy:custom_message` for a 'deploy' command)
452
+ - Use the exported helper functions (`resolveKey`, `resolveArgKey`, `resolveBuiltInKey`) to generate properly prefixed keys programmatically
453
+
287
454
  ## 🔤 Translation Interpolation
288
455
 
289
456
  The default translation adapter supports simple interpolation using `{$key}` syntax:
@@ -298,10 +465,10 @@ const resource = {
298
465
  }
299
466
  // In your command
300
467
  const { translate } = ctx.extensions['g:i18n']
301
- translate('welcome', { name: 'John' }) // "Welcome, John!"
302
- translate('items.count', { count: 5 }) // "You have 5 items"
303
- translate('file_deleted', { path: '/tmp/file.txt' }) // "Deleted /tmp/file.txt"
304
- translate('error_message', { error: 'File not found' }) // "Error: File not found"
468
+ translate(resolveKey('welcome'), { name: 'John' }) // "Welcome, John!"
469
+ translate(resolveKey('items.count'), { count: 5 }) // "You have 5 items"
470
+ translate(resolveKey('file_deleted'), { path: '/tmp/file.txt' }) // "Deleted /tmp/file.txt"
471
+ translate(resolveKey('error_message'), { error: 'File not found' }) // "Error: File not found"
305
472
  ```
306
473
 
307
474
  <!-- eslint-disable markdown/no-missing-label-refs -->
@@ -518,6 +685,43 @@ With Intlify, you get advanced features like:
518
685
 
519
686
  <!-- eslint-enable markdown/no-missing-label-refs -->
520
687
 
688
+ ## 🎯 Type-Safe Translation Keys
689
+
690
+ The i18n plugin provides sophisticated TypeScript type support for translation keys. When using TypeScript, the `translate` function will provide auto-completion and type checking for:
691
+
692
+ - Built-in keys (`USAGE`, `OPTIONS`, `COMMANDS`, etc.)
693
+ - Argument keys based on your command's args definition (`arg:name`, `arg:verbose`, etc.)
694
+ - Custom keys defined in your resource
695
+
696
+ ```ts
697
+ import { defineI18n } from '@gunshi/plugin-i18n'
698
+
699
+ const command = defineI18n({
700
+ name: 'example',
701
+ args: {
702
+ file: { type: 'string' },
703
+ verbose: { type: 'boolean' }
704
+ },
705
+ resource: async () => ({
706
+ description: 'Example command',
707
+ 'arg:file': 'File to process',
708
+ 'arg:verbose': 'Enable verbose output',
709
+ custom_message: 'This is a custom message'
710
+ }),
711
+ run: ctx => {
712
+ const { translate } = ctx.extensions['g:i18n']
713
+
714
+ // TypeScript knows these are valid keys
715
+ translate('USAGE') // Built-in key
716
+ translate('arg:file') // Arg key from command definition
717
+ translate('custom_message') // Custom key from resource
718
+
719
+ // TypeScript will error on invalid keys
720
+ // translate('invalid_key') // Type error!
721
+ }
722
+ })
723
+ ```
724
+
521
725
  ## 📚 API References
522
726
 
523
727
  See the [API References](./docs/index.md)
package/lib/index.d.ts CHANGED
@@ -1,78 +1,568 @@
1
- import { Awaitable, Command, CommandContext, DefaultGunshiParams, ExtractArgs, GunshiParams, GunshiParamsConstraint, NormalizeToGunshiParams, PluginWithExtension } from "@gunshi/plugin";
1
+ import { Args, Awaitable, Command, CommandContext, DefaultGunshiParams, ExtractArgs, GunshiParams, GunshiParamsConstraint, PluginWithExtension } from "@gunshi/plugin";
2
2
 
3
3
  //#region rolldown:runtime
4
4
 
5
5
  //#endregion
6
- //#region ../../node_modules/.pnpm/args-tokens@0.20.1/node_modules/args-tokens/lib/resolver-U72Jg6Ll.d.ts
6
+ //#region ../../node_modules/.pnpm/args-tokens@0.22.0/node_modules/args-tokens/lib/parser-Cbxholql.d.ts
7
+ //#region src/parser.d.ts
8
+ /**
9
+ * Entry point of argument parser.
10
+ * @module
11
+ */
12
+ /**
13
+ * forked from `nodejs/node` (`pkgjs/parseargs`)
14
+ * repository url: https://github.com/nodejs/node (https://github.com/pkgjs/parseargs)
15
+ * code url: https://github.com/nodejs/node/blob/main/lib/internal/util/parse_args/parse_args.js
16
+ *
17
+ * @author kazuya kawaguchi (a.k.a. kazupon)
18
+ * @license MIT
19
+ */
20
+ /**
21
+ * Argument token Kind.
22
+ * - `option`: option token, support short option (e.g. `-x`) and long option (e.g. `--foo`)
23
+ * - `option-terminator`: option terminator (`--`) token, see guideline 10 in https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html
24
+ * - `positional`: positional token
25
+ */
26
+ type ArgTokenKind = 'option' | 'option-terminator' | 'positional';
27
+ /**
28
+ * Argument token.
29
+ */
30
+ interface ArgToken {
31
+ /**
32
+ * Argument token kind.
33
+ */
34
+ kind: ArgTokenKind;
35
+ /**
36
+ * Argument token index, e.g `--foo bar` => `--foo` index is 0, `bar` index is 1.
37
+ */
38
+ index: number;
39
+ /**
40
+ * Option name, e.g. `--foo` => `foo`, `-x` => `x`.
41
+ */
42
+ name?: string;
43
+ /**
44
+ * Raw option name, e.g. `--foo` => `--foo`, `-x` => `-x`.
45
+ */
46
+ rawName?: string;
47
+ /**
48
+ * Option value, e.g. `--foo=bar` => `bar`, `-x=bar` => `bar`.
49
+ * If the `allowCompatible` option is `true`, short option value will be same as Node.js `parseArgs` behavior.
50
+ */
51
+ value?: string;
52
+ /**
53
+ * Inline value, e.g. `--foo=bar` => `true`, `-x=bar` => `true`.
54
+ */
55
+ inlineValue?: boolean;
56
+ }
57
+ /**
58
+ * Parser Options.
59
+ */
60
+ //#endregion
61
+ //#region ../../node_modules/.pnpm/args-tokens@0.22.0/node_modules/args-tokens/lib/resolver-BoS-UnqX.d.ts
7
62
  //#region src/resolver.d.ts
8
63
 
9
64
  /**
10
- * An argument schema
11
- * This schema is similar to the schema of the `node:utils`.
12
- * difference is that:
13
- * - `required` property and `description` property are added
14
- * - `type` is not only 'string' and 'boolean', but also 'number', 'enum', 'positional', 'custom' too.
15
- * - `default` property type, not support multiple types
16
- */
65
+ * An argument schema
66
+ * This schema is similar to the schema of the `node:utils`.
67
+ * difference is that:
68
+ * - `required` property and `description` property are added
69
+ * - `type` is not only 'string' and 'boolean', but also 'number', 'enum', 'positional', 'custom' too.
70
+ * - `default` property type, not support multiple types
71
+ */
17
72
  interface ArgSchema {
18
73
  /**
19
- * Type of argument.
20
- */
21
- type: "string" | "boolean" | "number" | "enum" | "positional" | "custom";
74
+ * Type of argument.
75
+ */
76
+ type: 'string' | 'boolean' | 'number' | 'enum' | 'positional' | 'custom';
22
77
  /**
23
- * A single character alias for the argument.
24
- */
78
+ * A single character alias for the argument.
79
+ */
25
80
  short?: string;
26
81
  /**
27
- * A description of the argument.
28
- */
82
+ * A description of the argument.
83
+ */
29
84
  description?: string;
30
85
  /**
31
- * Whether the argument is required or not.
32
- */
86
+ * Whether the argument is required or not.
87
+ */
33
88
  required?: true;
34
89
  /**
35
- * Whether the argument allow multiple values or not.
36
- */
90
+ * Whether the argument allow multiple values or not.
91
+ */
37
92
  multiple?: true;
38
93
  /**
39
- * Whether the negatable option for `boolean` type
40
- */
94
+ * Whether the negatable option for `boolean` type
95
+ */
41
96
  negatable?: boolean;
42
97
  /**
43
- * The allowed values of the argument, and string only. This property is only used when the type is 'enum'.
44
- */
98
+ * The allowed values of the argument, and string only. This property is only used when the type is 'enum'.
99
+ */
45
100
  choices?: string[] | readonly string[];
46
101
  /**
47
- * The default value of the argument.
48
- * if the type is 'enum', the default value must be one of the allowed values.
49
- */
102
+ * The default value of the argument.
103
+ * if the type is 'enum', the default value must be one of the allowed values.
104
+ */
50
105
  default?: string | boolean | number;
51
106
  /**
52
- * Whether to convert the argument name to kebab-case.
53
- */
107
+ * Whether to convert the argument name to kebab-case.
108
+ */
54
109
  toKebab?: true;
55
110
  /**
56
- * A function to parse the value of the argument. if the type is 'custom', this function is required.
57
- * If argument value will be invalid, this function have to throw an error.
58
- * @param value
59
- * @returns Parsed value
60
- * @throws An Error, If the value is invalid. Error type should be `Error` or extends it
61
- */
62
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
111
+ * A function to parse the value of the argument. if the type is 'custom', this function is required.
112
+ * If argument value will be invalid, this function have to throw an error.
113
+ * @param value
114
+ * @returns Parsed value
115
+ * @throws An Error, If the value is invalid. Error type should be `Error` or extends it
116
+ */
63
117
  parse?: (value: string) => any;
64
118
  }
65
119
  /**
66
- * An object that contains {@link ArgSchema | argument schema}.
67
- */
68
- interface Args {
120
+ * An object that contains {@link ArgSchema | argument schema}.
121
+ */
122
+ interface Args$1 {
69
123
  [option: string]: ArgSchema;
70
- } //#endregion
71
- //#region ../shared/src/constants.d.ts
124
+ }
125
+ /**
126
+ * An object that contains the values of the arguments.
127
+ */
128
+ type ArgValues<T> = T extends Args$1 ? ResolveArgValues<T, { [Arg in keyof T]: ExtractOptionValue<T[Arg]> }> : {
129
+ [option: string]: string | boolean | number | (string | boolean | number)[] | undefined;
130
+ };
131
+ type IsFunction<T> = T extends ((...args: any[]) => any) ? true : false;
132
+ /**
133
+ * @internal
134
+ */
135
+ type ExtractOptionValue<A extends ArgSchema> = A['type'] extends 'string' ? ResolveOptionValue<A, string> : A['type'] extends 'boolean' ? ResolveOptionValue<A, boolean> : A['type'] extends 'number' ? ResolveOptionValue<A, number> : A['type'] extends 'positional' ? ResolveOptionValue<A, string> : A['type'] extends 'enum' ? A['choices'] extends string[] | readonly string[] ? ResolveOptionValue<A, A['choices'][number]> : never : A['type'] extends 'custom' ? IsFunction<A['parse']> extends true ? ResolveOptionValue<A, ReturnType<NonNullable<A['parse']>>> : never : ResolveOptionValue<A, string | boolean | number>;
136
+ type ResolveOptionValue<A extends ArgSchema, T> = A['multiple'] extends true ? T[] : T;
137
+ /**
138
+ * @internal
139
+ */
140
+ type ResolveArgValues<A extends Args$1, V extends Record<keyof A, unknown>> = { -readonly [Arg in keyof A]?: V[Arg] } & FilterArgs<A, V, 'default'> & FilterArgs<A, V, 'required'> & FilterPositionalArgs<A, V> extends infer P ? { [K in keyof P]: P[K] } : never;
141
+ /**
142
+ * @internal
143
+ */
144
+ type FilterArgs<A extends Args$1, V extends Record<keyof A, unknown>, K extends keyof ArgSchema> = { [Arg in keyof A as A[Arg][K] extends {} ? Arg : never]: V[Arg] };
145
+ /**
146
+ * @internal
147
+ */
148
+ type FilterPositionalArgs<A extends Args$1, V extends Record<keyof A, unknown>> = { [Arg in keyof A as A[Arg]['type'] extends 'positional' ? Arg : never]: V[Arg] };
149
+ /**
150
+ * An arguments for {@link resolveArgs | resolve arguments}.
151
+ */
152
+
153
+ /**
154
+ * Tracks which arguments were explicitly provided by the user.
155
+ *
156
+ * Each property indicates whether the corresponding argument was explicitly
157
+ * provided (true) or is using a default value or not provided (false).
158
+ */
159
+ type ArgExplicitlyProvided<A extends Args$1> = { [K in keyof A]: boolean };
160
+ /**
161
+ * Resolve command line arguments.
162
+ * @param args - An arguments that contains {@link ArgSchema | arguments schema}.
163
+ * @param tokens - An array of {@link ArgToken | tokens}.
164
+ * @param resolveArgs - An arguments that contains {@link ResolveArgs | resolve arguments}.
165
+ * @returns An object that contains the values of the arguments, positional arguments, rest arguments, {@link AggregateError | validation errors}, and explicit provision status.
166
+ *
167
+ * @example
168
+ * ```typescript
169
+ * // passed tokens: --port 3000
170
+ *
171
+ * const { values, explicit } = resolveArgs({
172
+ * port: {
173
+ * type: 'number',
174
+ * default: 8080
175
+ * },
176
+ * host: {
177
+ * type: 'string',
178
+ * default: 'localhost'
179
+ * }
180
+ * }, parsedTokens)
181
+ *
182
+ * values.port // 3000
183
+ * values.host // 'localhost'
184
+ *
185
+ * explicit.port // true (explicitly provided)
186
+ * explicit.host // false (not provided, fallback to default)
187
+ * ```
188
+ */
189
+ //#endregion
190
+ //#region ../gunshi/src/types.d.ts
191
+ type Awaitable$1<T> = T | Promise<T>;
192
+ /**
193
+ * Extend command context type. This type is used to extend the command context with additional properties at {@link CommandContext.extensions}.
194
+ * @since v0.27.0
195
+ */
196
+ type ExtendContext = Record<string, unknown>;
197
+ /**
198
+ * Gunshi unified parameter type.
199
+ * This type combines both argument definitions and command context extensions.
200
+ * @since v0.27.0
201
+ */
202
+ interface GunshiParams$1<P extends {
203
+ args?: Args$1;
204
+ extensions?: ExtendContext;
205
+ } = {
206
+ args: Args$1;
207
+ extensions: {};
208
+ }> {
209
+ /**
210
+ * Command argument definitions
211
+ */
212
+ args: P extends {
213
+ args: infer A extends Args$1;
214
+ } ? A : Args$1;
215
+ /**
216
+ * Command context extensions
217
+ */
218
+ extensions: P extends {
219
+ extensions: infer E extends ExtendContext;
220
+ } ? E : {};
221
+ }
222
+ /**
223
+ * Default Gunshi parameters
224
+ * @since v0.27.0
225
+ */
226
+ type DefaultGunshiParams$1 = GunshiParams$1;
227
+ /**
228
+ * Generic constraint for command-related types.
229
+ * This type constraint allows both GunshiParams and objects with extensions.
230
+ * @since v0.27.0
231
+ */
232
+ type GunshiParamsConstraint$1 = GunshiParams$1<any> | {
233
+ extensions: ExtendContext;
234
+ };
235
+ /**
236
+ * Type helper to extract args from G
237
+ * @internal
238
+ */
239
+ type ExtractArgs$1<G> = G extends GunshiParams$1<any> ? G['args'] : Args$1;
240
+ /**
241
+ * Type helper to extract explicitly provided argument flags from G
242
+ * @internal
243
+ */
244
+ type ExtractArgExplicitlyProvided<G> = ArgExplicitlyProvided<ExtractArgs$1<G>>;
245
+ /**
246
+ * Type helper to extract extensions from G
247
+ * @internal
248
+ */
249
+ type ExtractExtensions<G> = G extends GunshiParams$1<any> ? G['extensions'] : G extends {
250
+ extensions: infer E;
251
+ } ? E : {};
252
+ /**
253
+ * Type helper to normalize G to GunshiParams
254
+ * @internal
255
+ */
256
+
257
+ /**
258
+ * Command environment.
259
+ */
260
+ interface CommandEnvironment<G extends GunshiParamsConstraint$1 = DefaultGunshiParams$1> {
261
+ /**
262
+ * Current working directory.
263
+ * @see {@link CliOptions.cwd}
264
+ */
265
+ cwd: string | undefined;
266
+ /**
267
+ * Command name.
268
+ * @see {@link CliOptions.name}
269
+ */
270
+ name: string | undefined;
271
+ /**
272
+ * Command description.
273
+ * @see {@link CliOptions.description}
274
+ *
275
+ */
276
+ description: string | undefined;
277
+ /**
278
+ * Command version.
279
+ * @see {@link CliOptions.version}
280
+ */
281
+ version: string | undefined;
282
+ /**
283
+ * Left margin of the command output.
284
+ * @default 2
285
+ * @see {@link CliOptions.leftMargin}
286
+ */
287
+ leftMargin: number;
288
+ /**
289
+ * Middle margin of the command output.
290
+ * @default 10
291
+ * @see {@link CliOptions.middleMargin}
292
+ */
293
+ middleMargin: number;
294
+ /**
295
+ * Whether to display the usage option type.
296
+ * @default false
297
+ * @see {@link CliOptions.usageOptionType}
298
+ */
299
+ usageOptionType: boolean;
300
+ /**
301
+ * Whether to display the option value.
302
+ * @default true
303
+ * @see {@link CliOptions.usageOptionValue}
304
+ */
305
+ usageOptionValue: boolean;
306
+ /**
307
+ * Whether to display the command usage.
308
+ * @default false
309
+ * @see {@link CliOptions.usageSilent}
310
+ */
311
+ usageSilent: boolean;
312
+ /**
313
+ * Sub commands.
314
+ * @see {@link CliOptions.subCommands}
315
+ */
316
+ subCommands: Map<string, Command$1<any> | LazyCommand<any>> | undefined;
317
+ /**
318
+ * Render function the command usage.
319
+ */
320
+ renderUsage: ((ctx: Readonly<CommandContext$1<G>>) => Promise<string>) | null | undefined;
321
+ /**
322
+ * Render function the header section in the command usage.
323
+ */
324
+ renderHeader: ((ctx: Readonly<CommandContext$1<G>>) => Promise<string>) | null | undefined;
325
+ /**
326
+ * Render function the validation errors.
327
+ */
328
+ renderValidationErrors: ((ctx: Readonly<CommandContext$1<G>>, error: AggregateError) => Promise<string>) | null | undefined;
329
+ /**
330
+ * Hook that runs before any command execution
331
+ * @see {@link CliOptions.onBeforeCommand}
332
+ * @since v0.27.0
333
+ */
334
+ onBeforeCommand: ((ctx: Readonly<CommandContext$1<G>>) => Awaitable$1<void>) | undefined;
335
+ /**
336
+ * Hook that runs after successful command execution
337
+ * @see {@link CliOptions.onAfterCommand}
338
+ * @since v0.27.0
339
+ */
340
+ onAfterCommand: ((ctx: Readonly<CommandContext$1<G>>, result: string | undefined) => Awaitable$1<void>) | undefined;
341
+ /**
342
+ * Hook that runs when a command throws an error
343
+ * @see {@link CliOptions.onErrorCommand}
344
+ * @since v0.27.0
345
+ */
346
+ onErrorCommand: ((ctx: Readonly<CommandContext$1<G>>, error: Error) => Awaitable$1<void>) | undefined;
347
+ }
348
+ /**
349
+ * CLI options of `cli` function.
350
+ */
351
+
352
+ /**
353
+ * Command call mode.
354
+ */
355
+ type CommandCallMode = 'entry' | 'subCommand' | 'unexpected';
356
+ /**
357
+ * Command context.
358
+ * Command context is the context of the command execution.
359
+ */
360
+ interface CommandContext$1<G extends GunshiParamsConstraint$1 = DefaultGunshiParams$1> {
361
+ /**
362
+ * Command name, that is the command that is executed.
363
+ * The command name is same {@link CommandEnvironment.name}.
364
+ */
365
+ name: string | undefined;
366
+ /**
367
+ * Command description, that is the description of the command that is executed.
368
+ * The command description is same {@link CommandEnvironment.description}.
369
+ */
370
+ description: string | undefined;
371
+ /**
372
+ * Command environment, that is the environment of the command that is executed.
373
+ * The command environment is same {@link CommandEnvironment}.
374
+ */
375
+ env: Readonly<CommandEnvironment<G>>;
376
+ /**
377
+ * Command arguments, that is the arguments of the command that is executed.
378
+ * The command arguments is same {@link Command.args}.
379
+ */
380
+ args: ExtractArgs$1<G>;
381
+ /**
382
+ * Whether arguments were explicitly provided by the user.
383
+ *
384
+ * - `true`: The argument was explicitly provided via command line
385
+ * - `false`: The argument was not explicitly provided. This means either:
386
+ * - The value comes from a default value defined in the argument schema
387
+ * - The value is `undefined` (no explicit input and no default value)
388
+ */
389
+ explicit: ExtractArgExplicitlyProvided<G>;
390
+ /**
391
+ * Command values, that is the values of the command that is executed.
392
+ * Resolve values with `resolveArgs` from command arguments and {@link Command.args}.
393
+ */
394
+ values: ArgValues<ExtractArgs$1<G>>;
395
+ /**
396
+ * Command positionals arguments, that is the positionals of the command that is executed.
397
+ * Resolve positionals with `resolveArgs` from command arguments.
398
+ */
399
+ positionals: string[];
400
+ /**
401
+ * Command rest arguments, that is the remaining argument not resolved by the optional command option delimiter `--`.
402
+ */
403
+ rest: string[];
404
+ /**
405
+ * Original command line arguments.
406
+ * This argument is passed from `cli` function.
407
+ */
408
+ _: string[];
409
+ /**
410
+ * Argument tokens, that is parsed by `parseArgs` function.
411
+ */
412
+ tokens: ArgToken[];
413
+ /**
414
+ * Whether the currently executing command has been executed with the sub-command name omitted.
415
+ */
416
+ omitted: boolean;
417
+ /**
418
+ * Command call mode.
419
+ * The command call mode is `entry` when the command is executed as an entry command, and `subCommand` when the command is executed as a sub-command.
420
+ */
421
+ callMode: CommandCallMode;
422
+ /**
423
+ * Whether to convert the camel-case style argument name to kebab-case.
424
+ * This context value is set from {@link Command.toKebab} option.
425
+ */
426
+ toKebab?: boolean;
427
+ /**
428
+ * Output a message.
429
+ * If {@link CommandEnvironment.usageSilent} is true, the message is not output.
430
+ * @param message an output message, @see {@link console.log}
431
+ * @param optionalParams an optional parameters, @see {@link console.log}
432
+ * @internal
433
+ */
434
+ log: (message?: any, ...optionalParams: any[]) => void;
435
+ /**
436
+ * Command context extensions.
437
+ * @since v0.27.0
438
+ */
439
+ extensions: keyof ExtractExtensions<G> extends never ? undefined : ExtractExtensions<G>;
440
+ /**
441
+ * Validation error from argument parsing.
442
+ * This will be set if argument validation fails during CLI execution.
443
+ */
444
+ validationError?: AggregateError;
445
+ }
446
+ /**
447
+ * CommandContextCore type (base type without extensions)
448
+ * @since v0.27.0
449
+ */
72
450
 
73
451
  /**
74
- * An object that contains the values of the arguments.
75
- */
452
+ * Rendering control options
453
+ * @since v0.27.0
454
+ */
455
+ interface RenderingOptions<G extends GunshiParamsConstraint$1 = DefaultGunshiParams$1> {
456
+ /**
457
+ * Header rendering configuration
458
+ * - `null`: Disable rendering
459
+ * - `function`: Use custom renderer
460
+ * - `undefined` (when omitted): Use default renderer
461
+ */
462
+ header?: ((ctx: Readonly<CommandContext$1<G>>) => Promise<string>) | null;
463
+ /**
464
+ * Usage rendering configuration
465
+ * - `null`: Disable rendering
466
+ * - `function`: Use custom renderer
467
+ * - `undefined` (when omitted): Use default renderer
468
+ */
469
+ usage?: ((ctx: Readonly<CommandContext$1<G>>) => Promise<string>) | null;
470
+ /**
471
+ * Validation errors rendering configuration
472
+ * - `null`: Disable rendering
473
+ * - `function`: Use custom renderer
474
+ * - `undefined` (when omitted): Use default renderer
475
+ */
476
+ validationErrors?: ((ctx: Readonly<CommandContext$1<G>>, error: AggregateError) => Promise<string>) | null;
477
+ }
478
+ /**
479
+ * Command interface.
480
+ */
481
+ interface Command$1<G extends GunshiParamsConstraint$1 = DefaultGunshiParams$1> {
482
+ /**
483
+ * Command name.
484
+ * It's used to find command line arguments to execute from sub commands, and it's recommended to specify.
485
+ */
486
+ name?: string;
487
+ /**
488
+ * Command description.
489
+ * It's used to describe the command in usage and it's recommended to specify.
490
+ */
491
+ description?: string;
492
+ /**
493
+ * Command arguments.
494
+ * Each argument can include a description property to describe the argument in usage.
495
+ */
496
+ args?: ExtractArgs$1<G>;
497
+ /**
498
+ * Command examples.
499
+ * examples of how to use the command.
500
+ */
501
+ examples?: string | CommandExamplesFetcher<G>;
502
+ /**
503
+ * Command runner. it's the command to be executed
504
+ */
505
+ run?: CommandRunner<G>;
506
+ /**
507
+ * Whether to convert the camel-case style argument name to kebab-case.
508
+ * If you will set to `true`, All {@link Command.args} names will be converted to kebab-case.
509
+ */
510
+ toKebab?: boolean;
511
+ /**
512
+ * Whether this is an internal command.
513
+ * Internal commands are not shown in help usage.
514
+ * @default false
515
+ * @since v0.27.0
516
+ */
517
+ internal?: boolean;
518
+ /**
519
+ * Whether this command is an entry command.
520
+ * @default undefined
521
+ * @since v0.27.0
522
+ */
523
+ entry?: boolean;
524
+ /**
525
+ * Rendering control options
526
+ * @since v0.27.0
527
+ */
528
+ rendering?: RenderingOptions<G>;
529
+ }
530
+ /**
531
+ * Lazy command interface.
532
+ * Lazy command that's not loaded until it is executed.
533
+ */
534
+ type LazyCommand<G extends GunshiParamsConstraint$1 = DefaultGunshiParams$1> = {
535
+ /**
536
+ * Command load function
537
+ */
538
+ (): Awaitable$1<Command$1<G> | CommandRunner<G>>;
539
+ /**
540
+ * Command name
541
+ */
542
+ commandName?: string;
543
+ } & Omit<Command$1<G>, 'run' | 'name'>;
544
+ /**
545
+ * Define a command type.
546
+ */
547
+
548
+ /**
549
+ * Command examples fetcher.
550
+ * @param ctx A {@link CommandContext | command context}
551
+ * @returns A fetched command examples.
552
+ */
553
+ type CommandExamplesFetcher<G extends GunshiParamsConstraint$1 = DefaultGunshiParams$1> = (ctx: Readonly<CommandContext$1<G>>) => Awaitable$1<string>;
554
+ /**
555
+ * Command runner.
556
+ * @param ctx A {@link CommandContext | command context}
557
+ * @returns void or string (for CLI output)
558
+ */
559
+ type CommandRunner<G extends GunshiParamsConstraint$1 = DefaultGunshiParams$1> = (ctx: Readonly<CommandContext$1<G>>) => Awaitable$1<string | void>;
560
+ /**
561
+ * Command loader.
562
+ * A function that returns a command or command runner.
563
+ * This is used to lazily load commands.
564
+ * @returns A command or command runner
565
+ */
76
566
  declare namespace constants_d_exports {
77
567
  export { ARG_NEGATABLE_PREFIX, ARG_PREFIX, ARG_PREFIX_AND_KEY_SEPARATOR, BUILD_IN_PREFIX_AND_KEY_SEPARATOR, BUILT_IN_KEY_SEPARATOR, BUILT_IN_PREFIX, COMMAND_BUILTIN_RESOURCE_KEYS, COMMON_ARGS, PLUGIN_PREFIX };
78
568
  }
@@ -101,7 +591,6 @@ type CommonArgType = {
101
591
  };
102
592
  declare const COMMON_ARGS: CommonArgType;
103
593
  declare const COMMAND_BUILTIN_RESOURCE_KEYS: readonly ["USAGE", "COMMAND", "SUBCOMMAND", "COMMANDS", "ARGUMENTS", "OPTIONS", "EXAMPLES", "FORMORE", "NEGATABLE", "DEFAULT", "CHOICES"];
104
-
105
594
  //#endregion
106
595
  //#region ../shared/src/types.d.ts
107
596
  type RemoveIndexSignature<T> = { [K in keyof T as string extends K ? never : number extends K ? never : K]: T[K] };
@@ -109,7 +598,7 @@ type RemoveIndexSignature<T> = { [K in keyof T as string extends K ? never : num
109
598
  * Remove index signature from object or record type.
110
599
  */
111
600
  type RemovedIndex<T> = RemoveIndexSignature<{ [K in keyof T]: T[K] }>;
112
- type KeyOfArgs<A extends Args> = keyof A | { [K in keyof A]: A[K]['type'] extends 'boolean' ? A[K]['negatable'] extends true ? `no-${Extract<K, string>}` : never : never }[keyof A];
601
+ type KeyOfArgs<A extends Args$1> = keyof A | { [K in keyof A]: A[K]['type'] extends 'boolean' ? A[K]['negatable'] extends true ? `no-${Extract<K, string>}` : never : never }[keyof A];
113
602
  /**
114
603
  * Generate a namespaced key.
115
604
  */
@@ -123,20 +612,59 @@ type CommandBuiltinArgsKeys = keyof (typeof constants_d_exports)['COMMON_ARGS'];
123
612
  */
124
613
  type CommandBuiltinResourceKeys = (typeof constants_d_exports)['COMMAND_BUILTIN_RESOURCE_KEYS'][number];
125
614
  /**
126
- * i18n built-in resource keys.
615
+ * Built-in resource keys.
127
616
  */
128
617
  type BuiltinResourceKeys = CommandBuiltinArgsKeys | CommandBuiltinResourceKeys;
129
618
  /**
130
- * Command i18n built-in keys.
131
- * The command i18n built-in keys are used by the i18n plugin for translation.
619
+ * Command built-in keys.
132
620
  */
133
- type CommandBuiltinKeys = GenerateNamespacedKey<BuiltinResourceKeys> | 'description' | 'examples';
621
+ type CommandBuiltinKeys = GenerateNamespacedKey<BuiltinResourceKeys>;
134
622
  /**
135
623
  * Command i18n option keys.
136
624
  * The command i18n option keys are used by the i18n plugin for translation.
137
625
  */
138
- type CommandArgKeys<A extends Args> = GenerateNamespacedKey<KeyOfArgs<RemovedIndex<A>>, typeof ARG_PREFIX>;
139
-
626
+ type CommandArgKeys<A extends Args$1, C = {}, K extends string = GenerateNamespacedKey<Extract<KeyOfArgs<RemovedIndex<A>>, string>, typeof ARG_PREFIX>> = C extends {
627
+ name: infer N;
628
+ } ? (N extends string ? GenerateNamespacedKey<K, N> : K) : K;
629
+ /**
630
+ * Resolve translation keys for command context.
631
+ */
632
+ type ResolveTranslationKeys<A extends Args$1, C = {},
633
+ // for CommandContext
634
+ E extends Record<string, string> = {},
635
+ // for extended resources
636
+ R extends string = keyof RemovedIndex<E>, T extends string = (C extends {
637
+ name: infer N;
638
+ } ? N extends string ? GenerateNamespacedKey<R, N> : R : R | CommandBuiltinKeys), O = CommandArgKeys<A, C>> = CommandBuiltinKeys | O | T;
639
+ /**
640
+ * Translation function interface
641
+ */
642
+ //#endregion
643
+ //#region ../shared/src/utils.d.ts
644
+ /**
645
+ * Resolve a namespaced key for built-in resources.
646
+ * Built-in keys are prefixed with "_:".
647
+ * @param key The built-in key to resolve.
648
+ * @returns Prefixed built-in key.
649
+ */
650
+ declare function resolveBuiltInKey<K extends string = CommandBuiltinArgsKeys | CommandBuiltinResourceKeys>(key: K): GenerateNamespacedKey<K>;
651
+ /**
652
+ * Resolve a namespaced key for argument resources.
653
+ * Argument keys are prefixed with "arg:".
654
+ * If the command name is provided, it will be prefixed with the command name (e.g. "cmd1:arg:foo").
655
+ * @param key The argument key to resolve.
656
+ * @param ctx The command context.
657
+ * @returns Prefixed argument key.
658
+ */
659
+ declare function resolveArgKey<A extends Args$1 = DefaultGunshiParams$1['args'], K extends string = KeyOfArgs<RemovedIndex<A>>>(key: K, ctx?: Readonly<CommandContext$1>): string;
660
+ /**
661
+ * Resolve a namespaced key for non-built-in resources.
662
+ * Non-built-in keys are not prefixed with any special characters. If the command name is provided, it will be prefixed with the command name (e.g. "cmd1:foo").
663
+ * @param key The non-built-in key to resolve.
664
+ * @param ctx The command context.
665
+ * @returns Prefixed non-built-in key.
666
+ */
667
+ declare function resolveKey<T extends Record<string, string> = {}, K = (keyof T extends string ? keyof T : string)>(key: K, ctx?: Readonly<CommandContext$1>): string;
140
668
  //#endregion
141
669
  //#region src/types.d.ts
142
670
  /**
@@ -155,16 +683,27 @@ interface I18nCommandContext<G extends GunshiParams<any> = DefaultGunshiParams>
155
683
  /**
156
684
  * Command locale
157
685
  */
158
- locale: string | Intl.Locale;
686
+ locale: Intl.Locale;
159
687
  /**
160
- * Translate a message
688
+ * Translate a message.
161
689
  * @param key Translation key
162
690
  * @param values Values to interpolate
163
691
  * @returns Translated message. If the key is not found:
164
692
  * - For custom keys: returns an empty string ('')
165
693
  * - For built-in keys (prefixed with '_:'): returns the key itself
166
694
  */
167
- translate: <T extends string = CommandBuiltinKeys, O = CommandArgKeys<G['args']>, K = CommandBuiltinKeys | O | T>(key: K, values?: Record<string, unknown>) => string;
695
+ translate: <A extends Args = G['args'], C = {},
696
+ // for CommandContext
697
+ E extends Record<string, string> = {},
698
+ // for extended resources
699
+ K = ResolveTranslationKeys<A, C, E>>(key: K, values?: Record<string, unknown>) => string;
700
+ /**
701
+ * Load command resources.
702
+ * @param ctx Command context
703
+ * @param command Command
704
+ * @returns Whether the resources were loaded successfully
705
+ */
706
+ loadResource: (locale: string | Intl.Locale, ctx: CommandContext, command: Command) => Promise<boolean>;
168
707
  }
169
708
  /**
170
709
  * i18n plugin options
@@ -243,19 +782,9 @@ type CommandResource<G extends GunshiParamsConstraint = DefaultGunshiParams> = {
243
782
  * Command description.
244
783
  */
245
784
  description: string;
246
- /**
247
- * Examples usage.
248
- */
249
- examples: string | CommandExamplesFetcher<NormalizeToGunshiParams<G>>;
250
785
  } & { [Arg in GenerateNamespacedKey<KeyOfArgs<RemovedIndex<ExtractArgs<G>>>, typeof ARG_PREFIX>]: string } & {
251
786
  [key: string]: string;
252
787
  };
253
- /**
254
- * Command examples fetcher.
255
- * @param ctx A {@link CommandContext | command context}
256
- * @returns A fetched command examples.
257
- */
258
- type CommandExamplesFetcher<G extends GunshiParamsConstraint = DefaultGunshiParams> = (ctx: Readonly<CommandContext<G>>) => Awaitable<string>;
259
788
  /**
260
789
  * Command resource fetcher.
261
790
  * @param ctx A {@link CommandContext | command context}
@@ -272,7 +801,6 @@ interface I18nCommand<G extends GunshiParamsConstraint = DefaultGunshiParams> ex
272
801
  */
273
802
  resource?: CommandResourceFetcher<G>;
274
803
  }
275
-
276
804
  //#endregion
277
805
  //#region src/helpers.d.ts
278
806
  /**
@@ -328,7 +856,6 @@ declare function defineI18n<G extends GunshiParamsConstraint = DefaultGunshiPara
328
856
  * ```
329
857
  */
330
858
  declare function withI18nResource<G extends GunshiParamsConstraint>(command: Command<G>, resource: CommandResourceFetcher<G>): I18nCommand<G>;
331
-
332
859
  //#endregion
333
860
  //#region src/translation.d.ts
334
861
  declare function createTranslationAdapter(options: TranslationAdapterFactoryOptions): TranslationAdapter;
@@ -340,7 +867,6 @@ declare class DefaultTranslation implements TranslationAdapter {
340
867
  getMessage(locale: string, key: string): string | undefined;
341
868
  translate(locale: string, key: string, values?: Record<string, unknown>): string | undefined;
342
869
  }
343
-
344
870
  //#endregion
345
871
  //#region src/index.d.ts
346
872
  /**
@@ -350,7 +876,6 @@ declare const DEFAULT_LOCALE = "en-US";
350
876
  /**
351
877
  * i18n plugin
352
878
  */
353
- declare function i18n(options?: I18nPluginOptions): PluginWithExtension<Promise<I18nCommandContext<DefaultGunshiParams>>>;
354
-
879
+ declare function i18n(options?: I18nPluginOptions): PluginWithExtension<I18nCommandContext<DefaultGunshiParams>>;
355
880
  //#endregion
356
- export { CommandExamplesFetcher, CommandResource, CommandResourceFetcher, DEFAULT_LOCALE, DefaultTranslation, I18nCommand, I18nCommandContext, I18nPluginOptions, PluginId, TranslationAdapter, TranslationAdapterFactory, TranslationAdapterFactoryOptions, createTranslationAdapter, i18n as default, defineI18n, pluginId, withI18nResource };
881
+ export { CommandResource, CommandResourceFetcher, DEFAULT_LOCALE, DefaultTranslation, I18nCommand, I18nCommandContext, I18nPluginOptions, PluginId, TranslationAdapter, TranslationAdapterFactory, TranslationAdapterFactoryOptions, createTranslationAdapter, i18n as default, defineI18n, pluginId, resolveArgKey, resolveBuiltInKey, resolveKey, withI18nResource };
package/lib/index.js CHANGED
@@ -45,11 +45,38 @@ var en_US_default = {
45
45
 
46
46
  //#endregion
47
47
  //#region ../shared/src/utils.ts
48
+ /**
49
+ * Resolve a namespaced key for built-in resources.
50
+ * Built-in keys are prefixed with "_:".
51
+ * @param key The built-in key to resolve.
52
+ * @returns Prefixed built-in key.
53
+ */
48
54
  function resolveBuiltInKey(key) {
49
55
  return `${BUILT_IN_PREFIX}${BUILT_IN_KEY_SEPARATOR}${key}`;
50
56
  }
51
- function resolveArgKey(key) {
52
- return `${ARG_PREFIX}${BUILT_IN_KEY_SEPARATOR}${key}`;
57
+ /**
58
+ * Resolve a namespaced key for argument resources.
59
+ * Argument keys are prefixed with "arg:".
60
+ * If the command name is provided, it will be prefixed with the command name (e.g. "cmd1:arg:foo").
61
+ * @param key The argument key to resolve.
62
+ * @param ctx The command context.
63
+ * @returns Prefixed argument key.
64
+ */
65
+ function resolveArgKey(key, ctx) {
66
+ return `${ctx?.name ? `${ctx.name}${BUILT_IN_KEY_SEPARATOR}` : ""}${ARG_PREFIX}${BUILT_IN_KEY_SEPARATOR}${key}`;
67
+ }
68
+ /**
69
+ * Resolve a namespaced key for non-built-in resources.
70
+ * Non-built-in keys are not prefixed with any special characters. If the command name is provided, it will be prefixed with the command name (e.g. "cmd1:foo").
71
+ * @param key The non-built-in key to resolve.
72
+ * @param ctx The command context.
73
+ * @returns Prefixed non-built-in key.
74
+ */
75
+ function resolveKey(key, ctx) {
76
+ return `${ctx?.name ? `${ctx.name}${BUILT_IN_KEY_SEPARATOR}` : ""}${key}`;
77
+ }
78
+ async function resolveExamples(ctx, examples) {
79
+ return typeof examples === "string" ? examples : typeof examples === "function" ? await examples(ctx) : "";
53
80
  }
54
81
  function namespacedId(id) {
55
82
  return `${PLUGIN_PREFIX}${BUILT_IN_KEY_SEPARATOR}${id}`;
@@ -169,7 +196,7 @@ const BUILT_IN_PREFIX_CODE = BUILT_IN_PREFIX.codePointAt(0);
169
196
  */
170
197
  function i18n(options = {}) {
171
198
  const locale = toLocale(options.locale);
172
- const localeStr = locale.toString();
199
+ const localeStr = toLocaleString(locale);
173
200
  const resources = options.resources || Object.create(null);
174
201
  const translationAdapterFactory = options.translationAdapterFactory || createTranslationAdapter;
175
202
  const adapter = translationAdapterFactory({
@@ -195,56 +222,69 @@ function i18n(options = {}) {
195
222
  }
196
223
  function getResource(locale$1) {
197
224
  const targetLocale = toLocale(locale$1);
198
- const targetLocaleStr = targetLocale.toString();
225
+ const targetLocaleStr = toLocaleString(targetLocale);
199
226
  return localeBuiltinResources.get(targetLocaleStr);
200
227
  }
201
228
  function setResource(locale$1, resource) {
202
229
  const targetLocale = toLocale(locale$1);
203
- const targetLocaleStr = targetLocale.toString();
230
+ const targetLocaleStr = toLocaleString(targetLocale);
204
231
  if (localeBuiltinResources.has(targetLocaleStr)) return;
205
- localeBuiltinResources.set(targetLocale.toString(), mapResourceWithBuiltinKey(resource));
232
+ localeBuiltinResources.set(targetLocaleStr, mapResourceWithBuiltinKey(resource));
206
233
  }
207
234
  setResource(DEFAULT_LOCALE, en_US_default);
208
235
  for (const [locale$1, resource] of Object.entries(resources)) setResource(locale$1, resource);
209
236
  builtInLoadedResources = getResource(locale);
237
+ async function loadResource(locale$1, ctx, cmd) {
238
+ let loaded = false;
239
+ const originalResource = await loadCommandResource(ctx, cmd);
240
+ if (originalResource) {
241
+ const resource = await normalizeResource(originalResource, ctx);
242
+ if (builtInLoadedResources) {
243
+ resource.help = builtInLoadedResources.help;
244
+ resource.version = builtInLoadedResources.version;
245
+ }
246
+ adapter.setResource(toLocaleString(locale$1), resource);
247
+ loaded = true;
248
+ }
249
+ return loaded;
250
+ }
210
251
  return {
211
252
  locale,
212
- translate
253
+ translate,
254
+ loadResource
213
255
  };
214
256
  },
215
257
  onExtension: async (ctx, cmd) => {
258
+ const i18n$1 = ctx.extensions[pluginId];
216
259
  /**
217
260
  * load command resources, after the command context is extended
218
261
  */
219
262
  const loadedOptionsResources = Object.entries(ctx.args).map(([key, schema]) => [key, schema.description || ""]);
220
263
  const defaultCommandResource = loadedOptionsResources.reduce((res, [key, value]) => {
221
- res[resolveArgKey(key)] = value;
264
+ res[resolveArgKey(key, ctx)] = value;
222
265
  return res;
223
266
  }, Object.create(null));
224
- defaultCommandResource.description = cmd.description || "";
225
- defaultCommandResource.examples = typeof cmd.examples === "string" ? cmd.examples : typeof cmd.examples === "function" ? await cmd.examples(ctx) : "";
267
+ defaultCommandResource[resolveKey("description", ctx)] = cmd.description || "";
268
+ defaultCommandResource[resolveKey("examples", ctx)] = await resolveExamples(ctx, cmd.examples);
226
269
  adapter.setResource(DEFAULT_LOCALE, defaultCommandResource);
227
- const originalResource = await loadCommandResource(ctx, cmd);
228
- if (originalResource) {
229
- const resource = Object.assign(Object.create(null), originalResource, { examples: typeof originalResource.examples === "string" ? originalResource.examples : typeof originalResource.examples === "function" ? await originalResource.examples(ctx) : "" });
230
- if (builtInLoadedResources) {
231
- resource.help = builtInLoadedResources.help;
232
- resource.version = builtInLoadedResources.version;
233
- }
234
- adapter.setResource(localeStr, resource);
235
- }
270
+ await i18n$1.loadResource(localeStr, ctx, cmd);
236
271
  }
237
272
  });
238
273
  }
239
274
  function toLocale(locale) {
240
275
  return locale instanceof Intl.Locale ? locale : typeof locale === "string" ? new Intl.Locale(locale) : new Intl.Locale(DEFAULT_LOCALE);
241
276
  }
277
+ function toLocaleString(locale) {
278
+ return locale instanceof Intl.Locale ? locale.toString() : locale;
279
+ }
242
280
  async function loadCommandResource(ctx, command) {
243
281
  if (!hasI18nResource(command)) return void 0;
244
282
  let resource;
245
283
  try {
246
284
  resource = await command.resource(ctx);
247
- } catch {}
285
+ } catch (error) {
286
+ console.error(`Failed to load resource for command "${command.name}":`, error);
287
+ }
248
288
  return resource;
249
289
  }
250
290
  function mapResourceWithBuiltinKey(resource) {
@@ -256,6 +296,13 @@ function mapResourceWithBuiltinKey(resource) {
256
296
  function hasI18nResource(command) {
257
297
  return "resource" in command && typeof command.resource === "function";
258
298
  }
299
+ async function normalizeResource(resource, ctx) {
300
+ const ret = Object.create(null);
301
+ for (const [key, value] of Object.entries(resource)) if (key.startsWith(ARG_PREFIX_AND_KEY_SEPARATOR)) ret[resolveKey(key, ctx)] = value;
302
+ else if (key === "examples") ret[resolveKey("examples", ctx)] = await resolveExamples(ctx, value);
303
+ else ret[resolveKey(key, ctx)] = value;
304
+ return ret;
305
+ }
259
306
 
260
307
  //#endregion
261
- export { DEFAULT_LOCALE, DefaultTranslation, createTranslationAdapter, i18n as default, defineI18n, pluginId, withI18nResource };
308
+ export { DEFAULT_LOCALE, DefaultTranslation, createTranslationAdapter, i18n as default, defineI18n, pluginId, resolveArgKey, resolveBuiltInKey, resolveKey, withI18nResource };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gunshi/plugin-i18n",
3
3
  "description": "internationalization plugin for gunshi",
4
- "version": "0.26.3",
4
+ "version": "0.27.0-alpha.10",
5
5
  "author": {
6
6
  "name": "kazuya kawaguchi",
7
7
  "email": "kawakazu80@gmail.com"
@@ -53,10 +53,10 @@
53
53
  }
54
54
  },
55
55
  "dependencies": {
56
- "@gunshi/plugin": "0.26.3"
56
+ "@gunshi/plugin": "0.27.0-alpha.10"
57
57
  },
58
58
  "peerDependencies": {
59
- "@gunshi/plugin-global": "0.26.3"
59
+ "@gunshi/plugin-global": "0.27.0-alpha.10"
60
60
  },
61
61
  "peerDependenciesMeta": {
62
62
  "@gunshi/plugin-global": {
@@ -65,16 +65,16 @@
65
65
  },
66
66
  "devDependencies": {
67
67
  "@intlify/core": "next",
68
- "deno": "^2.3.3",
69
- "jsr": "^0.13.4",
68
+ "deno": "^2.4.2",
69
+ "jsr": "^0.13.5",
70
70
  "jsr-exports-lint": "^0.4.1",
71
71
  "messageformat": "4.0.0-12",
72
72
  "publint": "^0.3.12",
73
- "tsdown": "^0.12.3",
74
- "typedoc": "^0.28.4",
75
- "typedoc-plugin-markdown": "^4.6.3",
76
- "@gunshi/shared": "0.26.3",
77
- "@gunshi/resources": "0.26.3"
73
+ "tsdown": "^0.13.0",
74
+ "typedoc": "^0.28.7",
75
+ "typedoc-plugin-markdown": "^4.7.1",
76
+ "@gunshi/resources": "0.27.0-alpha.10",
77
+ "@gunshi/shared": "0.27.0-alpha.10"
78
78
  },
79
79
  "scripts": {
80
80
  "build": "tsdown",