langctl 0.0.1 → 0.1.1
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 +300 -30
- package/SECURITY_FIX.md +167 -0
- 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 +33 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +30 -0
- package/dist/commands/debug.d.ts +3 -0
- package/dist/commands/debug.js +57 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +87 -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 +47 -0
- package/dist/config.js +69 -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 +30 -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
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { writeFileSync, mkdirSync } from 'fs';
|
|
4
|
+
import { dirname, join } from 'path';
|
|
5
|
+
import { getSupabase } from '../supabase.js';
|
|
6
|
+
import { isAuthenticated, getOrganizationId } from '../auth.js';
|
|
7
|
+
import { exportTranslations } from '../exporters/index.js';
|
|
8
|
+
/**
|
|
9
|
+
* Get platform-specific output directory
|
|
10
|
+
*/
|
|
11
|
+
function getPlatformDirectory(format) {
|
|
12
|
+
const platformDirs = {
|
|
13
|
+
'json': 'i18n',
|
|
14
|
+
'json-nested': 'i18n',
|
|
15
|
+
'ios': 'ios',
|
|
16
|
+
'android': 'android',
|
|
17
|
+
'flutter': 'flutter'
|
|
18
|
+
};
|
|
19
|
+
return platformDirs[format] || 'i18n';
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Resolve project identifier (name, slug, or UUID) to project UUID
|
|
23
|
+
*/
|
|
24
|
+
async function resolveProjectId(identifier, organizationId) {
|
|
25
|
+
const supabase = getSupabase();
|
|
26
|
+
// Check if it's already a UUID (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)
|
|
27
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
28
|
+
if (uuidRegex.test(identifier)) {
|
|
29
|
+
// Verify it exists
|
|
30
|
+
const { data } = await supabase
|
|
31
|
+
.from('projects')
|
|
32
|
+
.select('id')
|
|
33
|
+
.eq('id', identifier)
|
|
34
|
+
.eq('organization_id', organizationId)
|
|
35
|
+
.is('deleted_at', null)
|
|
36
|
+
.single();
|
|
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;
|
|
49
|
+
}
|
|
50
|
+
// Try to match by name (case-insensitive)
|
|
51
|
+
const { data: byName } = await supabase
|
|
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;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
export async function pullCommand(projectIdentifier, options) {
|
|
64
|
+
// Check authentication
|
|
65
|
+
if (!isAuthenticated()) {
|
|
66
|
+
console.log(chalk.red('✗ Not authenticated. Please run "langctl auth <api-key>" first.\n'));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const organizationId = getOrganizationId();
|
|
70
|
+
if (!organizationId) {
|
|
71
|
+
console.log(chalk.red('✗ Organization ID not found. Please run "langctl init" again.\n'));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
// Set defaults
|
|
75
|
+
const format = (options.format || 'json');
|
|
76
|
+
const publishedOnly = options.publishedOnly !== false; // Default to true
|
|
77
|
+
const specificLanguage = options.language; // If not specified, pull all languages
|
|
78
|
+
const baseDir = options.dir || './translations'; // Default to ./translations
|
|
79
|
+
// Validate format
|
|
80
|
+
const validFormats = ['json', 'json-nested', 'ios', 'android', 'flutter'];
|
|
81
|
+
if (!validFormats.includes(format)) {
|
|
82
|
+
console.log(chalk.red(`✗ Invalid format: ${format}`));
|
|
83
|
+
console.log(chalk.yellow(`Valid formats: ${validFormats.join(', ')}\n`));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const spinner = ora('Resolving project...').start();
|
|
87
|
+
try {
|
|
88
|
+
const supabase = getSupabase();
|
|
89
|
+
// Resolve project identifier to UUID
|
|
90
|
+
const projectId = await resolveProjectId(projectIdentifier, organizationId);
|
|
91
|
+
if (!projectId) {
|
|
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];
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
// Pull all languages
|
|
122
|
+
languagesToPull = project.languages;
|
|
123
|
+
}
|
|
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
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
spinner.fail(chalk.red('Failed to pull translations'));
|
|
191
|
+
console.error(chalk.red(`Error: ${error.message}\n`));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
//# sourceMappingURL=pull.js.map
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export interface LangctlConfig {
|
|
2
|
+
apiKey?: string;
|
|
3
|
+
organizationId?: string;
|
|
4
|
+
organizationName?: string;
|
|
5
|
+
defaultProject?: string;
|
|
6
|
+
defaultLanguage?: string;
|
|
7
|
+
}
|
|
8
|
+
declare class ConfigManager {
|
|
9
|
+
private config;
|
|
10
|
+
private configDir;
|
|
11
|
+
constructor();
|
|
12
|
+
/**
|
|
13
|
+
* Get a configuration value
|
|
14
|
+
*/
|
|
15
|
+
get<K extends keyof LangctlConfig>(key: K): LangctlConfig[K] | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* Set a configuration value
|
|
18
|
+
*/
|
|
19
|
+
set<K extends keyof LangctlConfig>(key: K, value: LangctlConfig[K]): void;
|
|
20
|
+
/**
|
|
21
|
+
* Get all configuration
|
|
22
|
+
*/
|
|
23
|
+
getAll(): LangctlConfig;
|
|
24
|
+
/**
|
|
25
|
+
* Set multiple configuration values
|
|
26
|
+
*/
|
|
27
|
+
setAll(config: Partial<LangctlConfig>): void;
|
|
28
|
+
/**
|
|
29
|
+
* Delete a configuration value
|
|
30
|
+
*/
|
|
31
|
+
delete(key: keyof LangctlConfig): void;
|
|
32
|
+
/**
|
|
33
|
+
* Clear all configuration
|
|
34
|
+
*/
|
|
35
|
+
clear(): void;
|
|
36
|
+
/**
|
|
37
|
+
* Check if user is authenticated
|
|
38
|
+
*/
|
|
39
|
+
isConfigured(): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Get the configuration file path
|
|
42
|
+
*/
|
|
43
|
+
getConfigPath(): string;
|
|
44
|
+
}
|
|
45
|
+
export declare const config: ConfigManager;
|
|
46
|
+
export {};
|
|
47
|
+
//# sourceMappingURL=config.d.ts.map
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
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 user is authenticated
|
|
55
|
+
*/
|
|
56
|
+
isConfigured() {
|
|
57
|
+
return !!(this.config.get('apiKey') &&
|
|
58
|
+
this.config.get('organizationId'));
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Get the configuration file path
|
|
62
|
+
*/
|
|
63
|
+
getConfigPath() {
|
|
64
|
+
return this.config.path;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Singleton instance
|
|
68
|
+
export const config = new ConfigManager();
|
|
69
|
+
//# 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