langctl 0.0.1 → 0.1.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/NPM_PUBLISH_GUIDE.md +293 -0
- package/QUICK_PUBLISH.md +64 -0
- package/README.md +287 -32
- package/dist/auth.d.ts +33 -0
- package/dist/auth.js +106 -0
- package/dist/commands/auth.d.ts +3 -0
- package/dist/commands/auth.js +41 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +33 -0
- package/dist/commands/debug.d.ts +3 -0
- package/dist/commands/debug.js +63 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +93 -0
- package/dist/commands/projects.d.ts +2 -0
- package/dist/commands/projects.js +54 -0
- package/dist/commands/pull.d.ts +9 -0
- package/dist/commands/pull.js +194 -0
- package/dist/config.d.ts +49 -0
- package/dist/config.js +71 -0
- package/dist/exporters/index.d.ts +36 -0
- package/dist/exporters/index.js +214 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +166 -0
- package/dist/supabase.d.ts +14 -0
- package/dist/supabase.js +33 -0
- package/dist/utils/banner.d.ts +9 -0
- package/dist/utils/banner.js +24 -0
- package/package.json +22 -3
- package/translations/en.json +5 -0
- package/translations/en.xml +9 -0
- package/translations/fr.json +4 -0
- package/translations/fr.xml +7 -0
- package/translations/hi.json +4 -0
- package/translations/hi.xml +7 -0
- package/translations/ios/en.strings +9 -0
- package/translations/ios/fr.strings +6 -0
- package/translations/ios/hi.strings +6 -0
- package/translations/ios/ja.strings +6 -0
- package/translations/ja.json +4 -0
- package/translations/ja.xml +7 -0
- package/PUBLISH_GUIDE.md +0 -209
- package/bin/langctl.js +0 -19
- package/index.js +0 -26
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export interface LangctlConfig {
|
|
2
|
+
supabaseUrl?: string;
|
|
3
|
+
supabaseAnonKey?: string;
|
|
4
|
+
apiKey?: string;
|
|
5
|
+
organizationId?: string;
|
|
6
|
+
organizationName?: string;
|
|
7
|
+
defaultProject?: string;
|
|
8
|
+
defaultLanguage?: string;
|
|
9
|
+
}
|
|
10
|
+
declare class ConfigManager {
|
|
11
|
+
private config;
|
|
12
|
+
private configDir;
|
|
13
|
+
constructor();
|
|
14
|
+
/**
|
|
15
|
+
* Get a configuration value
|
|
16
|
+
*/
|
|
17
|
+
get<K extends keyof LangctlConfig>(key: K): LangctlConfig[K] | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Set a configuration value
|
|
20
|
+
*/
|
|
21
|
+
set<K extends keyof LangctlConfig>(key: K, value: LangctlConfig[K]): void;
|
|
22
|
+
/**
|
|
23
|
+
* Get all configuration
|
|
24
|
+
*/
|
|
25
|
+
getAll(): LangctlConfig;
|
|
26
|
+
/**
|
|
27
|
+
* Set multiple configuration values
|
|
28
|
+
*/
|
|
29
|
+
setAll(config: Partial<LangctlConfig>): void;
|
|
30
|
+
/**
|
|
31
|
+
* Delete a configuration value
|
|
32
|
+
*/
|
|
33
|
+
delete(key: keyof LangctlConfig): void;
|
|
34
|
+
/**
|
|
35
|
+
* Clear all configuration
|
|
36
|
+
*/
|
|
37
|
+
clear(): void;
|
|
38
|
+
/**
|
|
39
|
+
* Check if configuration is complete
|
|
40
|
+
*/
|
|
41
|
+
isConfigured(): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Get the configuration file path
|
|
44
|
+
*/
|
|
45
|
+
getConfigPath(): string;
|
|
46
|
+
}
|
|
47
|
+
export declare const config: ConfigManager;
|
|
48
|
+
export {};
|
|
49
|
+
//# sourceMappingURL=config.d.ts.map
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import Conf from 'conf';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
class ConfigManager {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.configDir = join(homedir(), '.langctl');
|
|
7
|
+
this.config = new Conf({
|
|
8
|
+
projectName: 'langctl',
|
|
9
|
+
cwd: this.configDir,
|
|
10
|
+
configName: 'config'
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Get a configuration value
|
|
15
|
+
*/
|
|
16
|
+
get(key) {
|
|
17
|
+
return this.config.get(key);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Set a configuration value
|
|
21
|
+
*/
|
|
22
|
+
set(key, value) {
|
|
23
|
+
this.config.set(key, value);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Get all configuration
|
|
27
|
+
*/
|
|
28
|
+
getAll() {
|
|
29
|
+
return this.config.store;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Set multiple configuration values
|
|
33
|
+
*/
|
|
34
|
+
setAll(config) {
|
|
35
|
+
Object.entries(config).forEach(([key, value]) => {
|
|
36
|
+
if (value !== undefined) {
|
|
37
|
+
this.config.set(key, value);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Delete a configuration value
|
|
43
|
+
*/
|
|
44
|
+
delete(key) {
|
|
45
|
+
this.config.delete(key);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Clear all configuration
|
|
49
|
+
*/
|
|
50
|
+
clear() {
|
|
51
|
+
this.config.clear();
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Check if configuration is complete
|
|
55
|
+
*/
|
|
56
|
+
isConfigured() {
|
|
57
|
+
return !!(this.config.get('supabaseUrl') &&
|
|
58
|
+
this.config.get('supabaseAnonKey') &&
|
|
59
|
+
this.config.get('apiKey') &&
|
|
60
|
+
this.config.get('organizationId'));
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Get the configuration file path
|
|
64
|
+
*/
|
|
65
|
+
getConfigPath() {
|
|
66
|
+
return this.config.path;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Singleton instance
|
|
70
|
+
export const config = new ConfigManager();
|
|
71
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface TranslationKey {
|
|
2
|
+
key: string;
|
|
3
|
+
translations: Record<string, string>;
|
|
4
|
+
description?: string;
|
|
5
|
+
module?: string;
|
|
6
|
+
}
|
|
7
|
+
export type ExportFormat = 'json' | 'json-nested' | 'ios' | 'android' | 'flutter';
|
|
8
|
+
export interface ExportResult {
|
|
9
|
+
content: string;
|
|
10
|
+
filename: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Export as flat JSON (simple key-value pairs)
|
|
14
|
+
*/
|
|
15
|
+
export declare function exportFlatJson(keys: TranslationKey[], languageCode: string): ExportResult;
|
|
16
|
+
/**
|
|
17
|
+
* Export as nested JSON (organized by key structure)
|
|
18
|
+
*/
|
|
19
|
+
export declare function exportNestedJson(keys: TranslationKey[], languageCode: string): ExportResult;
|
|
20
|
+
/**
|
|
21
|
+
* Export as iOS .strings format
|
|
22
|
+
*/
|
|
23
|
+
export declare function exportIosStrings(keys: TranslationKey[], languageCode: string): ExportResult;
|
|
24
|
+
/**
|
|
25
|
+
* Export as Android XML format
|
|
26
|
+
*/
|
|
27
|
+
export declare function exportAndroidXml(keys: TranslationKey[], languageCode: string): ExportResult;
|
|
28
|
+
/**
|
|
29
|
+
* Export as Flutter ARB format
|
|
30
|
+
*/
|
|
31
|
+
export declare function exportFlutterArb(keys: TranslationKey[], languageCode: string): ExportResult;
|
|
32
|
+
/**
|
|
33
|
+
* Export translations in the specified format
|
|
34
|
+
*/
|
|
35
|
+
export declare function exportTranslations(keys: TranslationKey[], languageCode: string, format: ExportFormat): ExportResult;
|
|
36
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper: Set nested value in object based on dot-notation key
|
|
3
|
+
*/
|
|
4
|
+
function setNestedValue(obj, path, value) {
|
|
5
|
+
const keys = path.split('.');
|
|
6
|
+
let current = obj;
|
|
7
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
8
|
+
const key = keys[i];
|
|
9
|
+
if (!current[key] || typeof current[key] !== 'object') {
|
|
10
|
+
current[key] = {};
|
|
11
|
+
}
|
|
12
|
+
current = current[key];
|
|
13
|
+
}
|
|
14
|
+
current[keys[keys.length - 1]] = value;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Helper: Convert i18n format {{param}} to Android format %1$s, %2$s, etc.
|
|
18
|
+
*/
|
|
19
|
+
function convertToAndroidFormat(text) {
|
|
20
|
+
const matches = text.match(/\{\{([^}]+)\}\}/g);
|
|
21
|
+
if (!matches)
|
|
22
|
+
return text;
|
|
23
|
+
let result = text;
|
|
24
|
+
matches.forEach((match, index) => {
|
|
25
|
+
const paramNum = index + 1;
|
|
26
|
+
result = result.replace(match, `%${paramNum}$s`);
|
|
27
|
+
});
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Helper: Convert i18n format {{param}} to iOS format %1$@, %2$@, etc.
|
|
32
|
+
*/
|
|
33
|
+
function convertToIosFormat(text) {
|
|
34
|
+
const matches = text.match(/\{\{([^}]+)\}\}/g);
|
|
35
|
+
if (!matches)
|
|
36
|
+
return text;
|
|
37
|
+
let result = text;
|
|
38
|
+
matches.forEach((match, index) => {
|
|
39
|
+
const paramNum = index + 1;
|
|
40
|
+
result = result.replace(match, `%${paramNum}$@`);
|
|
41
|
+
});
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Helper: Convert i18n format {{param}} to Flutter format {param}
|
|
46
|
+
*/
|
|
47
|
+
function convertToFlutterFormat(text) {
|
|
48
|
+
return text.replace(/\{\{([^}]+)\}\}/g, '{$1}');
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Helper: Extract placeholder names from Flutter format {param}
|
|
52
|
+
*/
|
|
53
|
+
function extractPlaceholders(text) {
|
|
54
|
+
const matches = text.match(/\{([^}]+)\}/g);
|
|
55
|
+
if (!matches)
|
|
56
|
+
return [];
|
|
57
|
+
return matches.map(match => match.slice(1, -1));
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Helper: Escape XML special characters
|
|
61
|
+
*/
|
|
62
|
+
function escapeXml(text) {
|
|
63
|
+
return text
|
|
64
|
+
.replace(/&/g, '&')
|
|
65
|
+
.replace(/</g, '<')
|
|
66
|
+
.replace(/>/g, '>')
|
|
67
|
+
.replace(/"/g, '"')
|
|
68
|
+
.replace(/'/g, ''');
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Helper: Escape .strings special characters
|
|
72
|
+
*/
|
|
73
|
+
function escapeStrings(text) {
|
|
74
|
+
return text
|
|
75
|
+
.replace(/\\/g, '\\\\')
|
|
76
|
+
.replace(/"/g, '\\"')
|
|
77
|
+
.replace(/\n/g, '\\n')
|
|
78
|
+
.replace(/\r/g, '\\r')
|
|
79
|
+
.replace(/\t/g, '\\t');
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Export as flat JSON (simple key-value pairs)
|
|
83
|
+
*/
|
|
84
|
+
export function exportFlatJson(keys, languageCode) {
|
|
85
|
+
const translations = {};
|
|
86
|
+
keys.forEach(key => {
|
|
87
|
+
const translation = key.translations[languageCode];
|
|
88
|
+
if (translation) {
|
|
89
|
+
translations[key.key] = translation;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
return {
|
|
93
|
+
content: JSON.stringify(translations, null, 2),
|
|
94
|
+
filename: `${languageCode}.json`
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Export as nested JSON (organized by key structure)
|
|
99
|
+
*/
|
|
100
|
+
export function exportNestedJson(keys, languageCode) {
|
|
101
|
+
const translations = {};
|
|
102
|
+
keys.forEach(key => {
|
|
103
|
+
const translation = key.translations[languageCode];
|
|
104
|
+
if (translation) {
|
|
105
|
+
setNestedValue(translations, key.key, translation);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
return {
|
|
109
|
+
content: JSON.stringify(translations, null, 2),
|
|
110
|
+
filename: `${languageCode}.json`
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Export as iOS .strings format
|
|
115
|
+
*/
|
|
116
|
+
export function exportIosStrings(keys, languageCode) {
|
|
117
|
+
let content = '';
|
|
118
|
+
keys.forEach(key => {
|
|
119
|
+
const translation = key.translations[languageCode];
|
|
120
|
+
if (translation) {
|
|
121
|
+
const iosTranslation = convertToIosFormat(translation);
|
|
122
|
+
const escapedTranslation = escapeStrings(iosTranslation);
|
|
123
|
+
if (key.description) {
|
|
124
|
+
content += `/* ${key.description} */\n`;
|
|
125
|
+
}
|
|
126
|
+
content += `"${key.key}" = "${escapedTranslation}";\n\n`;
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
return {
|
|
130
|
+
content,
|
|
131
|
+
filename: `Localizable.strings`
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Export as Android XML format
|
|
136
|
+
*/
|
|
137
|
+
export function exportAndroidXml(keys, languageCode) {
|
|
138
|
+
let xml = '<?xml version="1.0" encoding="utf-8"?>\n';
|
|
139
|
+
xml += '<resources>\n';
|
|
140
|
+
keys.forEach(key => {
|
|
141
|
+
const translation = key.translations[languageCode];
|
|
142
|
+
if (translation) {
|
|
143
|
+
const androidTranslation = convertToAndroidFormat(translation);
|
|
144
|
+
const escapedTranslation = escapeXml(androidTranslation);
|
|
145
|
+
if (key.description) {
|
|
146
|
+
xml += ` <!-- ${escapeXml(key.description)} -->\n`;
|
|
147
|
+
}
|
|
148
|
+
xml += ` <string name="${key.key}">${escapedTranslation}</string>\n`;
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
xml += '</resources>\n';
|
|
152
|
+
return {
|
|
153
|
+
content: xml,
|
|
154
|
+
filename: `strings.xml`
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Export as Flutter ARB format
|
|
159
|
+
*/
|
|
160
|
+
export function exportFlutterArb(keys, languageCode) {
|
|
161
|
+
const arb = {
|
|
162
|
+
'@@locale': languageCode,
|
|
163
|
+
'@@last_modified': new Date().toISOString()
|
|
164
|
+
};
|
|
165
|
+
keys.forEach(key => {
|
|
166
|
+
const translation = key.translations[languageCode];
|
|
167
|
+
if (translation) {
|
|
168
|
+
const flutterTranslation = convertToFlutterFormat(translation);
|
|
169
|
+
arb[key.key] = flutterTranslation;
|
|
170
|
+
// Add metadata
|
|
171
|
+
const metadata = {};
|
|
172
|
+
if (key.description) {
|
|
173
|
+
metadata.description = key.description;
|
|
174
|
+
}
|
|
175
|
+
// Extract placeholders from translation
|
|
176
|
+
const placeholders = extractPlaceholders(flutterTranslation);
|
|
177
|
+
if (placeholders.length > 0) {
|
|
178
|
+
metadata.placeholders = {};
|
|
179
|
+
placeholders.forEach(placeholder => {
|
|
180
|
+
metadata.placeholders[placeholder] = {
|
|
181
|
+
type: 'String'
|
|
182
|
+
};
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
if (Object.keys(metadata).length > 0) {
|
|
186
|
+
arb[`@${key.key}`] = metadata;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
return {
|
|
191
|
+
content: JSON.stringify(arb, null, 2),
|
|
192
|
+
filename: `intl_${languageCode}.arb`
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Export translations in the specified format
|
|
197
|
+
*/
|
|
198
|
+
export function exportTranslations(keys, languageCode, format) {
|
|
199
|
+
switch (format) {
|
|
200
|
+
case 'json':
|
|
201
|
+
return exportFlatJson(keys, languageCode);
|
|
202
|
+
case 'json-nested':
|
|
203
|
+
return exportNestedJson(keys, languageCode);
|
|
204
|
+
case 'ios':
|
|
205
|
+
return exportIosStrings(keys, languageCode);
|
|
206
|
+
case 'android':
|
|
207
|
+
return exportAndroidXml(keys, languageCode);
|
|
208
|
+
case 'flutter':
|
|
209
|
+
return exportFlutterArb(keys, languageCode);
|
|
210
|
+
default:
|
|
211
|
+
throw new Error(`Unsupported export format: ${format}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
//# sourceMappingURL=index.js.map
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { initCommand } from './commands/init.js';
|
|
5
|
+
import { authCommand, logoutCommand } from './commands/auth.js';
|
|
6
|
+
import { configCommand } from './commands/config.js';
|
|
7
|
+
import { projectsListCommand } from './commands/projects.js';
|
|
8
|
+
import { pullCommand } from './commands/pull.js';
|
|
9
|
+
import { debugListApiKeys, debugListOrganizations } from './commands/debug.js';
|
|
10
|
+
import { showLogo } from './utils/banner.js';
|
|
11
|
+
const program = new Command();
|
|
12
|
+
// Package info
|
|
13
|
+
const packageJson = {
|
|
14
|
+
name: 'langctl',
|
|
15
|
+
version: '0.1.0',
|
|
16
|
+
description: 'CLI-first translation management for developers'
|
|
17
|
+
};
|
|
18
|
+
program
|
|
19
|
+
.name('langctl')
|
|
20
|
+
.description(packageJson.description)
|
|
21
|
+
.version(packageJson.version, '-v, --version', 'Show version number');
|
|
22
|
+
// Init command
|
|
23
|
+
program
|
|
24
|
+
.command('init')
|
|
25
|
+
.description('Interactive setup wizard')
|
|
26
|
+
.action(async () => {
|
|
27
|
+
try {
|
|
28
|
+
await initCommand();
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
console.error(chalk.red(`Error: ${error.message}\n`));
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
// Auth command
|
|
36
|
+
program
|
|
37
|
+
.command('auth <api-key>')
|
|
38
|
+
.description('Authenticate and save credentials')
|
|
39
|
+
.action(async (apiKey) => {
|
|
40
|
+
try {
|
|
41
|
+
await authCommand(apiKey);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
console.error(chalk.red(`Error: ${error.message}\n`));
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
// Logout command
|
|
49
|
+
program
|
|
50
|
+
.command('logout')
|
|
51
|
+
.description('Clear authentication credentials')
|
|
52
|
+
.action(async () => {
|
|
53
|
+
try {
|
|
54
|
+
await logoutCommand();
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
console.error(chalk.red(`Error: ${error.message}\n`));
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
// Config command
|
|
62
|
+
program
|
|
63
|
+
.command('config')
|
|
64
|
+
.description('View current configuration')
|
|
65
|
+
.action(() => {
|
|
66
|
+
try {
|
|
67
|
+
configCommand();
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
console.error(chalk.red(`Error: ${error.message}\n`));
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
// Projects command
|
|
75
|
+
const projects = program
|
|
76
|
+
.command('projects')
|
|
77
|
+
.description('Manage projects');
|
|
78
|
+
projects
|
|
79
|
+
.command('list')
|
|
80
|
+
.description('Show all accessible projects')
|
|
81
|
+
.action(async () => {
|
|
82
|
+
try {
|
|
83
|
+
await projectsListCommand();
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
console.error(chalk.red(`Error: ${error.message}\n`));
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
// Pull command
|
|
91
|
+
program
|
|
92
|
+
.command('pull <project>')
|
|
93
|
+
.description('Pull translations from a project (all languages by default)')
|
|
94
|
+
.option('-l, --language <code>', 'Pull specific language only (default: all)')
|
|
95
|
+
.option('-f, --format <type>', 'Export format: json, json-nested, ios, android, flutter (default: json)')
|
|
96
|
+
.option('-d, --dir <path>', 'Output directory (default: ./translations)')
|
|
97
|
+
.option('-o, --output <path>', 'Specific output file path (optional)')
|
|
98
|
+
.option('--no-published-only', 'Include unpublished translations')
|
|
99
|
+
.action(async (project, options) => {
|
|
100
|
+
try {
|
|
101
|
+
await pullCommand(project, options);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
console.error(chalk.red(`Error: ${error.message}\n`));
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
// Debug command
|
|
109
|
+
const debug = program
|
|
110
|
+
.command('debug')
|
|
111
|
+
.description('Debug helpers');
|
|
112
|
+
debug
|
|
113
|
+
.command('list-api-keys')
|
|
114
|
+
.description('List API keys visible to current Supabase credentials')
|
|
115
|
+
.action(async () => {
|
|
116
|
+
try {
|
|
117
|
+
await debugListApiKeys();
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
console.error(chalk.red(`Error: ${error.message}\n`));
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
debug
|
|
125
|
+
.command('list-organizations')
|
|
126
|
+
.description('List organizations visible to current Supabase credentials')
|
|
127
|
+
.action(async () => {
|
|
128
|
+
try {
|
|
129
|
+
await debugListOrganizations();
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
console.error(chalk.red(`Error: ${error.message}\n`));
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
// Help command
|
|
137
|
+
program.on('--help', () => {
|
|
138
|
+
console.log('');
|
|
139
|
+
console.log('Examples:');
|
|
140
|
+
console.log(' $ langctl init');
|
|
141
|
+
console.log(' $ langctl auth lc_abc123...');
|
|
142
|
+
console.log(' $ langctl projects list');
|
|
143
|
+
console.log('');
|
|
144
|
+
console.log(' # Pull all languages as JSON to ./translations/i18n/');
|
|
145
|
+
console.log(' $ langctl pull litcode');
|
|
146
|
+
console.log('');
|
|
147
|
+
console.log(' # Pull to custom directory (e.g., ./src/i18n/)');
|
|
148
|
+
console.log(' $ langctl pull litcode --dir ./src/i18n');
|
|
149
|
+
console.log('');
|
|
150
|
+
console.log(' # Pull specific language');
|
|
151
|
+
console.log(' $ langctl pull litcode --language en');
|
|
152
|
+
console.log('');
|
|
153
|
+
console.log(' # Pull all languages in iOS format to ./ios/localization/');
|
|
154
|
+
console.log(' $ langctl pull litcode --format ios --dir ./ios/localization');
|
|
155
|
+
console.log('');
|
|
156
|
+
console.log('For more information, visit: https://langctl.com/docs');
|
|
157
|
+
console.log('');
|
|
158
|
+
});
|
|
159
|
+
// Parse arguments
|
|
160
|
+
program.parse(process.argv);
|
|
161
|
+
// Show help if no command provided
|
|
162
|
+
if (!process.argv.slice(2).length) {
|
|
163
|
+
showLogo();
|
|
164
|
+
program.outputHelp();
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { SupabaseClient } from '@supabase/supabase-js';
|
|
2
|
+
/**
|
|
3
|
+
* Initialize Supabase client
|
|
4
|
+
*/
|
|
5
|
+
export declare function initializeSupabase(url?: string, anonKey?: string): SupabaseClient;
|
|
6
|
+
/**
|
|
7
|
+
* Get existing Supabase client or throw error
|
|
8
|
+
*/
|
|
9
|
+
export declare function getSupabase(): SupabaseClient;
|
|
10
|
+
/**
|
|
11
|
+
* Reset Supabase client (useful for tests or configuration changes)
|
|
12
|
+
*/
|
|
13
|
+
export declare function resetSupabase(): void;
|
|
14
|
+
//# sourceMappingURL=supabase.d.ts.map
|
package/dist/supabase.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createClient } from '@supabase/supabase-js';
|
|
2
|
+
import { config } from './config.js';
|
|
3
|
+
let supabaseClient = null;
|
|
4
|
+
/**
|
|
5
|
+
* Initialize Supabase client
|
|
6
|
+
*/
|
|
7
|
+
export function initializeSupabase(url, anonKey) {
|
|
8
|
+
const supabaseUrl = url || config.get('supabaseUrl');
|
|
9
|
+
const supabaseAnonKey = anonKey || config.get('supabaseAnonKey');
|
|
10
|
+
if (!supabaseUrl || !supabaseAnonKey) {
|
|
11
|
+
throw new Error('Supabase credentials not found. Please run "langctl init" first.');
|
|
12
|
+
}
|
|
13
|
+
if (!supabaseClient) {
|
|
14
|
+
supabaseClient = createClient(supabaseUrl, supabaseAnonKey);
|
|
15
|
+
}
|
|
16
|
+
return supabaseClient;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Get existing Supabase client or throw error
|
|
20
|
+
*/
|
|
21
|
+
export function getSupabase() {
|
|
22
|
+
if (!supabaseClient) {
|
|
23
|
+
return initializeSupabase();
|
|
24
|
+
}
|
|
25
|
+
return supabaseClient;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Reset Supabase client (useful for tests or configuration changes)
|
|
29
|
+
*/
|
|
30
|
+
export function resetSupabase() {
|
|
31
|
+
supabaseClient = null;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=supabase.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
/**
|
|
3
|
+
* Langctl ASCII Logo - Emerald Green
|
|
4
|
+
*/
|
|
5
|
+
export function showLogo() {
|
|
6
|
+
const logo = `
|
|
7
|
+
██╗ █████╗ ███╗ ██╗ ██████╗ ██████╗████████╗██╗
|
|
8
|
+
██║ ██╔══██╗████╗ ██║██╔════╝ ██╔════╝╚══██╔══╝██║
|
|
9
|
+
██║ ███████║██╔██╗ ██║██║ ███╗██║ ██║ ██║
|
|
10
|
+
██║ ██╔══██║██║╚██╗██║██║ ██║██║ ██║ ██║
|
|
11
|
+
███████╗██║ ██║██║ ╚████║╚██████╔╝╚██████╗ ██║ ███████╗
|
|
12
|
+
╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚═╝ ╚══════╝
|
|
13
|
+
`;
|
|
14
|
+
console.log(chalk.hex('#10b981')(logo)); // Emerald-500
|
|
15
|
+
console.log(chalk.gray(' CLI-first translation management for developers\n'));
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Simple banner for quick commands
|
|
19
|
+
*/
|
|
20
|
+
export function showBanner(title) {
|
|
21
|
+
console.log(chalk.hex('#10b981').bold(`\n${title}`));
|
|
22
|
+
console.log(chalk.gray('─'.repeat(title.length)));
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=banner.js.map
|
package/package.json
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "langctl",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "CLI-first translation management for developers. Affordable, AI-powered, and built for modern workflows.",
|
|
5
|
-
"main": "index.js",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"type": "module",
|
|
6
7
|
"bin": {
|
|
7
|
-
"langctl": "./
|
|
8
|
+
"langctl": "./dist/index.js"
|
|
8
9
|
},
|
|
9
10
|
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"dev": "tsx src/index.ts",
|
|
13
|
+
"start": "node dist/index.js",
|
|
14
|
+
"prepublishOnly": "npm run build",
|
|
10
15
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
11
16
|
},
|
|
12
17
|
"keywords": [
|
|
@@ -33,5 +38,19 @@
|
|
|
33
38
|
"homepage": "https://langctl.com",
|
|
34
39
|
"engines": {
|
|
35
40
|
"node": ">=16.0.0"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@supabase/supabase-js": "^2.89.0",
|
|
44
|
+
"chalk": "^5.6.2",
|
|
45
|
+
"commander": "^14.0.2",
|
|
46
|
+
"conf": "^15.0.2",
|
|
47
|
+
"inquirer": "^13.1.0",
|
|
48
|
+
"ora": "^9.0.0"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/inquirer": "^9.0.9",
|
|
52
|
+
"@types/node": "^25.0.3",
|
|
53
|
+
"tsx": "^4.21.0",
|
|
54
|
+
"typescript": "^5.9.3"
|
|
36
55
|
}
|
|
37
56
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<resources>
|
|
3
|
+
<!-- Hero text -->
|
|
4
|
+
<string name="home.translation_mgmt">Translation Management</string>
|
|
5
|
+
<!-- Homepage wlecome message -->
|
|
6
|
+
<string name="home.welcome_message">Welcome Home</string>
|
|
7
|
+
<!-- This is a new item -->
|
|
8
|
+
<string name="new_item">New Item</string>
|
|
9
|
+
</resources>
|