@gunshi/plugin-i18n 0.27.0-alpha.9 → 0.27.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +136 -101
  2. package/lib/index.d.ts +636 -540
  3. package/lib/index.js +211 -74
  4. package/package.json +14 -10
package/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @gunshi/plugin-i18n
2
2
 
3
+ [![Version][npm-version-src]][npm-version-href]
4
+ [![InstallSize][install-size-src]][install-size-src]
5
+ [![JSR][jsr-src]][jsr-href]
6
+
3
7
  > internationalization (i18n) plugin for gunshi.
4
8
 
5
9
  This plugin provides multi-language support for your CLI applications, allowing you to create commands that can display messages in different languages based on user locale.
@@ -27,16 +31,18 @@ bun add @gunshi/plugin-i18n
27
31
 
28
32
  ```ts
29
33
  import { cli } from 'gunshi'
30
- import i18n, { defineI18n } from '@gunshi/plugin-i18n'
34
+ import i18n, { pluginId as i18nId, defineI18nWithTypes } from '@gunshi/plugin-i18n'
35
+
36
+ import type { I18nExtension } from '@gunshi/plugin-i18n'
31
37
 
32
38
  /**
33
- * You can define a command with `defineI18n`, which is compatible with the `define` function.
39
+ * You can define a command with `defineI18nWithTypes`, which is compatible with the `define` function.
34
40
  * This provides full type safety for i18n commands - TypeScript will suggest the 'resource' option
35
41
  * and ensure your resource keys match the expected structure.
36
42
  */
37
43
 
38
44
  // Define a command
39
- const greetCommand = defineI18n({
45
+ const command = defineI18nWithTypes<{ extensions: { [i18nId]: I18nExtension } }>()({
40
46
  name: 'greet',
41
47
  description: 'Greet someone',
42
48
 
@@ -48,16 +54,16 @@ const greetCommand = defineI18n({
48
54
  },
49
55
 
50
56
  // Define resource fetcher for translations
51
- resource: async ctx => ({
57
+ resource: locale => ({
52
58
  description: 'Greet someone in their language',
53
59
  'arg:name': "The person's name",
54
60
  greeting: 'Hello, {$name}!'
55
61
  }),
56
62
 
57
- run: async ctx => {
63
+ run: ctx => {
58
64
  const { name } = ctx.values
59
65
  // Use translate function from context
60
- console.log(ctx.extensions['g:i18n'].translate('greeting', { name }))
66
+ console.log(ctx.extensions[i18nId].translate('greeting', { name }))
61
67
  }
62
68
  })
63
69
 
@@ -79,7 +85,7 @@ await cli(process.argv.slice(2), greetCommand, {
79
85
  - Default: `'en-US'`
80
86
  - Description: The locale to use for translations. Can be a BCP 47 language tag string or an `Intl.Locale` object.
81
87
 
82
- ### `resources`
88
+ ### `builtinResources`
83
89
 
84
90
  - Type: `Record<string, Record<BuiltinResourceKeys, string>>`
85
91
  - Default: `{}`
@@ -112,19 +118,16 @@ Example with globals plugin:
112
118
  ```ts
113
119
  import { cli } from 'gunshi'
114
120
  import i18n from '@gunshi/plugin-i18n'
115
- import globals from '@gunshi/plugin-global'
121
+ import globals from '@gunshi/plugin-global' // need install `@gunshi/plugin-global`
122
+ import jaJPResource from '@gunshi/resources/ja-JP' with { type: 'json' } // need install `@gunshi/resources`
116
123
 
