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/auth.js
CHANGED
|
@@ -1,33 +1,91 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
import { clearCredentials, configPath, maskApiKey, normalizeApiKey, readUserConfig, resolveApiUrl, resolveCredentials, writeUserConfig, } from '../core/config.js';
|
|
3
|
+
import { CliError, ExitCode, usageError } from '../core/errors.js';
|
|
4
|
+
import { ApiClient, validateKey } from '../core/http.js';
|
|
5
|
+
import { isInteractive, log, printJson, runtime, spinner } from '../core/output.js';
|
|
6
|
+
import { password, readStdin } from '../core/prompts.js';
|
|
7
|
+
/** Validate a key against the API and store it in the user config. Returns the organization. */
|
|
8
|
+
export async function saveApiKey(rawKey) {
|
|
9
|
+
const apiKey = normalizeApiKey(rawKey);
|
|
10
|
+
const apiUrl = resolveApiUrl();
|
|
11
|
+
const api = new ApiClient({ apiKey, apiUrl, source: 'flag' });
|
|
12
|
+
const spin = spinner('Verifying API key…');
|
|
8
13
|
try {
|
|
9
|
-
|
|
14
|
+
const info = await validateKey(api, apiKey);
|
|
15
|
+
if (!info) {
|
|
16
|
+
throw new CliError('This API key is invalid or has been revoked.', ExitCode.Auth, 'Create a new key at https://app.langctl.com/organization/api-keys.');
|
|
17
|
+
}
|
|
18
|
+
const org = await api.get(`/orgs/${info.organizationId}`);
|
|
19
|
+
writeUserConfig({ ...readUserConfig(), apiKey, organizationId: org.id, organizationName: org.name });
|
|
20
|
+
return { ...org, scopes: info.scopes };
|
|
10
21
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
22
|
+
finally {
|
|
23
|
+
spin.stop();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export async function authCommand(apiKeyArg, opts) {
|
|
27
|
+
let key = apiKeyArg;
|
|
28
|
+
if (opts.stdin) {
|
|
29
|
+
key = (await readStdin()).trim();
|
|
30
|
+
if (!key)
|
|
31
|
+
throw usageError('No API key received on stdin.', 'Example: echo "$LANGCTL_API_KEY" | langctl auth --stdin');
|
|
14
32
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
33
|
+
else if (key) {
|
|
34
|
+
log.warn('Passing the key as an argument stores it in your shell history. Prefer "langctl auth --stdin" or the interactive prompt.');
|
|
35
|
+
}
|
|
36
|
+
else if (isInteractive()) {
|
|
37
|
+
key = await password('Paste your API key');
|
|
20
38
|
}
|
|
21
39
|
else {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
40
|
+
throw usageError('No API key given.', 'Use "langctl auth --stdin" (e.g. echo "$KEY" | langctl auth --stdin), or set LANGCTL_API_KEY instead of storing a key in CI.');
|
|
41
|
+
}
|
|
42
|
+
const org = await saveApiKey(key);
|
|
43
|
+
if (runtime.json) {
|
|
44
|
+
printJson({ authenticated: true, organization: { id: org.id, name: org.name, plan: org.plan }, scopes: org.scopes, configPath: configPath() });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
log.success(`Authenticated to ${chalk.bold(org.name)} (${org.plan} plan)`);
|
|
48
|
+
log.info(chalk.dim(`Key saved to ${configPath()} (readable only by you). Scopes: ${org.scopes.join(', ')}`));
|
|
49
|
+
}
|
|
50
|
+
export function logoutCommand() {
|
|
51
|
+
const removed = clearCredentials();
|
|
52
|
+
if (runtime.json)
|
|
53
|
+
return printJson({ loggedOut: removed });
|
|
54
|
+
log.success(removed ? 'Logged out — stored API key removed.' : 'No stored API key — nothing to do.');
|
|
55
|
+
if (process.env.LANGCTL_API_KEY)
|
|
56
|
+
log.warn('LANGCTL_API_KEY is still set in your environment and will keep being used.');
|
|
57
|
+
}
|
|
58
|
+
export async function whoamiCommand() {
|
|
59
|
+
const creds = resolveCredentials();
|
|
60
|
+
if (!creds) {
|
|
61
|
+
if (runtime.json)
|
|
62
|
+
printJson({ authenticated: false, apiUrl: resolveApiUrl() });
|
|
63
|
+
throw new CliError('Not authenticated.', ExitCode.Auth, 'Run "langctl auth --stdin", or set LANGCTL_API_KEY.');
|
|
64
|
+
}
|
|
65
|
+
const api = new ApiClient(creds);
|
|
66
|
+
const started = Date.now();
|
|
67
|
+
const info = await validateKey(api, creds.apiKey);
|
|
68
|
+
const latency = Date.now() - started;
|
|
69
|
+
if (!info) {
|
|
70
|
+
throw new CliError(`The API key from ${sourceLabel(creds.source)} is invalid or revoked.`, ExitCode.Auth, 'Create a new key at https://app.langctl.com/organization/api-keys.');
|
|
27
71
|
}
|
|
72
|
+
const org = await api.get(`/orgs/${info.organizationId}`);
|
|
73
|
+
const data = {
|
|
74
|
+
authenticated: true,
|
|
75
|
+
organization: { id: org.id, name: org.name, slug: org.slug, plan: org.plan },
|
|
76
|
+
key: maskApiKey(creds.apiKey),
|
|
77
|
+
keySource: sourceLabel(creds.source),
|
|
78
|
+
scopes: info.scopes,
|
|
79
|
+
apiUrl: creds.apiUrl,
|
|
80
|
+
latencyMs: latency,
|
|
81
|
+
};
|
|
82
|
+
if (runtime.json)
|
|
83
|
+
return printJson(data);
|
|
84
|
+
log.out(`${chalk.bold(org.name)} ${chalk.dim(`(${org.slug}, ${org.plan} plan)`)}`);
|
|
85
|
+
log.out(` key ${data.key} ${chalk.dim(`from ${data.keySource}`)}`);
|
|
86
|
+
log.out(` scopes ${info.scopes.join(', ')}`);
|
|
87
|
+
log.out(` api ${creds.apiUrl} ${chalk.dim(`${latency}ms`)}`);
|
|
28
88
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
console.log(chalk.green('✓ Logged out successfully\n'));
|
|
89
|
+
function sourceLabel(source) {
|
|
90
|
+
return source === 'env' ? 'LANGCTL_API_KEY' : source === 'flag' ? '--api-key' : configPath();
|
|
32
91
|
}
|
|
33
|
-
//# sourceMappingURL=auth.js.map
|
package/dist/commands/config.js
CHANGED
|
@@ -1,30 +1,41 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import {
|
|
2
|
+
import { configPath, loadProjectConfig, maskApiKey, readUserConfig, resolveApiUrl, resolveCredentials } from '../core/config.js';
|
|
3
|
+
import { log, printJson, runtime } from '../core/output.js';
|
|
4
|
+
import { FORMATS } from '../formats/index.js';
|
|
5
|
+
/** Show effective configuration and where each value comes from (no network). */
|
|
3
6
|
export function configCommand() {
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
organizationName: allConfig.organizationName,
|
|
15
|
-
defaultProject: allConfig.defaultProject,
|
|
16
|
-
defaultLanguage: allConfig.defaultLanguage
|
|
7
|
+
const user = readUserConfig();
|
|
8
|
+
const creds = resolveCredentials();
|
|
9
|
+
const project = loadProjectConfig();
|
|
10
|
+
const data = {
|
|
11
|
+
apiUrl: resolveApiUrl(),
|
|
12
|
+
apiKey: creds ? maskApiKey(creds.apiKey) : null,
|
|
13
|
+
apiKeySource: creds ? (creds.source === 'env' ? 'LANGCTL_API_KEY' : creds.source === 'flag' ? '--api-key' : configPath()) : null,
|
|
14
|
+
organization: user.organizationName ?? null,
|
|
15
|
+
userConfig: configPath(),
|
|
16
|
+
projectConfig: project ? { path: project.path, ...project.config } : null,
|
|
17
17
|
};
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
if (runtime.json)
|
|
19
|
+
return printJson(data);
|
|
20
|
+
log.out(`api url ${data.apiUrl}`);
|
|
21
|
+
log.out(`api key ${data.apiKey ?? chalk.yellow('not set')}${data.apiKeySource ? chalk.dim(` (${data.apiKeySource})`) : ''}`);
|
|
22
|
+
if (data.organization)
|
|
23
|
+
log.out(`organization ${data.organization}`);
|
|
24
|
+
log.out(`user config ${data.userConfig}`);
|
|
25
|
+
if (project) {
|
|
26
|
+
log.out(`project file ${project.path}`);
|
|
27
|
+
for (const [k, v] of Object.entries(project.config))
|
|
28
|
+
log.out(chalk.dim(` ${k}: ${JSON.stringify(v)}`));
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
log.out(`project file ${chalk.dim('none — run "langctl init" to create langctl.json')}`);
|
|
32
|
+
}
|
|
24
33
|
}
|
|
25
|
-
function
|
|
26
|
-
if (
|
|
27
|
-
return
|
|
28
|
-
|
|
34
|
+
export function formatsCommand() {
|
|
35
|
+
if (runtime.json)
|
|
36
|
+
return printJson(FORMATS.map(f => ({ id: f.id, aliases: f.aliases, description: f.label, defaultOutput: f.defaultOutput })));
|
|
37
|
+
for (const f of FORMATS) {
|
|
38
|
+
log.out(`${chalk.bold(f.id.padEnd(12))} ${f.label}`);
|
|
39
|
+
log.out(chalk.dim(`${''.padEnd(12)} default path ${f.defaultOutput}${f.aliases.length ? ` · aliases ${f.aliases.join(', ')}` : ''}`));
|
|
40
|
+
}
|
|
29
41
|
}
|
|
30
|
-
//# sourceMappingURL=config.js.map
|
package/dist/commands/export.js
CHANGED
|
@@ -1,115 +1,14 @@
|
|
|
1
|
-
import
|
|
2
|
-
import ora from 'ora';
|
|
3
|
-
import { writeFileSync, mkdirSync } from 'fs';
|
|
4
|
-
import { join, dirname } from 'path';
|
|
5
|
-
import { isAuthenticated, getApiKey } from '../auth.js';
|
|
6
|
-
const EDGE_FUNCTION_URL = 'https://bcgnmvkgkbhbxzzflwdb.supabase.co/functions/v1/export-translations';
|
|
7
|
-
const LIST_PROJECTS_URL = 'https://bcgnmvkgkbhbxzzflwdb.supabase.co/functions/v1/list-projects';
|
|
1
|
+
import { pullCommand } from './pull.js';
|
|
8
2
|
/**
|
|
9
|
-
*
|
|
3
|
+
* `langctl export` — 0.2.x-compatible entry point. Same engine as `pull`, but
|
|
4
|
+
* aimed at one-off exports (`-l es -o es.json`); `pull` is the CI/repo workflow.
|
|
10
5
|
*/
|
|
11
|
-
async function
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
});
|
|
20
|
-
if (!response.ok) {
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
|
-
const data = await response.json();
|
|
24
|
-
if (!data.success || !data.projects) {
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
return data.projects.find((p) => p.slug === slug) || null;
|
|
28
|
-
}
|
|
29
|
-
catch (error) {
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
6
|
+
export async function exportCommand(projectArg, opts) {
|
|
7
|
+
await pullCommand(projectArg, {
|
|
8
|
+
languages: opts.languages ?? opts.language,
|
|
9
|
+
format: opts.format,
|
|
10
|
+
output: opts.output,
|
|
11
|
+
module: opts.module,
|
|
12
|
+
includeDrafts: opts.includeDrafts || opts.includeUnpublished,
|
|
13
|
+
});
|
|
32
14
|
}
|
|
33
|
-
/**
|
|
34
|
-
* Export translations command
|
|
35
|
-
*/
|
|
36
|
-
export async function exportCommand(projectSlug, options) {
|
|
37
|
-
// Check authentication
|
|
38
|
-
if (!isAuthenticated()) {
|
|
39
|
-
console.log(chalk.red('✗ Not authenticated. Please run "langctl auth <api-key>" first.\n'));
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
const apiKey = getApiKey();
|
|
43
|
-
if (!apiKey) {
|
|
44
|
-
console.log(chalk.red('✗ API key not found. Please run "langctl auth <api-key>" again.\n'));
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
const spinner = ora('Fetching project...').start();
|
|
48
|
-
try {
|
|
49
|
-
// Get project by slug
|
|
50
|
-
const project = await getProjectBySlug(apiKey, projectSlug);
|
|
51
|
-
if (!project) {
|
|
52
|
-
spinner.fail(chalk.red(`Project "${projectSlug}" not found`));
|
|
53
|
-
console.log(chalk.yellow('\nRun "langctl projects list" to see available projects.\n'));
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
spinner.text = 'Exporting translations...';
|
|
57
|
-
// Determine languages to export
|
|
58
|
-
const languages = options.language
|
|
59
|
-
? [options.language]
|
|
60
|
-
: project.languages;
|
|
61
|
-
// Validate language if specified
|
|
62
|
-
if (options.language && !project.languages.includes(options.language)) {
|
|
63
|
-
spinner.fail(chalk.red(`Language "${options.language}" not found in project`));
|
|
64
|
-
console.log(chalk.yellow(`Available languages: ${project.languages.join(', ')}\n`));
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
const format = options.format || 'flat-json';
|
|
68
|
-
const publishedOnly = !options.includeUnpublished;
|
|
69
|
-
// Export each language
|
|
70
|
-
for (const language of languages) {
|
|
71
|
-
spinner.text = `Exporting ${language}...`;
|
|
72
|
-
const response = await fetch(EDGE_FUNCTION_URL, {
|
|
73
|
-
method: 'POST',
|
|
74
|
-
headers: {
|
|
75
|
-
'Content-Type': 'application/json',
|
|
76
|
-
'X-API-Key': apiKey
|
|
77
|
-
},
|
|
78
|
-
body: JSON.stringify({
|
|
79
|
-
projectId: project.id,
|
|
80
|
-
language,
|
|
81
|
-
format,
|
|
82
|
-
publishedOnly,
|
|
83
|
-
module: options.module
|
|
84
|
-
})
|
|
85
|
-
});
|
|
86
|
-
if (!response.ok) {
|
|
87
|
-
const errorData = await response.json().catch(() => ({}));
|
|
88
|
-
throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);
|
|
89
|
-
}
|
|
90
|
-
const data = await response.json();
|
|
91
|
-
if (!data.success || !data.content) {
|
|
92
|
-
throw new Error(data.error || 'Failed to export translations');
|
|
93
|
-
}
|
|
94
|
-
// Determine output path
|
|
95
|
-
const outputPath = options.output
|
|
96
|
-
? options.output
|
|
97
|
-
: join(process.cwd(), 'translations', data.filename || `${language}.json`);
|
|
98
|
-
// Ensure directory exists
|
|
99
|
-
mkdirSync(dirname(outputPath), { recursive: true });
|
|
100
|
-
// Write file
|
|
101
|
-
writeFileSync(outputPath, data.content, 'utf-8');
|
|
102
|
-
spinner.succeed(chalk.green(`Exported ${language} → ${outputPath}`));
|
|
103
|
-
// Show spinner again if there are more languages
|
|
104
|
-
if (languages.indexOf(language) < languages.length - 1) {
|
|
105
|
-
spinner.start();
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
console.log(chalk.blue(`\n✓ Exported ${languages.length} language(s) successfully\n`));
|
|
109
|
-
}
|
|
110
|
-
catch (error) {
|
|
111
|
-
spinner.fail(chalk.red('Failed to export translations'));
|
|
112
|
-
console.error(chalk.red(`Error: ${error.message}\n`));
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
//# sourceMappingURL=export.js.map
|
package/dist/commands/import.js
CHANGED
|
@@ -1,171 +1,39 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
import { relative, resolve } from 'path';
|
|
3
|
+
import { loadProjectConfig } from '../core/config.js';
|
|
4
|
+
import { usageError } from '../core/errors.js';
|
|
5
|
+
import { getSession } from '../core/http.js';
|
|
6
|
+
import { log, printJson, runtime, spinner } from '../core/output.js';
|
|
7
|
+
import { assertLanguages, getProject, projectSlugFrom } from '../core/project.js';
|
|
8
|
+
import { getFormat } from '../formats/index.js';
|
|
9
|
+
import { readTranslationFile, uploadTranslations } from './push.js';
|
|
8
10
|
/**
|
|
9
|
-
*
|
|
11
|
+
* `langctl import [project] <file> --language <code>` — upload one file.
|
|
12
|
+
* The project may be omitted when langctl.json names it.
|
|
10
13
|
*/
|
|
11
|
-
async function
|
|
14
|
+
export async function importCommand(first, second, opts) {
|
|
15
|
+
const [projectArg, file] = second === undefined ? [undefined, first] : [first, second];
|
|
16
|
+
const session = await getSession();
|
|
17
|
+
const project = await getProject(session, projectSlugFrom(projectArg));
|
|
18
|
+
const language = opts.language ?? loadProjectConfig()?.config.sourceLanguage ?? project.defaultLanguage;
|
|
19
|
+
if (!opts.language)
|
|
20
|
+
log.info(chalk.dim(`No --language given; importing as ${language} (the project's default).`));
|
|
21
|
+
assertLanguages(project, [language]);
|
|
22
|
+
const path = resolve(file);
|
|
23
|
+
const translations = readTranslationFile(path, opts.format ? getFormat(opts.format) : undefined, language);
|
|
24
|
+
if (Object.keys(translations).length === 0)
|
|
25
|
+
throw usageError(`${relative(process.cwd(), path)} contains no translations.`);
|
|
26
|
+
const spin = spinner(`${opts.dryRun ? 'Checking' : 'Importing'} ${Object.keys(translations).length} keys…`);
|
|
27
|
+
let result;
|
|
12
28
|
try {
|
|
13
|
-
|
|
14
|
-
method: 'POST',
|
|
15
|
-
headers: {
|
|
16
|
-
'Content-Type': 'application/json',
|
|
17
|
-
'X-API-Key': apiKey
|
|
18
|
-
}
|
|
19
|
-
});
|
|
20
|
-
if (!response.ok) {
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
|
-
const data = await response.json();
|
|
24
|
-
if (!data.success || !data.projects) {
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
return data.projects.find((p) => p.slug === slug) || null;
|
|
29
|
+
result = await uploadTranslations(session, project, language, translations, opts);
|
|
28
30
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
finally {
|
|
32
|
+
spin.stop();
|
|
31
33
|
}
|
|
34
|
+
if (runtime.json)
|
|
35
|
+
return printJson({ project: project.slug, file: relative(process.cwd(), path), dryRun: Boolean(opts.dryRun), ...result });
|
|
36
|
+
log.out(`${language} ${chalk.green(`${result.created} new`)}, ${chalk.yellow(`${result.updated} updated`)}, ${chalk.dim(`${result.unchanged} unchanged`)}${result.published !== undefined ? `, ${chalk.cyan(`${result.published} published`)}` : ''}${opts.dryRun ? chalk.dim(' (dry run)') : ''}`);
|
|
37
|
+
if (!opts.overwrite && result.unchanged > 0)
|
|
38
|
+
log.info(chalk.dim('Existing translations are kept unless you pass --overwrite.'));
|
|
32
39
|
}
|
|
33
|
-
/**
|
|
34
|
-
* Parse translation file based on extension
|
|
35
|
-
*/
|
|
36
|
-
function parseTranslationFile(filePath) {
|
|
37
|
-
const content = readFileSync(filePath, 'utf-8');
|
|
38
|
-
const ext = filePath.split('.').pop()?.toLowerCase();
|
|
39
|
-
if (ext === 'json') {
|
|
40
|
-
const parsed = JSON.parse(content);
|
|
41
|
-
// Check if it's nested JSON and flatten it
|
|
42
|
-
if (isNestedObject(parsed)) {
|
|
43
|
-
return flattenObject(parsed);
|
|
44
|
-
}
|
|
45
|
-
return parsed;
|
|
46
|
-
}
|
|
47
|
-
throw new Error(`Unsupported file format: .${ext}. Only JSON files are currently supported.`);
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Check if object has nested objects
|
|
51
|
-
*/
|
|
52
|
-
function isNestedObject(obj) {
|
|
53
|
-
return Object.values(obj).some(value => typeof value === 'object' && value !== null && !Array.isArray(value));
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Flatten nested object to dot-notation keys
|
|
57
|
-
*/
|
|
58
|
-
function flattenObject(obj, prefix = '') {
|
|
59
|
-
const result = {};
|
|
60
|
-
Object.entries(obj).forEach(([key, value]) => {
|
|
61
|
-
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
62
|
-
if (typeof value === 'string') {
|
|
63
|
-
result[fullKey] = value;
|
|
64
|
-
}
|
|
65
|
-
else if (typeof value === 'object' && value !== null) {
|
|
66
|
-
Object.assign(result, flattenObject(value, fullKey));
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
return result;
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* Import translations command
|
|
73
|
-
*/
|
|
74
|
-
export async function importCommand(projectSlug, filePath, options) {
|
|
75
|
-
// Check authentication
|
|
76
|
-
if (!isAuthenticated()) {
|
|
77
|
-
console.log(chalk.red('✗ Not authenticated. Please run "langctl auth <api-key>" first.\n'));
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
const apiKey = getApiKey();
|
|
81
|
-
if (!apiKey) {
|
|
82
|
-
console.log(chalk.red('✗ API key not found. Please run "langctl auth <api-key>" again.\n'));
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
const spinner = ora('Loading file...').start();
|
|
86
|
-
try {
|
|
87
|
-
// Resolve file path
|
|
88
|
-
const resolvedPath = resolve(filePath);
|
|
89
|
-
// Parse translation file
|
|
90
|
-
spinner.text = 'Parsing translation file...';
|
|
91
|
-
const translations = parseTranslationFile(resolvedPath);
|
|
92
|
-
const keyCount = Object.keys(translations).length;
|
|
93
|
-
if (keyCount === 0) {
|
|
94
|
-
spinner.warn(chalk.yellow('No translations found in file'));
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
spinner.text = 'Fetching project...';
|
|
98
|
-
// Get project by slug
|
|
99
|
-
const project = await getProjectBySlug(apiKey, projectSlug);
|
|
100
|
-
if (!project) {
|
|
101
|
-
spinner.fail(chalk.red(`Project "${projectSlug}" not found`));
|
|
102
|
-
console.log(chalk.yellow('\nRun "langctl projects list" to see available projects.\n'));
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
// Validate language
|
|
106
|
-
if (!project.languages.includes(options.language)) {
|
|
107
|
-
spinner.fail(chalk.red(`Language "${options.language}" not found in project`));
|
|
108
|
-
console.log(chalk.yellow(`Available languages: ${project.languages.join(', ')}\n`));
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
spinner.text = `Importing ${keyCount} translations...`;
|
|
112
|
-
// Push translations
|
|
113
|
-
const response = await fetch(PUSH_TRANSLATIONS_URL, {
|
|
114
|
-
method: 'POST',
|
|
115
|
-
headers: {
|
|
116
|
-
'Content-Type': 'application/json',
|
|
117
|
-
'X-API-Key': apiKey
|
|
118
|
-
},
|
|
119
|
-
body: JSON.stringify({
|
|
120
|
-
projectId: project.id,
|
|
121
|
-
language: options.language,
|
|
122
|
-
translations,
|
|
123
|
-
publish: options.publish || false,
|
|
124
|
-
createMissing: true
|
|
125
|
-
})
|
|
126
|
-
});
|
|
127
|
-
if (!response.ok) {
|
|
128
|
-
const errorData = await response.json().catch(() => ({}));
|
|
129
|
-
throw new Error(errorData.error || `HTTP ${response.status}: ${response.statusText}`);
|
|
130
|
-
}
|
|
131
|
-
const data = await response.json();
|
|
132
|
-
if (!data.success && data.errors.length > 0) {
|
|
133
|
-
spinner.fail(chalk.red('Import completed with errors'));
|
|
134
|
-
console.log(chalk.green(`\n✓ Created: ${data.created}`));
|
|
135
|
-
console.log(chalk.blue(`✓ Updated: ${data.updated}`));
|
|
136
|
-
console.log(chalk.yellow(`⊘ Skipped: ${data.skipped}`));
|
|
137
|
-
console.log(chalk.red(`✗ Errors: ${data.errors.length}\n`));
|
|
138
|
-
if (data.errors.length > 0) {
|
|
139
|
-
console.log(chalk.red('Errors:'));
|
|
140
|
-
data.errors.slice(0, 5).forEach(err => {
|
|
141
|
-
console.log(chalk.red(` - ${err.key}: ${err.error}`));
|
|
142
|
-
});
|
|
143
|
-
if (data.errors.length > 5) {
|
|
144
|
-
console.log(chalk.gray(` ... and ${data.errors.length - 5} more errors\n`));
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
spinner.succeed(chalk.green('Import completed successfully'));
|
|
150
|
-
console.log(chalk.green(`\n✓ Created: ${data.created}`));
|
|
151
|
-
console.log(chalk.blue(`✓ Updated: ${data.updated}`));
|
|
152
|
-
console.log(chalk.yellow(`⊘ Skipped: ${data.skipped}`));
|
|
153
|
-
if (options.publish) {
|
|
154
|
-
console.log(chalk.green('✓ Keys published'));
|
|
155
|
-
}
|
|
156
|
-
console.log('');
|
|
157
|
-
}
|
|
158
|
-
catch (error) {
|
|
159
|
-
spinner.fail(chalk.red('Failed to import translations'));
|
|
160
|
-
if (error.code === 'ENOENT') {
|
|
161
|
-
console.error(chalk.red(`Error: File not found: ${filePath}\n`));
|
|
162
|
-
}
|
|
163
|
-
else if (error instanceof SyntaxError) {
|
|
164
|
-
console.error(chalk.red(`Error: Invalid JSON file\n`));
|
|
165
|
-
}
|
|
166
|
-
else {
|
|
167
|
-
console.error(chalk.red(`Error: ${error.message}\n`));
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
//# sourceMappingURL=import.js.map
|