langctl 0.1.2 → 0.2.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/.github/ISSUE_TEMPLATE/bug_report.md +26 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +19 -0
- package/CONTRIBUTING.md +33 -0
- package/LICENSE +1 -1
- package/README.md +52 -11
- package/dist/api.d.ts +20 -0
- package/dist/api.js +75 -0
- package/dist/auth.d.ts +3 -2
- package/dist/auth.js +21 -27
- package/dist/commands/auth.js +2 -10
- package/dist/commands/config.js +1 -0
- package/dist/commands/debug.js +45 -41
- package/dist/commands/export.d.ts +1 -2
- package/dist/commands/export.js +70 -60
- package/dist/commands/import.d.ts +1 -0
- package/dist/commands/import.js +51 -72
- package/dist/commands/init.js +0 -10
- package/dist/commands/keys.js +122 -224
- package/dist/commands/org.js +43 -72
- package/dist/commands/projects.d.ts +3 -0
- package/dist/commands/projects.js +146 -223
- package/dist/commands/pull.js +40 -95
- package/dist/commands/team.js +115 -130
- package/dist/config.d.ts +1 -0
- package/dist/index.js +3 -3
- package/package.json +2 -3
- package/NEXT_PHASE.md +0 -618
- package/WORKING_INSTRUCTIONS.md +0 -1389
- package/dist/supabase.d.ts +0 -14
- package/dist/supabase.js +0 -30
package/dist/commands/export.js
CHANGED
|
@@ -2,57 +2,65 @@ import chalk from 'chalk';
|
|
|
2
2
|
import ora from 'ora';
|
|
3
3
|
import { writeFileSync, mkdirSync } from 'fs';
|
|
4
4
|
import { join, dirname } from 'path';
|
|
5
|
-
import { isAuthenticated
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
import { isAuthenticated } from '../auth.js';
|
|
6
|
+
import { getApiClient } from '../api.js';
|
|
7
|
+
import { config } from '../config.js';
|
|
8
|
+
import { exportTranslations } from '../exporters/index.js';
|
|
8
9
|
/**
|
|
9
|
-
*
|
|
10
|
+
* Map CLI format names to exporter format names
|
|
10
11
|
*/
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
12
|
+
const formatMap = {
|
|
13
|
+
'flat-json': 'json',
|
|
14
|
+
'nested-json': 'json-nested',
|
|
15
|
+
'i18n-json': 'json',
|
|
16
|
+
'android-xml': 'android',
|
|
17
|
+
'ios-strings': 'ios',
|
|
18
|
+
'flutter-arb': 'flutter',
|
|
19
|
+
// Also accept exporter format names directly
|
|
20
|
+
'json': 'json',
|
|
21
|
+
'json-nested': 'json-nested',
|
|
22
|
+
'android': 'android',
|
|
23
|
+
'ios': 'ios',
|
|
24
|
+
'flutter': 'flutter',
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Get file extension for a given export format
|
|
28
|
+
*/
|
|
29
|
+
function getFileExtension(format, language) {
|
|
30
|
+
switch (format) {
|
|
31
|
+
case 'json':
|
|
32
|
+
case 'json-nested':
|
|
33
|
+
return `${language}.json`;
|
|
34
|
+
case 'android':
|
|
35
|
+
return `strings.xml`;
|
|
36
|
+
case 'ios':
|
|
37
|
+
return `Localizable.strings`;
|
|
38
|
+
case 'flutter':
|
|
39
|
+
return `intl_${language}.arb`;
|
|
40
|
+
default:
|
|
41
|
+
return `${language}.json`;
|
|
31
42
|
}
|
|
32
43
|
}
|
|
33
44
|
/**
|
|
34
45
|
* Export translations command
|
|
35
46
|
*/
|
|
36
47
|
export async function exportCommand(projectSlug, options) {
|
|
37
|
-
// Check authentication
|
|
38
48
|
if (!isAuthenticated()) {
|
|
39
49
|
console.log(chalk.red('✗ Not authenticated. Please run "langctl auth <api-key>" first.\n'));
|
|
50
|
+
process.exitCode = 1;
|
|
40
51
|
return;
|
|
41
52
|
}
|
|
42
|
-
const
|
|
43
|
-
if (!
|
|
44
|
-
console.log(chalk.red('✗
|
|
53
|
+
const orgId = config.get('organizationId');
|
|
54
|
+
if (!orgId) {
|
|
55
|
+
console.log(chalk.red('✗ Organization ID not found. Please run "langctl auth <api-key>" again.\n'));
|
|
56
|
+
process.exitCode = 1;
|
|
45
57
|
return;
|
|
46
58
|
}
|
|
47
59
|
const spinner = ora('Fetching project...').start();
|
|
48
60
|
try {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
}
|
|
61
|
+
const api = getApiClient();
|
|
62
|
+
// Resolve project by slug
|
|
63
|
+
const project = await api.get(`/orgs/${orgId}/projects/by-slug/${projectSlug}`);
|
|
56
64
|
spinner.text = 'Exporting translations...';
|
|
57
65
|
// Determine languages to export
|
|
58
66
|
const languages = options.language
|
|
@@ -64,41 +72,42 @@ export async function exportCommand(projectSlug, options) {
|
|
|
64
72
|
console.log(chalk.yellow(`Available languages: ${project.languages.join(', ')}\n`));
|
|
65
73
|
return;
|
|
66
74
|
}
|
|
67
|
-
|
|
75
|
+
// Resolve format
|
|
76
|
+
const cliFormat = options.format || 'flat-json';
|
|
77
|
+
const exportFormat = formatMap[cliFormat];
|
|
78
|
+
if (!exportFormat) {
|
|
79
|
+
spinner.fail(chalk.red(`Unsupported format: ${cliFormat}`));
|
|
80
|
+
console.log(chalk.yellow(`Valid formats: ${Object.keys(formatMap).join(', ')}\n`));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
68
83
|
const publishedOnly = !options.includeUnpublished;
|
|
69
84
|
// Export each language
|
|
70
85
|
for (const language of languages) {
|
|
71
86
|
spinner.text = `Exporting ${language}...`;
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
}
|
|
87
|
+
// Fetch flat translations from the API
|
|
88
|
+
const params = {
|
|
89
|
+
language,
|
|
90
|
+
publishedOnly: publishedOnly ? 'true' : 'false'
|
|
91
|
+
};
|
|
92
|
+
if (options.module)
|
|
93
|
+
params.module = options.module;
|
|
94
|
+
const result = await api.get(`/orgs/${orgId}/projects/${project.id}/export`, params);
|
|
95
|
+
// Convert flat translations to TranslationKey array for the exporter
|
|
96
|
+
const flatTranslations = result.translations || {};
|
|
97
|
+
const translationKeys = Object.entries(flatTranslations).map(([key, value]) => ({
|
|
98
|
+
key,
|
|
99
|
+
translations: { [language]: value }
|
|
100
|
+
}));
|
|
101
|
+
// Use client-side exporter for format conversion
|
|
102
|
+
const exportResult = exportTranslations(translationKeys, language, exportFormat);
|
|
94
103
|
// Determine output path
|
|
95
104
|
const outputPath = options.output
|
|
96
105
|
? options.output
|
|
97
|
-
: join(process.cwd(), 'translations',
|
|
106
|
+
: join(process.cwd(), 'translations', getFileExtension(exportFormat, language));
|
|
98
107
|
// Ensure directory exists
|
|
99
108
|
mkdirSync(dirname(outputPath), { recursive: true });
|
|
100
109
|
// Write file
|
|
101
|
-
writeFileSync(outputPath,
|
|
110
|
+
writeFileSync(outputPath, exportResult.content, 'utf-8');
|
|
102
111
|
spinner.succeed(chalk.green(`Exported ${language} → ${outputPath}`));
|
|
103
112
|
// Show spinner again if there are more languages
|
|
104
113
|
if (languages.indexOf(language) < languages.length - 1) {
|
|
@@ -110,6 +119,7 @@ export async function exportCommand(projectSlug, options) {
|
|
|
110
119
|
catch (error) {
|
|
111
120
|
spinner.fail(chalk.red('Failed to export translations'));
|
|
112
121
|
console.error(chalk.red(`Error: ${error.message}\n`));
|
|
122
|
+
process.exitCode = 1;
|
|
113
123
|
}
|
|
114
124
|
}
|
|
115
125
|
//# sourceMappingURL=export.js.map
|
package/dist/commands/import.js
CHANGED
|
@@ -2,34 +2,9 @@ import chalk from 'chalk';
|
|
|
2
2
|
import ora from 'ora';
|
|
3
3
|
import { readFileSync } from 'fs';
|
|
4
4
|
import { resolve } from 'path';
|
|
5
|
-
import { isAuthenticated
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Get project by slug
|
|
10
|
-
*/
|
|
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
|
-
}
|
|
32
|
-
}
|
|
5
|
+
import { isAuthenticated } from '../auth.js';
|
|
6
|
+
import { getApiClient } from '../api.js';
|
|
7
|
+
import { config } from '../config.js';
|
|
33
8
|
/**
|
|
34
9
|
* Parse translation file based on extension
|
|
35
10
|
*/
|
|
@@ -72,14 +47,15 @@ function flattenObject(obj, prefix = '') {
|
|
|
72
47
|
* Import translations command
|
|
73
48
|
*/
|
|
74
49
|
export async function importCommand(projectSlug, filePath, options) {
|
|
75
|
-
// Check authentication
|
|
76
50
|
if (!isAuthenticated()) {
|
|
77
51
|
console.log(chalk.red('✗ Not authenticated. Please run "langctl auth <api-key>" first.\n'));
|
|
52
|
+
process.exitCode = 1;
|
|
78
53
|
return;
|
|
79
54
|
}
|
|
80
|
-
const
|
|
81
|
-
if (!
|
|
82
|
-
console.log(chalk.red('✗
|
|
55
|
+
const orgId = config.get('organizationId');
|
|
56
|
+
if (!orgId) {
|
|
57
|
+
console.log(chalk.red('✗ Organization ID not found. Please run "langctl auth <api-key>" again.\n'));
|
|
58
|
+
process.exitCode = 1;
|
|
83
59
|
return;
|
|
84
60
|
}
|
|
85
61
|
const spinner = ora('Loading file...').start();
|
|
@@ -96,12 +72,8 @@ export async function importCommand(projectSlug, filePath, options) {
|
|
|
96
72
|
}
|
|
97
73
|
spinner.text = 'Fetching project...';
|
|
98
74
|
// Get project by slug
|
|
99
|
-
const
|
|
100
|
-
|
|
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
|
-
}
|
|
75
|
+
const api = getApiClient();
|
|
76
|
+
const project = await api.get(`/orgs/${orgId}/projects/by-slug/${projectSlug}`);
|
|
105
77
|
// Validate language
|
|
106
78
|
if (!project.languages.includes(options.language)) {
|
|
107
79
|
spinner.fail(chalk.red(`Language "${options.language}" not found in project`));
|
|
@@ -109,62 +81,69 @@ export async function importCommand(projectSlug, filePath, options) {
|
|
|
109
81
|
return;
|
|
110
82
|
}
|
|
111
83
|
spinner.text = `Importing ${keyCount} translations...`;
|
|
112
|
-
// Push translations
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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}`));
|
|
84
|
+
// Push translations via REST API
|
|
85
|
+
const body = {
|
|
86
|
+
language: options.language,
|
|
87
|
+
translations,
|
|
88
|
+
// Keep import safe by default; only overwrite when explicitly requested.
|
|
89
|
+
overwriteExisting: options.overwrite === true
|
|
90
|
+
};
|
|
91
|
+
if (options.module)
|
|
92
|
+
body.module = options.module;
|
|
93
|
+
const data = await api.post(`/orgs/${orgId}/projects/${project.id}/import`, body);
|
|
94
|
+
let publishedCount = 0;
|
|
95
|
+
if (options.publish) {
|
|
96
|
+
spinner.text = 'Publishing imported keys...';
|
|
97
|
+
const keyNames = new Set(Object.keys(translations));
|
|
98
|
+
const keyIds = [];
|
|
99
|
+
let page = 1;
|
|
100
|
+
const pageSize = 100;
|
|
101
|
+
while (true) {
|
|
102
|
+
const keysPage = await api.get(`/orgs/${orgId}/projects/${project.id}/keys`, {
|
|
103
|
+
page: String(page),
|
|
104
|
+
pageSize: String(pageSize)
|
|
142
105
|
});
|
|
143
|
-
|
|
144
|
-
|
|
106
|
+
for (const key of (keysPage.data || [])) {
|
|
107
|
+
if (keyNames.has(key.key)) {
|
|
108
|
+
keyIds.push(key.id);
|
|
109
|
+
}
|
|
145
110
|
}
|
|
111
|
+
const totalPages = keysPage.pagination?.totalPages || 1;
|
|
112
|
+
if (page >= totalPages)
|
|
113
|
+
break;
|
|
114
|
+
page += 1;
|
|
115
|
+
}
|
|
116
|
+
if (keyIds.length > 0) {
|
|
117
|
+
await api.post(`/orgs/${orgId}/projects/${project.id}/keys/bulk-publish`, {
|
|
118
|
+
keyIds,
|
|
119
|
+
published: true
|
|
120
|
+
});
|
|
121
|
+
publishedCount = keyIds.length;
|
|
146
122
|
}
|
|
147
|
-
return;
|
|
148
123
|
}
|
|
149
124
|
spinner.succeed(chalk.green('Import completed successfully'));
|
|
150
125
|
console.log(chalk.green(`\n✓ Created: ${data.created}`));
|
|
151
126
|
console.log(chalk.blue(`✓ Updated: ${data.updated}`));
|
|
152
127
|
console.log(chalk.yellow(`⊘ Skipped: ${data.skipped}`));
|
|
153
128
|
if (options.publish) {
|
|
154
|
-
console.log(chalk.
|
|
129
|
+
console.log(chalk.magenta(`✓ Published: ${publishedCount}`));
|
|
155
130
|
}
|
|
131
|
+
console.log(chalk.white(` Total: ${data.total}`));
|
|
156
132
|
console.log('');
|
|
157
133
|
}
|
|
158
134
|
catch (error) {
|
|
159
135
|
spinner.fail(chalk.red('Failed to import translations'));
|
|
160
136
|
if (error.code === 'ENOENT') {
|
|
161
137
|
console.error(chalk.red(`Error: File not found: ${filePath}\n`));
|
|
138
|
+
process.exitCode = 1;
|
|
162
139
|
}
|
|
163
140
|
else if (error instanceof SyntaxError) {
|
|
164
141
|
console.error(chalk.red(`Error: Invalid JSON file\n`));
|
|
142
|
+
process.exitCode = 1;
|
|
165
143
|
}
|
|
166
144
|
else {
|
|
167
145
|
console.error(chalk.red(`Error: ${error.message}\n`));
|
|
146
|
+
process.exitCode = 1;
|
|
168
147
|
}
|
|
169
148
|
}
|
|
170
149
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -3,7 +3,6 @@ import chalk from 'chalk';
|
|
|
3
3
|
import ora from 'ora';
|
|
4
4
|
import { config } from '../config.js';
|
|
5
5
|
import { authenticate } from '../auth.js';
|
|
6
|
-
import { initializeSupabase } from '../supabase.js';
|
|
7
6
|
import { showLogo } from '../utils/banner.js';
|
|
8
7
|
export async function initCommand() {
|
|
9
8
|
// Show logo
|
|
@@ -25,15 +24,6 @@ export async function initCommand() {
|
|
|
25
24
|
return;
|
|
26
25
|
}
|
|
27
26
|
}
|
|
28
|
-
// Initialize Supabase client (with hardcoded credentials - user doesn't need to know)
|
|
29
|
-
try {
|
|
30
|
-
initializeSupabase();
|
|
31
|
-
}
|
|
32
|
-
catch (error) {
|
|
33
|
-
console.error(chalk.red('✗ Failed to connect to Langctl servers'));
|
|
34
|
-
console.error(chalk.red('Please check your internet connection and try again.\n'));
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
27
|
// Ask for API key
|
|
38
28
|
const { apiKey } = await inquirer.prompt([
|
|
39
29
|
{
|