@gunshi/docs 0.27.0-beta.4
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.
- package/LICENSE +20 -0
- package/package.json +52 -0
- package/src/guide/advanced/advanced-lazy-loading.md +312 -0
- package/src/guide/advanced/command-hooks.md +469 -0
- package/src/guide/advanced/context-extensions.md +545 -0
- package/src/guide/advanced/custom-rendering.md +945 -0
- package/src/guide/advanced/docs-gen.md +594 -0
- package/src/guide/advanced/internationalization.md +677 -0
- package/src/guide/advanced/type-system.md +561 -0
- package/src/guide/essentials/auto-usage.md +281 -0
- package/src/guide/essentials/composable.md +332 -0
- package/src/guide/essentials/declarative.md +724 -0
- package/src/guide/essentials/getting-started.md +252 -0
- package/src/guide/essentials/lazy-async.md +408 -0
- package/src/guide/essentials/plugin-system.md +472 -0
- package/src/guide/essentials/type-safe.md +154 -0
- package/src/guide/introduction/setup.md +68 -0
- package/src/guide/introduction/what-is-gunshi.md +68 -0
- package/src/guide/plugin/decorators.md +545 -0
- package/src/guide/plugin/dependencies.md +519 -0
- package/src/guide/plugin/extensions.md +317 -0
- package/src/guide/plugin/getting-started.md +298 -0
- package/src/guide/plugin/guidelines.md +940 -0
- package/src/guide/plugin/introduction.md +294 -0
- package/src/guide/plugin/lifecycle.md +432 -0
- package/src/guide/plugin/list.md +37 -0
- package/src/guide/plugin/testing.md +843 -0
- package/src/guide/plugin/type-system.md +529 -0
- package/src/index.md +44 -0
- package/src/release/v0.27.md +722 -0
- package/src/showcase.md +11 -0
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
# Internationalization
|
|
2
|
+
|
|
3
|
+
Gunshi provides comprehensive internationalization (i18n) support through the official `@gunshi/plugin-i18n` plugin, allowing you to create command-line interfaces that can be used in multiple languages.
|
|
4
|
+
|
|
5
|
+
## Why Use Internationalization?
|
|
6
|
+
|
|
7
|
+
Internationalization offers several benefits:
|
|
8
|
+
|
|
9
|
+
- **Broader audience**: Make your CLI accessible to users who speak different languages
|
|
10
|
+
- **Better user experience**: Users can interact with your CLI in their preferred language
|
|
11
|
+
- **Consistency**: Maintain a consistent approach to translations across your application
|
|
12
|
+
- **Type safety**: Full TypeScript support for translation keys and interpolation
|
|
13
|
+
|
|
14
|
+
## Getting Started with i18n Plugin
|
|
15
|
+
|
|
16
|
+
First, install the i18n plugin and optional resource packages:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
npm install @gunshi/plugin-i18n @gunshi/resources
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Basic Internationalization
|
|
23
|
+
|
|
24
|
+
Here's how to implement basic internationalization using the i18n plugin:
|
|
25
|
+
|
|
26
|
+
```ts [cli.ts]
|
|
27
|
+
import { cli } from 'gunshi'
|
|
28
|
+
import resources from '@gunshi/resources'
|
|
29
|
+
import i18n, { defineI18nWithTypes, pluginId as i18nId, resolveKey } from '@gunshi/plugin-i18n'
|
|
30
|
+
|
|
31
|
+
import type { I18nExtension } from '@gunshi/plugin-i18n'
|
|
32
|
+
|
|
33
|
+
// Define a command with i18n support
|
|
34
|
+
const command = defineI18nWithTypes<{ extensions: { [i18nId]: I18nExtension } }>()({
|
|
35
|
+
name: 'greeter',
|
|
36
|
+
args: {
|
|
37
|
+
name: {
|
|
38
|
+
type: 'string',
|
|
39
|
+
short: 'n',
|
|
40
|
+
description: 'Name to greet'
|
|
41
|
+
},
|
|
42
|
+
formal: {
|
|
43
|
+
type: 'boolean',
|
|
44
|
+
short: 'f',
|
|
45
|
+
description: 'Use formal greeting'
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
// Define translation resources for the command
|
|
50
|
+
resource: locale => {
|
|
51
|
+
if (locale.toString() === 'ja-JP') {
|
|
52
|
+
return {
|
|
53
|
+
description: '挨拶アプリケーション',
|
|
54
|
+
'arg:name': '挨拶する相手の名前',
|
|
55
|
+
'arg:formal': '丁寧な挨拶を使用する',
|
|
56
|
+
informal_greeting: 'こんにちは',
|
|
57
|
+
formal_greeting: 'はじめまして'
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Default to English
|
|
62
|
+
return {
|
|
63
|
+
description: 'Greeting application',
|
|
64
|
+
'arg:name': 'Name to greet',
|
|
65
|
+
'arg:formal': 'Use formal greeting',
|
|
66
|
+
informal_greeting: 'Hello',
|
|
67
|
+
formal_greeting: 'Good day'
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
// Command execution function
|
|
72
|
+
run: ctx => {
|
|
73
|
+
const { name = 'World', formal } = ctx.values
|
|
74
|
+
const t = ctx.extensions[i18nId].translate
|
|
75
|
+
|
|
76
|
+
// Use resolveKey for custom keys with proper namespacing
|
|
77
|
+
const greetingKey = formal
|
|
78
|
+
? resolveKey('formal_greeting', ctx.name)
|
|
79
|
+
: resolveKey('informal_greeting', ctx.name)
|
|
80
|
+
|
|
81
|
+
const greeting = t(greetingKey)
|
|
82
|
+
console.log(`${greeting}, ${name}!`)
|
|
83
|
+
|
|
84
|
+
// Show translation information
|
|
85
|
+
const locale = ctx.extensions[i18nId].locale
|
|
86
|
+
console.log(`\nCurrent locale: ${locale}`)
|
|
87
|
+
|
|
88
|
+
// Use resolveKey for description as well
|
|
89
|
+
const descKey = resolveKey('description', ctx.name)
|
|
90
|
+
console.log(`Command Description: ${t(descKey)}`)
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
// Run the command with i18n plugin
|
|
95
|
+
await cli(process.argv.slice(2), command, {
|
|
96
|
+
name: 'i18n-example',
|
|
97
|
+
version: '1.0.0',
|
|
98
|
+
plugins: [
|
|
99
|
+
i18n({
|
|
100
|
+
// Set locale from environment or default to en-US
|
|
101
|
+
locale: process.env.MY_LANG || 'en-US',
|
|
102
|
+
// Provide built-in translations for common terms.
|
|
103
|
+
// See the support locales: https://github.com/kazupon/gunshi/tree/main/packages/resources#-supported-locales
|
|
104
|
+
builtinResources: resources
|
|
105
|
+
})
|
|
106
|
+
]
|
|
107
|
+
})
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
<!-- eslint-disable markdown/no-missing-label-refs -->
|
|
111
|
+
|
|
112
|
+
> [!TIP]
|
|
113
|
+
> The example fully code is [here](https://github.com/kazupon/gunshi/tree/main/playground/advanced/internationalization/basic).
|
|
114
|
+
|
|
115
|
+
<!-- eslint-enable markdown/no-missing-label-refs -->
|
|
116
|
+
|
|
117
|
+
<!-- eslint-disable markdown/no-missing-label-refs -->
|
|
118
|
+
|
|
119
|
+
> [!TIP]
|
|
120
|
+
> **About the helper functions used in this example:**
|
|
121
|
+
>
|
|
122
|
+
> - `defineI18nWithTypes`: A type-safe helper for creating commands with i18n support. It ensures proper TypeScript inference for translation keys. [Learn more](#defineI18nWithTypes)
|
|
123
|
+
> - `resolveKey`: A utility that handles namespace resolution for custom translation keys in commands and subcommands. Always use this for custom keys to ensure proper namespacing. [Learn more](#resolvekey)
|
|
124
|
+
|
|
125
|
+
<!-- eslint-enable markdown/no-missing-label-refs -->
|
|
126
|
+
|
|
127
|
+
To run this example with different locales:
|
|
128
|
+
|
|
129
|
+
```sh
|
|
130
|
+
# English (default)
|
|
131
|
+
node cli.ts --name John
|
|
132
|
+
|
|
133
|
+
# i18n-example (i18n-example v1.0.0)
|
|
134
|
+
#
|
|
135
|
+
# Hello, John!
|
|
136
|
+
#
|
|
137
|
+
# Current locale: en-US
|
|
138
|
+
# Command Description: Greeting application
|
|
139
|
+
|
|
140
|
+
# Japanese
|
|
141
|
+
MY_LANG=ja-JP node cli.ts --name 田中 --formal
|
|
142
|
+
|
|
143
|
+
# i18n-example (i18n-example v1.0.0)
|
|
144
|
+
#
|
|
145
|
+
# はじめまして, 田中!
|
|
146
|
+
#
|
|
147
|
+
# Current locale: ja-JP
|
|
148
|
+
# Command Description: 挨拶アプリケーション
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Using Built-in Resources
|
|
152
|
+
|
|
153
|
+
The `@gunshi/resources` package provides pre-translated resources for common CLI terms:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
import { cli } from 'gunshi'
|
|
157
|
+
import i18n from '@gunshi/plugin-i18n'
|
|
158
|
+
import resources from '@gunshi/resources'
|
|
159
|
+
|
|
160
|
+
const command = {
|
|
161
|
+
name: 'app',
|
|
162
|
+
run: ctx => {
|
|
163
|
+
console.log('Application running')
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
await cli(process.argv.slice(2), command, {
|
|
168
|
+
name: 'my-app',
|
|
169
|
+
version: '1.0.0',
|
|
170
|
+
plugins: [
|
|
171
|
+
i18n({
|
|
172
|
+
locale: 'en-US',
|
|
173
|
+
// Provide built-in translations for common terms.
|
|
174
|
+
// See the support locales: https://github.com/kazupon/gunshi/tree/main/packages/resources#-supported-locales
|
|
175
|
+
builtinResources: resources
|
|
176
|
+
})
|
|
177
|
+
]
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
// This automatically translates built-in messages like:
|
|
181
|
+
// - USAGE, OPTIONS, COMMANDS
|
|
182
|
+
// - Help and version descriptions
|
|
183
|
+
// - Error messages
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Loading Translations from Files
|
|
187
|
+
|
|
188
|
+
For better organization, load translations from separate files:
|
|
189
|
+
|
|
190
|
+
```ts [cli.ts]
|
|
191
|
+
import i18n, { defineI18nWithTypes, pluginId as i18nId, resolveKey } from '@gunshi/plugin-i18n'
|
|
192
|
+
import resources from '@gunshi/resources'
|
|
193
|
+
import { cli } from 'gunshi'
|
|
194
|
+
|
|
195
|
+
import type { I18nExtension } from '@gunshi/plugin-i18n'
|
|
196
|
+
|
|
197
|
+
const command = defineI18nWithTypes<{ extensions: { [i18nId]: I18nExtension } }>()({
|
|
198
|
+
name: 'greeter',
|
|
199
|
+
args: {
|
|
200
|
+
name: { type: 'string', short: 'n' },
|
|
201
|
+
formal: { type: 'boolean', short: 'f' }
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
// Load translations from files
|
|
205
|
+
resource: locale => {
|
|
206
|
+
if (locale.toString() === 'ja-JP') {
|
|
207
|
+
// Dynamic import for lazy loading
|
|
208
|
+
const jaJP = await import('./locales/ja-JP.json', {
|
|
209
|
+
with: { type: 'json' }
|
|
210
|
+
})
|
|
211
|
+
return jaJP.default
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Default to English
|
|
215
|
+
const enUS = await import('./locales/en-US.json', {
|
|
216
|
+
with: { type: 'json' }
|
|
217
|
+
})
|
|
218
|
+
return enUS.default
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
run: ctx => {
|
|
222
|
+
const { name = 'World', formal } = ctx.values
|
|
223
|
+
const t = ctx.extensions[i18nId].translate
|
|
224
|
+
|
|
225
|
+
// Always use resolveKey for custom keys
|
|
226
|
+
const greetingKey = formal
|
|
227
|
+
? resolveKey('formal_greeting', ctx.name)
|
|
228
|
+
: resolveKey('informal_greeting', ctx.name)
|
|
229
|
+
|
|
230
|
+
const greeting = t(greetingKey)
|
|
231
|
+
console.log(`${greeting}, ${name}!`)
|
|
232
|
+
}
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
await cli(process.argv.slice(2), command, {
|
|
236
|
+
name: 'i18n-example',
|
|
237
|
+
version: '1.0.0',
|
|
238
|
+
plugins: [
|
|
239
|
+
i18n({
|
|
240
|
+
locale: process.env.MY_LANG || 'en-US',
|
|
241
|
+
builtinResources: resources
|
|
242
|
+
})
|
|
243
|
+
]
|
|
244
|
+
})
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
<!-- eslint-disable markdown/no-missing-label-refs -->
|
|
248
|
+
|
|
249
|
+
> [!TIP]
|
|
250
|
+
> The example fully code is [here](https://github.com/kazupon/gunshi/tree/main/playground/advanced/internationalization/loading).
|
|
251
|
+
|
|
252
|
+
<!-- eslint-enable markdown/no-missing-label-refs -->
|
|
253
|
+
|
|
254
|
+
Example locale files:
|
|
255
|
+
|
|
256
|
+
```json [locales/en-US.json]
|
|
257
|
+
{
|
|
258
|
+
"description": "Greeting application",
|
|
259
|
+
"arg:name": "Name to greet",
|
|
260
|
+
"arg:formal": "Use formal greeting",
|
|
261
|
+
"informal_greeting": "Hello",
|
|
262
|
+
"formal_greeting": "Good day"
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
```json [locales/ja-JP.json]
|
|
267
|
+
{
|
|
268
|
+
"description": "挨拶アプリケーション",
|
|
269
|
+
"arg:name": "挨拶する相手の名前",
|
|
270
|
+
"arg:formal": "丁寧な挨拶を使用する",
|
|
271
|
+
"informal_greeting": "こんにちは",
|
|
272
|
+
"formal_greeting": "はじめまして"
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Translation with Interpolation
|
|
277
|
+
|
|
278
|
+
The i18n plugin supports message interpolation for dynamic content:
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
import i18n, { defineI18nWithTypes, pluginId as i18nId, resolveKey } from '@gunshi/plugin-i18n'
|
|
282
|
+
|
|
283
|
+
import type { I18nExtension } from '@gunshi/plugin-i18n'
|
|
284
|
+
|
|
285
|
+
const command = defineI18nWithTypes<{ extensions: { [i18nId]: I18nExtension } }>()({
|
|
286
|
+
name: 'deploy',
|
|
287
|
+
args: {
|
|
288
|
+
app: { type: 'string', required: true },
|
|
289
|
+
environment: { type: 'string', required: true }
|
|
290
|
+
},
|
|
291
|
+
resource: () => ({
|
|
292
|
+
deploying: 'Deploying {$app} to {$environment}...',
|
|
293
|
+
success: 'Successfully deployed {$app} to {$environment}!',
|
|
294
|
+
error: 'Failed to deploy: {$message}'
|
|
295
|
+
}),
|
|
296
|
+
run: ctx => {
|
|
297
|
+
const t = ctx.extensions[i18nId].translate
|
|
298
|
+
const { app, environment } = ctx.values
|
|
299
|
+
|
|
300
|
+
// Use resolveKey for all custom keys
|
|
301
|
+
const deployingKey = resolveKey('deploying', ctx.name)
|
|
302
|
+
const successKey = resolveKey('success', ctx.name)
|
|
303
|
+
const errorKey = resolveKey('error', ctx.name)
|
|
304
|
+
|
|
305
|
+
console.log(t(deployingKey, { app, environment }))
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
// Deployment logic
|
|
309
|
+
console.log(t(successKey, { app, environment }))
|
|
310
|
+
} catch (error) {
|
|
311
|
+
console.error(t(errorKey, { message: error.message }))
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
})
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Note: Interpolation placeholders use the format `{$variableName}` in the i18n plugin.
|
|
318
|
+
|
|
319
|
+
## Internationalization with Sub-commands
|
|
320
|
+
|
|
321
|
+
When working with sub-commands, each command has its own namespace for translations:
|
|
322
|
+
|
|
323
|
+
```ts [cli.ts]
|
|
324
|
+
import i18n, { defineI18nWithTypes, pluginId as i18nId, resolveKey } from '@gunshi/plugin-i18n'
|
|
325
|
+
import resources from '@gunshi/resources'
|
|
326
|
+
import { cli } from 'gunshi'
|
|
327
|
+
|
|
328
|
+
import type { I18nExtension } from '@gunshi/plugin-i18n'
|
|
329
|
+
|
|
330
|
+
// Sub-command with its own translations
|
|
331
|
+
const createCommand = defineI18nWithTypes<{ extensions: { [i18nId]: I18nExtension } }>()({
|
|
332
|
+
name: 'create',
|
|
333
|
+
args: {
|
|
334
|
+
name: { type: 'string', required: true }
|
|
335
|
+
},
|
|
336
|
+
resource: locale => {
|
|
337
|
+
return locale.toString() === 'ja-JP'
|
|
338
|
+
? {
|
|
339
|
+
description: 'リソースを作成',
|
|
340
|
+
'arg:name': 'リソース名',
|
|
341
|
+
creating: '作成中: {$name}',
|
|
342
|
+
success: '作成完了!'
|
|
343
|
+
}
|
|
344
|
+
: {
|
|
345
|
+
description: 'Create a resource',
|
|
346
|
+
'arg:name': 'Resource name',
|
|
347
|
+
creating: 'Creating: {$name}',
|
|
348
|
+
success: 'Created successfully!'
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
run: ctx => {
|
|
352
|
+
const t = ctx.extensions[i18nId].translate
|
|
353
|
+
const { name } = ctx.values
|
|
354
|
+
|
|
355
|
+
// For custom keys in subcommands, always use resolveKey helper
|
|
356
|
+
const creatingKey = resolveKey('creating', ctx.name)
|
|
357
|
+
const successKey = resolveKey('success', ctx.name)
|
|
358
|
+
|
|
359
|
+
console.log(t(creatingKey, { name }))
|
|
360
|
+
console.log(t(successKey))
|
|
361
|
+
}
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
// Main command
|
|
365
|
+
const mainCommand = defineI18nWithTypes<{ extensions: { [i18nId]: I18nExtension } }>()({
|
|
366
|
+
name: 'resource-manager',
|
|
367
|
+
resource: () => ({
|
|
368
|
+
description: 'Resource management tool',
|
|
369
|
+
usage_hint: 'Use a sub-command to manage resources'
|
|
370
|
+
}),
|
|
371
|
+
run: ctx => {
|
|
372
|
+
const t = ctx.extensions[i18nId].translate
|
|
373
|
+
|
|
374
|
+
// Use resolveKey for main command's custom keys too
|
|
375
|
+
const hintKey = resolveKey('usage_hint', ctx.name)
|
|
376
|
+
console.log(t(hintKey))
|
|
377
|
+
}
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
// Run with i18n plugin
|
|
381
|
+
await cli(process.argv.slice(2), mainCommand, {
|
|
382
|
+
name: 'resource-cli',
|
|
383
|
+
version: '1.0.0',
|
|
384
|
+
subCommands: {
|
|
385
|
+
create: createCommand
|
|
386
|
+
},
|
|
387
|
+
plugins: [
|
|
388
|
+
i18n({
|
|
389
|
+
locale: process.env.MY_LANG || 'en-US',
|
|
390
|
+
builtinResources: resources
|
|
391
|
+
})
|
|
392
|
+
]
|
|
393
|
+
})
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
<!-- eslint-disable markdown/no-missing-label-refs -->
|
|
397
|
+
|
|
398
|
+
> [!TIP]
|
|
399
|
+
> The example fully code is [here](https://github.com/kazupon/gunshi/tree/main/playground/advanced/internationalization/sub-command).
|
|
400
|
+
|
|
401
|
+
<!-- eslint-enable markdown/no-missing-label-refs -->
|
|
402
|
+
|
|
403
|
+
## Helper Functions
|
|
404
|
+
|
|
405
|
+
The i18n plugin provides helpful utilities for working with translations:
|
|
406
|
+
|
|
407
|
+
### `defineI18n`
|
|
408
|
+
|
|
409
|
+
Define an i18n-aware command.
|
|
410
|
+
|
|
411
|
+
```ts
|
|
412
|
+
import { defineI18n } from '@gunshi/plugin-i18n'
|
|
413
|
+
|
|
414
|
+
const greetCommand = defineI18n({
|
|
415
|
+
name: 'greet',
|
|
416
|
+
description: 'Greet someone',
|
|
417
|
+
args: {
|
|
418
|
+
name: {
|
|
419
|
+
type: 'string',
|
|
420
|
+
description: 'Name to greet'
|
|
421
|
+
}
|
|
422
|
+
},
|
|
423
|
+
resource: locale => {
|
|
424
|
+
switch (locale.toString()) {
|
|
425
|
+
case 'ja-JP': {
|
|
426
|
+
return {
|
|
427
|
+
description: '誰かにあいさつ',
|
|
428
|
+
'arg:name': 'あいさつするための名前'
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
// other locales ...
|
|
432
|
+
}
|
|
433
|
+
},
|
|
434
|
+
run: ctx => {
|
|
435
|
+
console.log(`Hello, ${ctx.values.name}!`)
|
|
436
|
+
}
|
|
437
|
+
})
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
The difference from the `define` function is that you can define a `resource` option that can load a locale.
|
|
441
|
+
|
|
442
|
+
### `defineI18nWithTypes`
|
|
443
|
+
|
|
444
|
+
Define an i18n-aware command with types
|
|
445
|
+
|
|
446
|
+
This helper function allows specifying the type parameter of `GunshiParams` while inferring the `Args` type, `ExtendContext` type from the definition.
|
|
447
|
+
|
|
448
|
+
```ts
|
|
449
|
+
import { defineI18nWithTypes } from '@gunshi/plugin-i18n'
|
|
450
|
+
|
|
451
|
+
// Define a command with specific extensions type
|
|
452
|
+
type MyExtensions = { logger: { log: (message: string) => void } }
|
|
453
|
+
|
|
454
|
+
const greetCommand = defineI18nWithTypes<{ extensions: MyExtensions }>()({
|
|
455
|
+
name: 'greet',
|
|
456
|
+
args: {
|
|
457
|
+
name: { type: 'string', description: 'Name to greet' }
|
|
458
|
+
},
|
|
459
|
+
resource: locale => {
|
|
460
|
+
switch (locale.toString()) {
|
|
461
|
+
case 'ja-JP': {
|
|
462
|
+
return {
|
|
463
|
+
description: '誰かにあいさつ',
|
|
464
|
+
'arg:name': 'あいさつするための名前'
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
// other locales ...
|
|
468
|
+
}
|
|
469
|
+
},
|
|
470
|
+
run: ctx => {
|
|
471
|
+
// ctx.values is inferred as { name?: string }
|
|
472
|
+
// ctx.extensions is MyExtensions
|
|
473
|
+
}
|
|
474
|
+
})
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### `withI18nResource`
|
|
478
|
+
|
|
479
|
+
Add i18n resources to existing commands:
|
|
480
|
+
|
|
481
|
+
```ts
|
|
482
|
+
import { define } from 'gunshi'
|
|
483
|
+
import { withI18nResource, resolveKey, pluginId as i18nId } from '@gunshi/plugin-i18n'
|
|
484
|
+
|
|
485
|
+
const existingCommand = define({
|
|
486
|
+
name: 'app',
|
|
487
|
+
run: ctx => {
|
|
488
|
+
const t = ctx.extensions[i18nId]?.translate
|
|
489
|
+
if (t) {
|
|
490
|
+
const messageKey = resolveKey('message', ctx.name)
|
|
491
|
+
console.log(t(messageKey))
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
const existingLocalizableCommand = withI18nResource(existingCommand, locale => ({
|
|
497
|
+
message: 'Hello from i18n!'
|
|
498
|
+
}))
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
### `resolveKey`
|
|
502
|
+
|
|
503
|
+
The `resolveKey` helper ensures proper command namespace handling for custom translation keys:
|
|
504
|
+
|
|
505
|
+
```ts
|
|
506
|
+
import { resolveKey } from '@gunshi/plugin-i18n'
|
|
507
|
+
|
|
508
|
+
// For a command named 'build'
|
|
509
|
+
const key = resolveKey('starting', ctx.name)
|
|
510
|
+
// Returns: 'build:starting'
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
## Resource Key Naming Conventions
|
|
514
|
+
|
|
515
|
+
When defining translation resources, follow these conventions:
|
|
516
|
+
|
|
517
|
+
- **Command Description**: Use the key `description`
|
|
518
|
+
- **Examples**: Use the key `examples`
|
|
519
|
+
- **Argument Descriptions**: Prefix with `arg:` (e.g., `arg:name`)
|
|
520
|
+
- **Negatable Arguments**: Use `arg:no-<option>` for custom negation descriptions
|
|
521
|
+
- **Built-in Keys**: Keys like `_:USAGE`, `_:OPTIONS` are handled by built-in resources
|
|
522
|
+
- **Custom Keys**: Free naming for your application-specific messages, but always use `resolveKey()` when accessing them
|
|
523
|
+
|
|
524
|
+
Example:
|
|
525
|
+
|
|
526
|
+
<!-- eslint-skip -->
|
|
527
|
+
|
|
528
|
+
```js
|
|
529
|
+
{
|
|
530
|
+
// Command metadata (accessed with resolveKey)
|
|
531
|
+
"description": "File processor",
|
|
532
|
+
"examples": "$ process --input file.txt",
|
|
533
|
+
|
|
534
|
+
// Argument descriptions (must use arg: prefix)
|
|
535
|
+
"arg:input": "Input file path",
|
|
536
|
+
"arg:verbose": "Enable verbose output",
|
|
537
|
+
"arg:no-verbose": "Disable verbose output",
|
|
538
|
+
|
|
539
|
+
// Custom application messages (accessed with resolveKey)
|
|
540
|
+
"processing": "Processing file...",
|
|
541
|
+
"complete": "Processing complete!",
|
|
542
|
+
"error_not_found": "File not found: {$path}"
|
|
543
|
+
}
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
<!-- eslint-disable markdown/no-missing-label-refs -->
|
|
547
|
+
|
|
548
|
+
> [!IMPORTANT]
|
|
549
|
+
> The resource object returned by the `resource` function (or loaded from external files like JSON) **must** be a flat key-value structure. Nested objects are not supported for translations using `translate()`. Keep your translation keys simple and at the top level.
|
|
550
|
+
|
|
551
|
+
<!-- eslint-enable markdown/no-missing-label-refs -->
|
|
552
|
+
|
|
553
|
+
Good Flat structure:
|
|
554
|
+
|
|
555
|
+
```json
|
|
556
|
+
{
|
|
557
|
+
"greeting": "Hello",
|
|
558
|
+
"farewell": "Goodbye"
|
|
559
|
+
}
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
Bad Nested structure (won't work with `translate('messages.greeting')`):
|
|
563
|
+
|
|
564
|
+
```json
|
|
565
|
+
{
|
|
566
|
+
"messages": {
|
|
567
|
+
"greeting": "Hello",
|
|
568
|
+
"farewell": "Goodbye"
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
## Detecting User Locale
|
|
574
|
+
|
|
575
|
+
The i18n plugin can automatically detect the user's locale:
|
|
576
|
+
|
|
577
|
+
```js
|
|
578
|
+
import i18n from '@gunshi/plugin-i18n'
|
|
579
|
+
|
|
580
|
+
// Use various detection methods
|
|
581
|
+
await cli(process.argv.slice(2), command, {
|
|
582
|
+
plugins: [
|
|
583
|
+
i18n({
|
|
584
|
+
// From environment variable
|
|
585
|
+
locale: process.env.MY_LANG || 'en-US'
|
|
586
|
+
// Or using Intl.Locale for advanced locale handling
|
|
587
|
+
// locale: new Intl.Locale(process.env.MY_LANG || 'en-US')
|
|
588
|
+
})
|
|
589
|
+
]
|
|
590
|
+
})
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
In Node.js v21 and later, you can also detect locale using the navigator API:
|
|
594
|
+
|
|
595
|
+
<!-- eslint-skip -->
|
|
596
|
+
|
|
597
|
+
```ts
|
|
598
|
+
// In browser or Node.js v21.2.0+ (experimental global navigator), use navigator.language
|
|
599
|
+
// Otherwise, fall back to environment- or Intl-based detection
|
|
600
|
+
const locale = (() => {
|
|
601
|
+
// Experimental global navigator in Node 21.2.0+
|
|
602
|
+
if (typeof globalThis.navigator !== 'undefined' && navigator.language) {
|
|
603
|
+
return navigator.language
|
|
604
|
+
}
|
|
605
|
+
// Fallback: read locale from environment variables
|
|
606
|
+
const env = process.env.LC_ALL || process.env.LC_MESSAGES || process.env.LANG || 'en-US'
|
|
607
|
+
const base = env.split('.')[0].replace('_', '-')
|
|
608
|
+
try {
|
|
609
|
+
// Normalize and validate with Intl.Locale
|
|
610
|
+
return new Intl.Locale(base).toString()
|
|
611
|
+
} catch {
|
|
612
|
+
return 'en-US'
|
|
613
|
+
}
|
|
614
|
+
})()
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
## Custom Translation Adapters
|
|
618
|
+
|
|
619
|
+
For advanced scenarios requiring custom interpolation syntax or translation logic, you can create custom translation adapters by implementing the TranslationAdapter interface.
|
|
620
|
+
|
|
621
|
+
This allows full control over how translations are stored, retrieved, and interpolated.
|
|
622
|
+
|
|
623
|
+
For detailed implementation guidance and examples, see the [Custom Translation Adapter documentation](https://github.com/kazupon/gunshi/tree/main/packages/plugin-i18n#-custom-translation-adapter) in the `@gunshi/plugin-i18n` package.
|
|
624
|
+
|
|
625
|
+
## Translating Help Messages
|
|
626
|
+
|
|
627
|
+
The i18n plugin automatically uses your translations for help messages.
|
|
628
|
+
|
|
629
|
+
When users run `--help` with different locales, they'll see help messages in their language:
|
|
630
|
+
|
|
631
|
+
English:
|
|
632
|
+
|
|
633
|
+
```sh
|
|
634
|
+
USAGE:
|
|
635
|
+
COMMAND <OPTIONS>
|
|
636
|
+
|
|
637
|
+
OPTIONS:
|
|
638
|
+
-n, --name <name> Name to greet
|
|
639
|
+
-f, --formal Use formal greeting
|
|
640
|
+
-h, --help Display this help message
|
|
641
|
+
-v, --version Display version
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
Japanese (with proper locale):
|
|
645
|
+
|
|
646
|
+
```sh
|
|
647
|
+
使用法:
|
|
648
|
+
COMMAND <オプション>
|
|
649
|
+
|
|
650
|
+
オプション:
|
|
651
|
+
-n, --name <name> 挨拶する相手の名前
|
|
652
|
+
-f, --formal 丁寧な挨拶を使用する
|
|
653
|
+
-h, --help このヘルプメッセージを表示
|
|
654
|
+
-v, --version バージョンを表示
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
## Important Notes on Custom Keys
|
|
658
|
+
|
|
659
|
+
<!-- eslint-disable markdown/no-missing-label-refs -->
|
|
660
|
+
|
|
661
|
+
> [!IMPORTANT]
|
|
662
|
+
> **Always use `resolveKey()` for custom translation keys!** This ensures proper namespace handling, especially in sub-commands. Without `resolveKey()`, your translations may not be found.
|
|
663
|
+
|
|
664
|
+
<!-- eslint-enable markdown/no-missing-label-refs -->
|
|
665
|
+
|
|
666
|
+
```ts
|
|
667
|
+
// ❌ Wrong - Don't access custom keys directly
|
|
668
|
+
const message = t('welcome')
|
|
669
|
+
|
|
670
|
+
// ✅ Correct - Always use resolveKey for custom keys
|
|
671
|
+
const welcomeKey = resolveKey('welcome', ctx.name)
|
|
672
|
+
const message = t(welcomeKey)
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
## Migration from v0.26
|
|
676
|
+
|
|
677
|
+
If you're migrating from Gunshi v0.26 where i18n was built into the CLI options, see the [v0.27 Release Notes](../../release/v0.27.md#internationalization-migration) for detailed migration instructions.
|