@devcoda/lokal-cli 1.0.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/bin/lokal +3 -0
- package/package.json +30 -0
- package/src/commands/init.ts +117 -0
- package/src/commands/scan.ts +131 -0
- package/src/commands/translate.ts +153 -0
- package/src/index.ts +39 -0
- package/src/types/lokal-core.d.ts +127 -0
- package/tsconfig.json +17 -0
package/bin/lokal
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@devcoda/lokal-cli",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "CLI tool for LOKAL - Automates string extraction and AI translations",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"lokal": "./bin/lokal"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "tsup --watch",
|
|
11
|
+
"clean": "rm -rf dist",
|
|
12
|
+
"lint": "echo 'Use root lint command'"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"lokal-core": "*",
|
|
16
|
+
"chalk": "^4.1.2",
|
|
17
|
+
"commander": "^11.1.0",
|
|
18
|
+
"ora": "^5.4.1"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^20.10.0",
|
|
22
|
+
"eslint": "^8.57.0",
|
|
23
|
+
"tsup": "^8.0.0",
|
|
24
|
+
"typescript": "^5.3.0"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18.0.0"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
|
|
7
|
+
interface InitOptions {
|
|
8
|
+
locales?: string;
|
|
9
|
+
defaultLocale?: string;
|
|
10
|
+
force?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Create the initial lokal config file and directory structure
|
|
15
|
+
*/
|
|
16
|
+
export async function initCommand(options: InitOptions): Promise<void> {
|
|
17
|
+
const spinner = ora('Initializing LOKAL...').start();
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const projectRoot = process.cwd();
|
|
21
|
+
const configPath = path.join(projectRoot, 'lokal.config.js');
|
|
22
|
+
const localesDir = path.join(projectRoot, 'locales');
|
|
23
|
+
|
|
24
|
+
// Parse locales
|
|
25
|
+
const locales = options.locales
|
|
26
|
+
? options.locales.split(',').map(l => l.trim())
|
|
27
|
+
: ['en'];
|
|
28
|
+
|
|
29
|
+
const defaultLocale = options.defaultLocale || locales[0];
|
|
30
|
+
|
|
31
|
+
// Check if already initialized
|
|
32
|
+
if (fs.existsSync(configPath) && !options.force) {
|
|
33
|
+
spinner.warn(chalk.yellow('LOKAL is already initialized. Use --force to reinitialize.'));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Create config file
|
|
38
|
+
const configContent = `module.exports = {
|
|
39
|
+
// Supported locales
|
|
40
|
+
locales: ${JSON.stringify(locales)},
|
|
41
|
+
|
|
42
|
+
// Default locale
|
|
43
|
+
defaultLocale: '${defaultLocale}',
|
|
44
|
+
|
|
45
|
+
// Function name for translations (t("key"))
|
|
46
|
+
functionName: 't',
|
|
47
|
+
|
|
48
|
+
// Component name for translations (<T>key</T>)
|
|
49
|
+
componentName: 'T',
|
|
50
|
+
|
|
51
|
+
// Source directory to scan
|
|
52
|
+
sourceDir: './src',
|
|
53
|
+
|
|
54
|
+
// Output directory for locale files
|
|
55
|
+
outputDir: './locales',
|
|
56
|
+
|
|
57
|
+
// AI Translation settings (optional)
|
|
58
|
+
// ai: {
|
|
59
|
+
// provider: 'openai', // or 'gemini'
|
|
60
|
+
// apiKey: process.env.OPENAI_API_KEY,
|
|
61
|
+
// model: 'gpt-4'
|
|
62
|
+
// }
|
|
63
|
+
};
|
|
64
|
+
`;
|
|
65
|
+
|
|
66
|
+
fs.writeFileSync(configPath, configContent, 'utf-8');
|
|
67
|
+
spinner.succeed(chalk.green(`Created ${chalk.bold('lokal.config.js')}`));
|
|
68
|
+
|
|
69
|
+
// Create locales directory with default locale
|
|
70
|
+
if (!fs.existsSync(localesDir)) {
|
|
71
|
+
fs.mkdirSync(localesDir, { recursive: true });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Create default locale file
|
|
75
|
+
const defaultLocalePath = path.join(localesDir, `${defaultLocale}.json`);
|
|
76
|
+
if (!fs.existsSync(defaultLocalePath)) {
|
|
77
|
+
const initialData = {
|
|
78
|
+
_meta: {
|
|
79
|
+
generated: new Date().toISOString(),
|
|
80
|
+
description: 'Default locale file'
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
fs.writeFileSync(defaultLocalePath, JSON.stringify(initialData, null, 2), 'utf-8');
|
|
84
|
+
spinner.succeed(chalk.green(`Created ${chalk.bold(`locales/${defaultLocale}.json`)}`));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Create .gitignore entry for locale files if .gitignore exists
|
|
88
|
+
const gitignorePath = path.join(projectRoot, '.gitignore');
|
|
89
|
+
if (fs.existsSync(gitignorePath)) {
|
|
90
|
+
const gitignoreContent = fs.readFileSync(gitignorePath, 'utf-8');
|
|
91
|
+
if (!gitignoreContent.includes('/locales/')) {
|
|
92
|
+
fs.appendFileSync(gitignorePath, '\n# LOKAL translations\nlocales/\n');
|
|
93
|
+
spinner.succeed(chalk.green('Updated .gitignore'));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.log(chalk.bold('\n✓ LOKAL initialized successfully!'));
|
|
98
|
+
console.log(chalk.gray('\nNext steps:'));
|
|
99
|
+
console.log(chalk.gray(' 1. Add translation strings to your code using t("key") or <T>key</T>'));
|
|
100
|
+
console.log(chalk.gray(' 2. Run ') + chalk.cyan('npx lokal scan') + chalk.gray(' to extract strings'));
|
|
101
|
+
console.log(chalk.gray(' 3. Run ') + chalk.cyan('npx lokal translate') + chalk.gray(' to translate with AI'));
|
|
102
|
+
|
|
103
|
+
} catch (error) {
|
|
104
|
+
spinner.fail(chalk.red(`Failed to initialize: ${error}`));
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function registerInitCommand(program: Command): void {
|
|
110
|
+
program
|
|
111
|
+
.command('init')
|
|
112
|
+
.description('Initialize LOKAL in your project')
|
|
113
|
+
.option('-l, --locales <locales>', 'Comma-separated list of locales', 'en')
|
|
114
|
+
.option('-d, --default-locale <locale>', 'Default locale', 'en')
|
|
115
|
+
.option('-f, --force', 'Force reinitialization', false)
|
|
116
|
+
.action(initCommand);
|
|
117
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import { ASTParser, ConfigLoader, FileStorage, type ExtractedString } from 'lokal-core';
|
|
6
|
+
|
|
7
|
+
interface ScanOptions {
|
|
8
|
+
config?: string;
|
|
9
|
+
output?: string;
|
|
10
|
+
verbose?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Scan source files for translation strings and update locale files
|
|
15
|
+
*/
|
|
16
|
+
export async function scanCommand(options: ScanOptions): Promise<void> {
|
|
17
|
+
const spinner = ora('Scanning for translation strings...').start();
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
// Load config
|
|
21
|
+
const configLoader = new ConfigLoader();
|
|
22
|
+
let config;
|
|
23
|
+
|
|
24
|
+
if (options.config) {
|
|
25
|
+
config = configLoader.loadSync(options.config);
|
|
26
|
+
} else {
|
|
27
|
+
config = await configLoader.load();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const projectRoot = process.cwd();
|
|
31
|
+
const sourceDir = path.resolve(projectRoot, config.sourceDir);
|
|
32
|
+
const outputDir = options.output
|
|
33
|
+
? path.resolve(projectRoot, options.output)
|
|
34
|
+
: path.resolve(projectRoot, config.outputDir);
|
|
35
|
+
|
|
36
|
+
spinner.text = `Scanning ${chalk.cyan(sourceDir)}...`;
|
|
37
|
+
|
|
38
|
+
// Create parser
|
|
39
|
+
const parser = new ASTParser({
|
|
40
|
+
filePath: sourceDir,
|
|
41
|
+
functionName: config.functionName,
|
|
42
|
+
componentName: config.componentName,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Scan directory
|
|
46
|
+
const result = parser.scanDirectory(sourceDir);
|
|
47
|
+
|
|
48
|
+
if (result.errors.length > 0 && options.verbose) {
|
|
49
|
+
for (const error of result.errors) {
|
|
50
|
+
spinner.warn(chalk.yellow(error));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
spinner.succeed(chalk.green(`Found ${chalk.bold(result.strings.length)} translation strings`));
|
|
55
|
+
|
|
56
|
+
if (result.strings.length === 0) {
|
|
57
|
+
spinner.info(chalk.gray('No strings found. Make sure to use t("key") or <T>key</T> in your code.'));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Create storage
|
|
62
|
+
const storage = new FileStorage(outputDir);
|
|
63
|
+
|
|
64
|
+
// Create unique key-value map
|
|
65
|
+
const uniqueStrings = new Map<string, ExtractedString>();
|
|
66
|
+
for (const str of result.strings) {
|
|
67
|
+
uniqueStrings.set(str.key, str);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Get the default locale
|
|
71
|
+
const defaultLocale = config.defaultLocale;
|
|
72
|
+
const existingLocale = storage.loadLocale(defaultLocale);
|
|
73
|
+
|
|
74
|
+
// Merge with existing keys
|
|
75
|
+
let existingData: Record<string, any> = {};
|
|
76
|
+
if (existingLocale) {
|
|
77
|
+
existingData = existingLocale.data as Record<string, any>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Convert extracted strings to flat key-value object
|
|
81
|
+
const newData: Record<string, string> = {};
|
|
82
|
+
for (const [key, value] of uniqueStrings) {
|
|
83
|
+
// Use the key itself as the translation value for now
|
|
84
|
+
newData[key] = existingData[key] || value.value;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Save the merged data
|
|
88
|
+
const mergedData = storage.mergeLocaleData(defaultLocale, newData);
|
|
89
|
+
storage.saveLocale(defaultLocale, mergedData);
|
|
90
|
+
|
|
91
|
+
spinner.succeed(chalk.green(`Updated ${chalk.bold(`locales/${defaultLocale}.json`)}`));
|
|
92
|
+
|
|
93
|
+
// Show sample of extracted strings
|
|
94
|
+
if (options.verbose) {
|
|
95
|
+
console.log(chalk.bold('\nExtracted strings:'));
|
|
96
|
+
const sampleKeys = Array.from(uniqueStrings.keys()).slice(0, 10);
|
|
97
|
+
for (const key of sampleKeys) {
|
|
98
|
+
console.log(chalk.gray(` • ${key}`));
|
|
99
|
+
}
|
|
100
|
+
if (uniqueStrings.size > 10) {
|
|
101
|
+
console.log(chalk.gray(` ... and ${uniqueStrings.size - 10} more`));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Check for other locales that need translation
|
|
106
|
+
const locales = storage.getAvailableLocales();
|
|
107
|
+
const otherLocales = locales.filter(l => l !== defaultLocale);
|
|
108
|
+
|
|
109
|
+
if (otherLocales.length > 0) {
|
|
110
|
+
console.log(chalk.gray(`\nOther locales detected: ${otherLocales.join(', ')}`));
|
|
111
|
+
console.log(chalk.gray('Run ') + chalk.cyan('npx lokal translate') + chalk.gray(' to translate missing strings'));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
} catch (error) {
|
|
115
|
+
spinner.fail(chalk.red(`Scan failed: ${error}`));
|
|
116
|
+
if (options.verbose) {
|
|
117
|
+
console.error(error);
|
|
118
|
+
}
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function registerScanCommand(program: Command): void {
|
|
124
|
+
program
|
|
125
|
+
.command('scan')
|
|
126
|
+
.description('Scan source files for translation strings')
|
|
127
|
+
.option('-c, --config <path>', 'Path to config file')
|
|
128
|
+
.option('-o, --output <path>', 'Output directory for locale files')
|
|
129
|
+
.option('-v, --verbose', 'Verbose output', false)
|
|
130
|
+
.action(scanCommand);
|
|
131
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import {
|
|
6
|
+
ConfigLoader,
|
|
7
|
+
FileStorage,
|
|
8
|
+
AITranslator,
|
|
9
|
+
TranslationProviderFactory,
|
|
10
|
+
type LocaleData
|
|
11
|
+
} from 'lokal-core';
|
|
12
|
+
|
|
13
|
+
interface TranslateOptions {
|
|
14
|
+
config?: string;
|
|
15
|
+
locale?: string;
|
|
16
|
+
all?: boolean;
|
|
17
|
+
verbose?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Translate missing strings using AI
|
|
22
|
+
*/
|
|
23
|
+
export async function translateCommand(options: TranslateOptions): Promise<void> {
|
|
24
|
+
const spinner = ora('Loading configuration...').start();
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
// Load config
|
|
28
|
+
const configLoader = new ConfigLoader();
|
|
29
|
+
let config;
|
|
30
|
+
|
|
31
|
+
if (options.config) {
|
|
32
|
+
config = configLoader.loadSync(options.config);
|
|
33
|
+
} else {
|
|
34
|
+
config = await configLoader.load();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Check if AI is configured
|
|
38
|
+
if (!config.ai) {
|
|
39
|
+
spinner.fail(chalk.red('AI provider not configured. Add ai configuration to lokal.config.js'));
|
|
40
|
+
console.log(chalk.gray('\nExample configuration:'));
|
|
41
|
+
console.log(chalk.gray(' ai: {'));
|
|
42
|
+
console.log(chalk.gray(' provider: "openai",'));
|
|
43
|
+
console.log(chalk.gray(' apiKey: process.env.OPENAI_API_KEY'));
|
|
44
|
+
console.log(chalk.gray(' }'));
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const apiKey = config.ai.apiKey || process.env.OPENAI_API_KEY || process.env.GEMINI_API_KEY;
|
|
49
|
+
if (!apiKey) {
|
|
50
|
+
spinner.fail(chalk.red('No API key found. Set ai.apiKey in config or environment variable.'));
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
spinner.text = 'Initializing AI translator...';
|
|
55
|
+
|
|
56
|
+
// Create provider
|
|
57
|
+
const provider = TranslationProviderFactory.create(
|
|
58
|
+
config.ai.provider,
|
|
59
|
+
apiKey,
|
|
60
|
+
config.ai.model
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// Create translator
|
|
64
|
+
const translator = new AITranslator(provider);
|
|
65
|
+
|
|
66
|
+
const projectRoot = process.cwd();
|
|
67
|
+
const outputDir = path.resolve(projectRoot, config.outputDir);
|
|
68
|
+
const storage = new FileStorage(outputDir);
|
|
69
|
+
|
|
70
|
+
spinner.succeed('AI translator ready');
|
|
71
|
+
|
|
72
|
+
// Determine which locales to translate
|
|
73
|
+
let targetLocales: string[] = [];
|
|
74
|
+
const availableLocales = config.locales || [];
|
|
75
|
+
|
|
76
|
+
if (options.all) {
|
|
77
|
+
targetLocales = availableLocales.filter(l => l !== config.defaultLocale);
|
|
78
|
+
} else if (options.locale) {
|
|
79
|
+
targetLocales = [options.locale];
|
|
80
|
+
} else {
|
|
81
|
+
// Default: translate to first non-default locale
|
|
82
|
+
targetLocales = availableLocales.filter(l => l !== config.defaultLocale).slice(0, 1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (targetLocales.length === 0) {
|
|
86
|
+
spinner.warn(chalk.yellow('No target locales to translate. Add more locales to your config.'));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Load source locale data
|
|
91
|
+
const sourceLocale = config.defaultLocale;
|
|
92
|
+
const sourceData = storage.loadLocale(sourceLocale);
|
|
93
|
+
|
|
94
|
+
if (!sourceData) {
|
|
95
|
+
spinner.fail(chalk.red(`Source locale ${sourceLocale} not found. Run 'lokal scan' first.`));
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Translate to each target locale
|
|
100
|
+
for (const targetLocale of targetLocales) {
|
|
101
|
+
const translateSpinner = ora(`Translating to ${chalk.cyan(targetLocale)}...`).start();
|
|
102
|
+
|
|
103
|
+
// Load existing target data (if any)
|
|
104
|
+
const targetLocaleFile = storage.loadLocale(targetLocale);
|
|
105
|
+
const targetData: LocaleData = targetLocaleFile ? targetLocaleFile.data : {};
|
|
106
|
+
|
|
107
|
+
// Translate missing keys
|
|
108
|
+
const translatedData = await translator.translateMissingKeys(
|
|
109
|
+
sourceData.data,
|
|
110
|
+
targetData,
|
|
111
|
+
sourceLocale,
|
|
112
|
+
targetLocale
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// Save translated data
|
|
116
|
+
storage.saveLocale(targetLocale, translatedData);
|
|
117
|
+
|
|
118
|
+
translateSpinner.succeed(chalk.green(`Translated to ${chalk.bold(targetLocale)}`));
|
|
119
|
+
|
|
120
|
+
if (options.verbose) {
|
|
121
|
+
// Show sample translations
|
|
122
|
+
const keys = Object.keys(translatedData).slice(0, 5);
|
|
123
|
+
for (const key of keys) {
|
|
124
|
+
const value = translatedData[key];
|
|
125
|
+
if (typeof value === 'string') {
|
|
126
|
+
console.log(chalk.gray(` ${key}: ${value}`));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
console.log(chalk.bold('\n✓ Translation complete!'));
|
|
133
|
+
console.log(chalk.gray(`\nRun `) + chalk.cyan('npx lokal scan') + chalk.gray(' to see updated translations'));
|
|
134
|
+
|
|
135
|
+
} catch (error) {
|
|
136
|
+
spinner.fail(chalk.red(`Translation failed: ${error}`));
|
|
137
|
+
if (options.verbose) {
|
|
138
|
+
console.error(error);
|
|
139
|
+
}
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function registerTranslateCommand(program: Command): void {
|
|
145
|
+
program
|
|
146
|
+
.command('translate')
|
|
147
|
+
.description('Translate missing strings using AI')
|
|
148
|
+
.option('-c, --config <path>', 'Path to config file')
|
|
149
|
+
.option('-l, --locale <locale>', 'Specific locale to translate')
|
|
150
|
+
.option('-a, --all', 'Translate all locales', false)
|
|
151
|
+
.option('-v, --verbose', 'Verbose output', false)
|
|
152
|
+
.action(translateCommand);
|
|
153
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { registerInitCommand } from './commands/init';
|
|
6
|
+
import { registerScanCommand } from './commands/scan';
|
|
7
|
+
import { registerTranslateCommand } from './commands/translate';
|
|
8
|
+
|
|
9
|
+
const program = new Command();
|
|
10
|
+
|
|
11
|
+
program
|
|
12
|
+
.name('lokal')
|
|
13
|
+
.description('AI-powered localization ecosystem for React and React Native')
|
|
14
|
+
.version('1.0.0');
|
|
15
|
+
|
|
16
|
+
// Register commands
|
|
17
|
+
registerInitCommand(program);
|
|
18
|
+
registerScanCommand(program);
|
|
19
|
+
registerTranslateCommand(program);
|
|
20
|
+
|
|
21
|
+
// Global options
|
|
22
|
+
program
|
|
23
|
+
.option('-v, --verbose', 'Verbose output')
|
|
24
|
+
.hook('preAction', (thisCommand) => {
|
|
25
|
+
const opts = thisCommand.opts();
|
|
26
|
+
if (opts.verbose) {
|
|
27
|
+
process.env.LOKAL_VERBOSE = 'true';
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Handle unknown commands
|
|
32
|
+
program.on('command:*', () => {
|
|
33
|
+
console.error(chalk.red(`Invalid command: ${program.args.join(' ')}`));
|
|
34
|
+
console.log(chalk.gray(`See ${chalk.cyan('--help')} for a list of available commands.`));
|
|
35
|
+
process.exit(1);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Run the program
|
|
39
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
declare module 'lokal-core' {
|
|
2
|
+
export class ASTParser {
|
|
3
|
+
constructor(options: {
|
|
4
|
+
filePath: string;
|
|
5
|
+
functionName?: string;
|
|
6
|
+
componentName?: string;
|
|
7
|
+
});
|
|
8
|
+
scanDirectory(dirPath: string): ScanResult;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ExtractedString {
|
|
12
|
+
key: string;
|
|
13
|
+
value: string;
|
|
14
|
+
location?: {
|
|
15
|
+
file: string;
|
|
16
|
+
line: number;
|
|
17
|
+
column: number;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ScanResult {
|
|
22
|
+
strings: ExtractedString[];
|
|
23
|
+
errors: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ScanOptions {
|
|
27
|
+
filePath: string;
|
|
28
|
+
functionName?: string;
|
|
29
|
+
componentName?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class ConfigLoader {
|
|
33
|
+
load(configPath?: string): Promise<LokalConfig>;
|
|
34
|
+
loadSync(configPath?: string): LokalConfig;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface LokalConfig {
|
|
38
|
+
sourceDir: string;
|
|
39
|
+
outputDir: string;
|
|
40
|
+
defaultLocale: string;
|
|
41
|
+
functionName: string;
|
|
42
|
+
componentName: string;
|
|
43
|
+
locales?: string[];
|
|
44
|
+
ai?: {
|
|
45
|
+
provider: 'openai' | 'gemini';
|
|
46
|
+
apiKey: string;
|
|
47
|
+
model?: string;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const defaultConfig: LokalConfig;
|
|
52
|
+
|
|
53
|
+
export class FileStorage {
|
|
54
|
+
constructor(outputDir: string);
|
|
55
|
+
loadLocale(locale: string): LocaleFile | null;
|
|
56
|
+
saveLocale(locale: string, data: LocaleData): void;
|
|
57
|
+
mergeLocaleData(locale: string, newData: Record<string, string>): LocaleData;
|
|
58
|
+
getAvailableLocales(): string[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface LocaleData {
|
|
62
|
+
[key: string]: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface LocaleFile {
|
|
66
|
+
locale: string;
|
|
67
|
+
data: LocaleData;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export class ContentHasher {
|
|
71
|
+
static hash(content: string): string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export class AITranslator {
|
|
75
|
+
constructor(provider: TranslationProvider, options?: {
|
|
76
|
+
batchSize?: number;
|
|
77
|
+
});
|
|
78
|
+
translateBatch(request: TranslationBatch): Promise<TranslationResult[]>;
|
|
79
|
+
translateMissingKeys(
|
|
80
|
+
sourceData: Record<string, string>,
|
|
81
|
+
targetData: Record<string, string>,
|
|
82
|
+
sourceLocale: string,
|
|
83
|
+
targetLocale: string
|
|
84
|
+
): Promise<Record<string, string>>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface TranslationProvider {
|
|
88
|
+
translate(request: TranslationRequest): Promise<TranslationResult>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface TranslationRequest {
|
|
92
|
+
text: string;
|
|
93
|
+
sourceLocale: string;
|
|
94
|
+
targetLocale: string;
|
|
95
|
+
context?: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface TranslationResult {
|
|
99
|
+
translatedText: string;
|
|
100
|
+
success: boolean;
|
|
101
|
+
error?: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface TranslationBatch {
|
|
105
|
+
items: TranslationRequest[];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export class OpenAIProvider implements TranslationProvider {
|
|
109
|
+
constructor(apiKey: string, options?: {
|
|
110
|
+
model?: string;
|
|
111
|
+
temperature?: number;
|
|
112
|
+
});
|
|
113
|
+
translate(request: TranslationRequest): Promise<TranslationResult>;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export class GeminiProvider implements TranslationProvider {
|
|
117
|
+
constructor(apiKey: string, options?: {
|
|
118
|
+
model?: string;
|
|
119
|
+
temperature?: number;
|
|
120
|
+
});
|
|
121
|
+
translate(request: TranslationRequest): Promise<TranslationResult>;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export class TranslationProviderFactory {
|
|
125
|
+
static create(provider: 'openai' | 'gemini', apiKey: string, model?: string): TranslationProvider;
|
|
126
|
+
}
|
|
127
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "./dist",
|
|
5
|
+
"rootDir": "./src",
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"target": "ES2020"
|
|
8
|
+
},
|
|
9
|
+
"include": [
|
|
10
|
+
"src/**/*"
|
|
11
|
+
],
|
|
12
|
+
"exclude": [
|
|
13
|
+
"node_modules",
|
|
14
|
+
"dist",
|
|
15
|
+
"**/*.test.ts"
|
|
16
|
+
]
|
|
17
|
+
}
|