langctl 0.1.2 → 0.3.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 +42 -0
- package/LICENSE +1 -1
- package/README.md +159 -1151
- package/dist/commands/auth.js +82 -24
- package/dist/commands/config.js +36 -25
- package/dist/commands/export.js +11 -112
- package/dist/commands/import.js +32 -164
- package/dist/commands/init.js +59 -80
- package/dist/commands/keys.js +145 -381
- package/dist/commands/org.js +45 -140
- package/dist/commands/projects.js +125 -390
- package/dist/commands/pull.js +78 -183
- package/dist/commands/push.js +125 -0
- package/dist/commands/team.js +68 -272
- 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 +121 -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 +261 -508
- package/dist/version.js +4 -0
- package/package.json +22 -16
- package/.gitattributes +0 -2
- package/NEXT_PHASE.md +0 -618
- package/WORKING_INSTRUCTIONS.md +0 -1389
- package/dist/auth.d.ts +0 -37
- package/dist/auth.js +0 -112
- 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 -57
- package/dist/commands/export.d.ts +0 -14
- package/dist/commands/import.d.ts +0 -11
- 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 -30
- package/dist/commands/pull.d.ts +0 -9
- package/dist/commands/team.d.ts +0 -29
- package/dist/config.d.ts +0 -47
- 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/supabase.d.ts +0 -14
- package/dist/supabase.js +0 -30
- 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/commands/pull.js
CHANGED
|
@@ -1,194 +1,89 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return data ? identifier : null;
|
|
38
|
-
}
|
|
39
|
-
// Try to match by slug (case-insensitive)
|
|
40
|
-
const { data: bySlug } = await supabase
|
|
41
|
-
.from('projects')
|
|
42
|
-
.select('id')
|
|
43
|
-
.eq('organization_id', organizationId)
|
|
44
|
-
.ilike('slug', identifier)
|
|
45
|
-
.is('deleted_at', null)
|
|
46
|
-
.single();
|
|
47
|
-
if (bySlug) {
|
|
48
|
-
return bySlug.id;
|
|
2
|
+
import { relative, resolve } from 'path';
|
|
3
|
+
import { loadProjectConfig } from '../core/config.js';
|
|
4
|
+
import { CliError, ExitCode, usageError } from '../core/errors.js';
|
|
5
|
+
import { syncFile } from '../core/files.js';
|
|
6
|
+
import { getSession } from '../core/http.js';
|
|
7
|
+
import { log, printJson, runtime, spinner } from '../core/output.js';
|
|
8
|
+
import { assertLanguages, getProject, projectSlugFrom } from '../core/project.js';
|
|
9
|
+
import { expandTemplate, getFormat, templateHasLanguage } from '../formats/index.js';
|
|
10
|
+
export async function pullCommand(projectArg, opts) {
|
|
11
|
+
const loaded = loadProjectConfig();
|
|
12
|
+
const cfg = loaded?.config ?? {};
|
|
13
|
+
const slug = projectSlugFrom(projectArg);
|
|
14
|
+
const format = getFormat(opts.format ?? cfg.format);
|
|
15
|
+
// Paths from flags are relative to the cwd; paths from langctl.json are relative to that file
|
|
16
|
+
let template;
|
|
17
|
+
if (opts.output)
|
|
18
|
+
template = resolve(opts.output);
|
|
19
|
+
else if (opts.dir)
|
|
20
|
+
template = resolve(opts.dir, format.defaultOutput);
|
|
21
|
+
else if (cfg.output && loaded)
|
|
22
|
+
template = resolve(loaded.root, cfg.output);
|
|
23
|
+
else
|
|
24
|
+
template = resolve(format.defaultOutput);
|
|
25
|
+
const includeDrafts = Boolean(opts.includeDrafts || opts.publishedOnly === false || cfg.includeDrafts);
|
|
26
|
+
const module = opts.module ?? cfg.module;
|
|
27
|
+
const dryRun = Boolean(opts.check || opts.dryRun);
|
|
28
|
+
const session = await getSession();
|
|
29
|
+
const spin = spinner(`Fetching ${slug}…`);
|
|
30
|
+
let project, snapshot;
|
|
31
|
+
try {
|
|
32
|
+
project = await getProject(session, slug);
|
|
33
|
+
snapshot = await session.api.get(`/orgs/${session.orgId}/projects/${project.id}/export`, {
|
|
34
|
+
publishedOnly: includeDrafts ? 'false' : 'true',
|
|
35
|
+
module,
|
|
36
|
+
});
|
|
49
37
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
.from('projects')
|
|
53
|
-
.select('id')
|
|
54
|
-
.eq('organization_id', organizationId)
|
|
55
|
-
.ilike('name', identifier)
|
|
56
|
-
.is('deleted_at', null)
|
|
57
|
-
.single();
|
|
58
|
-
if (byName) {
|
|
59
|
-
return byName.id;
|
|
38
|
+
finally {
|
|
39
|
+
spin.stop();
|
|
60
40
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (!isAuthenticated()) {
|
|
66
|
-
console.log(chalk.red('✗ Not authenticated. Please run "langctl auth <api-key>" first.\n'));
|
|
67
|
-
return;
|
|
41
|
+
const languages = opts.languages ? splitList(opts.languages) : (cfg.languages ?? project.languages);
|
|
42
|
+
assertLanguages(project, languages);
|
|
43
|
+
if (languages.length > 1 && !templateHasLanguage(template)) {
|
|
44
|
+
throw usageError(`Output "${relative(process.cwd(), template)}" has no {lang} placeholder, so ${languages.length} languages would overwrite each other.`, 'Use a template like "locales/{lang}.json", or pick one language with --languages.');
|
|
68
45
|
}
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
46
|
+
const files = [];
|
|
47
|
+
for (const lang of languages) {
|
|
48
|
+
const entries = snapshot.keys
|
|
49
|
+
.filter(k => typeof k.translations[lang] === 'string' && k.translations[lang] !== '')
|
|
50
|
+
.map(k => ({ key: k.key, value: k.translations[lang], description: k.description }));
|
|
51
|
+
const path = expandTemplate(template, lang, project.defaultLanguage);
|
|
52
|
+
const status = syncFile(path, format.serialize(entries, lang), dryRun, text => format.parse(text, lang));
|
|
53
|
+
files.push({ path, language: lang, status, keys: entries.length, translated: entries.length, total: snapshot.keys.length });
|
|
73
54
|
}
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
55
|
+
const changed = files.filter(f => f.status !== 'unchanged');
|
|
56
|
+
const incomplete = files.filter(f => f.translated < f.total);
|
|
57
|
+
if (runtime.json) {
|
|
58
|
+
printJson({
|
|
59
|
+
project: project.slug,
|
|
60
|
+
format: format.id,
|
|
61
|
+
publishedOnly: !includeDrafts,
|
|
62
|
+
keys: snapshot.keys.length,
|
|
63
|
+
dryRun,
|
|
64
|
+
files: files.map(f => ({ ...f, path: relative(process.cwd(), f.path) })),
|
|
65
|
+
changed: changed.length,
|
|
66
|
+
});
|
|
85
67
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
spinner.fail(chalk.red(`Project "${projectIdentifier}" not found`));
|
|
93
|
-
console.log(chalk.yellow('\nRun "langctl projects list" to see available projects.\n'));
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
// Fetch project details
|
|
97
|
-
spinner.text = 'Fetching project...';
|
|
98
|
-
const { data: project, error: projectError } = await supabase
|
|
99
|
-
.from('projects')
|
|
100
|
-
.select('id, name, slug, languages, default_language')
|
|
101
|
-
.eq('id', projectId)
|
|
102
|
-
.eq('organization_id', organizationId)
|
|
103
|
-
.is('deleted_at', null)
|
|
104
|
-
.single();
|
|
105
|
-
if (projectError || !project) {
|
|
106
|
-
spinner.fail(chalk.red('Project not found or access denied'));
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
// Determine which languages to pull
|
|
110
|
-
let languagesToPull;
|
|
111
|
-
if (specificLanguage) {
|
|
112
|
-
// Check if specified language exists in project
|
|
113
|
-
if (!project.languages.includes(specificLanguage)) {
|
|
114
|
-
spinner.fail(chalk.red(`Language "${specificLanguage}" not found in project`));
|
|
115
|
-
console.log(chalk.yellow(`Available languages: ${project.languages.join(', ')}\n`));
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
languagesToPull = [specificLanguage];
|
|
68
|
+
else {
|
|
69
|
+
for (const f of files) {
|
|
70
|
+
const icon = f.status === 'unchanged' ? chalk.dim('=') : f.status === 'created' ? chalk.green('+') : chalk.yellow('~');
|
|
71
|
+
const verb = dryRun && f.status !== 'unchanged' ? `would be ${f.status}` : f.status;
|
|
72
|
+
const coverage = f.translated < f.total ? chalk.yellow(` ${f.translated}/${f.total} translated`) : '';
|
|
73
|
+
log.out(`${icon} ${relative(process.cwd(), f.path)} ${chalk.dim(verb)}${coverage}`);
|
|
119
74
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
75
|
+
log.info(chalk.dim(`${project.slug}: ${snapshot.keys.length} ${includeDrafts ? '' : 'published '}keys, ${languages.length} language(s), format ${format.id}`));
|
|
76
|
+
if (snapshot.keys.length === 0 && !includeDrafts) {
|
|
77
|
+
log.warn('No published keys — drafts are excluded by default. Publish keys, or pass --include-drafts.');
|
|
123
78
|
}
|
|
124
|
-
spinner.text = `Fetching translations for "${project.name}"...`;
|
|
125
|
-
// Fetch translation keys
|
|
126
|
-
let query = supabase
|
|
127
|
-
.from('translation_keys')
|
|
128
|
-
.select('key, translations, description, module')
|
|
129
|
-
.eq('project_id', projectId)
|
|
130
|
-
.is('deleted_at', null);
|
|
131
|
-
if (publishedOnly) {
|
|
132
|
-
query = query.eq('published', true);
|
|
133
|
-
}
|
|
134
|
-
const { data: keys, error: keysError } = await query;
|
|
135
|
-
if (keysError) {
|
|
136
|
-
spinner.fail(chalk.red('Failed to fetch translations'));
|
|
137
|
-
console.error(chalk.red(`Error: ${keysError.message}\n`));
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
if (!keys || keys.length === 0) {
|
|
141
|
-
spinner.fail(chalk.yellow('No translations found'));
|
|
142
|
-
console.log(chalk.yellow(`\nNo ${publishedOnly ? 'published ' : ''}translations found for this project.\n`));
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
spinner.succeed(chalk.green(`Found ${keys.length} translation keys`));
|
|
146
|
-
// Get platform directory
|
|
147
|
-
const platformDir = getPlatformDirectory(format);
|
|
148
|
-
// Export translations for each language
|
|
149
|
-
const exportedFiles = [];
|
|
150
|
-
for (const language of languagesToPull) {
|
|
151
|
-
spinner.start(`Exporting ${language}...`);
|
|
152
|
-
const result = exportTranslations(keys, language, format);
|
|
153
|
-
// Determine output path
|
|
154
|
-
let outputPath;
|
|
155
|
-
if (options.output) {
|
|
156
|
-
// If custom output is specified and multiple languages, append language code
|
|
157
|
-
if (languagesToPull.length > 1) {
|
|
158
|
-
const ext = result.filename.split('.').pop();
|
|
159
|
-
const base = options.output.replace(/\.[^.]+$/, '');
|
|
160
|
-
outputPath = `${base}-${language}.${ext}`;
|
|
161
|
-
}
|
|
162
|
-
else {
|
|
163
|
-
outputPath = options.output;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
else {
|
|
167
|
-
// Default output path: <baseDir>/<platform>/<language>.<ext>
|
|
168
|
-
const ext = result.filename.split('.').pop();
|
|
169
|
-
outputPath = join(baseDir, platformDir, `${language}.${ext}`);
|
|
170
|
-
}
|
|
171
|
-
// Create directory if it doesn't exist
|
|
172
|
-
const dir = dirname(outputPath);
|
|
173
|
-
mkdirSync(dir, { recursive: true });
|
|
174
|
-
writeFileSync(outputPath, result.content, 'utf-8');
|
|
175
|
-
exportedFiles.push(outputPath);
|
|
176
|
-
spinner.succeed(chalk.green(`Exported ${language} → ${outputPath}`));
|
|
177
|
-
}
|
|
178
|
-
// Summary
|
|
179
|
-
console.log(chalk.hex('#10b981').bold('\n📦 Export Summary:'));
|
|
180
|
-
console.log(chalk.white(` Project: ${project.name}`));
|
|
181
|
-
console.log(chalk.white(` Format: ${format}`));
|
|
182
|
-
console.log(chalk.white(` Platform: ${platformDir}`));
|
|
183
|
-
console.log(chalk.white(` Output: ${baseDir}`));
|
|
184
|
-
console.log(chalk.white(` Keys: ${keys.length}`));
|
|
185
|
-
console.log(chalk.white(` Languages: ${languagesToPull.join(', ')}`));
|
|
186
|
-
console.log(chalk.white(` Files: ${exportedFiles.length}`));
|
|
187
|
-
console.log('');
|
|
188
79
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
80
|
+
if (opts.requireComplete && incomplete.length) {
|
|
81
|
+
throw new CliError(`Missing translations: ${incomplete.map(f => `${f.language} (${f.total - f.translated} missing)`).join(', ')}`, ExitCode.Error);
|
|
82
|
+
}
|
|
83
|
+
if (opts.check && changed.length) {
|
|
84
|
+
throw new CliError(`${changed.length} translation file(s) are out of date.`, ExitCode.Drift, 'Run "langctl pull" and commit the result.');
|
|
192
85
|
}
|
|
193
86
|
}
|
|
194
|
-
|
|
87
|
+
export function splitList(value) {
|
|
88
|
+
return value.split(',').map(s => s.trim()).filter(Boolean);
|
|
89
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { existsSync, readFileSync } from 'fs';
|
|
3
|
+
import { relative, resolve } from 'path';
|
|
4
|
+
import { loadProjectConfig } from '../core/config.js';
|
|
5
|
+
import { notFoundError, usageError } from '../core/errors.js';
|
|
6
|
+
import { getSession } from '../core/http.js';
|
|
7
|
+
import { log, printJson, runtime, spinner } from '../core/output.js';
|
|
8
|
+
import { assertLanguages, getProject, projectSlugFrom } from '../core/project.js';
|
|
9
|
+
import { expandTemplate, formatFromPath, getFormat, templateHasLanguage } from '../formats/index.js';
|
|
10
|
+
import { splitList } from './pull.js';
|
|
11
|
+
const CHUNK = 1000; // stay well under the API's 1 MB request body limit
|
|
12
|
+
/** Upload one language's translations. Shared by `push` and the single-file `import`. */
|
|
13
|
+
export async function uploadTranslations(session, project, language, translations, opts) {
|
|
14
|
+
const entries = Object.entries(translations);
|
|
15
|
+
const result = { language, keys: entries.length, created: 0, updated: 0, unchanged: 0, published: undefined };
|
|
16
|
+
if (entries.length === 0)
|
|
17
|
+
return result;
|
|
18
|
+
if (opts.dryRun) {
|
|
19
|
+
// Compare against the server, including drafts, without writing anything
|
|
20
|
+
const snap = await session.api.get(`/orgs/${session.orgId}/projects/${project.id}/export`, { language, publishedOnly: 'false' });
|
|
21
|
+
const all = await session.api.get(`/orgs/${session.orgId}/projects/${project.id}/export`, { publishedOnly: 'false' });
|
|
22
|
+
const existingKeys = new Set(all.keys.map(k => k.key));
|
|
23
|
+
for (const [key, value] of entries) {
|
|
24
|
+
if (!existingKeys.has(key))
|
|
25
|
+
result.created++;
|
|
26
|
+
else if (snap.translations[key] === value)
|
|
27
|
+
result.unchanged++;
|
|
28
|
+
else if (opts.overwrite || snap.translations[key] === undefined)
|
|
29
|
+
result.updated++;
|
|
30
|
+
else
|
|
31
|
+
result.unchanged++;
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
for (let i = 0; i < entries.length; i += CHUNK) {
|
|
36
|
+
const chunk = Object.fromEntries(entries.slice(i, i + CHUNK));
|
|
37
|
+
// Import is an upsert, so retrying after a dropped connection is safe
|
|
38
|
+
const res = await session.api.post(`/orgs/${session.orgId}/projects/${project.id}/import`, { language, translations: chunk, overwriteExisting: Boolean(opts.overwrite), module: opts.module }, true);
|
|
39
|
+
result.created += res.created;
|
|
40
|
+
result.updated += res.updated;
|
|
41
|
+
result.unchanged += res.skipped;
|
|
42
|
+
}
|
|
43
|
+
if (opts.publish) {
|
|
44
|
+
let published = 0;
|
|
45
|
+
const names = entries.map(([k]) => k);
|
|
46
|
+
for (let i = 0; i < names.length; i += 5000) {
|
|
47
|
+
const res = await session.api.post(`/orgs/${session.orgId}/projects/${project.id}/keys/bulk-publish`, { keys: names.slice(i, i + 5000), published: true }, true);
|
|
48
|
+
published += res.count;
|
|
49
|
+
}
|
|
50
|
+
result.published = published;
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
export function readTranslationFile(path, format, lang) {
|
|
55
|
+
if (!existsSync(path))
|
|
56
|
+
throw notFoundError(`File not found: ${relative(process.cwd(), path) || path}`);
|
|
57
|
+
const fmt = format ?? formatFromPath(path);
|
|
58
|
+
const translations = fmt.parse(readFileSync(path, 'utf-8'), lang);
|
|
59
|
+
if ((fmt.id === 'android' || fmt.id === 'ios') && Object.values(translations).some(v => /\{\{\d+\}\}/.test(v))) {
|
|
60
|
+
log.warn(`${relative(process.cwd(), path)}: positional placeholders (%1$s / %1$@) were imported as {{1}}, {{2}}… — named placeholders can't be recovered from ${fmt.id} files.`);
|
|
61
|
+
}
|
|
62
|
+
return translations;
|
|
63
|
+
}
|
|
64
|
+
export async function pushCommand(projectArg, opts) {
|
|
65
|
+
const loaded = loadProjectConfig();
|
|
66
|
+
const cfg = loaded?.config ?? {};
|
|
67
|
+
const slug = projectSlugFrom(projectArg);
|
|
68
|
+
const format = opts.format || cfg.format ? getFormat(opts.format ?? cfg.format) : undefined;
|
|
69
|
+
let template;
|
|
70
|
+
if (opts.input)
|
|
71
|
+
template = resolve(opts.input);
|
|
72
|
+
else if (cfg.output && loaded)
|
|
73
|
+
template = resolve(loaded.root, cfg.output);
|
|
74
|
+
else
|
|
75
|
+
template = resolve((format ?? getFormat('json')).defaultOutput);
|
|
76
|
+
const session = await getSession();
|
|
77
|
+
const spin = spinner(`Fetching ${slug}…`);
|
|
78
|
+
let project;
|
|
79
|
+
try {
|
|
80
|
+
project = await getProject(session, slug);
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
spin.stop();
|
|
84
|
+
}
|
|
85
|
+
// Default: push only the source language, so translators' work in other languages isn't overwritten
|
|
86
|
+
const explicit = Boolean(opts.languages);
|
|
87
|
+
const languages = opts.languages === 'all' ? project.languages
|
|
88
|
+
: opts.languages ? splitList(opts.languages)
|
|
89
|
+
: [cfg.sourceLanguage ?? project.defaultLanguage];
|
|
90
|
+
assertLanguages(project, languages);
|
|
91
|
+
if (languages.length > 1 && !templateHasLanguage(template)) {
|
|
92
|
+
throw usageError(`Input "${relative(process.cwd(), template)}" has no {lang} placeholder but ${languages.length} languages were requested.`);
|
|
93
|
+
}
|
|
94
|
+
const results = [];
|
|
95
|
+
for (const lang of languages) {
|
|
96
|
+
const path = expandTemplate(template, lang, project.defaultLanguage);
|
|
97
|
+
if (!existsSync(path)) {
|
|
98
|
+
if (explicit)
|
|
99
|
+
throw notFoundError(`File not found for ${lang}: ${relative(process.cwd(), path)}`);
|
|
100
|
+
throw notFoundError(`Source file not found: ${relative(process.cwd(), path)}`, 'Run "langctl pull" first, or set "output" in langctl.json / pass --input.');
|
|
101
|
+
}
|
|
102
|
+
const translations = readTranslationFile(path, format, lang);
|
|
103
|
+
const s = spinner(`${opts.dryRun ? 'Checking' : 'Uploading'} ${lang} (${Object.keys(translations).length} keys)…`);
|
|
104
|
+
try {
|
|
105
|
+
const r = await uploadTranslations(session, project, lang, translations, { ...opts, module: opts.module ?? cfg.module });
|
|
106
|
+
results.push({ ...r, file: relative(process.cwd(), path) });
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
s.stop();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (runtime.json) {
|
|
113
|
+
printJson({ project: project.slug, dryRun: Boolean(opts.dryRun), overwrite: Boolean(opts.overwrite), results });
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
for (const r of results) {
|
|
117
|
+
const parts = [`${chalk.green(`${r.created} new`)}`, `${chalk.yellow(`${r.updated} updated`)}`, chalk.dim(`${r.unchanged} unchanged`)];
|
|
118
|
+
if (r.published !== undefined)
|
|
119
|
+
parts.push(chalk.cyan(`${r.published} published`));
|
|
120
|
+
log.out(`${r.language} ${r.file} ${parts.join(', ')}${opts.dryRun ? chalk.dim(' (dry run)') : ''}`);
|
|
121
|
+
}
|
|
122
|
+
if (!opts.overwrite && results.some(r => r.unchanged > 0)) {
|
|
123
|
+
log.info(chalk.dim('Existing translations are never overwritten unless you pass --overwrite.'));
|
|
124
|
+
}
|
|
125
|
+
}
|