@gunshi/plugin-i18n 0.26.3
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/README.md +527 -0
- package/lib/index.d.ts +356 -0
- package/lib/index.js +261 -0
- package/package.json +85 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 kazuya kawaguchi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
7
|
+
the Software without restriction, including without limitation the rights to
|
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
10
|
+
subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
# @gunshi/plugin-i18n
|
|
2
|
+
|
|
3
|
+
> internationalization (i18n) plugin for gunshi.
|
|
4
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
## 💿 Installation
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
# npm
|
|
11
|
+
npm install --save @gunshi/plugin-i18n
|
|
12
|
+
|
|
13
|
+
# pnpm
|
|
14
|
+
pnpm add @gunshi/plugin-i18n
|
|
15
|
+
|
|
16
|
+
# yarn
|
|
17
|
+
yarn add @gunshi/plugin-i18n
|
|
18
|
+
|
|
19
|
+
# deno
|
|
20
|
+
deno add jsr:@gunshi/plugin-i18n
|
|
21
|
+
|
|
22
|
+
# bun
|
|
23
|
+
bun add @gunshi/plugin-i18n
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 🚀 Usage
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { cli } from 'gunshi'
|
|
30
|
+
import i18n, { defineI18n } from '@gunshi/plugin-i18n'
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* You can define a command with `defineI18n`, which is compatible with the `define` function.
|
|
34
|
+
* This provides full type safety for i18n commands - TypeScript will suggest the 'resource' option
|
|
35
|
+
* and ensure your resource keys match the expected structure.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
// Define a command
|
|
39
|
+
const greetCommand = defineI18n({
|
|
40
|
+
name: 'greet',
|
|
41
|
+
description: 'Greet someone',
|
|
42
|
+
|
|
43
|
+
args: {
|
|
44
|
+
name: {
|
|
45
|
+
type: 'string',
|
|
46
|
+
description: 'Name to greet'
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
// Define resource fetcher for translations
|
|
51
|
+
resource: async ctx => ({
|
|
52
|
+
description: 'Greet someone in their language',
|
|
53
|
+
examples: '$ your-cli --name yourname',
|
|
54
|
+
'arg:name': "The person's name",
|
|
55
|
+
greeting: 'Hello, {$name}!'
|
|
56
|
+
}),
|
|
57
|
+
|
|
58
|
+
run: async ctx => {
|
|
59
|
+
const { name } = ctx.values
|
|
60
|
+
// Use translate function from context
|
|
61
|
+
console.log(ctx.extensions['g:i18n'].translate('greeting', { name }))
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
// Run CLI with i18n plugin
|
|
66
|
+
await cli(process.argv.slice(2), greetCommand, {
|
|
67
|
+
plugins: [
|
|
68
|
+
i18n({
|
|
69
|
+
locale: 'en-US' // or use process.env.LANG, navigator.language, etc.
|
|
70
|
+
})
|
|
71
|
+
]
|
|
72
|
+
})
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## ⚙️ Plugin Options
|
|
76
|
+
|
|
77
|
+
### `locale`
|
|
78
|
+
|
|
79
|
+
- Type: `string | Intl.Locale`
|
|
80
|
+
- Default: `'en-US'`
|
|
81
|
+
- Description: The locale to use for translations. Can be a BCP 47 language tag string or an `Intl.Locale` object.
|
|
82
|
+
|
|
83
|
+
### `resources`
|
|
84
|
+
|
|
85
|
+
- Type: `Record<string, Record<BuiltinResourceKeys, string>>`
|
|
86
|
+
- Default: `{}`
|
|
87
|
+
- Description:
|
|
88
|
+
- 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)
|
|
90
|
+
|
|
91
|
+
### `translationAdapterFactory`
|
|
92
|
+
|
|
93
|
+
- Type: `(options: TranslationAdapterFactoryOptions) => TranslationAdapter`
|
|
94
|
+
- Default: `createTranslationAdapter`
|
|
95
|
+
- Description: Factory function to create a custom translation adapter. Useful for integrating with existing i18n libraries.
|
|
96
|
+
|
|
97
|
+
## 🔗 Plugin Dependencies
|
|
98
|
+
|
|
99
|
+
The i18n plugin has an optional dependency on the `g:global` plugin:
|
|
100
|
+
|
|
101
|
+
- **Plugin ID**: `g:global` (optional)
|
|
102
|
+
- **Purpose**: Provides global options support for `--help` and `--version`
|
|
103
|
+
- **Effect**: When the globals plugin is present, the i18n plugin automatically sets up translations for the `help` and `version` built-in resources
|
|
104
|
+
|
|
105
|
+
This means:
|
|
106
|
+
|
|
107
|
+
- If you're using the globals plugin (which adds `--help` and `--version` options), the i18n plugin will automatically handle their translations
|
|
108
|
+
- The translations for "Display this help message" and "Display this version" will be available in your configured locale
|
|
109
|
+
- You can override these translations by providing your own `help` and `version` keys in your built-in resources
|
|
110
|
+
|
|
111
|
+
Example with globals plugin:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
import { cli } from 'gunshi'
|
|
115
|
+
import i18n from '@gunshi/plugin-i18n'
|
|
116
|
+
import globals from '@gunshi/plugin-global'
|
|
117
|
+
|
|
118
|
+
await cli(args, command, {
|
|
119
|
+
plugins: [
|
|
120
|
+
globals(), // Adds --help and --version options
|
|
121
|
+
i18n({
|
|
122
|
+
locale: 'ja-JP',
|
|
123
|
+
resources: {
|
|
124
|
+
'ja-JP': {
|
|
125
|
+
help: 'ヘルプメッセージを表示', // Override help translation
|
|
126
|
+
version: 'バージョンを表示' // Override version translation
|
|
127
|
+
// ... other built-in translations
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
]
|
|
132
|
+
})
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## 🛠️ Helper Functions
|
|
136
|
+
|
|
137
|
+
### `defineI18n`
|
|
138
|
+
|
|
139
|
+
Type-safe helper to define an i18n-aware command.
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
import { defineI18n } from '@gunshi/plugin-i18n'
|
|
143
|
+
|
|
144
|
+
const command = defineI18n({
|
|
145
|
+
name: 'hello',
|
|
146
|
+
args: {
|
|
147
|
+
name: { type: 'string' }
|
|
148
|
+
},
|
|
149
|
+
resource: async ctx => ({
|
|
150
|
+
description: 'Say hello',
|
|
151
|
+
'arg:name': 'Your name'
|
|
152
|
+
}),
|
|
153
|
+
run: async ctx => {
|
|
154
|
+
console.log(`Hello, ${ctx.values.name}!`)
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### `withI18nResource`
|
|
160
|
+
|
|
161
|
+
Add i18n resource to an existing command. This helper is useful for extending an already defined command.
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
import { define } from 'gunshi' // 'gunshi/definition', or '@gunshi/definition'
|
|
165
|
+
import { withI18nResource } from '@gunshi/plugin-i18n'
|
|
166
|
+
|
|
167
|
+
const basicCommand = define({
|
|
168
|
+
name: 'test',
|
|
169
|
+
args: {
|
|
170
|
+
target: {
|
|
171
|
+
type: 'string',
|
|
172
|
+
description: 'The test target file',
|
|
173
|
+
required: true
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
run: ctx => console.log(`test: ${ctx.values.target}`)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
const i18nCommand = withI18nResource(basicCommand, async ctx => {
|
|
180
|
+
const resource = await import(
|
|
181
|
+
`./path/to/resources/test/${ctx.extensions['g:i18n'].locale.toString()}.json`,
|
|
182
|
+
{ with: { type: 'json' } }
|
|
183
|
+
).then(l => l.default || l)
|
|
184
|
+
return resource
|
|
185
|
+
})
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## 🧩 Context Extensions
|
|
189
|
+
|
|
190
|
+
When using the i18n plugin, your command context is extended via `ctx.extensions['g:i18n']`.
|
|
191
|
+
|
|
192
|
+
<!-- eslint-disable markdown/no-missing-label-refs -->
|
|
193
|
+
|
|
194
|
+
> [!IMPORTANT]
|
|
195
|
+
> This plugin extension is namespaced in `CommandContext.extensions` using this plugin ID `g:i18n` by the gunshi plugin system.
|
|
196
|
+
|
|
197
|
+
<!-- eslint-enable markdown/no-missing-label-refs -->
|
|
198
|
+
|
|
199
|
+
Available extensions:
|
|
200
|
+
|
|
201
|
+
- `locale: Intl.Locale`: The current locale
|
|
202
|
+
- `translate<T>(key: T, values?: Record<string, unknown>): string`: Translation function
|
|
203
|
+
|
|
204
|
+
## 📝 Resource Key Naming Conventions
|
|
205
|
+
|
|
206
|
+
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
|
+
|
|
208
|
+
- **Command Description**: Use the key `description` for the main description of the command.
|
|
209
|
+
- **Examples**: Use the key `examples` for usage examples.
|
|
210
|
+
- **Argument and Option Descriptions**: Keys for the descriptions of both command arguments (positional args) and options **must** be prefixed with `arg:`. For example:
|
|
211
|
+
- For an argument named `file`: use `arg:file`
|
|
212
|
+
- For an option named `verbose`: use `arg:verbose`
|
|
213
|
+
- **Negatable Argument Descriptions**: For boolean options (e.g., `--verbose`), Gunshi automatically generates a description for the negatable version (e.g., `--no-verbose`) using the built-in `NEGATABLE` key (e.g., "Negatable of --verbose"). To provide a custom translation for a specific negatable option, use the pattern `arg:no-<optionName>`, for example, `arg:no-verbose`.
|
|
214
|
+
- **Custom Keys**: Any other keys you define for custom translation messages (like greetings, error messages, etc.) do not require a prefix and can be named freely (e.g., `informal_greeting`, `error_file_not_found`).
|
|
215
|
+
- **Built-in Keys**: Keys for built-in functionalities are handled by Gunshi's default locales. The complete list includes:
|
|
216
|
+
- `USAGE` - Usage section header
|
|
217
|
+
- `OPTIONS` - Options section header
|
|
218
|
+
- `ARGUMENTS` - Arguments section header
|
|
219
|
+
- `COMMANDS` - Commands section header
|
|
220
|
+
- `EXAMPLES` - Examples section header
|
|
221
|
+
- `FORMORE` - Footer text for additional help
|
|
222
|
+
- `NEGATABLE` - Prefix for negatable options (e.g., "Negatable of --verbose")
|
|
223
|
+
- `DEFAULT` - Prefix for default values (e.g., "default: 5")
|
|
224
|
+
- `CHOICES` - Prefix for available choices (e.g., "choices: red, green, blue")
|
|
225
|
+
- `help` - Description for the help option ("Display this help message")
|
|
226
|
+
- `version` - Description for the version option ("Display this version")
|
|
227
|
+
|
|
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`).
|
|
229
|
+
|
|
230
|
+
Here's an example illustrating the convention:
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
import { defineI18n } from '@gunshi/plugin-i18n'
|
|
234
|
+
|
|
235
|
+
const command = defineI18n({
|
|
236
|
+
name: 'my-command',
|
|
237
|
+
args: {
|
|
238
|
+
target: { type: 'string' },
|
|
239
|
+
verbose: { type: 'boolean' }
|
|
240
|
+
},
|
|
241
|
+
resource: async ctx => {
|
|
242
|
+
// Example for 'en-US' locale
|
|
243
|
+
return {
|
|
244
|
+
description: 'This is my command.', // No prefix
|
|
245
|
+
examples: '$ my-command --target file.txt', // No prefix
|
|
246
|
+
'arg:target': 'The target file to process.', // 'arg:' prefix
|
|
247
|
+
'arg:verbose': 'Enable verbose output.', // 'arg:' prefix
|
|
248
|
+
'arg:no-verbose': 'Disable verbose logging specifically.', // Optional custom translation for the negatable option
|
|
249
|
+
processing_message: 'Processing target...' // No prefix
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
run: ctx => {
|
|
253
|
+
/* ... */
|
|
254
|
+
}
|
|
255
|
+
})
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
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
|
+
|
|
260
|
+
<!-- eslint-disable markdown/no-missing-label-refs -->
|
|
261
|
+
|
|
262
|
+
> [!IMPORTANT]
|
|
263
|
+
> 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 `ctx.extensions['g:i18n'].translate()`. Keep your translation keys simple and at the top level.
|
|
264
|
+
|
|
265
|
+
<!-- eslint-enable markdown/no-missing-label-refs -->
|
|
266
|
+
|
|
267
|
+
Good Flat structure:
|
|
268
|
+
|
|
269
|
+
```json
|
|
270
|
+
{
|
|
271
|
+
"greeting": "Hello",
|
|
272
|
+
"farewell": "Goodbye"
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Bad Nested structure (won't work with `ctx.extensions['g:i18n'].translate('messages.greeting')`):
|
|
277
|
+
|
|
278
|
+
```json
|
|
279
|
+
{
|
|
280
|
+
"messages": {
|
|
281
|
+
"greeting": "Hello",
|
|
282
|
+
"farewell": "Goodbye"
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
## 🔤 Translation Interpolation
|
|
288
|
+
|
|
289
|
+
The default translation adapter supports simple interpolation using `{$key}` syntax:
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
// In your resource
|
|
293
|
+
const resource = {
|
|
294
|
+
welcome: 'Welcome, {$name}!',
|
|
295
|
+
'items.count': 'You have {$count} items',
|
|
296
|
+
file_deleted: 'Deleted {$path}',
|
|
297
|
+
error_message: 'Error: {$error}'
|
|
298
|
+
}
|
|
299
|
+
// In your command
|
|
300
|
+
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"
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
<!-- eslint-disable markdown/no-missing-label-refs -->
|
|
308
|
+
|
|
309
|
+
> [!NOTE]
|
|
310
|
+
> The default adapter only supports simple string interpolation. For more advanced features like pluralization or formatting, you can provide a custom translation adapter.
|
|
311
|
+
|
|
312
|
+
<!-- eslint-enable markdown/no-missing-label-refs -->
|
|
313
|
+
|
|
314
|
+
## 🎨 Custom Translation Adapter
|
|
315
|
+
|
|
316
|
+
The default translation adapter provides basic string interpolation, but you might want to integrate with more powerful i18n libraries for features like:
|
|
317
|
+
|
|
318
|
+
- **Pluralization**: Different messages based on count values
|
|
319
|
+
- **Date/Time formatting**: Locale-aware formatting
|
|
320
|
+
- **Number formatting**: Currency, percentages, etc.
|
|
321
|
+
- **Complex interpolation**: Nested values, conditional messages
|
|
322
|
+
- **Message linking**: Reference other translations
|
|
323
|
+
- **Custom formatters**: Domain-specific formatting logic
|
|
324
|
+
|
|
325
|
+
### Creating a Translation Adapter
|
|
326
|
+
|
|
327
|
+
To create a custom translation adapter, you need to implement the `TranslationAdapter` interface:
|
|
328
|
+
|
|
329
|
+
```ts
|
|
330
|
+
interface TranslationAdapter {
|
|
331
|
+
// Get all resources for a locale
|
|
332
|
+
getResource(locale: string): Record<string, string> | undefined
|
|
333
|
+
|
|
334
|
+
// Set resources for a locale
|
|
335
|
+
setResource(locale: string, resource: Record<string, string>): void
|
|
336
|
+
|
|
337
|
+
// Get a single message
|
|
338
|
+
getMessage(locale: string, key: string): string | undefined
|
|
339
|
+
|
|
340
|
+
// Translate a message with interpolation
|
|
341
|
+
translate(locale: string, key: string, values?: Record<string, unknown>): string | undefined
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
The adapter factory receives options with locale information:
|
|
346
|
+
|
|
347
|
+
```ts
|
|
348
|
+
interface TranslationAdapterFactoryOptions {
|
|
349
|
+
locale: string // Current locale (BCP 47)
|
|
350
|
+
fallbackLocale: string // Fallback locale (default: 'en-US')
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### How Translation Adapters Work
|
|
355
|
+
|
|
356
|
+
When you provide a custom translation adapter:
|
|
357
|
+
|
|
358
|
+
1. **Initialization**: The i18n plugin calls your factory function with locale settings
|
|
359
|
+
2. **Resource Loading**: When a command defines a `resource` function, the plugin:
|
|
360
|
+
- Calls the resource function to get translations
|
|
361
|
+
- Passes them to your adapter via `setResource(locale, resource)`
|
|
362
|
+
3. **Translation**: When `translate()` is called:
|
|
363
|
+
- The plugin delegates to your adapter's `translate()` method
|
|
364
|
+
- Your adapter handles interpolation and formatting
|
|
365
|
+
- Returns the translated string or `undefined` if not found
|
|
366
|
+
|
|
367
|
+
### Custom Case: Integrating with Intlify (Vue I18n Core)
|
|
368
|
+
|
|
369
|
+
[Intlify](https://github.com/intlify/core) is the core of Intlify (Vue I18n), but it can be used independently. Here's how to create a translation adapter for Intlify:
|
|
370
|
+
|
|
371
|
+
```ts
|
|
372
|
+
import { cli } from 'gunshi'
|
|
373
|
+
import i18n, { defineI18n } from '@gunshi/plugin-i18n'
|
|
374
|
+
import {
|
|
375
|
+
createCoreContext,
|
|
376
|
+
getLocaleMessage,
|
|
377
|
+
NOT_RESOLVED,
|
|
378
|
+
setLocaleMessage,
|
|
379
|
+
translate as intlifyTranslate
|
|
380
|
+
} from '@intlify/core' // need to install `npm install --save @intlify/core@next`
|
|
381
|
+
|
|
382
|
+
// Create an Intlify translation adapter factory
|
|
383
|
+
function createIntlifyAdapterFactory(options) {
|
|
384
|
+
return new IntlifyTranslation(options)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
class IntlifyTranslation {
|
|
388
|
+
#options
|
|
389
|
+
#context
|
|
390
|
+
|
|
391
|
+
constructor(options) {
|
|
392
|
+
this.#options = options
|
|
393
|
+
|
|
394
|
+
const { locale, fallbackLocale } = options
|
|
395
|
+
const messages = {
|
|
396
|
+
[locale]: {}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (locale !== fallbackLocale) {
|
|
400
|
+
messages[fallbackLocale] = {}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Create the Intlify core context
|
|
404
|
+
this.#context = createCoreContext({
|
|
405
|
+
locale,
|
|
406
|
+
fallbackLocale,
|
|
407
|
+
messages
|
|
408
|
+
})
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
getResource(locale) {
|
|
412
|
+
return getLocaleMessage(this.#context, locale)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
setResource(locale, resource) {
|
|
416
|
+
setLocaleMessage(this.#context, locale, resource)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
getMessage(locale, key) {
|
|
420
|
+
const resource = this.getResource(locale)
|
|
421
|
+
if (resource) {
|
|
422
|
+
return resource[key]
|
|
423
|
+
}
|
|
424
|
+
return
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
translate(locale, key, values = {}) {
|
|
428
|
+
// Check if the message exists in the specified locale or fallback locale
|
|
429
|
+
const message =
|
|
430
|
+
this.getMessage(locale, key) || this.getMessage(this.#options.fallbackLocale, key)
|
|
431
|
+
if (message === undefined) {
|
|
432
|
+
return
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Use Intlify's translate function
|
|
436
|
+
const result = intlifyTranslate(this.#context, key, values)
|
|
437
|
+
return typeof result === 'number' && result === NOT_RESOLVED ? undefined : result
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Define your command
|
|
442
|
+
const command = defineI18n({
|
|
443
|
+
name: 'greeter',
|
|
444
|
+
|
|
445
|
+
args: {
|
|
446
|
+
name: {
|
|
447
|
+
type: 'string',
|
|
448
|
+
short: 'n'
|
|
449
|
+
},
|
|
450
|
+
count: {
|
|
451
|
+
type: 'number',
|
|
452
|
+
short: 'c',
|
|
453
|
+
default: 1
|
|
454
|
+
}
|
|
455
|
+
},
|
|
456
|
+
|
|
457
|
+
// Define a resource fetcher with Intlify syntax
|
|
458
|
+
resource: async ctx => {
|
|
459
|
+
const locale = ctx.extensions['g:i18n'].locale.toString()
|
|
460
|
+
|
|
461
|
+
if (locale === 'ja-JP') {
|
|
462
|
+
return {
|
|
463
|
+
description: '挨拶アプリケーション',
|
|
464
|
+
'arg:name': '挨拶する相手の名前',
|
|
465
|
+
'arg:count': '挨拶の回数',
|
|
466
|
+
greeting: 'こんにちは、{name}さん!',
|
|
467
|
+
greeting_plural: 'こんにちは、{name}さん!({count}回目)'
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return {
|
|
472
|
+
description: 'Greeting application',
|
|
473
|
+
'arg:name': 'Name to greet',
|
|
474
|
+
'arg:count': 'Number of greetings',
|
|
475
|
+
greeting: 'Hello, {name}!',
|
|
476
|
+
greeting_plural: 'Hello, {name}! (greeting #{count})'
|
|
477
|
+
}
|
|
478
|
+
},
|
|
479
|
+
|
|
480
|
+
run: ctx => {
|
|
481
|
+
const { name = 'World', count } = ctx.values
|
|
482
|
+
const { translate } = ctx.extensions['g:i18n']
|
|
483
|
+
|
|
484
|
+
// Use the translation function with Intlify
|
|
485
|
+
const key = count > 1 ? 'greeting_plural' : 'greeting'
|
|
486
|
+
const message = translate(key, { name, count })
|
|
487
|
+
|
|
488
|
+
console.log(message)
|
|
489
|
+
}
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
// Run the command with the Intlify translation adapter
|
|
493
|
+
await cli(process.argv.slice(2), command, {
|
|
494
|
+
name: 'intlify-example',
|
|
495
|
+
version: '1.0.0',
|
|
496
|
+
plugins: [
|
|
497
|
+
i18n({
|
|
498
|
+
locale: process.env.MY_LOCALE || 'en-US',
|
|
499
|
+
translationAdapterFactory: createIntlifyAdapterFactory
|
|
500
|
+
})
|
|
501
|
+
]
|
|
502
|
+
})
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
With Intlify, you get advanced features like:
|
|
506
|
+
|
|
507
|
+
- Named interpolation: `{name}` instead of `{$name}`
|
|
508
|
+
- Pluralization support
|
|
509
|
+
- Linked messages
|
|
510
|
+
- HTML formatting
|
|
511
|
+
- Custom modifiers
|
|
512
|
+
- And more
|
|
513
|
+
|
|
514
|
+
<!-- eslint-disable markdown/no-missing-label-refs -->
|
|
515
|
+
|
|
516
|
+
> [!TIP]
|
|
517
|
+
> Intlify uses `{name}` syntax for interpolation (without the `$` prefix), which is different from Gunshi's default adapter that uses `{$name}`.
|
|
518
|
+
|
|
519
|
+
<!-- eslint-enable markdown/no-missing-label-refs -->
|
|
520
|
+
|
|
521
|
+
## 📚 API References
|
|
522
|
+
|
|
523
|
+
See the [API References](./docs/index.md)
|
|
524
|
+
|
|
525
|
+
## ©️ License
|
|
526
|
+
|
|
527
|
+
[MIT](http://opensource.org/licenses/MIT)
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import { Awaitable, Command, CommandContext, DefaultGunshiParams, ExtractArgs, GunshiParams, GunshiParamsConstraint, NormalizeToGunshiParams, PluginWithExtension } from "@gunshi/plugin";
|
|
2
|
+
|
|
3
|
+
//#region rolldown:runtime
|
|
4
|
+
|
|
5
|
+
//#endregion
|
|
6
|
+
//#region ../../node_modules/.pnpm/args-tokens@0.20.1/node_modules/args-tokens/lib/resolver-U72Jg6Ll.d.ts
|
|
7
|
+
//#region src/resolver.d.ts
|
|
8
|
+
|
|
9
|
+
/**
|
|
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
|
+
*/
|
|
17
|
+
interface ArgSchema {
|
|
18
|
+
/**
|
|
19
|
+
* Type of argument.
|
|
20
|
+
*/
|
|
21
|
+
type: "string" | "boolean" | "number" | "enum" | "positional" | "custom";
|
|
22
|
+
/**
|
|
23
|
+
* A single character alias for the argument.
|
|
24
|
+
*/
|
|
25
|
+
short?: string;
|
|
26
|
+
/**
|
|
27
|
+
* A description of the argument.
|
|
28
|
+
*/
|
|
29
|
+
description?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Whether the argument is required or not.
|
|
32
|
+
*/
|
|
33
|
+
required?: true;
|
|
34
|
+
/**
|
|
35
|
+
* Whether the argument allow multiple values or not.
|
|
36
|
+
*/
|
|
37
|
+
multiple?: true;
|
|
38
|
+
/**
|
|
39
|
+
* Whether the negatable option for `boolean` type
|
|
40
|
+
*/
|
|
41
|
+
negatable?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* The allowed values of the argument, and string only. This property is only used when the type is 'enum'.
|
|
44
|
+
*/
|
|
45
|
+
choices?: string[] | readonly string[];
|
|
46
|
+
/**
|
|
47
|
+
* The default value of the argument.
|
|
48
|
+
* if the type is 'enum', the default value must be one of the allowed values.
|
|
49
|
+
*/
|
|
50
|
+
default?: string | boolean | number;
|
|
51
|
+
/**
|
|
52
|
+
* Whether to convert the argument name to kebab-case.
|
|
53
|
+
*/
|
|
54
|
+
toKebab?: true;
|
|
55
|
+
/**
|
|
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
|
|
63
|
+
parse?: (value: string) => any;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* An object that contains {@link ArgSchema | argument schema}.
|
|
67
|
+
*/
|
|
68
|
+
interface Args {
|
|
69
|
+
[option: string]: ArgSchema;
|
|
70
|
+
} //#endregion
|
|
71
|
+
//#region ../shared/src/constants.d.ts
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* An object that contains the values of the arguments.
|
|
75
|
+
*/
|
|
76
|
+
declare namespace constants_d_exports {
|
|
77
|
+
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
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* @author kazuya kawaguchi (a.k.a. kazupon)
|
|
81
|
+
* @license MIT
|
|
82
|
+
*/
|
|
83
|
+
declare const BUILT_IN_PREFIX = "_";
|
|
84
|
+
declare const PLUGIN_PREFIX = "g";
|
|
85
|
+
declare const ARG_PREFIX = "arg";
|
|
86
|
+
declare const BUILT_IN_KEY_SEPARATOR = ":";
|
|
87
|
+
declare const BUILD_IN_PREFIX_AND_KEY_SEPARATOR: string;
|
|
88
|
+
declare const ARG_PREFIX_AND_KEY_SEPARATOR: string;
|
|
89
|
+
declare const ARG_NEGATABLE_PREFIX = "no-";
|
|
90
|
+
type CommonArgType = {
|
|
91
|
+
readonly help: {
|
|
92
|
+
readonly type: 'boolean';
|
|
93
|
+
readonly short: 'h';
|
|
94
|
+
readonly description: string;
|
|
95
|
+
};
|
|
96
|
+
readonly version: {
|
|
97
|
+
readonly type: 'boolean';
|
|
98
|
+
readonly short: 'v';
|
|
99
|
+
readonly description: string;
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
declare const COMMON_ARGS: CommonArgType;
|
|
103
|
+
declare const COMMAND_BUILTIN_RESOURCE_KEYS: readonly ["USAGE", "COMMAND", "SUBCOMMAND", "COMMANDS", "ARGUMENTS", "OPTIONS", "EXAMPLES", "FORMORE", "NEGATABLE", "DEFAULT", "CHOICES"];
|
|
104
|
+
|
|
105
|
+
//#endregion
|
|
106
|
+
//#region ../shared/src/types.d.ts
|
|
107
|
+
type RemoveIndexSignature<T> = { [K in keyof T as string extends K ? never : number extends K ? never : K]: T[K] };
|
|
108
|
+
/**
|
|
109
|
+
* Remove index signature from object or record type.
|
|
110
|
+
*/
|
|
111
|
+
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];
|
|
113
|
+
/**
|
|
114
|
+
* Generate a namespaced key.
|
|
115
|
+
*/
|
|
116
|
+
type GenerateNamespacedKey<Key extends string, Prefixed extends string = typeof BUILT_IN_PREFIX> = `${Prefixed}${typeof BUILT_IN_KEY_SEPARATOR}${Key}`;
|
|
117
|
+
/**
|
|
118
|
+
* Command i18n built-in arguments keys.
|
|
119
|
+
*/
|
|
120
|
+
type CommandBuiltinArgsKeys = keyof (typeof constants_d_exports)['COMMON_ARGS'];
|
|
121
|
+
/**
|
|
122
|
+
* Command i18n built-in resource keys.
|
|
123
|
+
*/
|
|
124
|
+
type CommandBuiltinResourceKeys = (typeof constants_d_exports)['COMMAND_BUILTIN_RESOURCE_KEYS'][number];
|
|
125
|
+
/**
|
|
126
|
+
* i18n built-in resource keys.
|
|
127
|
+
*/
|
|
128
|
+
type BuiltinResourceKeys = CommandBuiltinArgsKeys | CommandBuiltinResourceKeys;
|
|
129
|
+
/**
|
|
130
|
+
* Command i18n built-in keys.
|
|
131
|
+
* The command i18n built-in keys are used by the i18n plugin for translation.
|
|
132
|
+
*/
|
|
133
|
+
type CommandBuiltinKeys = GenerateNamespacedKey<BuiltinResourceKeys> | 'description' | 'examples';
|
|
134
|
+
/**
|
|
135
|
+
* Command i18n option keys.
|
|
136
|
+
* The command i18n option keys are used by the i18n plugin for translation.
|
|
137
|
+
*/
|
|
138
|
+
type CommandArgKeys<A extends Args> = GenerateNamespacedKey<KeyOfArgs<RemovedIndex<A>>, typeof ARG_PREFIX>;
|
|
139
|
+
|
|
140
|
+
//#endregion
|
|
141
|
+
//#region src/types.d.ts
|
|
142
|
+
/**
|
|
143
|
+
* The unique identifier for the i18n plugin.
|
|
144
|
+
*/
|
|
145
|
+
declare const pluginId: GenerateNamespacedKey<'i18n', typeof PLUGIN_PREFIX>;
|
|
146
|
+
/**
|
|
147
|
+
* Type representing the unique identifier for i18n plugin.
|
|
148
|
+
*/
|
|
149
|
+
type PluginId = typeof pluginId;
|
|
150
|
+
/**
|
|
151
|
+
* Extended command context which provides utilities via i18n plugin.
|
|
152
|
+
* These utilities are available via `CommandContext.extensions['g:i18n']`.
|
|
153
|
+
*/
|
|
154
|
+
interface I18nCommandContext<G extends GunshiParams<any> = DefaultGunshiParams> {
|
|
155
|
+
/**
|
|
156
|
+
* Command locale
|
|
157
|
+
*/
|
|
158
|
+
locale: string | Intl.Locale;
|
|
159
|
+
/**
|
|
160
|
+
* Translate a message
|
|
161
|
+
* @param key Translation key
|
|
162
|
+
* @param values Values to interpolate
|
|
163
|
+
* @returns Translated message. If the key is not found:
|
|
164
|
+
* - For custom keys: returns an empty string ('')
|
|
165
|
+
* - For built-in keys (prefixed with '_:'): returns the key itself
|
|
166
|
+
*/
|
|
167
|
+
translate: <T extends string = CommandBuiltinKeys, O = CommandArgKeys<G['args']>, K = CommandBuiltinKeys | O | T>(key: K, values?: Record<string, unknown>) => string;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* i18n plugin options
|
|
171
|
+
*/
|
|
172
|
+
interface I18nPluginOptions {
|
|
173
|
+
/**
|
|
174
|
+
* Locale to use for translations
|
|
175
|
+
*/
|
|
176
|
+
locale?: string | Intl.Locale;
|
|
177
|
+
/**
|
|
178
|
+
* Translation adapter factory
|
|
179
|
+
*/
|
|
180
|
+
translationAdapterFactory?: TranslationAdapterFactory;
|
|
181
|
+
/**
|
|
182
|
+
* Built-in localizable resources
|
|
183
|
+
*/
|
|
184
|
+
resources?: Record<string, Record<BuiltinResourceKeys, string>>;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Translation adapter factory.
|
|
188
|
+
*/
|
|
189
|
+
type TranslationAdapterFactory = (options: TranslationAdapterFactoryOptions) => TranslationAdapter;
|
|
190
|
+
/**
|
|
191
|
+
* Translation adapter factory options.
|
|
192
|
+
*/
|
|
193
|
+
interface TranslationAdapterFactoryOptions {
|
|
194
|
+
/**
|
|
195
|
+
* A locale (BCP 47 language tag).
|
|
196
|
+
*/
|
|
197
|
+
locale: string;
|
|
198
|
+
/**
|
|
199
|
+
* A fallback locale.
|
|
200
|
+
* @default DEFAULT_LOCALE ('en-US')
|
|
201
|
+
*/
|
|
202
|
+
fallbackLocale: string;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Translation adapter.
|
|
206
|
+
* This adapter is used to custom message formatter like {@link https://github.com/intlify/vue-i18n/blob/master/spec/syntax.ebnf | Intlify message format}, {@link https://github.com/tc39/proposal-intl-messageformat | `Intl.MessageFormat` (MF2)}, and etc.
|
|
207
|
+
* This adapter will support localization with your preferred message format.
|
|
208
|
+
*/
|
|
209
|
+
interface TranslationAdapter<MessageResource = string> {
|
|
210
|
+
/**
|
|
211
|
+
* Get a resource of locale.
|
|
212
|
+
* @param locale A Locale at the time of command execution. That is Unicord locale ID (BCP 47)
|
|
213
|
+
* @returns A resource of locale. if resource not found, return `undefined`.
|
|
214
|
+
*/
|
|
215
|
+
getResource(locale: string): Record<string, string> | undefined;
|
|
216
|
+
/**
|
|
217
|
+
* Set a resource of locale.
|
|
218
|
+
* @param locale A Locale at the time of command execution. That is Unicord locale ID (BCP 47)
|
|
219
|
+
* @param resource A resource of locale
|
|
220
|
+
*/
|
|
221
|
+
setResource(locale: string, resource: Record<string, string>): void;
|
|
222
|
+
/**
|
|
223
|
+
* Get a message of locale.
|
|
224
|
+
* @param locale A Locale at the time of command execution. That is Unicord locale ID (BCP 47)
|
|
225
|
+
* @param key A key of message resource
|
|
226
|
+
* @returns A message of locale. if message not found, return `undefined`.
|
|
227
|
+
*/
|
|
228
|
+
getMessage(locale: string, key: string): MessageResource | undefined;
|
|
229
|
+
/**
|
|
230
|
+
* Translate a message.
|
|
231
|
+
* @param locale A Locale at the time of command execution. That is Unicord locale ID (BCP 47)
|
|
232
|
+
* @param key A key of message resource
|
|
233
|
+
* @param values A values to be resolved in the message
|
|
234
|
+
* @returns A translated message, if message is not translated, return `undefined`.
|
|
235
|
+
*/
|
|
236
|
+
translate(locale: string, key: string, values?: Record<string, unknown>): string | undefined;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Command resource type for i18n plugin.
|
|
240
|
+
*/
|
|
241
|
+
type CommandResource<G extends GunshiParamsConstraint = DefaultGunshiParams> = {
|
|
242
|
+
/**
|
|
243
|
+
* Command description.
|
|
244
|
+
*/
|
|
245
|
+
description: string;
|
|
246
|
+
/**
|
|
247
|
+
* Examples usage.
|
|
248
|
+
*/
|
|
249
|
+
examples: string | CommandExamplesFetcher<NormalizeToGunshiParams<G>>;
|
|
250
|
+
} & { [Arg in GenerateNamespacedKey<KeyOfArgs<RemovedIndex<ExtractArgs<G>>>, typeof ARG_PREFIX>]: string } & {
|
|
251
|
+
[key: string]: string;
|
|
252
|
+
};
|
|
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
|
+
/**
|
|
260
|
+
* Command resource fetcher.
|
|
261
|
+
* @param ctx A {@link CommandContext | command context}
|
|
262
|
+
* @returns A fetched {@link CommandResource | command resource}.
|
|
263
|
+
*/
|
|
264
|
+
type CommandResourceFetcher<G extends GunshiParamsConstraint = DefaultGunshiParams> = (ctx: Readonly<CommandContext<G>>) => Awaitable<CommandResource<G>>;
|
|
265
|
+
/**
|
|
266
|
+
* I18n-aware command interface that extends the base Command with resource support
|
|
267
|
+
*/
|
|
268
|
+
interface I18nCommand<G extends GunshiParamsConstraint = DefaultGunshiParams> extends Command<G> {
|
|
269
|
+
/**
|
|
270
|
+
* Command resource fetcher for i18n support.
|
|
271
|
+
* This property is specific to i18n-enabled commands.
|
|
272
|
+
*/
|
|
273
|
+
resource?: CommandResourceFetcher<G>;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
//#endregion
|
|
277
|
+
//#region src/helpers.d.ts
|
|
278
|
+
/**
|
|
279
|
+
* Define an i18n-aware command with type safety
|
|
280
|
+
*
|
|
281
|
+
* @example
|
|
282
|
+
* ```ts
|
|
283
|
+
* import { defineI18n } from '@gunshi/plugin-i18n'
|
|
284
|
+
*
|
|
285
|
+
* const greetCommand = defineI18n({
|
|
286
|
+
* name: 'greet',
|
|
287
|
+
* args: {
|
|
288
|
+
* name: { type: 'string', description: 'Name to greet' }
|
|
289
|
+
* },
|
|
290
|
+
* resource: async (ctx) => ({
|
|
291
|
+
* description: 'Greet someone',
|
|
292
|
+
* 'arg:name': 'The name to greet'
|
|
293
|
+
* }),
|
|
294
|
+
* run: async (ctx) => {
|
|
295
|
+
* console.log(`Hello, ${ctx.values.name}!`)
|
|
296
|
+
* }
|
|
297
|
+
* })
|
|
298
|
+
* ```
|
|
299
|
+
*/
|
|
300
|
+
declare function defineI18n<G extends GunshiParamsConstraint = DefaultGunshiParams>(command: Command<G> & {
|
|
301
|
+
resource?: CommandResourceFetcher<G>;
|
|
302
|
+
}): I18nCommand<G>;
|
|
303
|
+
/**
|
|
304
|
+
* Add i18n resource to an existing command
|
|
305
|
+
*
|
|
306
|
+
* @example
|
|
307
|
+
* ```ts
|
|
308
|
+
* import { define } from '@gunshi/definition'
|
|
309
|
+
* import { withI18nResource } from '@gunshi/plugin-i18n'
|
|
310
|
+
*
|
|
311
|
+
* const myCommand = define({
|
|
312
|
+
* name: 'myCommand',
|
|
313
|
+
* args: {
|
|
314
|
+
* input: { type: 'string', description: 'Input value' }
|
|
315
|
+
* },
|
|
316
|
+
* run: async (ctx) => {
|
|
317
|
+
* console.log(`Input: ${ctx.values.input}`)
|
|
318
|
+
* }
|
|
319
|
+
* })
|
|
320
|
+
*
|
|
321
|
+
* const i18nCommand = withI18nResource(basicCommand, async ctx => {
|
|
322
|
+
* const resource = await import(
|
|
323
|
+
* `./path/to/resources/test/${ctx.extensions['g:i18n'].locale.toString()}.json`,
|
|
324
|
+
* { with: { type: 'json' } }
|
|
325
|
+
* ).then(l => l.default || l)
|
|
326
|
+
* return resource
|
|
327
|
+
* })
|
|
328
|
+
* ```
|
|
329
|
+
*/
|
|
330
|
+
declare function withI18nResource<G extends GunshiParamsConstraint>(command: Command<G>, resource: CommandResourceFetcher<G>): I18nCommand<G>;
|
|
331
|
+
|
|
332
|
+
//#endregion
|
|
333
|
+
//#region src/translation.d.ts
|
|
334
|
+
declare function createTranslationAdapter(options: TranslationAdapterFactoryOptions): TranslationAdapter;
|
|
335
|
+
declare class DefaultTranslation implements TranslationAdapter {
|
|
336
|
+
#private;
|
|
337
|
+
constructor(options: TranslationAdapterFactoryOptions);
|
|
338
|
+
getResource(locale: string): Record<string, string> | undefined;
|
|
339
|
+
setResource(locale: string, resource: Record<string, string>): void;
|
|
340
|
+
getMessage(locale: string, key: string): string | undefined;
|
|
341
|
+
translate(locale: string, key: string, values?: Record<string, unknown>): string | undefined;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
//#endregion
|
|
345
|
+
//#region src/index.d.ts
|
|
346
|
+
/**
|
|
347
|
+
* The default locale string, which format is BCP 47 language tag.
|
|
348
|
+
*/
|
|
349
|
+
declare const DEFAULT_LOCALE = "en-US";
|
|
350
|
+
/**
|
|
351
|
+
* i18n plugin
|
|
352
|
+
*/
|
|
353
|
+
declare function i18n(options?: I18nPluginOptions): PluginWithExtension<Promise<I18nCommandContext<DefaultGunshiParams>>>;
|
|
354
|
+
|
|
355
|
+
//#endregion
|
|
356
|
+
export { CommandExamplesFetcher, CommandResource, CommandResourceFetcher, DEFAULT_LOCALE, DefaultTranslation, I18nCommand, I18nCommandContext, I18nPluginOptions, PluginId, TranslationAdapter, TranslationAdapterFactory, TranslationAdapterFactoryOptions, createTranslationAdapter, i18n as default, defineI18n, pluginId, withI18nResource };
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { plugin } from "@gunshi/plugin";
|
|
2
|
+
|
|
3
|
+
//#region ../shared/src/constants.ts
|
|
4
|
+
/**
|
|
5
|
+
* @author kazuya kawaguchi (a.k.a. kazupon)
|
|
6
|
+
* @license MIT
|
|
7
|
+
*/
|
|
8
|
+
const BUILT_IN_PREFIX = "_";
|
|
9
|
+
const PLUGIN_PREFIX = "g";
|
|
10
|
+
const ARG_PREFIX = "arg";
|
|
11
|
+
const BUILT_IN_KEY_SEPARATOR = ":";
|
|
12
|
+
const BUILD_IN_PREFIX_AND_KEY_SEPARATOR = `${BUILT_IN_PREFIX}${BUILT_IN_KEY_SEPARATOR}`;
|
|
13
|
+
const ARG_PREFIX_AND_KEY_SEPARATOR = `${ARG_PREFIX}${BUILT_IN_KEY_SEPARATOR}`;
|
|
14
|
+
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region ../resources/locales/en-US.json
|
|
17
|
+
var COMMAND = "COMMAND";
|
|
18
|
+
var COMMANDS = "COMMANDS";
|
|
19
|
+
var SUBCOMMAND = "SUBCOMMAND";
|
|
20
|
+
var USAGE = "USAGE";
|
|
21
|
+
var ARGUMENTS = "ARGUMENTS";
|
|
22
|
+
var OPTIONS = "OPTIONS";
|
|
23
|
+
var EXAMPLES = "EXAMPLES";
|
|
24
|
+
var FORMORE = "For more info, run any command with the `--help` flag";
|
|
25
|
+
var NEGATABLE = "Negatable of";
|
|
26
|
+
var DEFAULT = "default";
|
|
27
|
+
var CHOICES = "choices";
|
|
28
|
+
var help = "Display this help message";
|
|
29
|
+
var version = "Display this version";
|
|
30
|
+
var en_US_default = {
|
|
31
|
+
COMMAND,
|
|
32
|
+
COMMANDS,
|
|
33
|
+
SUBCOMMAND,
|
|
34
|
+
USAGE,
|
|
35
|
+
ARGUMENTS,
|
|
36
|
+
OPTIONS,
|
|
37
|
+
EXAMPLES,
|
|
38
|
+
FORMORE,
|
|
39
|
+
NEGATABLE,
|
|
40
|
+
DEFAULT,
|
|
41
|
+
CHOICES,
|
|
42
|
+
help,
|
|
43
|
+
version
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region ../shared/src/utils.ts
|
|
48
|
+
function resolveBuiltInKey(key) {
|
|
49
|
+
return `${BUILT_IN_PREFIX}${BUILT_IN_KEY_SEPARATOR}${key}`;
|
|
50
|
+
}
|
|
51
|
+
function resolveArgKey(key) {
|
|
52
|
+
return `${ARG_PREFIX}${BUILT_IN_KEY_SEPARATOR}${key}`;
|
|
53
|
+
}
|
|
54
|
+
function namespacedId(id) {
|
|
55
|
+
return `${PLUGIN_PREFIX}${BUILT_IN_KEY_SEPARATOR}${id}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region src/translation.ts
|
|
60
|
+
function createTranslationAdapter(options) {
|
|
61
|
+
return new DefaultTranslation(options);
|
|
62
|
+
}
|
|
63
|
+
var DefaultTranslation = class {
|
|
64
|
+
#resources = new Map();
|
|
65
|
+
#options;
|
|
66
|
+
constructor(options) {
|
|
67
|
+
this.#options = options;
|
|
68
|
+
this.#resources.set(options.locale, Object.create(null));
|
|
69
|
+
if (options.locale !== options.fallbackLocale) this.#resources.set(options.fallbackLocale, Object.create(null));
|
|
70
|
+
}
|
|
71
|
+
getResource(locale) {
|
|
72
|
+
return this.#resources.get(locale);
|
|
73
|
+
}
|
|
74
|
+
setResource(locale, resource) {
|
|
75
|
+
this.#resources.set(locale, resource);
|
|
76
|
+
}
|
|
77
|
+
getMessage(locale, key) {
|
|
78
|
+
const resource = this.getResource(locale);
|
|
79
|
+
if (resource) return resource[key];
|
|
80
|
+
return void 0;
|
|
81
|
+
}
|
|
82
|
+
translate(locale, key, values = Object.create(null)) {
|
|
83
|
+
let message = this.getMessage(locale, key);
|
|
84
|
+
if (message === void 0 && locale !== this.#options.fallbackLocale) message = this.getMessage(this.#options.fallbackLocale, key);
|
|
85
|
+
if (message === void 0) return;
|
|
86
|
+
return message.replaceAll(/\{\$(\w+)\}/g, (_, name) => {
|
|
87
|
+
return values[name] == null ? "" : values[name].toString();
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
//#endregion
|
|
93
|
+
//#region src/types.ts
|
|
94
|
+
/**
|
|
95
|
+
* The unique identifier for the i18n plugin.
|
|
96
|
+
*/
|
|
97
|
+
const pluginId = namespacedId("i18n");
|
|
98
|
+
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region src/helpers.ts
|
|
101
|
+
/**
|
|
102
|
+
* Define an i18n-aware command with type safety
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```ts
|
|
106
|
+
* import { defineI18n } from '@gunshi/plugin-i18n'
|
|
107
|
+
*
|
|
108
|
+
* const greetCommand = defineI18n({
|
|
109
|
+
* name: 'greet',
|
|
110
|
+
* args: {
|
|
111
|
+
* name: { type: 'string', description: 'Name to greet' }
|
|
112
|
+
* },
|
|
113
|
+
* resource: async (ctx) => ({
|
|
114
|
+
* description: 'Greet someone',
|
|
115
|
+
* 'arg:name': 'The name to greet'
|
|
116
|
+
* }),
|
|
117
|
+
* run: async (ctx) => {
|
|
118
|
+
* console.log(`Hello, ${ctx.values.name}!`)
|
|
119
|
+
* }
|
|
120
|
+
* })
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
function defineI18n(command) {
|
|
124
|
+
return command;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Add i18n resource to an existing command
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```ts
|
|
131
|
+
* import { define } from '@gunshi/definition'
|
|
132
|
+
* import { withI18nResource } from '@gunshi/plugin-i18n'
|
|
133
|
+
*
|
|
134
|
+
* const myCommand = define({
|
|
135
|
+
* name: 'myCommand',
|
|
136
|
+
* args: {
|
|
137
|
+
* input: { type: 'string', description: 'Input value' }
|
|
138
|
+
* },
|
|
139
|
+
* run: async (ctx) => {
|
|
140
|
+
* console.log(`Input: ${ctx.values.input}`)
|
|
141
|
+
* }
|
|
142
|
+
* })
|
|
143
|
+
*
|
|
144
|
+
* const i18nCommand = withI18nResource(basicCommand, async ctx => {
|
|
145
|
+
* const resource = await import(
|
|
146
|
+
* `./path/to/resources/test/${ctx.extensions['g:i18n'].locale.toString()}.json`,
|
|
147
|
+
* { with: { type: 'json' } }
|
|
148
|
+
* ).then(l => l.default || l)
|
|
149
|
+
* return resource
|
|
150
|
+
* })
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
function withI18nResource(command, resource) {
|
|
154
|
+
return {
|
|
155
|
+
...command,
|
|
156
|
+
resource
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
//#endregion
|
|
161
|
+
//#region src/index.ts
|
|
162
|
+
/**
|
|
163
|
+
* The default locale string, which format is BCP 47 language tag.
|
|
164
|
+
*/
|
|
165
|
+
const DEFAULT_LOCALE = "en-US";
|
|
166
|
+
const BUILT_IN_PREFIX_CODE = BUILT_IN_PREFIX.codePointAt(0);
|
|
167
|
+
/**
|
|
168
|
+
* i18n plugin
|
|
169
|
+
*/
|
|
170
|
+
function i18n(options = {}) {
|
|
171
|
+
const locale = toLocale(options.locale);
|
|
172
|
+
const localeStr = locale.toString();
|
|
173
|
+
const resources = options.resources || Object.create(null);
|
|
174
|
+
const translationAdapterFactory = options.translationAdapterFactory || createTranslationAdapter;
|
|
175
|
+
const adapter = translationAdapterFactory({
|
|
176
|
+
locale: localeStr,
|
|
177
|
+
fallbackLocale: DEFAULT_LOCALE
|
|
178
|
+
});
|
|
179
|
+
const localeBuiltinResources = new Map();
|
|
180
|
+
let builtInLoadedResources;
|
|
181
|
+
return plugin({
|
|
182
|
+
id: pluginId,
|
|
183
|
+
name: "internationalization",
|
|
184
|
+
dependencies: [{
|
|
185
|
+
id: namespacedId("global"),
|
|
186
|
+
optional: true
|
|
187
|
+
}],
|
|
188
|
+
extension: async () => {
|
|
189
|
+
function translate(key, values = Object.create(null)) {
|
|
190
|
+
const strKey = key;
|
|
191
|
+
if (strKey.codePointAt(0) === BUILT_IN_PREFIX_CODE) {
|
|
192
|
+
const resource = localeBuiltinResources.get(localeStr) || localeBuiltinResources.get(DEFAULT_LOCALE);
|
|
193
|
+
return resource[strKey] || strKey;
|
|
194
|
+
} else return adapter.translate(localeStr, strKey, values) || "";
|
|
195
|
+
}
|
|
196
|
+
function getResource(locale$1) {
|
|
197
|
+
const targetLocale = toLocale(locale$1);
|
|
198
|
+
const targetLocaleStr = targetLocale.toString();
|
|
199
|
+
return localeBuiltinResources.get(targetLocaleStr);
|
|
200
|
+
}
|
|
201
|
+
function setResource(locale$1, resource) {
|
|
202
|
+
const targetLocale = toLocale(locale$1);
|
|
203
|
+
const targetLocaleStr = targetLocale.toString();
|
|
204
|
+
if (localeBuiltinResources.has(targetLocaleStr)) return;
|
|
205
|
+
localeBuiltinResources.set(targetLocale.toString(), mapResourceWithBuiltinKey(resource));
|
|
206
|
+
}
|
|
207
|
+
setResource(DEFAULT_LOCALE, en_US_default);
|
|
208
|
+
for (const [locale$1, resource] of Object.entries(resources)) setResource(locale$1, resource);
|
|
209
|
+
builtInLoadedResources = getResource(locale);
|
|
210
|
+
return {
|
|
211
|
+
locale,
|
|
212
|
+
translate
|
|
213
|
+
};
|
|
214
|
+
},
|
|
215
|
+
onExtension: async (ctx, cmd) => {
|
|
216
|
+
/**
|
|
217
|
+
* load command resources, after the command context is extended
|
|
218
|
+
*/
|
|
219
|
+
const loadedOptionsResources = Object.entries(ctx.args).map(([key, schema]) => [key, schema.description || ""]);
|
|
220
|
+
const defaultCommandResource = loadedOptionsResources.reduce((res, [key, value]) => {
|
|
221
|
+
res[resolveArgKey(key)] = value;
|
|
222
|
+
return res;
|
|
223
|
+
}, 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) : "";
|
|
226
|
+
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
|
+
}
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
function toLocale(locale) {
|
|
240
|
+
return locale instanceof Intl.Locale ? locale : typeof locale === "string" ? new Intl.Locale(locale) : new Intl.Locale(DEFAULT_LOCALE);
|
|
241
|
+
}
|
|
242
|
+
async function loadCommandResource(ctx, command) {
|
|
243
|
+
if (!hasI18nResource(command)) return void 0;
|
|
244
|
+
let resource;
|
|
245
|
+
try {
|
|
246
|
+
resource = await command.resource(ctx);
|
|
247
|
+
} catch {}
|
|
248
|
+
return resource;
|
|
249
|
+
}
|
|
250
|
+
function mapResourceWithBuiltinKey(resource) {
|
|
251
|
+
return Object.entries(resource).reduce((acc, [key, value]) => {
|
|
252
|
+
acc[resolveBuiltInKey(key)] = value;
|
|
253
|
+
return acc;
|
|
254
|
+
}, Object.create(null));
|
|
255
|
+
}
|
|
256
|
+
function hasI18nResource(command) {
|
|
257
|
+
return "resource" in command && typeof command.resource === "function";
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
//#endregion
|
|
261
|
+
export { DEFAULT_LOCALE, DefaultTranslation, createTranslationAdapter, i18n as default, defineI18n, pluginId, withI18nResource };
|
package/package.json
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gunshi/plugin-i18n",
|
|
3
|
+
"description": "internationalization plugin for gunshi",
|
|
4
|
+
"version": "0.26.3",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "kazuya kawaguchi",
|
|
7
|
+
"email": "kawakazu80@gmail.com"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"funding": "https://github.com/sponsors/kazupon",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/kazupon/gunshi/issues"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/kazupon/gunshi.git",
|
|
17
|
+
"directory": "packages/plugin-i18n"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"gunshi",
|
|
21
|
+
"i18n",
|
|
22
|
+
"internationalization",
|
|
23
|
+
"plugin",
|
|
24
|
+
"cli"
|
|
25
|
+
],
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">= 20"
|
|
31
|
+
},
|
|
32
|
+
"type": "module",
|
|
33
|
+
"files": [
|
|
34
|
+
"lib"
|
|
35
|
+
],
|
|
36
|
+
"module": "lib/index.js",
|
|
37
|
+
"exports": {
|
|
38
|
+
".": {
|
|
39
|
+
"types": "./lib/index.d.ts",
|
|
40
|
+
"import": "./lib/index.js",
|
|
41
|
+
"require": "./lib/index.js",
|
|
42
|
+
"default": "./lib/index.js"
|
|
43
|
+
},
|
|
44
|
+
"./package.json": "./package.json"
|
|
45
|
+
},
|
|
46
|
+
"types": "lib/index.d.ts",
|
|
47
|
+
"typesVersions": {
|
|
48
|
+
"*": {
|
|
49
|
+
"*": [
|
|
50
|
+
"./lib/*",
|
|
51
|
+
"./*"
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"@gunshi/plugin": "0.26.3"
|
|
57
|
+
},
|
|
58
|
+
"peerDependencies": {
|
|
59
|
+
"@gunshi/plugin-global": "0.26.3"
|
|
60
|
+
},
|
|
61
|
+
"peerDependenciesMeta": {
|
|
62
|
+
"@gunshi/plugin-global": {
|
|
63
|
+
"optional": true
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"devDependencies": {
|
|
67
|
+
"@intlify/core": "next",
|
|
68
|
+
"deno": "^2.3.3",
|
|
69
|
+
"jsr": "^0.13.4",
|
|
70
|
+
"jsr-exports-lint": "^0.4.1",
|
|
71
|
+
"messageformat": "4.0.0-12",
|
|
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"
|
|
78
|
+
},
|
|
79
|
+
"scripts": {
|
|
80
|
+
"build": "tsdown",
|
|
81
|
+
"build:docs": "typedoc --excludeInternal",
|
|
82
|
+
"lint:jsr": "jsr publish --dry-run --allow-dirty",
|
|
83
|
+
"typecheck:deno": "deno check --import-map=../../importmap.json ./src"
|
|
84
|
+
}
|
|
85
|
+
}
|