langctl 0.2.0 → 0.4.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.
- package/CHANGELOG.md +53 -0
- package/README.md +158 -1188
- package/dist/commands/auth.js +85 -19
- package/dist/commands/config.js +36 -26
- package/dist/commands/export.js +11 -122
- package/dist/commands/import.js +32 -143
- package/dist/commands/init.js +61 -72
- package/dist/commands/keys.js +145 -279
- package/dist/commands/org.js +45 -111
- package/dist/commands/projects.js +124 -312
- package/dist/commands/pull.js +86 -129
- package/dist/commands/push.js +125 -0
- package/dist/commands/review.js +65 -0
- package/dist/commands/team.js +68 -257
- package/dist/commands/translate.js +99 -0
- package/dist/core/cache.js +48 -0
- package/dist/core/config.js +130 -0
- package/dist/core/errors.js +67 -0
- package/dist/core/files.js +38 -0
- package/dist/core/http.js +156 -0
- package/dist/core/output.js +96 -0
- package/dist/core/project.js +58 -0
- package/dist/core/prompts.js +56 -0
- package/dist/formats/index.js +285 -0
- package/dist/index.js +277 -508
- package/dist/version.js +4 -0
- package/package.json +22 -15
- package/.gitattributes +0 -2
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -26
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
- package/CONTRIBUTING.md +0 -33
- package/dist/api.d.ts +0 -20
- package/dist/api.js +0 -75
- package/dist/auth.d.ts +0 -38
- package/dist/auth.js +0 -106
- package/dist/commands/auth.d.ts +0 -3
- package/dist/commands/config.d.ts +0 -2
- package/dist/commands/debug.d.ts +0 -3
- package/dist/commands/debug.js +0 -61
- package/dist/commands/export.d.ts +0 -13
- package/dist/commands/import.d.ts +0 -12
- package/dist/commands/init.d.ts +0 -2
- package/dist/commands/keys.d.ts +0 -25
- package/dist/commands/org.d.ts +0 -13
- package/dist/commands/projects.d.ts +0 -33
- package/dist/commands/pull.d.ts +0 -9
- package/dist/commands/team.d.ts +0 -29
- package/dist/config.d.ts +0 -48
- package/dist/config.js +0 -69
- package/dist/exporters/index.d.ts +0 -36
- package/dist/exporters/index.js +0 -214
- package/dist/index.d.ts +0 -3
- package/dist/utils/banner.d.ts +0 -9
- package/dist/utils/banner.js +0 -24
- package/translations/en.json +0 -5
- package/translations/en.xml +0 -9
- package/translations/fr.json +0 -4
- package/translations/fr.xml +0 -7
- package/translations/hi.json +0 -4
- package/translations/hi.xml +0 -7
- package/translations/ios/en.strings +0 -9
- package/translations/ios/fr.strings +0 -6
- package/translations/ios/hi.strings +0 -6
- package/translations/ios/ja.strings +0 -6
- package/translations/ja.json +0 -4
- package/translations/ja.xml +0 -7
package/dist/index.js
CHANGED
|
@@ -1,517 +1,286 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { Command } from 'commander';
|
|
2
|
+
import { Command, CommanderError, Option } from 'commander';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { configCommand } from './commands/config.js';
|
|
7
|
-
import { projectsListCommand, projectsCreateCommand, projectsGetCommand, projectsUpdateCommand, projectsDeleteCommand, projectsAddLanguageCommand, projectsRemoveLanguageCommand, projectsStatsCommand } from './commands/projects.js';
|
|
8
|
-
import { listKeysCommand, getKeyCommand, createKeyCommand, deleteKeyCommand, translateKeyCommand, publishKeysCommand } from './commands/keys.js';
|
|
9
|
-
import { listTeamCommand, getTeamMemberCommand, inviteTeamMemberCommand, removeTeamMemberCommand, updateTeamRoleCommand, listInvitationsCommand, revokeInvitationCommand } from './commands/team.js';
|
|
10
|
-
import { orgInfoCommand, orgStatsCommand, orgPlanCommand } from './commands/org.js';
|
|
11
|
-
import { pullCommand } from './commands/pull.js';
|
|
4
|
+
import { authCommand, logoutCommand, whoamiCommand } from './commands/auth.js';
|
|
5
|
+
import { configCommand, formatsCommand } from './commands/config.js';
|
|
12
6
|
import { exportCommand } from './commands/export.js';
|
|
13
7
|
import { importCommand } from './commands/import.js';
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
8
|
+
import { initCommand } from './commands/init.js';
|
|
9
|
+
import { createKeyCommand, deleteKeyCommand, getKeyCommand, listKeysCommand, publishKeysCommand, updateKeyCommand } from './commands/keys.js';
|
|
10
|
+
import { orgInfoCommand, orgPlanCommand, orgStatsCommand } from './commands/org.js';
|
|
11
|
+
import { projectsAddLanguageCommand, projectsCreateCommand, projectsDeleteCommand, projectsGetCommand, projectsListCommand, projectsRemoveLanguageCommand, projectsStatsCommand, projectsUpdateCommand, } from './commands/projects.js';
|
|
12
|
+
import { pullCommand } from './commands/pull.js';
|
|
13
|
+
import { pushCommand } from './commands/push.js';
|
|
14
|
+
import { translateCommand } from './commands/translate.js';
|
|
15
|
+
import { reviewCommand } from './commands/review.js';
|
|
16
|
+
import { getTeamMemberCommand, inviteTeamMemberCommand, listInvitationsCommand, listTeamCommand, removeTeamMemberCommand, revokeInvitationCommand, updateTeamRoleCommand, } from './commands/team.js';
|
|
17
|
+
import { flagOverrides } from './core/config.js';
|
|
18
|
+
import { CliError, ExitCode, usageError } from './core/errors.js';
|
|
19
|
+
import { httpSettings } from './core/http.js';
|
|
20
|
+
import { log, printJson, runtime } from './core/output.js';
|
|
21
|
+
import { VERSION } from './version.js';
|
|
22
|
+
const collect = (value, previous = []) => [...previous, value];
|
|
23
|
+
/** Global flags, accepted before or after the subcommand (`langctl --json pull` and `langctl pull --json`). */
|
|
24
|
+
function withGlobals(cmd) {
|
|
25
|
+
return cmd
|
|
26
|
+
.option('--json', 'machine-readable JSON output on stdout')
|
|
27
|
+
.option('-q, --quiet', 'only print results and errors')
|
|
28
|
+
.option('--verbose', 'log HTTP requests to stderr')
|
|
29
|
+
.option('-y, --yes', 'skip confirmation prompts (required for destructive commands in CI)')
|
|
30
|
+
.option('--api-key <key>', 'API key (prefer the LANGCTL_API_KEY env var)')
|
|
31
|
+
.option('--api-url <url>', 'API base URL (env: LANGCTL_API_URL)')
|
|
32
|
+
.option('--timeout <seconds>', 'request timeout in seconds (env: LANGCTL_TIMEOUT)')
|
|
33
|
+
.option('--no-color', 'disable colors (also honors NO_COLOR)');
|
|
34
|
+
}
|
|
35
|
+
/** Wrap an action so every command shares the same error handling and exit codes. */
|
|
36
|
+
function run(fn) {
|
|
37
|
+
return async (...args) => {
|
|
38
|
+
await fn(...args);
|
|
39
|
+
};
|
|
40
|
+
}
|
|
16
41
|
const program = new Command();
|
|
17
|
-
// Package info
|
|
18
|
-
const packageJson = {
|
|
19
|
-
name: 'langctl',
|
|
20
|
-
version: '0.2.0',
|
|
21
|
-
description: 'CLI-first translation management for developers'
|
|
22
|
-
};
|
|
23
42
|
program
|
|
24
43
|
.name('langctl')
|
|
25
|
-
.description(
|
|
26
|
-
.version(
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
.
|
|
56
|
-
.
|
|
57
|
-
.action(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
.description('
|
|
70
|
-
.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
.
|
|
82
|
-
.
|
|
83
|
-
|
|
84
|
-
.
|
|
85
|
-
.
|
|
86
|
-
.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
.
|
|
97
|
-
.
|
|
98
|
-
.option('-
|
|
99
|
-
.option('-
|
|
100
|
-
.option('--
|
|
101
|
-
.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
.
|
|
124
|
-
.
|
|
125
|
-
.option('-
|
|
126
|
-
.
|
|
127
|
-
|
|
128
|
-
.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
.
|
|
140
|
-
.
|
|
141
|
-
.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
.
|
|
152
|
-
.
|
|
153
|
-
.
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
.
|
|
165
|
-
.
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
.
|
|
177
|
-
.
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
.
|
|
189
|
-
.
|
|
190
|
-
|
|
191
|
-
.
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
.option('--
|
|
198
|
-
.action(
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
.
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
}
|
|
245
|
-
catch (error) {
|
|
246
|
-
console.error(chalk.red(`Error: ${error.message}\n`));
|
|
247
|
-
process.exit(1);
|
|
248
|
-
}
|
|
249
|
-
});
|
|
250
|
-
keys
|
|
251
|
-
.command('translate <project> <key>')
|
|
252
|
-
.description('Update translation for a key')
|
|
253
|
-
.requiredOption('-l, --language <code>', 'Language code')
|
|
254
|
-
.requiredOption('-v, --value <text>', 'Translation value')
|
|
255
|
-
.action(async (project, key, options) => {
|
|
256
|
-
try {
|
|
257
|
-
await translateKeyCommand(project, key, options);
|
|
258
|
-
}
|
|
259
|
-
catch (error) {
|
|
260
|
-
console.error(chalk.red(`Error: ${error.message}\n`));
|
|
261
|
-
process.exit(1);
|
|
262
|
-
}
|
|
263
|
-
});
|
|
264
|
-
keys
|
|
265
|
-
.command('publish <project> <keys...>')
|
|
266
|
-
.description('Publish translation keys')
|
|
267
|
-
.option('--unpublish', 'Unpublish instead of publish')
|
|
268
|
-
.action(async (project, keysList, options) => {
|
|
269
|
-
try {
|
|
270
|
-
await publishKeysCommand(project, keysList, options);
|
|
271
|
-
}
|
|
272
|
-
catch (error) {
|
|
273
|
-
console.error(chalk.red(`Error: ${error.message}\n`));
|
|
274
|
-
process.exit(1);
|
|
275
|
-
}
|
|
276
|
-
});
|
|
277
|
-
// Team command
|
|
278
|
-
const team = program
|
|
279
|
-
.command('team')
|
|
280
|
-
.description('Manage team members');
|
|
281
|
-
team
|
|
282
|
-
.command('list')
|
|
283
|
-
.description('List team members')
|
|
284
|
-
.action(async () => {
|
|
285
|
-
try {
|
|
286
|
-
await listTeamCommand();
|
|
287
|
-
}
|
|
288
|
-
catch (error) {
|
|
289
|
-
console.error(chalk.red(`Error: ${error.message}\n`));
|
|
290
|
-
process.exit(1);
|
|
291
|
-
}
|
|
292
|
-
});
|
|
293
|
-
team
|
|
294
|
-
.command('get <email>')
|
|
295
|
-
.description('Get team member details')
|
|
296
|
-
.action(async (email) => {
|
|
297
|
-
try {
|
|
298
|
-
await getTeamMemberCommand(email);
|
|
299
|
-
}
|
|
300
|
-
catch (error) {
|
|
301
|
-
console.error(chalk.red(`Error: ${error.message}\n`));
|
|
302
|
-
process.exit(1);
|
|
303
|
-
}
|
|
304
|
-
});
|
|
305
|
-
team
|
|
306
|
-
.command('invite <email>')
|
|
307
|
-
.description('Invite team member')
|
|
308
|
-
.option('-r, --role <role>', 'Member role: viewer, member, admin', 'member')
|
|
309
|
-
.action(async (email, options) => {
|
|
310
|
-
try {
|
|
311
|
-
await inviteTeamMemberCommand(email, options);
|
|
312
|
-
}
|
|
313
|
-
catch (error) {
|
|
314
|
-
console.error(chalk.red(`Error: ${error.message}\n`));
|
|
315
|
-
process.exit(1);
|
|
44
|
+
.description('Translation management for developers — sync i18n files between your code, CI and Langctl.')
|
|
45
|
+
.version(VERSION, '-v, --version', 'print the version')
|
|
46
|
+
.showHelpAfterError('(run with --help for usage)')
|
|
47
|
+
.configureHelp({ sortSubcommands: false })
|
|
48
|
+
.exitOverride();
|
|
49
|
+
withGlobals(program);
|
|
50
|
+
program.hook('preAction', (_root, action) => {
|
|
51
|
+
const o = action.optsWithGlobals();
|
|
52
|
+
runtime.json = Boolean(o.json);
|
|
53
|
+
runtime.quiet = Boolean(o.quiet);
|
|
54
|
+
runtime.verbose = Boolean(o.verbose);
|
|
55
|
+
runtime.yes = Boolean(o.yes);
|
|
56
|
+
if (o.color === false || process.env.NO_COLOR)
|
|
57
|
+
chalk.level = 0;
|
|
58
|
+
if (typeof o.apiKey === 'string')
|
|
59
|
+
flagOverrides.apiKey = o.apiKey;
|
|
60
|
+
if (typeof o.apiUrl === 'string')
|
|
61
|
+
flagOverrides.apiUrl = o.apiUrl;
|
|
62
|
+
if (o.timeout !== undefined) {
|
|
63
|
+
const s = Number(o.timeout);
|
|
64
|
+
if (!Number.isFinite(s) || s <= 0)
|
|
65
|
+
throw usageError('--timeout must be a positive number of seconds');
|
|
66
|
+
httpSettings.timeoutMs = s * 1000;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
// ── Setup & auth ────────────────────────────────────────────────
|
|
70
|
+
withGlobals(program.command('init'))
|
|
71
|
+
.description('set up this repository: authenticate if needed and write langctl.json')
|
|
72
|
+
.option('-p, --project <slug>', 'project slug')
|
|
73
|
+
.option('-f, --format <format>', 'file format (see "langctl formats")')
|
|
74
|
+
.option('-o, --output <template>', 'path template for files, e.g. src/locales/{lang}.json')
|
|
75
|
+
.option('--force', 'overwrite an existing langctl.json')
|
|
76
|
+
.action(run(opts => initCommand(opts)));
|
|
77
|
+
withGlobals(program.command('auth [api-key]'))
|
|
78
|
+
.description('store an API key (prefer --stdin so the key stays out of shell history)')
|
|
79
|
+
.option('--stdin', 'read the API key from stdin')
|
|
80
|
+
.action(run((key, opts) => authCommand(key, opts)));
|
|
81
|
+
withGlobals(program.command('logout')).description('remove the stored API key').action(run(() => logoutCommand()));
|
|
82
|
+
withGlobals(program.command('whoami')).alias('status').description('show the organization, key and scopes in use, and check connectivity')
|
|
83
|
+
.action(run(() => whoamiCommand()));
|
|
84
|
+
withGlobals(program.command('config')).description('show effective configuration and where it comes from').action(run(() => configCommand()));
|
|
85
|
+
withGlobals(program.command('formats')).description('list supported file formats').action(run(() => formatsCommand()));
|
|
86
|
+
// ── Sync ────────────────────────────────────────────────────────
|
|
87
|
+
withGlobals(program.command('pull [project]'))
|
|
88
|
+
.description('download translations into files (project/format/output default to langctl.json)')
|
|
89
|
+
.option('-l, --languages <codes>', 'comma-separated languages (default: all)')
|
|
90
|
+
.addOption(new Option('--language <code>').hideHelp())
|
|
91
|
+
.option('-f, --format <format>', 'file format (default: json)')
|
|
92
|
+
.option('-o, --output <template>', 'path template, e.g. "src/locales/{lang}.json"')
|
|
93
|
+
.addOption(new Option('-d, --dir <path>', 'legacy: base directory for the default layout').hideHelp())
|
|
94
|
+
.option('-m, --module <name>', 'only keys from this module')
|
|
95
|
+
.option('--include-drafts', 'include unpublished keys (excluded by default)')
|
|
96
|
+
.option('--include-unreviewed', 'include AI translations nobody has reviewed yet (excluded by default)')
|
|
97
|
+
.addOption(new Option('--no-published-only').hideHelp())
|
|
98
|
+
.option('--check', 'do not write; exit 7 if any file is out of date (for CI)')
|
|
99
|
+
.option('--dry-run', 'show what would change without writing')
|
|
100
|
+
.option('--require-complete', 'exit 1 if any language is missing translations')
|
|
101
|
+
.action(run((project, opts) => pullCommand(project, { ...opts, languages: opts.languages ?? opts.language })));
|
|
102
|
+
withGlobals(program.command('push [project]'))
|
|
103
|
+
.description('upload translation files (by default only the source language)')
|
|
104
|
+
.option('-l, --languages <codes>', 'comma-separated languages, or "all" (default: source language)')
|
|
105
|
+
.option('-f, --format <format>', 'file format (default: from langctl.json or file extension)')
|
|
106
|
+
.option('-i, --input <template>', 'path template to read, e.g. "src/locales/{lang}.json"')
|
|
107
|
+
.option('-m, --module <name>', 'assign new keys to this module')
|
|
108
|
+
.option('--overwrite', 'replace existing translations (default: only add new keys/languages)')
|
|
109
|
+
.option('--publish', 'publish the uploaded keys')
|
|
110
|
+
.option('--dry-run', 'show what would change without uploading')
|
|
111
|
+
.action(run((project, opts) => pushCommand(project, opts)));
|
|
112
|
+
withGlobals(program.command('export [project]'))
|
|
113
|
+
.description('export translations to a file (one-off; "pull" is the repo workflow)')
|
|
114
|
+
.option('-l, --language <code>', 'language to export (default: all)')
|
|
115
|
+
.option('--languages <codes>', 'comma-separated languages')
|
|
116
|
+
.option('-f, --format <format>', 'file format (default: json)')
|
|
117
|
+
.option('-o, --output <path>', 'output file, or template with {lang}')
|
|
118
|
+
.option('-m, --module <name>', 'only keys from this module')
|
|
119
|
+
.option('--include-drafts', 'include unpublished keys')
|
|
120
|
+
.addOption(new Option('--include-unpublished').hideHelp())
|
|
121
|
+
.action(run((project, opts) => exportCommand(project, opts)));
|
|
122
|
+
withGlobals(program.command('import <project-or-file> [file]'))
|
|
123
|
+
.description('import one translation file (project may come from langctl.json)')
|
|
124
|
+
.option('-l, --language <code>', 'language of the file (default: project default language)')
|
|
125
|
+
.option('-f, --format <format>', 'file format (default: from extension)')
|
|
126
|
+
.option('-m, --module <name>', 'assign new keys to this module')
|
|
127
|
+
.option('--overwrite', 'replace existing translations')
|
|
128
|
+
.option('--publish', 'publish the imported keys')
|
|
129
|
+
.option('--dry-run', 'show what would change without uploading')
|
|
130
|
+
.action(run((a, b, opts) => importCommand(a, b, opts)));
|
|
131
|
+
// ── Projects ────────────────────────────────────────────────────
|
|
132
|
+
const projects = program.command('projects').alias('project').description('manage projects');
|
|
133
|
+
withGlobals(projects.command('list')).alias('ls').description('list projects').action(run(() => projectsListCommand()));
|
|
134
|
+
withGlobals(projects.command('get <slug>')).description('show a project').action(run((slug) => projectsGetCommand(slug)));
|
|
135
|
+
withGlobals(projects.command('create <name>')).description('create a project')
|
|
136
|
+
.option('-d, --description <text>', 'description')
|
|
137
|
+
.option('-l, --languages <codes>', 'comma-separated languages', 'en')
|
|
138
|
+
.option('--default-language <code>', 'default (source) language — defaults to the first of --languages')
|
|
139
|
+
.action(run((name, opts) => projectsCreateCommand(name, opts)));
|
|
140
|
+
withGlobals(projects.command('update <slug>')).description('update a project')
|
|
141
|
+
.option('-n, --name <name>', 'new name (the slug never changes)')
|
|
142
|
+
.option('-d, --description <text>', 'new description')
|
|
143
|
+
.option('-l, --languages <codes>', 'set the full language list (removing a language deletes its translations)')
|
|
144
|
+
.option('--default-language <code>', 'default language')
|
|
145
|
+
.action(run((slug, opts) => projectsUpdateCommand(slug, opts)));
|
|
146
|
+
withGlobals(projects.command('delete <slug>')).description('delete a project and its keys (asks to confirm; --yes in CI)')
|
|
147
|
+
.action(run((slug) => projectsDeleteCommand(slug)));
|
|
148
|
+
withGlobals(projects.command('add-language <slug> <codes...>')).description('add languages to a project')
|
|
149
|
+
.action(run((slug, codes) => projectsAddLanguageCommand(slug, codes)));
|
|
150
|
+
withGlobals(projects.command('remove-language <slug> <codes...>')).description('remove languages and their translations')
|
|
151
|
+
.action(run((slug, codes) => projectsRemoveLanguageCommand(slug, codes)));
|
|
152
|
+
withGlobals(projects.command('stats <slug>')).description('key counts and translation coverage per language')
|
|
153
|
+
.action(run((slug) => projectsStatsCommand(slug)));
|
|
154
|
+
// ── Keys ────────────────────────────────────────────────────────
|
|
155
|
+
const keys = program.command('keys').alias('key').description('manage translation keys');
|
|
156
|
+
withGlobals(keys.command('list [project]')).alias('ls').description('list keys')
|
|
157
|
+
.option('-m, --module <name>', 'filter by module')
|
|
158
|
+
.option('-p, --published', 'only published keys')
|
|
159
|
+
.option('--drafts', 'only unpublished keys')
|
|
160
|
+
.option('-s, --search <text>', 'search key names and descriptions')
|
|
161
|
+
.option('--limit <n>', 'show at most n keys')
|
|
162
|
+
.addOption(new Option('--offset <n>').hideHelp())
|
|
163
|
+
.action(run((project, opts) => listKeysCommand(project, opts)));
|
|
164
|
+
withGlobals(keys.command('get <project> <key>')).description('show a key and all its translations')
|
|
165
|
+
.action(run((project, key) => getKeyCommand(project, key)));
|
|
166
|
+
withGlobals(keys.command('create <project> <key>')).description('create a key')
|
|
167
|
+
.option('--value <lang=text>', 'translation, repeatable: --value en="Hi" --value es="Hola"', collect)
|
|
168
|
+
.option('-d, --description <text>', 'context for translators')
|
|
169
|
+
.option('-m, --module <name>', 'module')
|
|
170
|
+
.option('--tags <tags>', 'comma-separated tags')
|
|
171
|
+
.option('--publish', 'publish immediately (default: draft)')
|
|
172
|
+
.addOption(new Option('--value-en <text>').hideHelp())
|
|
173
|
+
.addOption(new Option('--value-es <text>').hideHelp())
|
|
174
|
+
.addOption(new Option('--value-fr <text>').hideHelp())
|
|
175
|
+
.addOption(new Option('--value-de <text>').hideHelp())
|
|
176
|
+
.action(run((project, key, opts) => createKeyCommand(project, key, opts)));
|
|
177
|
+
withGlobals(keys.command('update <project> <key>')).description('update translations, description or module')
|
|
178
|
+
.option('--value <lang=text>', 'translation, repeatable', collect)
|
|
179
|
+
.option('-d, --description <text>', 'description')
|
|
180
|
+
.option('-m, --module <name>', 'module')
|
|
181
|
+
.action(run((project, key, opts) => updateKeyCommand(project, key, opts)));
|
|
182
|
+
withGlobals(keys.command('translate <project> <key>')).description('set one translation (same as update --value)')
|
|
183
|
+
.requiredOption('-l, --language <code>', 'language')
|
|
184
|
+
.requiredOption('-t, --text <text>', 'translation text')
|
|
185
|
+
.addOption(new Option('--value <text>').hideHelp())
|
|
186
|
+
.action(run((project, key, opts) => updateKeyCommand(project, key, { language: opts.language, text: opts.text ?? opts.value })));
|
|
187
|
+
withGlobals(keys.command('delete <project> <keys...>')).alias('rm').description('delete keys (asks to confirm; --yes in CI)')
|
|
188
|
+
.action(run((project, names) => deleteKeyCommand(project, names)));
|
|
189
|
+
withGlobals(keys.command('publish <project> [keys...]')).description('publish keys so pull/export include them')
|
|
190
|
+
.option('--all', 'publish every draft key')
|
|
191
|
+
.option('-m, --module <name>', 'publish every draft key in a module')
|
|
192
|
+
.addOption(new Option('--unpublish').hideHelp())
|
|
193
|
+
.action(run((project, names, opts) => publishKeysCommand(project, names, opts, true)));
|
|
194
|
+
withGlobals(keys.command('unpublish <project> [keys...]')).description('move keys back to draft')
|
|
195
|
+
.option('--all', 'unpublish every published key')
|
|
196
|
+
.option('-m, --module <name>', 'unpublish every key in a module')
|
|
197
|
+
.action(run((project, names, opts) => publishKeysCommand(project, names, opts, false)));
|
|
198
|
+
// ── AI translation ──────────────────────────────────────────────
|
|
199
|
+
withGlobals(program.command('translate [project]')).description('fill missing translations with AI (DeepL) from the default language')
|
|
200
|
+
.option('-t, --to <codes>', 'comma-separated target languages (default: every non-default language)')
|
|
201
|
+
.option('-k, --keys <names>', 'only these keys (comma-separated)')
|
|
202
|
+
.option('-m, --module <name>', 'only keys in a module')
|
|
203
|
+
.option('--overwrite', 'retranslate keys that already have a translation')
|
|
204
|
+
.option('--dry-run', 'show what would be translated without using any AI translations')
|
|
205
|
+
.action(run((project, opts) => translateCommand(project, opts)));
|
|
206
|
+
withGlobals(program.command('review [project]')).description('list AI translations awaiting review, or approve them')
|
|
207
|
+
.option('--approve', 'approve the listed translations (asks to confirm; --yes in CI)')
|
|
208
|
+
.option('-k, --keys <names>', 'only these keys (comma-separated)')
|
|
209
|
+
.option('-l, --languages <codes>', 'only these languages (comma-separated)')
|
|
210
|
+
.action(run((project, opts) => reviewCommand(project, opts)));
|
|
211
|
+
// ── Team & org ──────────────────────────────────────────────────
|
|
212
|
+
const team = program.command('team').description('manage team members (needs an API key with the org:admin scope)');
|
|
213
|
+
withGlobals(team.command('list')).alias('ls').description('list members').action(run(() => listTeamCommand()));
|
|
214
|
+
withGlobals(team.command('get <email>')).description('show a member').action(run((email) => getTeamMemberCommand(email)));
|
|
215
|
+
withGlobals(team.command('invite <email>')).description('invite someone')
|
|
216
|
+
.option('-r, --role <role>', 'viewer, member or admin', 'member')
|
|
217
|
+
.action(run((email, opts) => inviteTeamMemberCommand(email, opts)));
|
|
218
|
+
withGlobals(team.command('remove <email>')).description('remove a member (asks to confirm; --yes in CI)')
|
|
219
|
+
.action(run((email) => removeTeamMemberCommand(email)));
|
|
220
|
+
withGlobals(team.command('update-role <email> <role>')).description('change a member\'s role')
|
|
221
|
+
.action(run((email, role) => updateTeamRoleCommand(email, role)));
|
|
222
|
+
withGlobals(team.command('invitations')).description('list invitations')
|
|
223
|
+
.option('-p, --pending', 'only pending invitations')
|
|
224
|
+
.option('--status <status>', 'pending, accepted, expired or revoked')
|
|
225
|
+
.action(run(opts => listInvitationsCommand(opts)));
|
|
226
|
+
withGlobals(team.command('revoke-invitation <email>')).description('revoke a pending invitation')
|
|
227
|
+
.action(run((email) => revokeInvitationCommand(email)));
|
|
228
|
+
const org = program.command('org').description('organization info');
|
|
229
|
+
withGlobals(org.command('info')).description('organization details').action(run(() => orgInfoCommand()));
|
|
230
|
+
withGlobals(org.command('stats')).description('member, project and key counts').action(run(() => orgStatsCommand()));
|
|
231
|
+
withGlobals(org.command('plan')).description('plan limits and usage').action(run(() => orgPlanCommand()));
|
|
232
|
+
program.addHelpText('after', `
|
|
233
|
+
Getting started:
|
|
234
|
+
$ langctl init # authenticate and create langctl.json
|
|
235
|
+
$ langctl pull # download translations
|
|
236
|
+
$ langctl push # upload new source strings
|
|
237
|
+
|
|
238
|
+
In CI (no config file needed):
|
|
239
|
+
env LANGCTL_API_KEY=lc_… (store it as a secret)
|
|
240
|
+
$ langctl pull --check # fail if committed files are out of date
|
|
241
|
+
$ langctl push --dry-run --json # preview what would be uploaded
|
|
242
|
+
|
|
243
|
+
Exit codes:
|
|
244
|
+
0 ok · 1 error · 2 invalid usage · 3 auth/permission · 4 not found
|
|
245
|
+
5 network/API unavailable · 6 plan limit reached · 7 files out of date (--check)
|
|
246
|
+
|
|
247
|
+
Docs: https://langctl.com/docs`);
|
|
248
|
+
// ── Run ─────────────────────────────────────────────────────────
|
|
249
|
+
process.on('SIGINT', () => {
|
|
250
|
+
process.stderr.write('\n');
|
|
251
|
+
process.exit(130);
|
|
252
|
+
});
|
|
253
|
+
async function main() {
|
|
254
|
+
if (process.argv.length <= 2) {
|
|
255
|
+
program.outputHelp();
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
await program.parseAsync(process.argv);
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
process.exitCode = handleError(err);
|
|
316
263
|
}
|
|
317
|
-
});
|
|
318
|
-
team
|
|
319
|
-
.command('remove <email>')
|
|
320
|
-
.description('Remove team member')
|
|
321
|
-
.action(async (email) => {
|
|
322
|
-
try {
|
|
323
|
-
await removeTeamMemberCommand(email);
|
|
324
|
-
}
|
|
325
|
-
catch (error) {
|
|
326
|
-
console.error(chalk.red(`Error: ${error.message}\n`));
|
|
327
|
-
process.exit(1);
|
|
328
|
-
}
|
|
329
|
-
});
|
|
330
|
-
team
|
|
331
|
-
.command('update-role <email> <role>')
|
|
332
|
-
.description('Update member role (viewer, member, admin)')
|
|
333
|
-
.action(async (email, role) => {
|
|
334
|
-
try {
|
|
335
|
-
await updateTeamRoleCommand(email, role);
|
|
336
|
-
}
|
|
337
|
-
catch (error) {
|
|
338
|
-
console.error(chalk.red(`Error: ${error.message}\n`));
|
|
339
|
-
process.exit(1);
|
|
340
|
-
}
|
|
341
|
-
});
|
|
342
|
-
team
|
|
343
|
-
.command('invitations')
|
|
344
|
-
.description('List invitations')
|
|
345
|
-
.option('-p, --pending', 'Show only pending invitations')
|
|
346
|
-
.action(async (options) => {
|
|
347
|
-
try {
|
|
348
|
-
await listInvitationsCommand(options);
|
|
349
|
-
}
|
|
350
|
-
catch (error) {
|
|
351
|
-
console.error(chalk.red(`Error: ${error.message}\n`));
|
|
352
|
-
process.exit(1);
|
|
353
|
-
}
|
|
354
|
-
});
|
|
355
|
-
team
|
|
356
|
-
.command('revoke-invitation <email>')
|
|
357
|
-
.description('Revoke pending invitation')
|
|
358
|
-
.action(async (email) => {
|
|
359
|
-
try {
|
|
360
|
-
await revokeInvitationCommand(email);
|
|
361
|
-
}
|
|
362
|
-
catch (error) {
|
|
363
|
-
console.error(chalk.red(`Error: ${error.message}\n`));
|
|
364
|
-
process.exit(1);
|
|
365
|
-
}
|
|
366
|
-
});
|
|
367
|
-
// Organization command
|
|
368
|
-
const org = program
|
|
369
|
-
.command('org')
|
|
370
|
-
.description('Manage organization');
|
|
371
|
-
org
|
|
372
|
-
.command('info')
|
|
373
|
-
.description('Get organization information')
|
|
374
|
-
.action(async () => {
|
|
375
|
-
try {
|
|
376
|
-
await orgInfoCommand();
|
|
377
|
-
}
|
|
378
|
-
catch (error) {
|
|
379
|
-
console.error(chalk.red(`Error: ${error.message}\n`));
|
|
380
|
-
process.exit(1);
|
|
381
|
-
}
|
|
382
|
-
});
|
|
383
|
-
org
|
|
384
|
-
.command('stats')
|
|
385
|
-
.description('Get organization statistics')
|
|
386
|
-
.action(async () => {
|
|
387
|
-
try {
|
|
388
|
-
await orgStatsCommand();
|
|
389
|
-
}
|
|
390
|
-
catch (error) {
|
|
391
|
-
console.error(chalk.red(`Error: ${error.message}\n`));
|
|
392
|
-
process.exit(1);
|
|
393
|
-
}
|
|
394
|
-
});
|
|
395
|
-
org
|
|
396
|
-
.command('plan')
|
|
397
|
-
.description('Get subscription plan and limits')
|
|
398
|
-
.action(async () => {
|
|
399
|
-
try {
|
|
400
|
-
await orgPlanCommand();
|
|
401
|
-
}
|
|
402
|
-
catch (error) {
|
|
403
|
-
console.error(chalk.red(`Error: ${error.message}\n`));
|
|
404
|
-
process.exit(1);
|
|
405
|
-
}
|
|
406
|
-
});
|
|
407
|
-
// Pull command (deprecated, use export)
|
|
408
|
-
program
|
|
409
|
-
.command('pull <project>')
|
|
410
|
-
.description('Pull translations from a project (deprecated, use export)')
|
|
411
|
-
.option('-l, --language <code>', 'Pull specific language only (default: all)')
|
|
412
|
-
.option('-f, --format <type>', 'Export format: json, json-nested, ios, android, flutter (default: json)')
|
|
413
|
-
.option('-d, --dir <path>', 'Output directory (default: ./translations)')
|
|
414
|
-
.option('-o, --output <path>', 'Specific output file path (optional)')
|
|
415
|
-
.option('--no-published-only', 'Include unpublished translations')
|
|
416
|
-
.action(async (project, options) => {
|
|
417
|
-
try {
|
|
418
|
-
await pullCommand(project, options);
|
|
419
|
-
}
|
|
420
|
-
catch (error) {
|
|
421
|
-
console.error(chalk.red(`Error: ${error.message}\n`));
|
|
422
|
-
process.exit(1);
|
|
423
|
-
}
|
|
424
|
-
});
|
|
425
|
-
// Export command
|
|
426
|
-
program
|
|
427
|
-
.command('export <project>')
|
|
428
|
-
.description('Export translations from a project')
|
|
429
|
-
.option('-l, --language <code>', 'Export specific language (default: all)')
|
|
430
|
-
.option('-f, --format <type>', 'Export format: flat-json, nested-json, i18n-json, android-xml, ios-strings, flutter-arb (default: flat-json)')
|
|
431
|
-
.option('-o, --output <path>', 'Output file path')
|
|
432
|
-
.option('-m, --module <name>', 'Export only keys from specific module')
|
|
433
|
-
.option('--include-unpublished', 'Include unpublished translations')
|
|
434
|
-
.action(async (project, options) => {
|
|
435
|
-
try {
|
|
436
|
-
await exportCommand(project, options);
|
|
437
|
-
}
|
|
438
|
-
catch (error) {
|
|
439
|
-
console.error(chalk.red(`Error: ${error.message}\n`));
|
|
440
|
-
process.exit(1);
|
|
441
|
-
}
|
|
442
|
-
});
|
|
443
|
-
// Import command
|
|
444
|
-
program
|
|
445
|
-
.command('import <project> <file>')
|
|
446
|
-
.description('Import translations from a file')
|
|
447
|
-
.requiredOption('-l, --language <code>', 'Target language code')
|
|
448
|
-
.option('--overwrite', 'Overwrite existing translations')
|
|
449
|
-
.option('--publish', 'Auto-publish after import')
|
|
450
|
-
.action(async (project, file, options) => {
|
|
451
|
-
try {
|
|
452
|
-
await importCommand(project, file, options);
|
|
453
|
-
}
|
|
454
|
-
catch (error) {
|
|
455
|
-
console.error(chalk.red(`Error: ${error.message}\n`));
|
|
456
|
-
process.exit(1);
|
|
457
|
-
}
|
|
458
|
-
});
|
|
459
|
-
// Debug command
|
|
460
|
-
const debug = program
|
|
461
|
-
.command('debug')
|
|
462
|
-
.description('Debug helpers');
|
|
463
|
-
debug
|
|
464
|
-
.command('list-api-keys')
|
|
465
|
-
.description('Test API connection and show debug info')
|
|
466
|
-
.action(async () => {
|
|
467
|
-
try {
|
|
468
|
-
await debugListApiKeys();
|
|
469
|
-
}
|
|
470
|
-
catch (error) {
|
|
471
|
-
console.error(chalk.red(`Error: ${error.message}\n`));
|
|
472
|
-
process.exit(1);
|
|
473
|
-
}
|
|
474
|
-
});
|
|
475
|
-
debug
|
|
476
|
-
.command('list-organizations')
|
|
477
|
-
.description('Show current organization details')
|
|
478
|
-
.action(async () => {
|
|
479
|
-
try {
|
|
480
|
-
await debugListOrganizations();
|
|
481
|
-
}
|
|
482
|
-
catch (error) {
|
|
483
|
-
console.error(chalk.red(`Error: ${error.message}\n`));
|
|
484
|
-
process.exit(1);
|
|
485
|
-
}
|
|
486
|
-
});
|
|
487
|
-
// Help command
|
|
488
|
-
program.on('--help', () => {
|
|
489
|
-
console.log('');
|
|
490
|
-
console.log('Examples:');
|
|
491
|
-
console.log(' $ langctl init');
|
|
492
|
-
console.log(' $ langctl auth lc_abc123...');
|
|
493
|
-
console.log(' $ langctl projects list');
|
|
494
|
-
console.log('');
|
|
495
|
-
console.log(' # Pull all languages as JSON to ./translations/i18n/');
|
|
496
|
-
console.log(' $ langctl pull litcode');
|
|
497
|
-
console.log('');
|
|
498
|
-
console.log(' # Pull to custom directory (e.g., ./src/i18n/)');
|
|
499
|
-
console.log(' $ langctl pull litcode --dir ./src/i18n');
|
|
500
|
-
console.log('');
|
|
501
|
-
console.log(' # Pull specific language');
|
|
502
|
-
console.log(' $ langctl pull litcode --language en');
|
|
503
|
-
console.log('');
|
|
504
|
-
console.log(' # Pull all languages in iOS format to ./ios/localization/');
|
|
505
|
-
console.log(' $ langctl pull litcode --format ios --dir ./ios/localization');
|
|
506
|
-
console.log('');
|
|
507
|
-
console.log('For more information, visit: https://langctl.com/docs');
|
|
508
|
-
console.log('');
|
|
509
|
-
});
|
|
510
|
-
// Parse arguments
|
|
511
|
-
program.parse(process.argv);
|
|
512
|
-
// Show help if no command provided
|
|
513
|
-
if (!process.argv.slice(2).length) {
|
|
514
|
-
showLogo();
|
|
515
|
-
program.outputHelp();
|
|
516
264
|
}
|
|
517
|
-
|
|
265
|
+
function handleError(err) {
|
|
266
|
+
if (err instanceof CommanderError) {
|
|
267
|
+
// help/version requests exit cleanly; everything else commander rejects is a usage error
|
|
268
|
+
if (err.code === 'commander.helpDisplayed' || err.code === 'commander.version' || err.code === 'commander.help')
|
|
269
|
+
return 0;
|
|
270
|
+
return ExitCode.Usage;
|
|
271
|
+
}
|
|
272
|
+
if (err instanceof CliError) {
|
|
273
|
+
if (runtime.json)
|
|
274
|
+
printJson({ error: { message: err.message, exitCode: err.exitCode, hint: err.hint ?? null, status: err.status ?? null } });
|
|
275
|
+
log.error(err.message, err.hint);
|
|
276
|
+
return err.exitCode;
|
|
277
|
+
}
|
|
278
|
+
const e = err;
|
|
279
|
+
if (runtime.json)
|
|
280
|
+
printJson({ error: { message: e?.message ?? String(err), exitCode: ExitCode.Error } });
|
|
281
|
+
log.error(e?.message ?? String(err), 'This looks like a bug — please report it at https://github.com/litcode-pvt-ltd/langctl-cli/issues (run with --verbose for details).');
|
|
282
|
+
if (runtime.verbose && e?.stack)
|
|
283
|
+
process.stderr.write(e.stack + '\n');
|
|
284
|
+
return ExitCode.Error;
|
|
285
|
+
}
|
|
286
|
+
void main();
|