@devcoda/lokal-cli 1.2.0 → 1.3.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/dist/index.js +425 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +407 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +7 -2
- package/src/commands/wrap.ts +178 -0
- package/src/index.ts +2 -0
- package/src/types/lokal-core.d.ts +30 -0
- package/tsconfig.json +0 -17
package/dist/index.js
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// packages/cli/src/index.ts
|
|
27
|
+
var import_commander = require("commander");
|
|
28
|
+
var import_chalk5 = __toESM(require("chalk"));
|
|
29
|
+
|
|
30
|
+
// packages/cli/src/commands/init.ts
|
|
31
|
+
var fs = __toESM(require("fs"));
|
|
32
|
+
var path = __toESM(require("path"));
|
|
33
|
+
var import_chalk = __toESM(require("chalk"));
|
|
34
|
+
var import_ora = __toESM(require("ora"));
|
|
35
|
+
async function initCommand(options) {
|
|
36
|
+
const spinner = (0, import_ora.default)("Initializing LOKAL...").start();
|
|
37
|
+
try {
|
|
38
|
+
const projectRoot = process.cwd();
|
|
39
|
+
const configPath = path.join(projectRoot, "lokal.config.js");
|
|
40
|
+
const localesDir = path.join(projectRoot, "locales");
|
|
41
|
+
const locales = options.locales ? options.locales.split(",").map((l) => l.trim()) : ["en"];
|
|
42
|
+
const defaultLocale = options.defaultLocale || locales[0];
|
|
43
|
+
if (fs.existsSync(configPath) && !options.force) {
|
|
44
|
+
spinner.warn(import_chalk.default.yellow("LOKAL is already initialized. Use --force to reinitialize."));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const configContent = `module.exports = {
|
|
48
|
+
// Supported locales
|
|
49
|
+
locales: ${JSON.stringify(locales)},
|
|
50
|
+
|
|
51
|
+
// Default locale
|
|
52
|
+
defaultLocale: '${defaultLocale}',
|
|
53
|
+
|
|
54
|
+
// Function name for translations (t("key"))
|
|
55
|
+
functionName: 't',
|
|
56
|
+
|
|
57
|
+
// Component name for translations (<T>key</T>)
|
|
58
|
+
componentName: 'T',
|
|
59
|
+
|
|
60
|
+
// Source directory to scan
|
|
61
|
+
sourceDir: './src',
|
|
62
|
+
|
|
63
|
+
// Output directory for locale files
|
|
64
|
+
outputDir: './locales',
|
|
65
|
+
|
|
66
|
+
// AI Translation settings (optional)
|
|
67
|
+
// ai: {
|
|
68
|
+
// provider: 'openai', // or 'gemini'
|
|
69
|
+
// apiKey: process.env.OPENAI_API_KEY,
|
|
70
|
+
// model: 'gpt-4'
|
|
71
|
+
// }
|
|
72
|
+
};
|
|
73
|
+
`;
|
|
74
|
+
fs.writeFileSync(configPath, configContent, "utf-8");
|
|
75
|
+
spinner.succeed(import_chalk.default.green(`Created ${import_chalk.default.bold("lokal.config.js")}`));
|
|
76
|
+
if (!fs.existsSync(localesDir)) {
|
|
77
|
+
fs.mkdirSync(localesDir, { recursive: true });
|
|
78
|
+
}
|
|
79
|
+
const defaultLocalePath = path.join(localesDir, `${defaultLocale}.json`);
|
|
80
|
+
if (!fs.existsSync(defaultLocalePath)) {
|
|
81
|
+
const initialData = {
|
|
82
|
+
_meta: {
|
|
83
|
+
generated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
84
|
+
description: "Default locale file"
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
fs.writeFileSync(defaultLocalePath, JSON.stringify(initialData, null, 2), "utf-8");
|
|
88
|
+
spinner.succeed(import_chalk.default.green(`Created ${import_chalk.default.bold(`locales/${defaultLocale}.json`)}`));
|
|
89
|
+
}
|
|
90
|
+
const gitignorePath = path.join(projectRoot, ".gitignore");
|
|
91
|
+
if (fs.existsSync(gitignorePath)) {
|
|
92
|
+
const gitignoreContent = fs.readFileSync(gitignorePath, "utf-8");
|
|
93
|
+
if (!gitignoreContent.includes("/locales/")) {
|
|
94
|
+
fs.appendFileSync(gitignorePath, "\n# LOKAL translations\nlocales/\n");
|
|
95
|
+
spinner.succeed(import_chalk.default.green("Updated .gitignore"));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
console.log(import_chalk.default.bold("\n\u2713 LOKAL initialized successfully!"));
|
|
99
|
+
console.log(import_chalk.default.gray("\nNext steps:"));
|
|
100
|
+
console.log(import_chalk.default.gray(' 1. Add translation strings to your code using t("key") or <T>key</T>'));
|
|
101
|
+
console.log(import_chalk.default.gray(" 2. Run ") + import_chalk.default.cyan("npx lokal scan") + import_chalk.default.gray(" to extract strings"));
|
|
102
|
+
console.log(import_chalk.default.gray(" 3. Run ") + import_chalk.default.cyan("npx lokal translate") + import_chalk.default.gray(" to translate with AI"));
|
|
103
|
+
} catch (error) {
|
|
104
|
+
spinner.fail(import_chalk.default.red(`Failed to initialize: ${error}`));
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function registerInitCommand(program2) {
|
|
109
|
+
program2.command("init").description("Initialize LOKAL in your project").option("-l, --locales <locales>", "Comma-separated list of locales", "en").option("-d, --default-locale <locale>", "Default locale", "en").option("-f, --force", "Force reinitialization", false).action(initCommand);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// packages/cli/src/commands/scan.ts
|
|
113
|
+
var path2 = __toESM(require("path"));
|
|
114
|
+
var import_chalk2 = __toESM(require("chalk"));
|
|
115
|
+
var import_ora2 = __toESM(require("ora"));
|
|
116
|
+
var import_lokal_core = require("lokal-core");
|
|
117
|
+
async function scanCommand(options) {
|
|
118
|
+
const spinner = (0, import_ora2.default)("Scanning for translation strings...").start();
|
|
119
|
+
try {
|
|
120
|
+
const configLoader = new import_lokal_core.ConfigLoader();
|
|
121
|
+
let config;
|
|
122
|
+
if (options.config) {
|
|
123
|
+
config = configLoader.loadSync(options.config);
|
|
124
|
+
} else {
|
|
125
|
+
config = await configLoader.load();
|
|
126
|
+
}
|
|
127
|
+
const projectRoot = process.cwd();
|
|
128
|
+
const sourceDir = path2.resolve(projectRoot, config.sourceDir);
|
|
129
|
+
const outputDir = options.output ? path2.resolve(projectRoot, options.output) : path2.resolve(projectRoot, config.outputDir);
|
|
130
|
+
spinner.text = `Scanning ${import_chalk2.default.cyan(sourceDir)}...`;
|
|
131
|
+
const parser = new import_lokal_core.ASTParser({
|
|
132
|
+
filePath: sourceDir,
|
|
133
|
+
functionName: config.functionName,
|
|
134
|
+
componentName: config.componentName
|
|
135
|
+
});
|
|
136
|
+
const result = parser.scanDirectory(sourceDir);
|
|
137
|
+
if (result.errors.length > 0 && options.verbose) {
|
|
138
|
+
for (const error of result.errors) {
|
|
139
|
+
spinner.warn(import_chalk2.default.yellow(error));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
spinner.succeed(import_chalk2.default.green(`Found ${import_chalk2.default.bold(result.strings.length)} translation strings`));
|
|
143
|
+
if (result.strings.length === 0) {
|
|
144
|
+
spinner.info(import_chalk2.default.gray('No strings found. Make sure to use t("key") or <T>key</T> in your code.'));
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const storage = new import_lokal_core.FileStorage(outputDir);
|
|
148
|
+
const uniqueStrings = /* @__PURE__ */ new Map();
|
|
149
|
+
for (const str of result.strings) {
|
|
150
|
+
uniqueStrings.set(str.key, str);
|
|
151
|
+
}
|
|
152
|
+
const defaultLocale = config.defaultLocale;
|
|
153
|
+
const existingLocale = storage.loadLocale(defaultLocale);
|
|
154
|
+
let existingData = {};
|
|
155
|
+
if (existingLocale) {
|
|
156
|
+
existingData = existingLocale.data;
|
|
157
|
+
}
|
|
158
|
+
const newData = {};
|
|
159
|
+
for (const [key, value] of uniqueStrings) {
|
|
160
|
+
newData[key] = existingData[key] || value.value;
|
|
161
|
+
}
|
|
162
|
+
const mergedData = storage.mergeLocaleData(defaultLocale, newData);
|
|
163
|
+
storage.saveLocale(defaultLocale, mergedData);
|
|
164
|
+
spinner.succeed(import_chalk2.default.green(`Updated ${import_chalk2.default.bold(`locales/${defaultLocale}.json`)}`));
|
|
165
|
+
if (options.verbose) {
|
|
166
|
+
console.log(import_chalk2.default.bold("\nExtracted strings:"));
|
|
167
|
+
const sampleKeys = Array.from(uniqueStrings.keys()).slice(0, 10);
|
|
168
|
+
for (const key of sampleKeys) {
|
|
169
|
+
console.log(import_chalk2.default.gray(` \u2022 ${key}`));
|
|
170
|
+
}
|
|
171
|
+
if (uniqueStrings.size > 10) {
|
|
172
|
+
console.log(import_chalk2.default.gray(` ... and ${uniqueStrings.size - 10} more`));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const locales = storage.getAvailableLocales();
|
|
176
|
+
const otherLocales = locales.filter((l) => l !== defaultLocale);
|
|
177
|
+
if (otherLocales.length > 0) {
|
|
178
|
+
console.log(import_chalk2.default.gray(`
|
|
179
|
+
Other locales detected: ${otherLocales.join(", ")}`));
|
|
180
|
+
console.log(import_chalk2.default.gray("Run ") + import_chalk2.default.cyan("npx lokal translate") + import_chalk2.default.gray(" to translate missing strings"));
|
|
181
|
+
}
|
|
182
|
+
} catch (error) {
|
|
183
|
+
spinner.fail(import_chalk2.default.red(`Scan failed: ${error}`));
|
|
184
|
+
if (options.verbose) {
|
|
185
|
+
console.error(error);
|
|
186
|
+
}
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function registerScanCommand(program2) {
|
|
191
|
+
program2.command("scan").description("Scan source files for translation strings").option("-c, --config <path>", "Path to config file").option("-o, --output <path>", "Output directory for locale files").option("-v, --verbose", "Verbose output", false).action(scanCommand);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// packages/cli/src/commands/translate.ts
|
|
195
|
+
var path3 = __toESM(require("path"));
|
|
196
|
+
var import_chalk3 = __toESM(require("chalk"));
|
|
197
|
+
var import_ora3 = __toESM(require("ora"));
|
|
198
|
+
var import_lokal_core2 = require("lokal-core");
|
|
199
|
+
async function translateCommand(options) {
|
|
200
|
+
const spinner = (0, import_ora3.default)("Loading configuration...").start();
|
|
201
|
+
try {
|
|
202
|
+
const configLoader = new import_lokal_core2.ConfigLoader();
|
|
203
|
+
let config;
|
|
204
|
+
if (options.config) {
|
|
205
|
+
config = configLoader.loadSync(options.config);
|
|
206
|
+
} else {
|
|
207
|
+
config = await configLoader.load();
|
|
208
|
+
}
|
|
209
|
+
if (!config.ai) {
|
|
210
|
+
spinner.fail(import_chalk3.default.red("AI provider not configured. Add ai configuration to lokal.config.js"));
|
|
211
|
+
console.log(import_chalk3.default.gray("\nExample configuration:"));
|
|
212
|
+
console.log(import_chalk3.default.gray(" ai: {"));
|
|
213
|
+
console.log(import_chalk3.default.gray(' provider: "openai",'));
|
|
214
|
+
console.log(import_chalk3.default.gray(" apiKey: process.env.OPENAI_API_KEY"));
|
|
215
|
+
console.log(import_chalk3.default.gray(" }"));
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
const apiKey = config.ai.apiKey || process.env.OPENAI_API_KEY || process.env.GEMINI_API_KEY;
|
|
219
|
+
if (!apiKey) {
|
|
220
|
+
spinner.fail(import_chalk3.default.red("No API key found. Set ai.apiKey in config or environment variable."));
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
spinner.text = "Initializing AI translator...";
|
|
224
|
+
const provider = import_lokal_core2.TranslationProviderFactory.create(
|
|
225
|
+
config.ai.provider,
|
|
226
|
+
apiKey,
|
|
227
|
+
config.ai.model
|
|
228
|
+
);
|
|
229
|
+
const translator = new import_lokal_core2.AITranslator(provider);
|
|
230
|
+
const projectRoot = process.cwd();
|
|
231
|
+
const outputDir = path3.resolve(projectRoot, config.outputDir);
|
|
232
|
+
const storage = new import_lokal_core2.FileStorage(outputDir);
|
|
233
|
+
spinner.succeed("AI translator ready");
|
|
234
|
+
let targetLocales = [];
|
|
235
|
+
const availableLocales = config.locales || [];
|
|
236
|
+
if (options.all) {
|
|
237
|
+
targetLocales = availableLocales.filter((l) => l !== config.defaultLocale);
|
|
238
|
+
} else if (options.locale) {
|
|
239
|
+
targetLocales = [options.locale];
|
|
240
|
+
} else {
|
|
241
|
+
targetLocales = availableLocales.filter((l) => l !== config.defaultLocale).slice(0, 1);
|
|
242
|
+
}
|
|
243
|
+
if (targetLocales.length === 0) {
|
|
244
|
+
spinner.warn(import_chalk3.default.yellow("No target locales to translate. Add more locales to your config."));
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const sourceLocale = config.defaultLocale;
|
|
248
|
+
const sourceData = storage.loadLocale(sourceLocale);
|
|
249
|
+
if (!sourceData) {
|
|
250
|
+
spinner.fail(import_chalk3.default.red(`Source locale ${sourceLocale} not found. Run 'lokal scan' first.`));
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
for (const targetLocale of targetLocales) {
|
|
254
|
+
const translateSpinner = (0, import_ora3.default)(`Translating to ${import_chalk3.default.cyan(targetLocale)}...`).start();
|
|
255
|
+
const targetLocaleFile = storage.loadLocale(targetLocale);
|
|
256
|
+
const targetData = targetLocaleFile ? targetLocaleFile.data : {};
|
|
257
|
+
const translatedData = await translator.translateMissingKeys(
|
|
258
|
+
sourceData.data,
|
|
259
|
+
targetData,
|
|
260
|
+
sourceLocale,
|
|
261
|
+
targetLocale
|
|
262
|
+
);
|
|
263
|
+
storage.saveLocale(targetLocale, translatedData);
|
|
264
|
+
translateSpinner.succeed(import_chalk3.default.green(`Translated to ${import_chalk3.default.bold(targetLocale)}`));
|
|
265
|
+
if (options.verbose) {
|
|
266
|
+
const keys = Object.keys(translatedData).slice(0, 5);
|
|
267
|
+
for (const key of keys) {
|
|
268
|
+
const value = translatedData[key];
|
|
269
|
+
if (typeof value === "string") {
|
|
270
|
+
console.log(import_chalk3.default.gray(` ${key}: ${value}`));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
console.log(import_chalk3.default.bold("\n\u2713 Translation complete!"));
|
|
276
|
+
console.log(import_chalk3.default.gray(`
|
|
277
|
+
Run `) + import_chalk3.default.cyan("npx lokal scan") + import_chalk3.default.gray(" to see updated translations"));
|
|
278
|
+
} catch (error) {
|
|
279
|
+
spinner.fail(import_chalk3.default.red(`Translation failed: ${error}`));
|
|
280
|
+
if (options.verbose) {
|
|
281
|
+
console.error(error);
|
|
282
|
+
}
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
function registerTranslateCommand(program2) {
|
|
287
|
+
program2.command("translate").description("Translate missing strings using AI").option("-c, --config <path>", "Path to config file").option("-l, --locale <locale>", "Specific locale to translate").option("-a, --all", "Translate all locales", false).option("-v, --verbose", "Verbose output", false).action(translateCommand);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// packages/cli/src/commands/wrap.ts
|
|
291
|
+
var path4 = __toESM(require("path"));
|
|
292
|
+
var fs2 = __toESM(require("fs"));
|
|
293
|
+
var import_chalk4 = __toESM(require("chalk"));
|
|
294
|
+
var import_ora4 = __toESM(require("ora"));
|
|
295
|
+
var import_lokal_core3 = require("lokal-core");
|
|
296
|
+
async function wrapCommand(options) {
|
|
297
|
+
const spinner = (0, import_ora4.default)("Preparing to wrap strings...").start();
|
|
298
|
+
try {
|
|
299
|
+
const configLoader = new import_lokal_core3.ConfigLoader();
|
|
300
|
+
let config;
|
|
301
|
+
if (options.config) {
|
|
302
|
+
config = configLoader.loadSync(options.config);
|
|
303
|
+
} else {
|
|
304
|
+
config = await configLoader.load();
|
|
305
|
+
}
|
|
306
|
+
const projectRoot = process.cwd();
|
|
307
|
+
const sourceDir = options.src ? path4.resolve(projectRoot, options.src) : path4.resolve(projectRoot, config.sourceDir);
|
|
308
|
+
if (!fs2.existsSync(sourceDir)) {
|
|
309
|
+
spinner.fail(import_chalk4.default.red(`Source directory not found: ${sourceDir}`));
|
|
310
|
+
process.exit(1);
|
|
311
|
+
}
|
|
312
|
+
const wrapper = new import_lokal_core3.ASTWrapper({
|
|
313
|
+
functionName: options.function || config.functionName || "t",
|
|
314
|
+
componentName: config.componentName || "T"
|
|
315
|
+
});
|
|
316
|
+
spinner.text = `Scanning ${import_chalk4.default.cyan(sourceDir)} for translatable strings...`;
|
|
317
|
+
const results = wrapper.wrapDirectory(sourceDir, [".js", ".jsx", ".ts", ".tsx"], true);
|
|
318
|
+
let totalStrings = 0;
|
|
319
|
+
const fileResults = [];
|
|
320
|
+
for (const result of results.results) {
|
|
321
|
+
if (result.wrapped.length > 0) {
|
|
322
|
+
fileResults.push({
|
|
323
|
+
file: result.file,
|
|
324
|
+
wrapped: result.wrapped
|
|
325
|
+
});
|
|
326
|
+
totalStrings += result.wrapped.length;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
spinner.succeed(import_chalk4.default.green(`Found ${import_chalk4.default.bold(totalStrings)} strings to wrap in ${import_chalk4.default.bold(results.modifiedFiles)} files`));
|
|
330
|
+
if (totalStrings === 0) {
|
|
331
|
+
spinner.info(import_chalk4.default.gray("No strings found that need wrapping."));
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (!options.dryRun) {
|
|
335
|
+
console.log(import_chalk4.default.bold("\nPreview (first 10 files):"));
|
|
336
|
+
let count = 0;
|
|
337
|
+
for (const fileResult of fileResults.slice(0, 10)) {
|
|
338
|
+
console.log(import_chalk4.default.cyan(`
|
|
339
|
+
${path4.relative(projectRoot, fileResult.file)}:`));
|
|
340
|
+
for (const wrapped of fileResult.wrapped.slice(0, 5)) {
|
|
341
|
+
console.log(import_chalk4.default.gray(` "${wrapped.original}" \u2192 ${wrapped.wrapped}`));
|
|
342
|
+
count++;
|
|
343
|
+
}
|
|
344
|
+
if (fileResult.wrapped.length > 5) {
|
|
345
|
+
console.log(import_chalk4.default.gray(` ... and ${fileResult.wrapped.length - 5} more`));
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (fileResults.length > 10) {
|
|
349
|
+
console.log(import_chalk4.default.gray(`
|
|
350
|
+
... and ${fileResults.length - 10} more files`));
|
|
351
|
+
}
|
|
352
|
+
console.log(import_chalk4.default.bold("\n\u26A0 This will modify your source files!"));
|
|
353
|
+
console.log(import_chalk4.default.gray("Use --dry-run to preview without making changes"));
|
|
354
|
+
} else {
|
|
355
|
+
console.log(import_chalk4.default.bold("\nDry Run - Files that would be modified:"));
|
|
356
|
+
for (const fileResult of fileResults) {
|
|
357
|
+
console.log(import_chalk4.default.cyan(` ${path4.relative(projectRoot, fileResult.file)}`) + import_chalk4.default.gray(` (${fileResult.wrapped.length} strings)`));
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (options.dryRun) {
|
|
361
|
+
spinner.info(import_chalk4.default.yellow("Dry run complete. No files were modified."));
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const confirmSpinner = (0, import_ora4.default)("Applying changes...").start();
|
|
365
|
+
let modifiedCount = 0;
|
|
366
|
+
for (const fileResult of fileResults) {
|
|
367
|
+
const result = wrapper.wrapFile(fileResult.file);
|
|
368
|
+
if (result.modified) {
|
|
369
|
+
modifiedCount++;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
confirmSpinner.succeed(import_chalk4.default.green(`Modified ${import_chalk4.default.bold(modifiedCount)} files`));
|
|
373
|
+
const outputDir = path4.resolve(projectRoot, config.outputDir);
|
|
374
|
+
const storage = new import_lokal_core3.FileStorage(outputDir);
|
|
375
|
+
const newKeys = {};
|
|
376
|
+
for (const fileResult of fileResults) {
|
|
377
|
+
for (const wrapped of fileResult.wrapped) {
|
|
378
|
+
newKeys[wrapped.key] = wrapped.original;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
const defaultLocale = config.defaultLocale;
|
|
382
|
+
const existingLocale = storage.loadLocale(defaultLocale);
|
|
383
|
+
let existingData = {};
|
|
384
|
+
if (existingLocale) {
|
|
385
|
+
existingData = existingLocale.data;
|
|
386
|
+
}
|
|
387
|
+
const mergedData = storage.mergeLocaleData(defaultLocale, newKeys);
|
|
388
|
+
storage.saveLocale(defaultLocale, mergedData);
|
|
389
|
+
console.log(import_chalk4.default.green(`
|
|
390
|
+
\u2713 Added ${import_chalk4.default.bold(Object.keys(newKeys).length)} new keys to locales/${defaultLocale}.json`));
|
|
391
|
+
console.log(import_chalk4.default.bold("\nNext steps:"));
|
|
392
|
+
console.log(import_chalk4.default.gray(" 1. Review the changes in your source files"));
|
|
393
|
+
console.log(import_chalk4.default.gray(" 2. Run ") + import_chalk4.default.cyan("npx lokal translate") + import_chalk4.default.gray(" to translate to other locales"));
|
|
394
|
+
} catch (error) {
|
|
395
|
+
spinner.fail(import_chalk4.default.red(`Wrap failed: ${error}`));
|
|
396
|
+
if (options.verbose) {
|
|
397
|
+
console.error(error);
|
|
398
|
+
}
|
|
399
|
+
process.exit(1);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
function registerWrapCommand(program2) {
|
|
403
|
+
program2.command("wrap").description("Automatically wrap translatable strings in source files").option("-c, --config <path>", "Path to config file").option("-s, --src <path>", "Source directory to scan").option("-f, --function <name>", "Translation function name", "t").option("-d, --dry-run", "Preview changes without modifying files", false).option("-v, --verbose", "Verbose output", false).action(wrapCommand);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// packages/cli/src/index.ts
|
|
407
|
+
var program = new import_commander.Command();
|
|
408
|
+
program.name("lokal").description("AI-powered localization ecosystem for React and React Native").version("1.0.0");
|
|
409
|
+
registerInitCommand(program);
|
|
410
|
+
registerScanCommand(program);
|
|
411
|
+
registerTranslateCommand(program);
|
|
412
|
+
registerWrapCommand(program);
|
|
413
|
+
program.option("-v, --verbose", "Verbose output").hook("preAction", (thisCommand) => {
|
|
414
|
+
const opts = thisCommand.opts();
|
|
415
|
+
if (opts.verbose) {
|
|
416
|
+
process.env.LOKAL_VERBOSE = "true";
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
program.on("command:*", () => {
|
|
420
|
+
console.error(import_chalk5.default.red(`Invalid command: ${program.args.join(" ")}`));
|
|
421
|
+
console.log(import_chalk5.default.gray(`See ${import_chalk5.default.cyan("--help")} for a list of available commands.`));
|
|
422
|
+
process.exit(1);
|
|
423
|
+
});
|
|
424
|
+
program.parse(process.argv);
|
|
425
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/commands/init.ts","../src/commands/scan.ts","../src/commands/translate.ts","../src/commands/wrap.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { Command } from 'commander';\nimport chalk from 'chalk';\nimport { registerInitCommand } from './commands/init';\nimport { registerScanCommand } from './commands/scan';\nimport { registerTranslateCommand } from './commands/translate';\nimport { registerWrapCommand } from './commands/wrap';\n\nconst program = new Command();\n\nprogram\n .name('lokal')\n .description('AI-powered localization ecosystem for React and React Native')\n .version('1.0.0');\n\n// Register commands\nregisterInitCommand(program);\nregisterScanCommand(program);\nregisterTranslateCommand(program);\nregisterWrapCommand(program);\n\n// Global options\nprogram\n .option('-v, --verbose', 'Verbose output')\n .hook('preAction', (thisCommand) => {\n const opts = thisCommand.opts();\n if (opts.verbose) {\n process.env.LOKAL_VERBOSE = 'true';\n }\n });\n\n// Handle unknown commands\nprogram.on('command:*', () => {\n console.error(chalk.red(`Invalid command: ${program.args.join(' ')}`));\n console.log(chalk.gray(`See ${chalk.cyan('--help')} for a list of available commands.`));\n process.exit(1);\n});\n\n// Run the program\nprogram.parse(process.argv);\n","import * as fs from 'fs';\nimport * as path from 'path';\nimport { Command } from 'commander';\nimport chalk from 'chalk';\nimport ora from 'ora';\n\ninterface InitOptions {\n locales?: string;\n defaultLocale?: string;\n force?: boolean;\n}\n\n/**\n * Create the initial lokal config file and directory structure\n */\nexport async function initCommand(options: InitOptions): Promise<void> {\n const spinner = ora('Initializing LOKAL...').start();\n\n try {\n const projectRoot = process.cwd();\n const configPath = path.join(projectRoot, 'lokal.config.js');\n const localesDir = path.join(projectRoot, 'locales');\n\n // Parse locales\n const locales = options.locales\n ? options.locales.split(',').map(l => l.trim())\n : ['en'];\n\n const defaultLocale = options.defaultLocale || locales[0];\n\n // Check if already initialized\n if (fs.existsSync(configPath) && !options.force) {\n spinner.warn(chalk.yellow('LOKAL is already initialized. Use --force to reinitialize.'));\n return;\n }\n\n // Create config file\n const configContent = `module.exports = {\n // Supported locales\n locales: ${JSON.stringify(locales)},\n \n // Default locale\n defaultLocale: '${defaultLocale}',\n \n // Function name for translations (t(\"key\"))\n functionName: 't',\n \n // Component name for translations (<T>key</T>)\n componentName: 'T',\n \n // Source directory to scan\n sourceDir: './src',\n \n // Output directory for locale files\n outputDir: './locales',\n \n // AI Translation settings (optional)\n // ai: {\n // provider: 'openai', // or 'gemini'\n // apiKey: process.env.OPENAI_API_KEY,\n // model: 'gpt-4'\n // }\n};\n`;\n\n fs.writeFileSync(configPath, configContent, 'utf-8');\n spinner.succeed(chalk.green(`Created ${chalk.bold('lokal.config.js')}`));\n\n // Create locales directory with default locale\n if (!fs.existsSync(localesDir)) {\n fs.mkdirSync(localesDir, { recursive: true });\n }\n\n // Create default locale file\n const defaultLocalePath = path.join(localesDir, `${defaultLocale}.json`);\n if (!fs.existsSync(defaultLocalePath)) {\n const initialData = {\n _meta: {\n generated: new Date().toISOString(),\n description: 'Default locale file'\n }\n };\n fs.writeFileSync(defaultLocalePath, JSON.stringify(initialData, null, 2), 'utf-8');\n spinner.succeed(chalk.green(`Created ${chalk.bold(`locales/${defaultLocale}.json`)}`));\n }\n\n // Create .gitignore entry for locale files if .gitignore exists\n const gitignorePath = path.join(projectRoot, '.gitignore');\n if (fs.existsSync(gitignorePath)) {\n const gitignoreContent = fs.readFileSync(gitignorePath, 'utf-8');\n if (!gitignoreContent.includes('/locales/')) {\n fs.appendFileSync(gitignorePath, '\\n# LOKAL translations\\nlocales/\\n');\n spinner.succeed(chalk.green('Updated .gitignore'));\n }\n }\n\n console.log(chalk.bold('\\n✓ LOKAL initialized successfully!'));\n console.log(chalk.gray('\\nNext steps:'));\n console.log(chalk.gray(' 1. Add translation strings to your code using t(\"key\") or <T>key</T>'));\n console.log(chalk.gray(' 2. Run ') + chalk.cyan('npx lokal scan') + chalk.gray(' to extract strings'));\n console.log(chalk.gray(' 3. Run ') + chalk.cyan('npx lokal translate') + chalk.gray(' to translate with AI'));\n\n } catch (error) {\n spinner.fail(chalk.red(`Failed to initialize: ${error}`));\n process.exit(1);\n }\n}\n\nexport function registerInitCommand(program: Command): void {\n program\n .command('init')\n .description('Initialize LOKAL in your project')\n .option('-l, --locales <locales>', 'Comma-separated list of locales', 'en')\n .option('-d, --default-locale <locale>', 'Default locale', 'en')\n .option('-f, --force', 'Force reinitialization', false)\n .action(initCommand);\n}\n","import * as path from 'path';\nimport { Command } from 'commander';\nimport chalk from 'chalk';\nimport ora from 'ora';\nimport { ASTParser, ConfigLoader, FileStorage, type ExtractedString } from 'lokal-core';\n\ninterface ScanOptions {\n config?: string;\n output?: string;\n verbose?: boolean;\n}\n\n/**\n * Scan source files for translation strings and update locale files\n */\nexport async function scanCommand(options: ScanOptions): Promise<void> {\n const spinner = ora('Scanning for translation strings...').start();\n\n try {\n // Load config\n const configLoader = new ConfigLoader();\n let config;\n\n if (options.config) {\n config = configLoader.loadSync(options.config);\n } else {\n config = await configLoader.load();\n }\n\n const projectRoot = process.cwd();\n const sourceDir = path.resolve(projectRoot, config.sourceDir);\n const outputDir = options.output\n ? path.resolve(projectRoot, options.output)\n : path.resolve(projectRoot, config.outputDir);\n\n spinner.text = `Scanning ${chalk.cyan(sourceDir)}...`;\n\n // Create parser\n const parser = new ASTParser({\n filePath: sourceDir,\n functionName: config.functionName,\n componentName: config.componentName,\n });\n\n // Scan directory\n const result = parser.scanDirectory(sourceDir);\n\n if (result.errors.length > 0 && options.verbose) {\n for (const error of result.errors) {\n spinner.warn(chalk.yellow(error));\n }\n }\n\n spinner.succeed(chalk.green(`Found ${chalk.bold(result.strings.length)} translation strings`));\n\n if (result.strings.length === 0) {\n spinner.info(chalk.gray('No strings found. Make sure to use t(\"key\") or <T>key</T> in your code.'));\n return;\n }\n\n // Create storage\n const storage = new FileStorage(outputDir);\n\n // Create unique key-value map\n const uniqueStrings = new Map<string, ExtractedString>();\n for (const str of result.strings) {\n uniqueStrings.set(str.key, str);\n }\n\n // Get the default locale\n const defaultLocale = config.defaultLocale;\n const existingLocale = storage.loadLocale(defaultLocale);\n\n // Merge with existing keys\n let existingData: Record<string, any> = {};\n if (existingLocale) {\n existingData = existingLocale.data as Record<string, any>;\n }\n\n // Convert extracted strings to flat key-value object\n const newData: Record<string, string> = {};\n for (const [key, value] of uniqueStrings) {\n // Use the key itself as the translation value for now\n newData[key] = existingData[key] || value.value;\n }\n\n // Save the merged data\n const mergedData = storage.mergeLocaleData(defaultLocale, newData);\n storage.saveLocale(defaultLocale, mergedData);\n\n spinner.succeed(chalk.green(`Updated ${chalk.bold(`locales/${defaultLocale}.json`)}`));\n\n // Show sample of extracted strings\n if (options.verbose) {\n console.log(chalk.bold('\\nExtracted strings:'));\n const sampleKeys = Array.from(uniqueStrings.keys()).slice(0, 10);\n for (const key of sampleKeys) {\n console.log(chalk.gray(` • ${key}`));\n }\n if (uniqueStrings.size > 10) {\n console.log(chalk.gray(` ... and ${uniqueStrings.size - 10} more`));\n }\n }\n\n // Check for other locales that need translation\n const locales = storage.getAvailableLocales();\n const otherLocales = locales.filter(l => l !== defaultLocale);\n\n if (otherLocales.length > 0) {\n console.log(chalk.gray(`\\nOther locales detected: ${otherLocales.join(', ')}`));\n console.log(chalk.gray('Run ') + chalk.cyan('npx lokal translate') + chalk.gray(' to translate missing strings'));\n }\n\n } catch (error) {\n spinner.fail(chalk.red(`Scan failed: ${error}`));\n if (options.verbose) {\n console.error(error);\n }\n process.exit(1);\n }\n}\n\nexport function registerScanCommand(program: Command): void {\n program\n .command('scan')\n .description('Scan source files for translation strings')\n .option('-c, --config <path>', 'Path to config file')\n .option('-o, --output <path>', 'Output directory for locale files')\n .option('-v, --verbose', 'Verbose output', false)\n .action(scanCommand);\n}\n","import * as path from 'path';\nimport { Command } from 'commander';\nimport chalk from 'chalk';\nimport ora from 'ora';\nimport {\n ConfigLoader,\n FileStorage,\n AITranslator,\n TranslationProviderFactory,\n type LocaleData\n} from 'lokal-core';\n\ninterface TranslateOptions {\n config?: string;\n locale?: string;\n all?: boolean;\n verbose?: boolean;\n}\n\n/**\n * Translate missing strings using AI\n */\nexport async function translateCommand(options: TranslateOptions): Promise<void> {\n const spinner = ora('Loading configuration...').start();\n\n try {\n // Load config\n const configLoader = new ConfigLoader();\n let config;\n\n if (options.config) {\n config = configLoader.loadSync(options.config);\n } else {\n config = await configLoader.load();\n }\n\n // Check if AI is configured\n if (!config.ai) {\n spinner.fail(chalk.red('AI provider not configured. Add ai configuration to lokal.config.js'));\n console.log(chalk.gray('\\nExample configuration:'));\n console.log(chalk.gray(' ai: {'));\n console.log(chalk.gray(' provider: \"openai\",'));\n console.log(chalk.gray(' apiKey: process.env.OPENAI_API_KEY'));\n console.log(chalk.gray(' }'));\n process.exit(1);\n }\n\n const apiKey = config.ai.apiKey || process.env.OPENAI_API_KEY || process.env.GEMINI_API_KEY;\n if (!apiKey) {\n spinner.fail(chalk.red('No API key found. Set ai.apiKey in config or environment variable.'));\n process.exit(1);\n }\n\n spinner.text = 'Initializing AI translator...';\n\n // Create provider\n const provider = TranslationProviderFactory.create(\n config.ai.provider,\n apiKey,\n config.ai.model\n );\n\n // Create translator\n const translator = new AITranslator(provider);\n\n const projectRoot = process.cwd();\n const outputDir = path.resolve(projectRoot, config.outputDir);\n const storage = new FileStorage(outputDir);\n\n spinner.succeed('AI translator ready');\n\n // Determine which locales to translate\n let targetLocales: string[] = [];\n const availableLocales = config.locales || [];\n\n if (options.all) {\n targetLocales = availableLocales.filter(l => l !== config.defaultLocale);\n } else if (options.locale) {\n targetLocales = [options.locale];\n } else {\n // Default: translate to first non-default locale\n targetLocales = availableLocales.filter(l => l !== config.defaultLocale).slice(0, 1);\n }\n\n if (targetLocales.length === 0) {\n spinner.warn(chalk.yellow('No target locales to translate. Add more locales to your config.'));\n return;\n }\n\n // Load source locale data\n const sourceLocale = config.defaultLocale;\n const sourceData = storage.loadLocale(sourceLocale);\n\n if (!sourceData) {\n spinner.fail(chalk.red(`Source locale ${sourceLocale} not found. Run 'lokal scan' first.`));\n process.exit(1);\n }\n\n // Translate to each target locale\n for (const targetLocale of targetLocales) {\n const translateSpinner = ora(`Translating to ${chalk.cyan(targetLocale)}...`).start();\n\n // Load existing target data (if any)\n const targetLocaleFile = storage.loadLocale(targetLocale);\n const targetData: LocaleData = targetLocaleFile ? targetLocaleFile.data : {};\n\n // Translate missing keys\n const translatedData = await translator.translateMissingKeys(\n sourceData.data,\n targetData,\n sourceLocale,\n targetLocale\n );\n\n // Save translated data\n storage.saveLocale(targetLocale, translatedData);\n\n translateSpinner.succeed(chalk.green(`Translated to ${chalk.bold(targetLocale)}`));\n\n if (options.verbose) {\n // Show sample translations\n const keys = Object.keys(translatedData).slice(0, 5);\n for (const key of keys) {\n const value = translatedData[key];\n if (typeof value === 'string') {\n console.log(chalk.gray(` ${key}: ${value}`));\n }\n }\n }\n }\n\n console.log(chalk.bold('\\n✓ Translation complete!'));\n console.log(chalk.gray(`\\nRun `) + chalk.cyan('npx lokal scan') + chalk.gray(' to see updated translations'));\n\n } catch (error) {\n spinner.fail(chalk.red(`Translation failed: ${error}`));\n if (options.verbose) {\n console.error(error);\n }\n process.exit(1);\n }\n}\n\nexport function registerTranslateCommand(program: Command): void {\n program\n .command('translate')\n .description('Translate missing strings using AI')\n .option('-c, --config <path>', 'Path to config file')\n .option('-l, --locale <locale>', 'Specific locale to translate')\n .option('-a, --all', 'Translate all locales', false)\n .option('-v, --verbose', 'Verbose output', false)\n .action(translateCommand);\n}\n","import * as path from 'path';\nimport * as fs from 'fs';\nimport { Command } from 'commander';\nimport chalk from 'chalk';\nimport ora from 'ora';\nimport { ASTWrapper, ConfigLoader, FileStorage, type WrappedString } from 'lokal-core';\n\ninterface WrapOptions {\n config?: string;\n src?: string;\n function?: string;\n dryRun?: boolean;\n verbose?: boolean;\n}\n\n/**\n * Auto-wrap translatable strings in source files\n * Converts plain text to t(\"key\") calls\n */\nexport async function wrapCommand(options: WrapOptions): Promise<void> {\n const spinner = ora('Preparing to wrap strings...').start();\n\n try {\n // Load config\n const configLoader = new ConfigLoader();\n let config;\n\n if (options.config) {\n config = configLoader.loadSync(options.config);\n } else {\n config = await configLoader.load();\n }\n\n const projectRoot = process.cwd();\n \n // Determine source directory\n const sourceDir = options.src\n ? path.resolve(projectRoot, options.src)\n : path.resolve(projectRoot, config.sourceDir);\n\n // Check if source directory exists\n if (!fs.existsSync(sourceDir)) {\n spinner.fail(chalk.red(`Source directory not found: ${sourceDir}`));\n process.exit(1);\n }\n\n // Create wrapper\n const wrapper = new ASTWrapper({\n functionName: options.function || config.functionName || 't',\n componentName: config.componentName || 'T',\n });\n\n spinner.text = `Scanning ${chalk.cyan(sourceDir)} for translatable strings...`;\n\n // First pass: collect all wrapped strings without modifying\n const results = wrapper.wrapDirectory(sourceDir, ['.js', '.jsx', '.ts', '.tsx'], true);\n \n // Count total strings to wrap\n let totalStrings = 0;\n const fileResults: { file: string, wrapped: WrappedString[] }[] = [];\n \n for (const result of results.results) {\n if (result.wrapped.length > 0) {\n fileResults.push({\n file: result.file,\n wrapped: result.wrapped\n });\n totalStrings += result.wrapped.length;\n }\n }\n\n spinner.succeed(chalk.green(`Found ${chalk.bold(totalStrings)} strings to wrap in ${chalk.bold(results.modifiedFiles)} files`));\n\n if (totalStrings === 0) {\n spinner.info(chalk.gray('No strings found that need wrapping.'));\n return;\n }\n\n // Show preview\n if (!options.dryRun) {\n console.log(chalk.bold('\\nPreview (first 10 files):'));\n let count = 0;\n for (const fileResult of fileResults.slice(0, 10)) {\n console.log(chalk.cyan(`\\n${path.relative(projectRoot, fileResult.file)}:`));\n for (const wrapped of fileResult.wrapped.slice(0, 5)) {\n console.log(chalk.gray(` \"${wrapped.original}\" → ${wrapped.wrapped}`));\n count++;\n }\n if (fileResult.wrapped.length > 5) {\n console.log(chalk.gray(` ... and ${fileResult.wrapped.length - 5} more`));\n }\n }\n if (fileResults.length > 10) {\n console.log(chalk.gray(`\\n... and ${fileResults.length - 10} more files`));\n }\n \n console.log(chalk.bold('\\n⚠ This will modify your source files!'));\n console.log(chalk.gray('Use --dry-run to preview without making changes'));\n } else {\n // Dry run - show what would be changed\n console.log(chalk.bold('\\nDry Run - Files that would be modified:'));\n for (const fileResult of fileResults) {\n console.log(chalk.cyan(` ${path.relative(projectRoot, fileResult.file)}`) + \n chalk.gray(` (${fileResult.wrapped.length} strings)`));\n }\n }\n\n if (options.dryRun) {\n spinner.info(chalk.yellow('Dry run complete. No files were modified.'));\n return;\n }\n\n // Confirm before making changes\n const confirmSpinner = ora('Applying changes...').start();\n\n // Apply changes to each file\n let modifiedCount = 0;\n for (const fileResult of fileResults) {\n const result = wrapper.wrapFile(fileResult.file);\n if (result.modified) {\n modifiedCount++;\n }\n }\n\n confirmSpinner.succeed(chalk.green(`Modified ${chalk.bold(modifiedCount)} files`));\n\n // Also update the locale file with the new keys\n const outputDir = path.resolve(projectRoot, config.outputDir);\n const storage = new FileStorage(outputDir);\n \n // Collect all new keys\n const newKeys: Record<string, string> = {};\n for (const fileResult of fileResults) {\n for (const wrapped of fileResult.wrapped) {\n newKeys[wrapped.key] = wrapped.original;\n }\n }\n\n // Add keys to default locale\n const defaultLocale = config.defaultLocale;\n const existingLocale = storage.loadLocale(defaultLocale);\n let existingData: Record<string, any> = {};\n \n if (existingLocale) {\n existingData = existingLocale.data as Record<string, any>;\n }\n\n // Merge new keys\n const mergedData = storage.mergeLocaleData(defaultLocale, newKeys);\n storage.saveLocale(defaultLocale, mergedData);\n\n console.log(chalk.green(`\\n✓ Added ${chalk.bold(Object.keys(newKeys).length)} new keys to locales/${defaultLocale}.json`));\n\n // Show next steps\n console.log(chalk.bold('\\nNext steps:'));\n console.log(chalk.gray(' 1. Review the changes in your source files'));\n console.log(chalk.gray(' 2. Run ') + chalk.cyan('npx lokal translate') + chalk.gray(' to translate to other locales'));\n \n } catch (error) {\n spinner.fail(chalk.red(`Wrap failed: ${error}`));\n if (options.verbose) {\n console.error(error);\n }\n process.exit(1);\n }\n}\n\nexport function registerWrapCommand(program: Command): void {\n program\n .command('wrap')\n .description('Automatically wrap translatable strings in source files')\n .option('-c, --config <path>', 'Path to config file')\n .option('-s, --src <path>', 'Source directory to scan')\n .option('-f, --function <name>', 'Translation function name', 't')\n .option('-d, --dry-run', 'Preview changes without modifying files', false)\n .option('-v, --verbose', 'Verbose output', false)\n .action(wrapCommand);\n}\n"],"mappings":"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","names":["import_chalk","ora","chalk","program","path","import_chalk","import_ora","ora","chalk","program","path","import_chalk","import_ora","import_lokal_core","ora","chalk","program","path","fs","import_chalk","import_ora","import_lokal_core","ora","chalk","program","chalk"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// packages/cli/src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import chalk5 from "chalk";
|
|
6
|
+
|
|
7
|
+
// packages/cli/src/commands/init.ts
|
|
8
|
+
import * as fs from "fs";
|
|
9
|
+
import * as path from "path";
|
|
10
|
+
import chalk from "chalk";
|
|
11
|
+
import ora from "ora";
|
|
12
|
+
async function initCommand(options) {
|
|
13
|
+
const spinner = ora("Initializing LOKAL...").start();
|
|
14
|
+
try {
|
|
15
|
+
const projectRoot = process.cwd();
|
|
16
|
+
const configPath = path.join(projectRoot, "lokal.config.js");
|
|
17
|
+
const localesDir = path.join(projectRoot, "locales");
|
|
18
|
+
const locales = options.locales ? options.locales.split(",").map((l) => l.trim()) : ["en"];
|
|
19
|
+
const defaultLocale = options.defaultLocale || locales[0];
|
|
20
|
+
if (fs.existsSync(configPath) && !options.force) {
|
|
21
|
+
spinner.warn(chalk.yellow("LOKAL is already initialized. Use --force to reinitialize."));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const configContent = `module.exports = {
|
|
25
|
+
// Supported locales
|
|
26
|
+
locales: ${JSON.stringify(locales)},
|
|
27
|
+
|
|
28
|
+
// Default locale
|
|
29
|
+
defaultLocale: '${defaultLocale}',
|
|
30
|
+
|
|
31
|
+
// Function name for translations (t("key"))
|
|
32
|
+
functionName: 't',
|
|
33
|
+
|
|
34
|
+
// Component name for translations (<T>key</T>)
|
|
35
|
+
componentName: 'T',
|
|
36
|
+
|
|
37
|
+
// Source directory to scan
|
|
38
|
+
sourceDir: './src',
|
|
39
|
+
|
|
40
|
+
// Output directory for locale files
|
|
41
|
+
outputDir: './locales',
|
|
42
|
+
|
|
43
|
+
// AI Translation settings (optional)
|
|
44
|
+
// ai: {
|
|
45
|
+
// provider: 'openai', // or 'gemini'
|
|
46
|
+
// apiKey: process.env.OPENAI_API_KEY,
|
|
47
|
+
// model: 'gpt-4'
|
|
48
|
+
// }
|
|
49
|
+
};
|
|
50
|
+
`;
|
|
51
|
+
fs.writeFileSync(configPath, configContent, "utf-8");
|
|
52
|
+
spinner.succeed(chalk.green(`Created ${chalk.bold("lokal.config.js")}`));
|
|
53
|
+
if (!fs.existsSync(localesDir)) {
|
|
54
|
+
fs.mkdirSync(localesDir, { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
const defaultLocalePath = path.join(localesDir, `${defaultLocale}.json`);
|
|
57
|
+
if (!fs.existsSync(defaultLocalePath)) {
|
|
58
|
+
const initialData = {
|
|
59
|
+
_meta: {
|
|
60
|
+
generated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
61
|
+
description: "Default locale file"
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
fs.writeFileSync(defaultLocalePath, JSON.stringify(initialData, null, 2), "utf-8");
|
|
65
|
+
spinner.succeed(chalk.green(`Created ${chalk.bold(`locales/${defaultLocale}.json`)}`));
|
|
66
|
+
}
|
|
67
|
+
const gitignorePath = path.join(projectRoot, ".gitignore");
|
|
68
|
+
if (fs.existsSync(gitignorePath)) {
|
|
69
|
+
const gitignoreContent = fs.readFileSync(gitignorePath, "utf-8");
|
|
70
|
+
if (!gitignoreContent.includes("/locales/")) {
|
|
71
|
+
fs.appendFileSync(gitignorePath, "\n# LOKAL translations\nlocales/\n");
|
|
72
|
+
spinner.succeed(chalk.green("Updated .gitignore"));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
console.log(chalk.bold("\n\u2713 LOKAL initialized successfully!"));
|
|
76
|
+
console.log(chalk.gray("\nNext steps:"));
|
|
77
|
+
console.log(chalk.gray(' 1. Add translation strings to your code using t("key") or <T>key</T>'));
|
|
78
|
+
console.log(chalk.gray(" 2. Run ") + chalk.cyan("npx lokal scan") + chalk.gray(" to extract strings"));
|
|
79
|
+
console.log(chalk.gray(" 3. Run ") + chalk.cyan("npx lokal translate") + chalk.gray(" to translate with AI"));
|
|
80
|
+
} catch (error) {
|
|
81
|
+
spinner.fail(chalk.red(`Failed to initialize: ${error}`));
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function registerInitCommand(program2) {
|
|
86
|
+
program2.command("init").description("Initialize LOKAL in your project").option("-l, --locales <locales>", "Comma-separated list of locales", "en").option("-d, --default-locale <locale>", "Default locale", "en").option("-f, --force", "Force reinitialization", false).action(initCommand);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// packages/cli/src/commands/scan.ts
|
|
90
|
+
import * as path2 from "path";
|
|
91
|
+
import chalk2 from "chalk";
|
|
92
|
+
import ora2 from "ora";
|
|
93
|
+
import { ASTParser, ConfigLoader, FileStorage } from "lokal-core";
|
|
94
|
+
async function scanCommand(options) {
|
|
95
|
+
const spinner = ora2("Scanning for translation strings...").start();
|
|
96
|
+
try {
|
|
97
|
+
const configLoader = new ConfigLoader();
|
|
98
|
+
let config;
|
|
99
|
+
if (options.config) {
|
|
100
|
+
config = configLoader.loadSync(options.config);
|
|
101
|
+
} else {
|
|
102
|
+
config = await configLoader.load();
|
|
103
|
+
}
|
|
104
|
+
const projectRoot = process.cwd();
|
|
105
|
+
const sourceDir = path2.resolve(projectRoot, config.sourceDir);
|
|
106
|
+
const outputDir = options.output ? path2.resolve(projectRoot, options.output) : path2.resolve(projectRoot, config.outputDir);
|
|
107
|
+
spinner.text = `Scanning ${chalk2.cyan(sourceDir)}...`;
|
|
108
|
+
const parser = new ASTParser({
|
|
109
|
+
filePath: sourceDir,
|
|
110
|
+
functionName: config.functionName,
|
|
111
|
+
componentName: config.componentName
|
|
112
|
+
});
|
|
113
|
+
const result = parser.scanDirectory(sourceDir);
|
|
114
|
+
if (result.errors.length > 0 && options.verbose) {
|
|
115
|
+
for (const error of result.errors) {
|
|
116
|
+
spinner.warn(chalk2.yellow(error));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
spinner.succeed(chalk2.green(`Found ${chalk2.bold(result.strings.length)} translation strings`));
|
|
120
|
+
if (result.strings.length === 0) {
|
|
121
|
+
spinner.info(chalk2.gray('No strings found. Make sure to use t("key") or <T>key</T> in your code.'));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const storage = new FileStorage(outputDir);
|
|
125
|
+
const uniqueStrings = /* @__PURE__ */ new Map();
|
|
126
|
+
for (const str of result.strings) {
|
|
127
|
+
uniqueStrings.set(str.key, str);
|
|
128
|
+
}
|
|
129
|
+
const defaultLocale = config.defaultLocale;
|
|
130
|
+
const existingLocale = storage.loadLocale(defaultLocale);
|
|
131
|
+
let existingData = {};
|
|
132
|
+
if (existingLocale) {
|
|
133
|
+
existingData = existingLocale.data;
|
|
134
|
+
}
|
|
135
|
+
const newData = {};
|
|
136
|
+
for (const [key, value] of uniqueStrings) {
|
|
137
|
+
newData[key] = existingData[key] || value.value;
|
|
138
|
+
}
|
|
139
|
+
const mergedData = storage.mergeLocaleData(defaultLocale, newData);
|
|
140
|
+
storage.saveLocale(defaultLocale, mergedData);
|
|
141
|
+
spinner.succeed(chalk2.green(`Updated ${chalk2.bold(`locales/${defaultLocale}.json`)}`));
|
|
142
|
+
if (options.verbose) {
|
|
143
|
+
console.log(chalk2.bold("\nExtracted strings:"));
|
|
144
|
+
const sampleKeys = Array.from(uniqueStrings.keys()).slice(0, 10);
|
|
145
|
+
for (const key of sampleKeys) {
|
|
146
|
+
console.log(chalk2.gray(` \u2022 ${key}`));
|
|
147
|
+
}
|
|
148
|
+
if (uniqueStrings.size > 10) {
|
|
149
|
+
console.log(chalk2.gray(` ... and ${uniqueStrings.size - 10} more`));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const locales = storage.getAvailableLocales();
|
|
153
|
+
const otherLocales = locales.filter((l) => l !== defaultLocale);
|
|
154
|
+
if (otherLocales.length > 0) {
|
|
155
|
+
console.log(chalk2.gray(`
|
|
156
|
+
Other locales detected: ${otherLocales.join(", ")}`));
|
|
157
|
+
console.log(chalk2.gray("Run ") + chalk2.cyan("npx lokal translate") + chalk2.gray(" to translate missing strings"));
|
|
158
|
+
}
|
|
159
|
+
} catch (error) {
|
|
160
|
+
spinner.fail(chalk2.red(`Scan failed: ${error}`));
|
|
161
|
+
if (options.verbose) {
|
|
162
|
+
console.error(error);
|
|
163
|
+
}
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function registerScanCommand(program2) {
|
|
168
|
+
program2.command("scan").description("Scan source files for translation strings").option("-c, --config <path>", "Path to config file").option("-o, --output <path>", "Output directory for locale files").option("-v, --verbose", "Verbose output", false).action(scanCommand);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// packages/cli/src/commands/translate.ts
|
|
172
|
+
import * as path3 from "path";
|
|
173
|
+
import chalk3 from "chalk";
|
|
174
|
+
import ora3 from "ora";
|
|
175
|
+
import {
|
|
176
|
+
ConfigLoader as ConfigLoader2,
|
|
177
|
+
FileStorage as FileStorage2,
|
|
178
|
+
AITranslator,
|
|
179
|
+
TranslationProviderFactory
|
|
180
|
+
} from "lokal-core";
|
|
181
|
+
async function translateCommand(options) {
|
|
182
|
+
const spinner = ora3("Loading configuration...").start();
|
|
183
|
+
try {
|
|
184
|
+
const configLoader = new ConfigLoader2();
|
|
185
|
+
let config;
|
|
186
|
+
if (options.config) {
|
|
187
|
+
config = configLoader.loadSync(options.config);
|
|
188
|
+
} else {
|
|
189
|
+
config = await configLoader.load();
|
|
190
|
+
}
|
|
191
|
+
if (!config.ai) {
|
|
192
|
+
spinner.fail(chalk3.red("AI provider not configured. Add ai configuration to lokal.config.js"));
|
|
193
|
+
console.log(chalk3.gray("\nExample configuration:"));
|
|
194
|
+
console.log(chalk3.gray(" ai: {"));
|
|
195
|
+
console.log(chalk3.gray(' provider: "openai",'));
|
|
196
|
+
console.log(chalk3.gray(" apiKey: process.env.OPENAI_API_KEY"));
|
|
197
|
+
console.log(chalk3.gray(" }"));
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
const apiKey = config.ai.apiKey || process.env.OPENAI_API_KEY || process.env.GEMINI_API_KEY;
|
|
201
|
+
if (!apiKey) {
|
|
202
|
+
spinner.fail(chalk3.red("No API key found. Set ai.apiKey in config or environment variable."));
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
spinner.text = "Initializing AI translator...";
|
|
206
|
+
const provider = TranslationProviderFactory.create(
|
|
207
|
+
config.ai.provider,
|
|
208
|
+
apiKey,
|
|
209
|
+
config.ai.model
|
|
210
|
+
);
|
|
211
|
+
const translator = new AITranslator(provider);
|
|
212
|
+
const projectRoot = process.cwd();
|
|
213
|
+
const outputDir = path3.resolve(projectRoot, config.outputDir);
|
|
214
|
+
const storage = new FileStorage2(outputDir);
|
|
215
|
+
spinner.succeed("AI translator ready");
|
|
216
|
+
let targetLocales = [];
|
|
217
|
+
const availableLocales = config.locales || [];
|
|
218
|
+
if (options.all) {
|
|
219
|
+
targetLocales = availableLocales.filter((l) => l !== config.defaultLocale);
|
|
220
|
+
} else if (options.locale) {
|
|
221
|
+
targetLocales = [options.locale];
|
|
222
|
+
} else {
|
|
223
|
+
targetLocales = availableLocales.filter((l) => l !== config.defaultLocale).slice(0, 1);
|
|
224
|
+
}
|
|
225
|
+
if (targetLocales.length === 0) {
|
|
226
|
+
spinner.warn(chalk3.yellow("No target locales to translate. Add more locales to your config."));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const sourceLocale = config.defaultLocale;
|
|
230
|
+
const sourceData = storage.loadLocale(sourceLocale);
|
|
231
|
+
if (!sourceData) {
|
|
232
|
+
spinner.fail(chalk3.red(`Source locale ${sourceLocale} not found. Run 'lokal scan' first.`));
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
for (const targetLocale of targetLocales) {
|
|
236
|
+
const translateSpinner = ora3(`Translating to ${chalk3.cyan(targetLocale)}...`).start();
|
|
237
|
+
const targetLocaleFile = storage.loadLocale(targetLocale);
|
|
238
|
+
const targetData = targetLocaleFile ? targetLocaleFile.data : {};
|
|
239
|
+
const translatedData = await translator.translateMissingKeys(
|
|
240
|
+
sourceData.data,
|
|
241
|
+
targetData,
|
|
242
|
+
sourceLocale,
|
|
243
|
+
targetLocale
|
|
244
|
+
);
|
|
245
|
+
storage.saveLocale(targetLocale, translatedData);
|
|
246
|
+
translateSpinner.succeed(chalk3.green(`Translated to ${chalk3.bold(targetLocale)}`));
|
|
247
|
+
if (options.verbose) {
|
|
248
|
+
const keys = Object.keys(translatedData).slice(0, 5);
|
|
249
|
+
for (const key of keys) {
|
|
250
|
+
const value = translatedData[key];
|
|
251
|
+
if (typeof value === "string") {
|
|
252
|
+
console.log(chalk3.gray(` ${key}: ${value}`));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
console.log(chalk3.bold("\n\u2713 Translation complete!"));
|
|
258
|
+
console.log(chalk3.gray(`
|
|
259
|
+
Run `) + chalk3.cyan("npx lokal scan") + chalk3.gray(" to see updated translations"));
|
|
260
|
+
} catch (error) {
|
|
261
|
+
spinner.fail(chalk3.red(`Translation failed: ${error}`));
|
|
262
|
+
if (options.verbose) {
|
|
263
|
+
console.error(error);
|
|
264
|
+
}
|
|
265
|
+
process.exit(1);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function registerTranslateCommand(program2) {
|
|
269
|
+
program2.command("translate").description("Translate missing strings using AI").option("-c, --config <path>", "Path to config file").option("-l, --locale <locale>", "Specific locale to translate").option("-a, --all", "Translate all locales", false).option("-v, --verbose", "Verbose output", false).action(translateCommand);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// packages/cli/src/commands/wrap.ts
|
|
273
|
+
import * as path4 from "path";
|
|
274
|
+
import * as fs2 from "fs";
|
|
275
|
+
import chalk4 from "chalk";
|
|
276
|
+
import ora4 from "ora";
|
|
277
|
+
import { ASTWrapper, ConfigLoader as ConfigLoader3, FileStorage as FileStorage3 } from "lokal-core";
|
|
278
|
+
async function wrapCommand(options) {
|
|
279
|
+
const spinner = ora4("Preparing to wrap strings...").start();
|
|
280
|
+
try {
|
|
281
|
+
const configLoader = new ConfigLoader3();
|
|
282
|
+
let config;
|
|
283
|
+
if (options.config) {
|
|
284
|
+
config = configLoader.loadSync(options.config);
|
|
285
|
+
} else {
|
|
286
|
+
config = await configLoader.load();
|
|
287
|
+
}
|
|
288
|
+
const projectRoot = process.cwd();
|
|
289
|
+
const sourceDir = options.src ? path4.resolve(projectRoot, options.src) : path4.resolve(projectRoot, config.sourceDir);
|
|
290
|
+
if (!fs2.existsSync(sourceDir)) {
|
|
291
|
+
spinner.fail(chalk4.red(`Source directory not found: ${sourceDir}`));
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
const wrapper = new ASTWrapper({
|
|
295
|
+
functionName: options.function || config.functionName || "t",
|
|
296
|
+
componentName: config.componentName || "T"
|
|
297
|
+
});
|
|
298
|
+
spinner.text = `Scanning ${chalk4.cyan(sourceDir)} for translatable strings...`;
|
|
299
|
+
const results = wrapper.wrapDirectory(sourceDir, [".js", ".jsx", ".ts", ".tsx"], true);
|
|
300
|
+
let totalStrings = 0;
|
|
301
|
+
const fileResults = [];
|
|
302
|
+
for (const result of results.results) {
|
|
303
|
+
if (result.wrapped.length > 0) {
|
|
304
|
+
fileResults.push({
|
|
305
|
+
file: result.file,
|
|
306
|
+
wrapped: result.wrapped
|
|
307
|
+
});
|
|
308
|
+
totalStrings += result.wrapped.length;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
spinner.succeed(chalk4.green(`Found ${chalk4.bold(totalStrings)} strings to wrap in ${chalk4.bold(results.modifiedFiles)} files`));
|
|
312
|
+
if (totalStrings === 0) {
|
|
313
|
+
spinner.info(chalk4.gray("No strings found that need wrapping."));
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (!options.dryRun) {
|
|
317
|
+
console.log(chalk4.bold("\nPreview (first 10 files):"));
|
|
318
|
+
let count = 0;
|
|
319
|
+
for (const fileResult of fileResults.slice(0, 10)) {
|
|
320
|
+
console.log(chalk4.cyan(`
|
|
321
|
+
${path4.relative(projectRoot, fileResult.file)}:`));
|
|
322
|
+
for (const wrapped of fileResult.wrapped.slice(0, 5)) {
|
|
323
|
+
console.log(chalk4.gray(` "${wrapped.original}" \u2192 ${wrapped.wrapped}`));
|
|
324
|
+
count++;
|
|
325
|
+
}
|
|
326
|
+
if (fileResult.wrapped.length > 5) {
|
|
327
|
+
console.log(chalk4.gray(` ... and ${fileResult.wrapped.length - 5} more`));
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (fileResults.length > 10) {
|
|
331
|
+
console.log(chalk4.gray(`
|
|
332
|
+
... and ${fileResults.length - 10} more files`));
|
|
333
|
+
}
|
|
334
|
+
console.log(chalk4.bold("\n\u26A0 This will modify your source files!"));
|
|
335
|
+
console.log(chalk4.gray("Use --dry-run to preview without making changes"));
|
|
336
|
+
} else {
|
|
337
|
+
console.log(chalk4.bold("\nDry Run - Files that would be modified:"));
|
|
338
|
+
for (const fileResult of fileResults) {
|
|
339
|
+
console.log(chalk4.cyan(` ${path4.relative(projectRoot, fileResult.file)}`) + chalk4.gray(` (${fileResult.wrapped.length} strings)`));
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (options.dryRun) {
|
|
343
|
+
spinner.info(chalk4.yellow("Dry run complete. No files were modified."));
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const confirmSpinner = ora4("Applying changes...").start();
|
|
347
|
+
let modifiedCount = 0;
|
|
348
|
+
for (const fileResult of fileResults) {
|
|
349
|
+
const result = wrapper.wrapFile(fileResult.file);
|
|
350
|
+
if (result.modified) {
|
|
351
|
+
modifiedCount++;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
confirmSpinner.succeed(chalk4.green(`Modified ${chalk4.bold(modifiedCount)} files`));
|
|
355
|
+
const outputDir = path4.resolve(projectRoot, config.outputDir);
|
|
356
|
+
const storage = new FileStorage3(outputDir);
|
|
357
|
+
const newKeys = {};
|
|
358
|
+
for (const fileResult of fileResults) {
|
|
359
|
+
for (const wrapped of fileResult.wrapped) {
|
|
360
|
+
newKeys[wrapped.key] = wrapped.original;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
const defaultLocale = config.defaultLocale;
|
|
364
|
+
const existingLocale = storage.loadLocale(defaultLocale);
|
|
365
|
+
let existingData = {};
|
|
366
|
+
if (existingLocale) {
|
|
367
|
+
existingData = existingLocale.data;
|
|
368
|
+
}
|
|
369
|
+
const mergedData = storage.mergeLocaleData(defaultLocale, newKeys);
|
|
370
|
+
storage.saveLocale(defaultLocale, mergedData);
|
|
371
|
+
console.log(chalk4.green(`
|
|
372
|
+
\u2713 Added ${chalk4.bold(Object.keys(newKeys).length)} new keys to locales/${defaultLocale}.json`));
|
|
373
|
+
console.log(chalk4.bold("\nNext steps:"));
|
|
374
|
+
console.log(chalk4.gray(" 1. Review the changes in your source files"));
|
|
375
|
+
console.log(chalk4.gray(" 2. Run ") + chalk4.cyan("npx lokal translate") + chalk4.gray(" to translate to other locales"));
|
|
376
|
+
} catch (error) {
|
|
377
|
+
spinner.fail(chalk4.red(`Wrap failed: ${error}`));
|
|
378
|
+
if (options.verbose) {
|
|
379
|
+
console.error(error);
|
|
380
|
+
}
|
|
381
|
+
process.exit(1);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
function registerWrapCommand(program2) {
|
|
385
|
+
program2.command("wrap").description("Automatically wrap translatable strings in source files").option("-c, --config <path>", "Path to config file").option("-s, --src <path>", "Source directory to scan").option("-f, --function <name>", "Translation function name", "t").option("-d, --dry-run", "Preview changes without modifying files", false).option("-v, --verbose", "Verbose output", false).action(wrapCommand);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// packages/cli/src/index.ts
|
|
389
|
+
var program = new Command();
|
|
390
|
+
program.name("lokal").description("AI-powered localization ecosystem for React and React Native").version("1.0.0");
|
|
391
|
+
registerInitCommand(program);
|
|
392
|
+
registerScanCommand(program);
|
|
393
|
+
registerTranslateCommand(program);
|
|
394
|
+
registerWrapCommand(program);
|
|
395
|
+
program.option("-v, --verbose", "Verbose output").hook("preAction", (thisCommand) => {
|
|
396
|
+
const opts = thisCommand.opts();
|
|
397
|
+
if (opts.verbose) {
|
|
398
|
+
process.env.LOKAL_VERBOSE = "true";
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
program.on("command:*", () => {
|
|
402
|
+
console.error(chalk5.red(`Invalid command: ${program.args.join(" ")}`));
|
|
403
|
+
console.log(chalk5.gray(`See ${chalk5.cyan("--help")} for a list of available commands.`));
|
|
404
|
+
process.exit(1);
|
|
405
|
+
});
|
|
406
|
+
program.parse(process.argv);
|
|
407
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/commands/init.ts","../src/commands/scan.ts","../src/commands/translate.ts","../src/commands/wrap.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { Command } from 'commander';\nimport chalk from 'chalk';\nimport { registerInitCommand } from './commands/init';\nimport { registerScanCommand } from './commands/scan';\nimport { registerTranslateCommand } from './commands/translate';\nimport { registerWrapCommand } from './commands/wrap';\n\nconst program = new Command();\n\nprogram\n .name('lokal')\n .description('AI-powered localization ecosystem for React and React Native')\n .version('1.0.0');\n\n// Register commands\nregisterInitCommand(program);\nregisterScanCommand(program);\nregisterTranslateCommand(program);\nregisterWrapCommand(program);\n\n// Global options\nprogram\n .option('-v, --verbose', 'Verbose output')\n .hook('preAction', (thisCommand) => {\n const opts = thisCommand.opts();\n if (opts.verbose) {\n process.env.LOKAL_VERBOSE = 'true';\n }\n });\n\n// Handle unknown commands\nprogram.on('command:*', () => {\n console.error(chalk.red(`Invalid command: ${program.args.join(' ')}`));\n console.log(chalk.gray(`See ${chalk.cyan('--help')} for a list of available commands.`));\n process.exit(1);\n});\n\n// Run the program\nprogram.parse(process.argv);\n","import * as fs from 'fs';\nimport * as path from 'path';\nimport { Command } from 'commander';\nimport chalk from 'chalk';\nimport ora from 'ora';\n\ninterface InitOptions {\n locales?: string;\n defaultLocale?: string;\n force?: boolean;\n}\n\n/**\n * Create the initial lokal config file and directory structure\n */\nexport async function initCommand(options: InitOptions): Promise<void> {\n const spinner = ora('Initializing LOKAL...').start();\n\n try {\n const projectRoot = process.cwd();\n const configPath = path.join(projectRoot, 'lokal.config.js');\n const localesDir = path.join(projectRoot, 'locales');\n\n // Parse locales\n const locales = options.locales\n ? options.locales.split(',').map(l => l.trim())\n : ['en'];\n\n const defaultLocale = options.defaultLocale || locales[0];\n\n // Check if already initialized\n if (fs.existsSync(configPath) && !options.force) {\n spinner.warn(chalk.yellow('LOKAL is already initialized. Use --force to reinitialize.'));\n return;\n }\n\n // Create config file\n const configContent = `module.exports = {\n // Supported locales\n locales: ${JSON.stringify(locales)},\n \n // Default locale\n defaultLocale: '${defaultLocale}',\n \n // Function name for translations (t(\"key\"))\n functionName: 't',\n \n // Component name for translations (<T>key</T>)\n componentName: 'T',\n \n // Source directory to scan\n sourceDir: './src',\n \n // Output directory for locale files\n outputDir: './locales',\n \n // AI Translation settings (optional)\n // ai: {\n // provider: 'openai', // or 'gemini'\n // apiKey: process.env.OPENAI_API_KEY,\n // model: 'gpt-4'\n // }\n};\n`;\n\n fs.writeFileSync(configPath, configContent, 'utf-8');\n spinner.succeed(chalk.green(`Created ${chalk.bold('lokal.config.js')}`));\n\n // Create locales directory with default locale\n if (!fs.existsSync(localesDir)) {\n fs.mkdirSync(localesDir, { recursive: true });\n }\n\n // Create default locale file\n const defaultLocalePath = path.join(localesDir, `${defaultLocale}.json`);\n if (!fs.existsSync(defaultLocalePath)) {\n const initialData = {\n _meta: {\n generated: new Date().toISOString(),\n description: 'Default locale file'\n }\n };\n fs.writeFileSync(defaultLocalePath, JSON.stringify(initialData, null, 2), 'utf-8');\n spinner.succeed(chalk.green(`Created ${chalk.bold(`locales/${defaultLocale}.json`)}`));\n }\n\n // Create .gitignore entry for locale files if .gitignore exists\n const gitignorePath = path.join(projectRoot, '.gitignore');\n if (fs.existsSync(gitignorePath)) {\n const gitignoreContent = fs.readFileSync(gitignorePath, 'utf-8');\n if (!gitignoreContent.includes('/locales/')) {\n fs.appendFileSync(gitignorePath, '\\n# LOKAL translations\\nlocales/\\n');\n spinner.succeed(chalk.green('Updated .gitignore'));\n }\n }\n\n console.log(chalk.bold('\\n✓ LOKAL initialized successfully!'));\n console.log(chalk.gray('\\nNext steps:'));\n console.log(chalk.gray(' 1. Add translation strings to your code using t(\"key\") or <T>key</T>'));\n console.log(chalk.gray(' 2. Run ') + chalk.cyan('npx lokal scan') + chalk.gray(' to extract strings'));\n console.log(chalk.gray(' 3. Run ') + chalk.cyan('npx lokal translate') + chalk.gray(' to translate with AI'));\n\n } catch (error) {\n spinner.fail(chalk.red(`Failed to initialize: ${error}`));\n process.exit(1);\n }\n}\n\nexport function registerInitCommand(program: Command): void {\n program\n .command('init')\n .description('Initialize LOKAL in your project')\n .option('-l, --locales <locales>', 'Comma-separated list of locales', 'en')\n .option('-d, --default-locale <locale>', 'Default locale', 'en')\n .option('-f, --force', 'Force reinitialization', false)\n .action(initCommand);\n}\n","import * as path from 'path';\nimport { Command } from 'commander';\nimport chalk from 'chalk';\nimport ora from 'ora';\nimport { ASTParser, ConfigLoader, FileStorage, type ExtractedString } from 'lokal-core';\n\ninterface ScanOptions {\n config?: string;\n output?: string;\n verbose?: boolean;\n}\n\n/**\n * Scan source files for translation strings and update locale files\n */\nexport async function scanCommand(options: ScanOptions): Promise<void> {\n const spinner = ora('Scanning for translation strings...').start();\n\n try {\n // Load config\n const configLoader = new ConfigLoader();\n let config;\n\n if (options.config) {\n config = configLoader.loadSync(options.config);\n } else {\n config = await configLoader.load();\n }\n\n const projectRoot = process.cwd();\n const sourceDir = path.resolve(projectRoot, config.sourceDir);\n const outputDir = options.output\n ? path.resolve(projectRoot, options.output)\n : path.resolve(projectRoot, config.outputDir);\n\n spinner.text = `Scanning ${chalk.cyan(sourceDir)}...`;\n\n // Create parser\n const parser = new ASTParser({\n filePath: sourceDir,\n functionName: config.functionName,\n componentName: config.componentName,\n });\n\n // Scan directory\n const result = parser.scanDirectory(sourceDir);\n\n if (result.errors.length > 0 && options.verbose) {\n for (const error of result.errors) {\n spinner.warn(chalk.yellow(error));\n }\n }\n\n spinner.succeed(chalk.green(`Found ${chalk.bold(result.strings.length)} translation strings`));\n\n if (result.strings.length === 0) {\n spinner.info(chalk.gray('No strings found. Make sure to use t(\"key\") or <T>key</T> in your code.'));\n return;\n }\n\n // Create storage\n const storage = new FileStorage(outputDir);\n\n // Create unique key-value map\n const uniqueStrings = new Map<string, ExtractedString>();\n for (const str of result.strings) {\n uniqueStrings.set(str.key, str);\n }\n\n // Get the default locale\n const defaultLocale = config.defaultLocale;\n const existingLocale = storage.loadLocale(defaultLocale);\n\n // Merge with existing keys\n let existingData: Record<string, any> = {};\n if (existingLocale) {\n existingData = existingLocale.data as Record<string, any>;\n }\n\n // Convert extracted strings to flat key-value object\n const newData: Record<string, string> = {};\n for (const [key, value] of uniqueStrings) {\n // Use the key itself as the translation value for now\n newData[key] = existingData[key] || value.value;\n }\n\n // Save the merged data\n const mergedData = storage.mergeLocaleData(defaultLocale, newData);\n storage.saveLocale(defaultLocale, mergedData);\n\n spinner.succeed(chalk.green(`Updated ${chalk.bold(`locales/${defaultLocale}.json`)}`));\n\n // Show sample of extracted strings\n if (options.verbose) {\n console.log(chalk.bold('\\nExtracted strings:'));\n const sampleKeys = Array.from(uniqueStrings.keys()).slice(0, 10);\n for (const key of sampleKeys) {\n console.log(chalk.gray(` • ${key}`));\n }\n if (uniqueStrings.size > 10) {\n console.log(chalk.gray(` ... and ${uniqueStrings.size - 10} more`));\n }\n }\n\n // Check for other locales that need translation\n const locales = storage.getAvailableLocales();\n const otherLocales = locales.filter(l => l !== defaultLocale);\n\n if (otherLocales.length > 0) {\n console.log(chalk.gray(`\\nOther locales detected: ${otherLocales.join(', ')}`));\n console.log(chalk.gray('Run ') + chalk.cyan('npx lokal translate') + chalk.gray(' to translate missing strings'));\n }\n\n } catch (error) {\n spinner.fail(chalk.red(`Scan failed: ${error}`));\n if (options.verbose) {\n console.error(error);\n }\n process.exit(1);\n }\n}\n\nexport function registerScanCommand(program: Command): void {\n program\n .command('scan')\n .description('Scan source files for translation strings')\n .option('-c, --config <path>', 'Path to config file')\n .option('-o, --output <path>', 'Output directory for locale files')\n .option('-v, --verbose', 'Verbose output', false)\n .action(scanCommand);\n}\n","import * as path from 'path';\nimport { Command } from 'commander';\nimport chalk from 'chalk';\nimport ora from 'ora';\nimport {\n ConfigLoader,\n FileStorage,\n AITranslator,\n TranslationProviderFactory,\n type LocaleData\n} from 'lokal-core';\n\ninterface TranslateOptions {\n config?: string;\n locale?: string;\n all?: boolean;\n verbose?: boolean;\n}\n\n/**\n * Translate missing strings using AI\n */\nexport async function translateCommand(options: TranslateOptions): Promise<void> {\n const spinner = ora('Loading configuration...').start();\n\n try {\n // Load config\n const configLoader = new ConfigLoader();\n let config;\n\n if (options.config) {\n config = configLoader.loadSync(options.config);\n } else {\n config = await configLoader.load();\n }\n\n // Check if AI is configured\n if (!config.ai) {\n spinner.fail(chalk.red('AI provider not configured. Add ai configuration to lokal.config.js'));\n console.log(chalk.gray('\\nExample configuration:'));\n console.log(chalk.gray(' ai: {'));\n console.log(chalk.gray(' provider: \"openai\",'));\n console.log(chalk.gray(' apiKey: process.env.OPENAI_API_KEY'));\n console.log(chalk.gray(' }'));\n process.exit(1);\n }\n\n const apiKey = config.ai.apiKey || process.env.OPENAI_API_KEY || process.env.GEMINI_API_KEY;\n if (!apiKey) {\n spinner.fail(chalk.red('No API key found. Set ai.apiKey in config or environment variable.'));\n process.exit(1);\n }\n\n spinner.text = 'Initializing AI translator...';\n\n // Create provider\n const provider = TranslationProviderFactory.create(\n config.ai.provider,\n apiKey,\n config.ai.model\n );\n\n // Create translator\n const translator = new AITranslator(provider);\n\n const projectRoot = process.cwd();\n const outputDir = path.resolve(projectRoot, config.outputDir);\n const storage = new FileStorage(outputDir);\n\n spinner.succeed('AI translator ready');\n\n // Determine which locales to translate\n let targetLocales: string[] = [];\n const availableLocales = config.locales || [];\n\n if (options.all) {\n targetLocales = availableLocales.filter(l => l !== config.defaultLocale);\n } else if (options.locale) {\n targetLocales = [options.locale];\n } else {\n // Default: translate to first non-default locale\n targetLocales = availableLocales.filter(l => l !== config.defaultLocale).slice(0, 1);\n }\n\n if (targetLocales.length === 0) {\n spinner.warn(chalk.yellow('No target locales to translate. Add more locales to your config.'));\n return;\n }\n\n // Load source locale data\n const sourceLocale = config.defaultLocale;\n const sourceData = storage.loadLocale(sourceLocale);\n\n if (!sourceData) {\n spinner.fail(chalk.red(`Source locale ${sourceLocale} not found. Run 'lokal scan' first.`));\n process.exit(1);\n }\n\n // Translate to each target locale\n for (const targetLocale of targetLocales) {\n const translateSpinner = ora(`Translating to ${chalk.cyan(targetLocale)}...`).start();\n\n // Load existing target data (if any)\n const targetLocaleFile = storage.loadLocale(targetLocale);\n const targetData: LocaleData = targetLocaleFile ? targetLocaleFile.data : {};\n\n // Translate missing keys\n const translatedData = await translator.translateMissingKeys(\n sourceData.data,\n targetData,\n sourceLocale,\n targetLocale\n );\n\n // Save translated data\n storage.saveLocale(targetLocale, translatedData);\n\n translateSpinner.succeed(chalk.green(`Translated to ${chalk.bold(targetLocale)}`));\n\n if (options.verbose) {\n // Show sample translations\n const keys = Object.keys(translatedData).slice(0, 5);\n for (const key of keys) {\n const value = translatedData[key];\n if (typeof value === 'string') {\n console.log(chalk.gray(` ${key}: ${value}`));\n }\n }\n }\n }\n\n console.log(chalk.bold('\\n✓ Translation complete!'));\n console.log(chalk.gray(`\\nRun `) + chalk.cyan('npx lokal scan') + chalk.gray(' to see updated translations'));\n\n } catch (error) {\n spinner.fail(chalk.red(`Translation failed: ${error}`));\n if (options.verbose) {\n console.error(error);\n }\n process.exit(1);\n }\n}\n\nexport function registerTranslateCommand(program: Command): void {\n program\n .command('translate')\n .description('Translate missing strings using AI')\n .option('-c, --config <path>', 'Path to config file')\n .option('-l, --locale <locale>', 'Specific locale to translate')\n .option('-a, --all', 'Translate all locales', false)\n .option('-v, --verbose', 'Verbose output', false)\n .action(translateCommand);\n}\n","import * as path from 'path';\nimport * as fs from 'fs';\nimport { Command } from 'commander';\nimport chalk from 'chalk';\nimport ora from 'ora';\nimport { ASTWrapper, ConfigLoader, FileStorage, type WrappedString } from 'lokal-core';\n\ninterface WrapOptions {\n config?: string;\n src?: string;\n function?: string;\n dryRun?: boolean;\n verbose?: boolean;\n}\n\n/**\n * Auto-wrap translatable strings in source files\n * Converts plain text to t(\"key\") calls\n */\nexport async function wrapCommand(options: WrapOptions): Promise<void> {\n const spinner = ora('Preparing to wrap strings...').start();\n\n try {\n // Load config\n const configLoader = new ConfigLoader();\n let config;\n\n if (options.config) {\n config = configLoader.loadSync(options.config);\n } else {\n config = await configLoader.load();\n }\n\n const projectRoot = process.cwd();\n \n // Determine source directory\n const sourceDir = options.src\n ? path.resolve(projectRoot, options.src)\n : path.resolve(projectRoot, config.sourceDir);\n\n // Check if source directory exists\n if (!fs.existsSync(sourceDir)) {\n spinner.fail(chalk.red(`Source directory not found: ${sourceDir}`));\n process.exit(1);\n }\n\n // Create wrapper\n const wrapper = new ASTWrapper({\n functionName: options.function || config.functionName || 't',\n componentName: config.componentName || 'T',\n });\n\n spinner.text = `Scanning ${chalk.cyan(sourceDir)} for translatable strings...`;\n\n // First pass: collect all wrapped strings without modifying\n const results = wrapper.wrapDirectory(sourceDir, ['.js', '.jsx', '.ts', '.tsx'], true);\n \n // Count total strings to wrap\n let totalStrings = 0;\n const fileResults: { file: string, wrapped: WrappedString[] }[] = [];\n \n for (const result of results.results) {\n if (result.wrapped.length > 0) {\n fileResults.push({\n file: result.file,\n wrapped: result.wrapped\n });\n totalStrings += result.wrapped.length;\n }\n }\n\n spinner.succeed(chalk.green(`Found ${chalk.bold(totalStrings)} strings to wrap in ${chalk.bold(results.modifiedFiles)} files`));\n\n if (totalStrings === 0) {\n spinner.info(chalk.gray('No strings found that need wrapping.'));\n return;\n }\n\n // Show preview\n if (!options.dryRun) {\n console.log(chalk.bold('\\nPreview (first 10 files):'));\n let count = 0;\n for (const fileResult of fileResults.slice(0, 10)) {\n console.log(chalk.cyan(`\\n${path.relative(projectRoot, fileResult.file)}:`));\n for (const wrapped of fileResult.wrapped.slice(0, 5)) {\n console.log(chalk.gray(` \"${wrapped.original}\" → ${wrapped.wrapped}`));\n count++;\n }\n if (fileResult.wrapped.length > 5) {\n console.log(chalk.gray(` ... and ${fileResult.wrapped.length - 5} more`));\n }\n }\n if (fileResults.length > 10) {\n console.log(chalk.gray(`\\n... and ${fileResults.length - 10} more files`));\n }\n \n console.log(chalk.bold('\\n⚠ This will modify your source files!'));\n console.log(chalk.gray('Use --dry-run to preview without making changes'));\n } else {\n // Dry run - show what would be changed\n console.log(chalk.bold('\\nDry Run - Files that would be modified:'));\n for (const fileResult of fileResults) {\n console.log(chalk.cyan(` ${path.relative(projectRoot, fileResult.file)}`) + \n chalk.gray(` (${fileResult.wrapped.length} strings)`));\n }\n }\n\n if (options.dryRun) {\n spinner.info(chalk.yellow('Dry run complete. No files were modified.'));\n return;\n }\n\n // Confirm before making changes\n const confirmSpinner = ora('Applying changes...').start();\n\n // Apply changes to each file\n let modifiedCount = 0;\n for (const fileResult of fileResults) {\n const result = wrapper.wrapFile(fileResult.file);\n if (result.modified) {\n modifiedCount++;\n }\n }\n\n confirmSpinner.succeed(chalk.green(`Modified ${chalk.bold(modifiedCount)} files`));\n\n // Also update the locale file with the new keys\n const outputDir = path.resolve(projectRoot, config.outputDir);\n const storage = new FileStorage(outputDir);\n \n // Collect all new keys\n const newKeys: Record<string, string> = {};\n for (const fileResult of fileResults) {\n for (const wrapped of fileResult.wrapped) {\n newKeys[wrapped.key] = wrapped.original;\n }\n }\n\n // Add keys to default locale\n const defaultLocale = config.defaultLocale;\n const existingLocale = storage.loadLocale(defaultLocale);\n let existingData: Record<string, any> = {};\n \n if (existingLocale) {\n existingData = existingLocale.data as Record<string, any>;\n }\n\n // Merge new keys\n const mergedData = storage.mergeLocaleData(defaultLocale, newKeys);\n storage.saveLocale(defaultLocale, mergedData);\n\n console.log(chalk.green(`\\n✓ Added ${chalk.bold(Object.keys(newKeys).length)} new keys to locales/${defaultLocale}.json`));\n\n // Show next steps\n console.log(chalk.bold('\\nNext steps:'));\n console.log(chalk.gray(' 1. Review the changes in your source files'));\n console.log(chalk.gray(' 2. Run ') + chalk.cyan('npx lokal translate') + chalk.gray(' to translate to other locales'));\n \n } catch (error) {\n spinner.fail(chalk.red(`Wrap failed: ${error}`));\n if (options.verbose) {\n console.error(error);\n }\n process.exit(1);\n }\n}\n\nexport function registerWrapCommand(program: Command): void {\n program\n .command('wrap')\n .description('Automatically wrap translatable strings in source files')\n .option('-c, --config <path>', 'Path to config file')\n .option('-s, --src <path>', 'Source directory to scan')\n .option('-f, --function <name>', 'Translation function name', 't')\n .option('-d, --dry-run', 'Preview changes without modifying files', false)\n .option('-v, --verbose', 'Verbose output', false)\n .action(wrapCommand);\n}\n"],"mappings":"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","names":["chalk","program","path","chalk","ora","program","path","chalk","ora","ConfigLoader","FileStorage","program","path","fs","chalk","ora","ConfigLoader","FileStorage","program","chalk"]}
|
package/package.json
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devcoda/lokal-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "CLI tool for LOKAL - Automates string extraction and AI translations",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"lokal": "./bin/lokal"
|
|
8
8
|
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"bin",
|
|
12
|
+
"src"
|
|
13
|
+
],
|
|
9
14
|
"scripts": {
|
|
10
15
|
"dev": "tsup --watch",
|
|
11
16
|
"clean": "rm -rf dist",
|
|
12
17
|
"lint": "echo 'Use root lint command'"
|
|
13
18
|
},
|
|
14
19
|
"dependencies": {
|
|
15
|
-
"lokal-core": "
|
|
20
|
+
"lokal-core": "file:../core",
|
|
16
21
|
"chalk": "^4.1.2",
|
|
17
22
|
"commander": "^11.1.0",
|
|
18
23
|
"ora": "^5.4.1"
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
import { ASTWrapper, ConfigLoader, FileStorage, type WrappedString } from 'lokal-core';
|
|
7
|
+
|
|
8
|
+
interface WrapOptions {
|
|
9
|
+
config?: string;
|
|
10
|
+
src?: string;
|
|
11
|
+
function?: string;
|
|
12
|
+
dryRun?: boolean;
|
|
13
|
+
verbose?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Auto-wrap translatable strings in source files
|
|
18
|
+
* Converts plain text to t("key") calls
|
|
19
|
+
*/
|
|
20
|
+
export async function wrapCommand(options: WrapOptions): Promise<void> {
|
|
21
|
+
const spinner = ora('Preparing to wrap strings...').start();
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
// Load config
|
|
25
|
+
const configLoader = new ConfigLoader();
|
|
26
|
+
let config;
|
|
27
|
+
|
|
28
|
+
if (options.config) {
|
|
29
|
+
config = configLoader.loadSync(options.config);
|
|
30
|
+
} else {
|
|
31
|
+
config = await configLoader.load();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const projectRoot = process.cwd();
|
|
35
|
+
|
|
36
|
+
// Determine source directory
|
|
37
|
+
const sourceDir = options.src
|
|
38
|
+
? path.resolve(projectRoot, options.src)
|
|
39
|
+
: path.resolve(projectRoot, config.sourceDir);
|
|
40
|
+
|
|
41
|
+
// Check if source directory exists
|
|
42
|
+
if (!fs.existsSync(sourceDir)) {
|
|
43
|
+
spinner.fail(chalk.red(`Source directory not found: ${sourceDir}`));
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Create wrapper
|
|
48
|
+
const wrapper = new ASTWrapper({
|
|
49
|
+
functionName: options.function || config.functionName || 't',
|
|
50
|
+
componentName: config.componentName || 'T',
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
spinner.text = `Scanning ${chalk.cyan(sourceDir)} for translatable strings...`;
|
|
54
|
+
|
|
55
|
+
// First pass: collect all wrapped strings without modifying
|
|
56
|
+
const results = wrapper.wrapDirectory(sourceDir, ['.js', '.jsx', '.ts', '.tsx'], true);
|
|
57
|
+
|
|
58
|
+
// Count total strings to wrap
|
|
59
|
+
let totalStrings = 0;
|
|
60
|
+
const fileResults: { file: string, wrapped: WrappedString[] }[] = [];
|
|
61
|
+
|
|
62
|
+
for (const result of results.results) {
|
|
63
|
+
if (result.wrapped.length > 0) {
|
|
64
|
+
fileResults.push({
|
|
65
|
+
file: result.file,
|
|
66
|
+
wrapped: result.wrapped
|
|
67
|
+
});
|
|
68
|
+
totalStrings += result.wrapped.length;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
spinner.succeed(chalk.green(`Found ${chalk.bold(totalStrings)} strings to wrap in ${chalk.bold(results.modifiedFiles)} files`));
|
|
73
|
+
|
|
74
|
+
if (totalStrings === 0) {
|
|
75
|
+
spinner.info(chalk.gray('No strings found that need wrapping.'));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Show preview
|
|
80
|
+
if (!options.dryRun) {
|
|
81
|
+
console.log(chalk.bold('\nPreview (first 10 files):'));
|
|
82
|
+
let count = 0;
|
|
83
|
+
for (const fileResult of fileResults.slice(0, 10)) {
|
|
84
|
+
console.log(chalk.cyan(`\n${path.relative(projectRoot, fileResult.file)}:`));
|
|
85
|
+
for (const wrapped of fileResult.wrapped.slice(0, 5)) {
|
|
86
|
+
console.log(chalk.gray(` "${wrapped.original}" → ${wrapped.wrapped}`));
|
|
87
|
+
count++;
|
|
88
|
+
}
|
|
89
|
+
if (fileResult.wrapped.length > 5) {
|
|
90
|
+
console.log(chalk.gray(` ... and ${fileResult.wrapped.length - 5} more`));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (fileResults.length > 10) {
|
|
94
|
+
console.log(chalk.gray(`\n... and ${fileResults.length - 10} more files`));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.log(chalk.bold('\n⚠ This will modify your source files!'));
|
|
98
|
+
console.log(chalk.gray('Use --dry-run to preview without making changes'));
|
|
99
|
+
} else {
|
|
100
|
+
// Dry run - show what would be changed
|
|
101
|
+
console.log(chalk.bold('\nDry Run - Files that would be modified:'));
|
|
102
|
+
for (const fileResult of fileResults) {
|
|
103
|
+
console.log(chalk.cyan(` ${path.relative(projectRoot, fileResult.file)}`) +
|
|
104
|
+
chalk.gray(` (${fileResult.wrapped.length} strings)`));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (options.dryRun) {
|
|
109
|
+
spinner.info(chalk.yellow('Dry run complete. No files were modified.'));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Confirm before making changes
|
|
114
|
+
const confirmSpinner = ora('Applying changes...').start();
|
|
115
|
+
|
|
116
|
+
// Apply changes to each file
|
|
117
|
+
let modifiedCount = 0;
|
|
118
|
+
for (const fileResult of fileResults) {
|
|
119
|
+
const result = wrapper.wrapFile(fileResult.file);
|
|
120
|
+
if (result.modified) {
|
|
121
|
+
modifiedCount++;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
confirmSpinner.succeed(chalk.green(`Modified ${chalk.bold(modifiedCount)} files`));
|
|
126
|
+
|
|
127
|
+
// Also update the locale file with the new keys
|
|
128
|
+
const outputDir = path.resolve(projectRoot, config.outputDir);
|
|
129
|
+
const storage = new FileStorage(outputDir);
|
|
130
|
+
|
|
131
|
+
// Collect all new keys
|
|
132
|
+
const newKeys: Record<string, string> = {};
|
|
133
|
+
for (const fileResult of fileResults) {
|
|
134
|
+
for (const wrapped of fileResult.wrapped) {
|
|
135
|
+
newKeys[wrapped.key] = wrapped.original;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Add keys to default locale
|
|
140
|
+
const defaultLocale = config.defaultLocale;
|
|
141
|
+
const existingLocale = storage.loadLocale(defaultLocale);
|
|
142
|
+
let existingData: Record<string, any> = {};
|
|
143
|
+
|
|
144
|
+
if (existingLocale) {
|
|
145
|
+
existingData = existingLocale.data as Record<string, any>;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Merge new keys
|
|
149
|
+
const mergedData = storage.mergeLocaleData(defaultLocale, newKeys);
|
|
150
|
+
storage.saveLocale(defaultLocale, mergedData);
|
|
151
|
+
|
|
152
|
+
console.log(chalk.green(`\n✓ Added ${chalk.bold(Object.keys(newKeys).length)} new keys to locales/${defaultLocale}.json`));
|
|
153
|
+
|
|
154
|
+
// Show next steps
|
|
155
|
+
console.log(chalk.bold('\nNext steps:'));
|
|
156
|
+
console.log(chalk.gray(' 1. Review the changes in your source files'));
|
|
157
|
+
console.log(chalk.gray(' 2. Run ') + chalk.cyan('npx lokal translate') + chalk.gray(' to translate to other locales'));
|
|
158
|
+
|
|
159
|
+
} catch (error) {
|
|
160
|
+
spinner.fail(chalk.red(`Wrap failed: ${error}`));
|
|
161
|
+
if (options.verbose) {
|
|
162
|
+
console.error(error);
|
|
163
|
+
}
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function registerWrapCommand(program: Command): void {
|
|
169
|
+
program
|
|
170
|
+
.command('wrap')
|
|
171
|
+
.description('Automatically wrap translatable strings in source files')
|
|
172
|
+
.option('-c, --config <path>', 'Path to config file')
|
|
173
|
+
.option('-s, --src <path>', 'Source directory to scan')
|
|
174
|
+
.option('-f, --function <name>', 'Translation function name', 't')
|
|
175
|
+
.option('-d, --dry-run', 'Preview changes without modifying files', false)
|
|
176
|
+
.option('-v, --verbose', 'Verbose output', false)
|
|
177
|
+
.action(wrapCommand);
|
|
178
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import chalk from 'chalk';
|
|
|
5
5
|
import { registerInitCommand } from './commands/init';
|
|
6
6
|
import { registerScanCommand } from './commands/scan';
|
|
7
7
|
import { registerTranslateCommand } from './commands/translate';
|
|
8
|
+
import { registerWrapCommand } from './commands/wrap';
|
|
8
9
|
|
|
9
10
|
const program = new Command();
|
|
10
11
|
|
|
@@ -17,6 +18,7 @@ program
|
|
|
17
18
|
registerInitCommand(program);
|
|
18
19
|
registerScanCommand(program);
|
|
19
20
|
registerTranslateCommand(program);
|
|
21
|
+
registerWrapCommand(program);
|
|
20
22
|
|
|
21
23
|
// Global options
|
|
22
24
|
program
|
|
@@ -29,6 +29,36 @@ declare module 'lokal-core' {
|
|
|
29
29
|
componentName?: string;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
export interface WrapOptions {
|
|
33
|
+
functionName?: string;
|
|
34
|
+
componentName?: string;
|
|
35
|
+
excludePatterns?: RegExp[];
|
|
36
|
+
keyPrefix?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface WrappedString {
|
|
40
|
+
original: string;
|
|
41
|
+
wrapped: string;
|
|
42
|
+
key: string;
|
|
43
|
+
line: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface WrapResult {
|
|
47
|
+
file: string;
|
|
48
|
+
wrapped: WrappedString[];
|
|
49
|
+
errors: string[];
|
|
50
|
+
modified: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class ASTWrapper {
|
|
54
|
+
constructor(options?: WrapOptions);
|
|
55
|
+
generateKey(text: string, filePath: string): string;
|
|
56
|
+
shouldExclude(text: string): boolean;
|
|
57
|
+
wrapFile(filePath: string): WrapResult;
|
|
58
|
+
wrapContent(content: string, filePath?: string): WrapResult;
|
|
59
|
+
wrapDirectory(dirPath: string, extensions?: string[], dryRun?: boolean): { results: WrapResult[], modifiedFiles: number };
|
|
60
|
+
}
|
|
61
|
+
|
|
32
62
|
export class ConfigLoader {
|
|
33
63
|
load(configPath?: string): Promise<LokalConfig>;
|
|
34
64
|
loadSync(configPath?: string): LokalConfig;
|
package/tsconfig.json
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
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
|
-
}
|