117
124
  await cli(args, command, {
118
125
  plugins: [
119
126
  globals(), // Adds --help and --version options
120
127
  i18n({
121
128
  locale: 'ja-JP',
122
- resources: {
123
- 'ja-JP': {
124
- help: 'ヘルプメッセージを表示', // Override help translation
125
- version: 'バージョンを表示' // Override version translation
126
- // ... other built-in translations
127
- }
129
+ builtinResources: {
130
+ 'ja-JP': jaJPResource // Set from with providing gunshi built-in resources
128
131
  }
129
132
  })
130
133
  ]
@@ -135,33 +138,81 @@ await cli(args, command, {
135
138
 
136
139
  ### `defineI18n`
137
140
 
138
- Type-safe helper to define an i18n-aware command.
141
+ Define an i18n-aware command.
139
142
 
140
143
  ```ts
141
144
  import { defineI18n } from '@gunshi/plugin-i18n'
142
145
 
143
- const command = defineI18n({
144
- name: 'hello',
146
+ const greetCommand = defineI18n({
147
+ name: 'greet',
148
+ description: 'Greet someone',
145
149
  args: {
146
- name: { type: 'string' }
150
+ name: {
151
+ type: 'string',
152
+ description: 'Name to greet'
153
+ }
147
154
  },
148
- resource: async ctx => ({
149
- description: 'Say hello',
150
- 'arg:name': 'Your name'
151
- }),
152
- run: async ctx => {
155
+ resource: locale => {
156
+ switch (locale.toString()) {
157
+ case 'ja-JP': {
158
+ return {
159
+ description: '誰かにあいさつ',
160
+ 'arg:name': 'あいさつするための名前'
161
+ }
162
+ }
163
+ // other locales ...
164
+ }
165
+ },
166
+ run: ctx => {
153
167
  console.log(`Hello, ${ctx.values.name}!`)
154
168
  }
155
169
  })
156
170
  ```
157
171
 
172
+ The difference from the `define` function is that you can define a `resource` option that can load a locale.
173
+
174
+ ### `defineI18nWithTypes`
175
+
176
+ Define an i18n-aware command with types
177
+
178
+ This helper function allows specifying the type parameter of `GunshiParams` while inferring the `Args` type, `ExtendContext` type from the definition.
179
+
180
+ ```ts
181
+ import { defineI18nWithTypes } from '@gunshi/plugin-i18n'
182
+
183
+ // Define a command with specific extensions type
184
+ type MyExtensions = { logger: { log: (message: string) => void } }
185
+
186
+ const greetCommand = defineI18nWithTypes<{ extensions: MyExtensions }>()({
187
+ name: 'greet',
188
+ args: {
189
+ name: { type: 'string', description: 'Name to greet' }
190
+ },
191
+ resource: locale => {
192
+ switch (locale.toString()) {
193
+ case 'ja-JP': {
194
+ return {
195
+ description: '誰かにあいさつ',
196
+ 'arg:name': 'あいさつするための名前'
197
+ }
198
+ }
199
+ // other locales ...
200
+ }
201
+ },
202
+ run: ctx => {
203
+ // ctx.values is inferred as { name?: string }
204
+ // ctx.extensions is MyExtensions
205
+ }
206
+ })
207
+ ```
208
+
158
209
  ### `withI18nResource`
159
210
 
160
- Add i18n resource to an existing command. This helper is useful for extending an already defined command.
211
+ Add i18n resource to an existing command. This helper is useful for extending an already defined command with `define` function.
161
212
 
162
213
  ```ts
163
- import { define } from 'gunshi' // 'gunshi/definition', or '@gunshi/definition'
164
- import { withI18nResource } from '@gunshi/plugin-i18n'
214
+ import { define } from 'gunshi' // alternative 'gunshi/definition', or '@gunshi/definition'
215
+ import { withI18nResource, pluginId as i18nId } from '@gunshi/plugin-i18n'
165
216
 
166
217
  const basicCommand = define({
167
218
  name: 'test',
@@ -175,11 +226,10 @@ const basicCommand = define({
175
226
  run: ctx => console.log(`test: ${ctx.values.target}`)
176
227
  })
177
228
 
178
- const i18nCommand = withI18nResource(basicCommand, async ctx => {
179
- const resource = await import(
180
- `./path/to/resources/test/${ctx.extensions['g:i18n'].locale.toString()}.json`,
181
- { with: { type: 'json' } }
182
- ).then(l => l.default || l)
229
+ const i18nCommand = withI18nResource(basicCommand, async locale => {
230
+ const resource = await import(`./path/to/resources/test/${locale.toString()}.json`, {
231
+ with: { type: 'json' }
232
+ }).then(l => l.default || l)
183
233
  return resource
184
234
  })
185
235
  ```
@@ -188,16 +238,16 @@ const i18nCommand = withI18nResource(basicCommand, async ctx => {
188
238
 
189
239
  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
240
 
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
241
+ - `resolveKey(key: string, name?: string): string` - Resolves a custom key with command namespace if applicable
242
+ - `resolveArgKey(key: string, name?: string): string` - Resolves an argument key with `arg:` prefix and namespace
193
243
  - `resolveBuiltInKey(key: string): string` - Resolves a built-in key with `_:` prefix
194
244
 
195
245
  ```ts
196
246
  import { resolveKey, resolveArgKey, resolveBuiltInKey } from '@gunshi/plugin-i18n'
197
247
 
198
248
  // 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
249
+ resolveKey('description', ctx.name) // Returns namespaced key for description
250
+ resolveArgKey('verbose', ctx.name) // Returns 'arg:verbose' or 'command:arg:verbose' based on command namespace
201
251
  resolveBuiltInKey('USAGE') // Returns '_:USAGE'
202
252
  ```
203
253
 
@@ -226,35 +276,43 @@ While the `translate` function accepts keys without prefixes in most cases, usin
226
276
  #### Example Usage
227
277
 
228
278
  ```ts
229
- import { defineI18n, resolveKey, resolveArgKey, resolveBuiltInKey } from '@gunshi/plugin-i18n'
279
+ import {
280
+ defineI18nWithTypes,
281
+ pluginId as i18nId,
282
+ resolveKey,
283
+ resolveArgKey,
284
+ resolveBuiltInKey
285
+ } from '@gunshi/plugin-i18n'
230
286
 
231
- const command = defineI18n({
287
+ import type { I18nExtension } from '@gunshi/plugin-i18n'
288
+
289
+ const createCommand = defineI18nWithTypes<{ extensions: { [i18nId]: I18nExtension } }>()({
232
290
  name: 'deploy',
233
291
  args: {
234
292
  environment: { type: 'string', short: 'e' },
235
293
  force: { type: 'boolean', short: 'f' }
236
294
  },
237
- resource: async ctx => ({
295
+ resource: locale => ({
238
296
  description: 'Deploy application',
239
297
  'arg:environment': 'Target environment',
240
298
  'arg:force': 'Force deployment',
241
- deploy_start: 'Starting deployment...',
242
- deploy_success: 'Deployment completed successfully!'
299
+ start: 'Starting deployment...',
300
+ success: 'Deployment completed successfully!'
243
301
  }),
244
- run: async ctx => {
245
- const { translate } = ctx.extensions['g:i18n']
302
+ run: ctx => {
303
+ const { translate } = ctx.extensions[i18nId]
246
304
 
247
- // Direct usage (need to prefix with command name)
248
- console.log(translate('deploy:deploy_start'))
305
+ // Usage helper for custom keys (prefix with command name)
306
+ console.log(translate(resolveKey('start', ctx.name)))
249
307
 
250
308
  // Using helpers for explicit control
251
- const envKey = resolveArgKey('environment', ctx)
309
+ const envKey = resolveArgKey('environment', ctx.name)
252
310
  console.log(translate(envKey)) // Same as translate('arg:environment')
253
311
 
254
312
  // Useful when building dynamic keys
255
313
  const args = ['environment', 'force']
256
314
  for (const arg of args) {
257
- const key = resolveArgKey(arg, ctx)
315
+ const key = resolveArgKey(arg, ctx.name)
258
316
  const description = translate(key)
259
317
  console.log(`${arg}: ${description}`)
260
318
  }
@@ -282,6 +340,7 @@ Available extensions:
282
340
  - `locale: Intl.Locale`: The current locale
283
341
  - `translate<T>(key: T, values?: Record<string, unknown>): string`: Translation function
284
342
  - `loadResource(locale: string | Intl.Locale, ctx: CommandContext, command: Command): Promise<boolean>`: Manually load resources for a specific locale and command
343
+ - `registerGlobalOptionResources: (option: string, resources: Record<string, string>) => void`: Register global option resources. If your global option description needs the localization, you can install resource of it at `extension` or `onExtension` hook
285
344
 
286
345
  ## 📝 Resource Key Naming Conventions
287
346
 
@@ -321,20 +380,21 @@ Here's an example illustrating the convention:
321
380
  ```ts
322
381
  import { defineI18n } from '@gunshi/plugin-i18n'
323
382
 
383
+ // if you want to use `I18nExtension` in `run`, you should use `defineI18nWithTypes`, not `defineI18n`
324
384
  const command = defineI18n({
325
385
  name: 'my-command',
326
386
  args: {
327
387
  target: { type: 'string' },
328
388
  verbose: { type: 'boolean' }
329
389
  },
330
- resource: async ctx => {
390
+ resource: locale => {
331
391
  // Example for 'en-US' locale
332
392
  return {
333
- description: 'This is my command.', // No prefix
334
- 'arg:target': 'The target file to process.', // 'arg:' prefix
335
- 'arg:verbose': 'Enable verbose output.', // 'arg:' prefix
393
+ description: 'This is my command.', // built-in key, No prefix
394
+ 'arg:target': 'The target file to process.', // argument key, 'arg:' prefix
395
+ 'arg:verbose': 'Enable verbose output.', // argument key, 'arg:' prefix
336
396
  'arg:no-verbose': 'Disable verbose logging specifically.', // Optional custom translation for the negatable option
337
- processing_message: 'Processing target...' // No prefix
397
+ processing_message: 'Processing target...' // custom keys, No prefix
338
398
  }
339
399
  },
340
400
  run: ctx => {
@@ -393,13 +453,15 @@ The `translate` function has different behaviors depending on the key type:
393
453
  This difference is important for error handling and fallback displays:
394
454
 
395
455
  ```ts
456
+ import { resolveKey, resolveBuiltInKey } from '@gunshi/plugin-i18n'
457
+
396
458
  const { translate } = ctx.extensions['g:i18n']
397
459
 
398
460
  // Custom key not found
399
- translate('nonexistent_key') // Returns: ''
461
+ translate(resolveKey('nonexistent_key', ctx.name)) // Returns: ''
400
462
 
401
463
  // Built-in key not found (if not overridden)
402
- translate('USAGE') // Returns: 'USAGE'
464
+ translate(resolveBuiltInKey('USAGE')) // Returns: 'USAGE'
403
465
  ```
404
466
 
405
467
  ### Resource Loading Behavior
@@ -456,17 +518,19 @@ await cli(args, command, {
456
518
  The default translation adapter supports simple interpolation using `{$key}` syntax:
457
519
 
458
520
  ```ts
521
+ import { pluginId as i18nId, resolveKey } from '@gunshi/plugin-i18n'
522
+
459
523
  // In your resource
460
524
  const resource = {
461
525
  welcome: 'Welcome, {$name}!',
462
- 'items.count': 'You have {$count} items',
526
+ items_count: 'You have {$count} items',
463
527
  file_deleted: 'Deleted {$path}',
464
528
  error_message: 'Error: {$error}'
465
529
  }
466
530
  // In your command
467
- const { translate } = ctx.extensions['g:i18n']
531
+ const { translate } = ctx.extensions[i18nId]
468
532
  translate(resolveKey('welcome'), { name: 'John' }) // "Welcome, John!"
469
- translate(resolveKey('items.count'), { count: 5 }) // "You have 5 items"
533
+ translate(resolveKey('items_count'), { count: 5 }) // "You have 5 items"
470
534
  translate(resolveKey('file_deleted'), { path: '/tmp/file.txt' }) // "Deleted /tmp/file.txt"
471
535
  translate(resolveKey('error_message'), { error: 'File not found' }) // "Error: File not found"
472
536
  ```
@@ -537,7 +601,7 @@ When you provide a custom translation adapter:
537
601
 
538
602
  ```ts
539
603
  import { cli } from 'gunshi'
540
- import i18n, { defineI18n } from '@gunshi/plugin-i18n'
604
+ import i18n, { defineI18nWithTypes, pluginId as i18nId, resolveKey } from '@gunshi/plugin-i18n'
541
605
  import {
542
606
  createCoreContext,
543
607
  getLocaleMessage,
@@ -546,6 +610,8 @@ import {
546
610
  translate as intlifyTranslate
547
611
  } from '@intlify/core' // need to install `npm install --save @intlify/core@next`
548
612
 
613
+ import type { I18nExtension } from '@gunshi/plugin-i18n'
614
+
549
615
  // Create an Intlify translation adapter factory
550
616
  function createIntlifyAdapterFactory(options) {
551
617
  return new IntlifyTranslation(options)
@@ -606,7 +672,7 @@ class IntlifyTranslation {
606
672
  }
607
673
 
608
674
  // Define your command
609
- const command = defineI18n({
675
+ const command = defineI18nWithTypes<{ extensions: { [i18nId]: I18nExtension } }>()({
610
676
  name: 'greeter',
611
677
 
612
678
  args: {
@@ -622,10 +688,8 @@ const command = defineI18n({
622
688
  },
623
689
 
624
690
  // Define a resource fetcher with Intlify syntax
625
- resource: async ctx => {
626
- const locale = ctx.extensions['g:i18n'].locale.toString()
627
-
628
- if (locale === 'ja-JP') {
691
+ resource: locale => {
692
+ if (locale.toString() === 'ja-JP') {
629
693
  return {
630
694
  description: '挨拶アプリケーション',
631
695
  'arg:name': '挨拶する相手の名前',
@@ -646,11 +710,11 @@ const command = defineI18n({
646
710
 
647
711
  run: ctx => {
648
712
  const { name = 'World', count } = ctx.values
649
- const { translate } = ctx.extensions['g:i18n']
713
+ const { translate } = ctx.extensions[i18nId]
650
714
 
651
715
  // Use the translation function with Intlify
652
716
  const key = count > 1 ? 'greeting_plural' : 'greeting'
653
- const message = translate(key, { name, count })
717
+ const message = translate(resolveKey(key, ctx.name), { name, count })
654
718
 
655
719
  console.log(message)
656
720
  }
@@ -685,43 +749,6 @@ With Intlify, you get advanced features like:
685
749
 
686
750
  <!-- eslint-enable markdown/no-missing-label-refs -->
687
751
 
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
-
725
752
  ## 📚 API References
726
753
 
727
754
  See the [API References](./docs/index.md)
@@ -729,3 +756,11 @@ See the [API References](./docs/index.md)
729
756
  ## ©️ License
730
757
 
731
758
  [MIT](http://opensource.org/licenses/MIT)
759
+
760
+ <!-- Badges -->
761
+
762
+ [npm-version-src]: https://img.shields.io/npm/v/@gunshi/plugin-i18n?style=flat
763
+ [npm-version-href]: https://npmjs.com/package/@gunshi/plugin-i18n@alpha
764
+ [jsr-src]: https://jsr.io/badges/@gunshi/plugin-i18n
765
+ [jsr-href]: https://jsr.io/@gunshi/plugin-i18n
766
+ [install-size-src]: https://pkg-size.dev/badge/install/67599