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.
Files changed (63) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/LICENSE +1 -1
  3. package/README.md +159 -1151
  4. package/dist/commands/auth.js +82 -24
  5. package/dist/commands/config.js +36 -25
  6. package/dist/commands/export.js +11 -112
  7. package/dist/commands/import.js +32 -164
  8. package/dist/commands/init.js +59 -80
  9. package/dist/commands/keys.js +145 -381
  10. package/dist/commands/org.js +45 -140
  11. package/dist/commands/projects.js +125 -390
  12. package/dist/commands/pull.js +78 -183
  13. package/dist/commands/push.js +125 -0
  14. package/dist/commands/team.js +68 -272
  15. package/dist/core/config.js +130 -0
  16. package/dist/core/errors.js +67 -0
  17. package/dist/core/files.js +38 -0
  18. package/dist/core/http.js +121 -0
  19. package/dist/core/output.js +96 -0
  20. package/dist/core/project.js +58 -0
  21. package/dist/core/prompts.js +56 -0
  22. package/dist/formats/index.js +285 -0
  23. package/dist/index.js +261 -508
  24. package/dist/version.js +4 -0
  25. package/package.json +22 -16
  26. package/.gitattributes +0 -2
  27. package/NEXT_PHASE.md +0 -618
  28. package/WORKING_INSTRUCTIONS.md +0 -1389
  29. package/dist/auth.d.ts +0 -37
  30. package/dist/auth.js +0 -112
  31. package/dist/commands/auth.d.ts +0 -3
  32. package/dist/commands/config.d.ts +0 -2
  33. package/dist/commands/debug.d.ts +0 -3
  34. package/dist/commands/debug.js +0 -57
  35. package/dist/commands/export.d.ts +0 -14
  36. package/dist/commands/import.d.ts +0 -11
  37. package/dist/commands/init.d.ts +0 -2
  38. package/dist/commands/keys.d.ts +0 -25
  39. package/dist/commands/org.d.ts +0 -13
  40. package/dist/commands/projects.d.ts +0 -30
  41. package/dist/commands/pull.d.ts +0 -9
  42. package/dist/commands/team.d.ts +0 -29
  43. package/dist/config.d.ts +0 -47
  44. package/dist/config.js +0 -69
  45. package/dist/exporters/index.d.ts +0 -36
  46. package/dist/exporters/index.js +0 -214
  47. package/dist/index.d.ts +0 -3
  48. package/dist/supabase.d.ts +0 -14
  49. package/dist/supabase.js +0 -30
  50. package/dist/utils/banner.d.ts +0 -9
  51. package/dist/utils/banner.js +0 -24
  52. package/translations/en.json +0 -5
  53. package/translations/en.xml +0 -9
  54. package/translations/fr.json +0 -4
  55. package/translations/fr.xml +0 -7
  56. package/translations/hi.json +0 -4
  57. package/translations/hi.xml +0 -7
  58. package/translations/ios/en.strings +0 -9
  59. package/translations/ios/fr.strings +0 -6
  60. package/translations/ios/hi.strings +0 -6
  61. package/translations/ios/ja.strings +0 -6
  62. package/translations/ja.json +0 -4
  63. package/translations/ja.xml +0 -7
@@ -1,33 +1,91 @@
1
1
  import chalk from 'chalk';
2
- import ora from 'ora';
3
- import { authenticate, logout } from '../auth.js';
4
- import { config } from '../config.js';
5
- import { initializeSupabase } from '../supabase.js';
6
- export async function authCommand(apiKey) {
7
- // Initialize Supabase (with hardcoded credentials)
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
- initializeSupabase();
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
- catch (error) {
12
- console.log(chalk.red('✗ Failed to connect to Langctl servers.\n'));
13
- return;
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
- const spinner = ora('Authenticating...').start();
16
- const result = await authenticate(apiKey);
17
- if (result.success) {
18
- spinner.succeed(chalk.green(`Authenticated as ${result.data?.organization.name}`));
19
- console.log(chalk.gray(`\nAPI Key saved to: ${config.getConfigPath()}\n`));
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
- spinner.fail(chalk.red(result.message));
23
- console.log(chalk.yellow('\nTroubleshooting:'));
24
- console.log(chalk.white(' - Check that your API key is correct'));
25
- console.log(chalk.white(' - Verify the key is not revoked in the dashboard'));
26
- console.log(chalk.white(' - Generate a new key: https://langctl.com/dashboard/settings/api-keys\n'));
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
- export async function logoutCommand() {
30
- logout();
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
@@ -1,30 +1,41 @@
1
1
  import chalk from 'chalk';
2
- import { config } from '../config.js';
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
- console.log(chalk.blue.bold('\n📋 Langctl Configuration\n'));
5
- const allConfig = config.getAll();
6
- if (Object.keys(allConfig).length === 0) {
7
- console.log(chalk.yellow('No configuration found. Run "langctl init" to set up.\n'));
8
- return;
9
- }
10
- // Display config with masked sensitive values
11
- const displayConfig = {
12
- apiKey: allConfig.apiKey ? maskApiKey(allConfig.apiKey) : undefined,
13
- organizationId: allConfig.organizationId,
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
- Object.entries(displayConfig).forEach(([key, value]) => {
19
- if (value !== undefined) {
20
- console.log(chalk.white(`${key}: ${chalk.cyan(value)}`));
21
- }
22
- });
23
- console.log(chalk.gray(`\nConfig file: ${config.getConfigPath()}\n`));
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 maskApiKey(key) {
26
- if (key.length < 10)
27
- return '***';
28
- return `${key.substring(0, 10)}...${key.substring(key.length - 4)}`;
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
@@ -1,115 +1,14 @@
1
- import chalk from 'chalk';
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
- * Get project by slug
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 getProjectBySlug(apiKey, slug) {
12
- try {
13
- const response = await fetch(LIST_PROJECTS_URL, {
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;
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
@@ -1,171 +1,39 @@
1
1
  import chalk from 'chalk';
2
- import ora from 'ora';
3
- import { readFileSync } from 'fs';
4
- import { resolve } from 'path';
5
- import { isAuthenticated, getApiKey } from '../auth.js';
6
- const PUSH_TRANSLATIONS_URL = 'https://bcgnmvkgkbhbxzzflwdb.supabase.co/functions/v1/push-translations';
7
- const LIST_PROJECTS_URL = 'https://bcgnmvkgkbhbxzzflwdb.supabase.co/functions/v1/list-projects';
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
- * Get project by slug
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 getProjectBySlug(apiKey, slug) {
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
- const response = await fetch(LIST_PROJECTS_URL, {
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
- catch (error) {
30
- return null;
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