locai 2.5.2 → 2.6.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/README.md +1 -0
- package/dist/providers/anthropic.js +1 -1
- package/dist/providers/dashscope.js +2 -2
- package/dist/providers/deepseek.js +1 -1
- package/dist/providers/gemini.js +1 -1
- package/dist/providers/openai.js +1 -1
- package/dist/providers/xai.js +1 -1
- package/dist/services/language-processor.js +18 -2
- package/dist/services/translation-service.js +28 -7
- package/dist/utils/file-manager.d.ts +6 -0
- package/dist/utils/file-manager.js +39 -5
- package/dist/utils/rate-limiter.js +1 -1
- package/package.json +16 -16
package/README.md
CHANGED
|
@@ -59,6 +59,7 @@ Contributing to Locai? Learn how to build, test, and run the project locally.
|
|
|
59
59
|
- **Framework Agnostic**: Works with Next.js, Flutter, Django, and more.
|
|
60
60
|
- **Context Aware**: Knows the difference between "Home" (House) and "Home" (Page).
|
|
61
61
|
- **Cost Efficient**: Smart caching and incremental updates save ~90% of AI costs.
|
|
62
|
+
- **Flexible Structure**: Supports both flat (`en.json`) and nested (`en/translation.json`) directory structures automatically.
|
|
62
63
|
- **Developer Friendly**: TypeScript config, type safety, and great error messages.
|
|
63
64
|
|
|
64
65
|
## 🤝 Community & Contributing
|
|
@@ -20,7 +20,7 @@ class DashScopeProvider extends BaseProvider {
|
|
|
20
20
|
...this.commonHeaders,
|
|
21
21
|
Authorization: `Bearer ${this.getApiKey()}`,
|
|
22
22
|
},
|
|
23
|
-
timeout:
|
|
23
|
+
timeout: config.timeoutMs || 60000,
|
|
24
24
|
maxRedirects: 0,
|
|
25
25
|
validateStatus: (status) => status < 500,
|
|
26
26
|
});
|
|
@@ -31,7 +31,7 @@ class DashScopeProvider extends BaseProvider {
|
|
|
31
31
|
...this.commonHeaders,
|
|
32
32
|
Authorization: `Bearer ${this.getApiKey()}`,
|
|
33
33
|
},
|
|
34
|
-
timeout:
|
|
34
|
+
timeout: config.timeoutMs || 60000,
|
|
35
35
|
maxRedirects: 0,
|
|
36
36
|
validateStatus: (status) => status < 500,
|
|
37
37
|
});
|
package/dist/providers/gemini.js
CHANGED
package/dist/providers/openai.js
CHANGED
package/dist/providers/xai.js
CHANGED
|
@@ -43,8 +43,24 @@ export class LanguageProcessor {
|
|
|
43
43
|
let savedMessage = undefined;
|
|
44
44
|
statisticsManager.initLanguageStats(safeTargetLang);
|
|
45
45
|
const sourceDir = path.dirname(sourceFile);
|
|
46
|
-
|
|
47
|
-
const
|
|
46
|
+
// Detect structure based on source file name
|
|
47
|
+
const isNestedStructure = path.basename(sourceFile) === "translation.json";
|
|
48
|
+
let targetPath;
|
|
49
|
+
if (isNestedStructure) {
|
|
50
|
+
// Nested: locales/tr/translation.json
|
|
51
|
+
// We assume sourceDir ends with the language code (e.g. .../en)
|
|
52
|
+
// So we need to go up one level and then into target lang
|
|
53
|
+
const localesRoot = path.dirname(sourceDir);
|
|
54
|
+
const targetDir = path.join(localesRoot, safeTargetLang);
|
|
55
|
+
// Ensure target directory exists for nested structure
|
|
56
|
+
await FileManager.ensureDir(targetDir);
|
|
57
|
+
targetPath = path.join(targetDir, "translation.json");
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
// Flat: locales/tr.json
|
|
61
|
+
const safeTargetFilename = `${safeTargetLang}.json`;
|
|
62
|
+
targetPath = InputValidator.createSafeFilePath(sourceDir, safeTargetFilename);
|
|
63
|
+
}
|
|
48
64
|
let targetContent = {};
|
|
49
65
|
try {
|
|
50
66
|
targetContent = await FileManager.readJSON(targetPath);
|
|
@@ -248,16 +248,28 @@ class TranslationService {
|
|
|
248
248
|
*/
|
|
249
249
|
async removeDeletedKeysFromTargets(sourceFile, deletedKeys, options) {
|
|
250
250
|
const sourceDir = path.dirname(sourceFile);
|
|
251
|
+
// Detect structure based on source file name
|
|
252
|
+
const isNestedStructure = path.basename(sourceFile) === "translation.json";
|
|
251
253
|
let totalRemoved = 0;
|
|
252
254
|
let filesProcessed = 0;
|
|
253
255
|
for (const targetLang of options.targets || []) {
|
|
254
256
|
try {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
+
let targetPath;
|
|
258
|
+
if (isNestedStructure) {
|
|
259
|
+
// Nested: locales/tr/translation.json
|
|
260
|
+
const localesRoot = path.dirname(sourceDir);
|
|
261
|
+
const targetDir = path.join(localesRoot, targetLang);
|
|
262
|
+
targetPath = path.join(targetDir, "translation.json");
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
// Flat: locales/tr.json
|
|
266
|
+
const safeTargetFilename = `${targetLang}.json`;
|
|
267
|
+
targetPath = InputValidator.createSafeFilePath(sourceDir, safeTargetFilename);
|
|
268
|
+
}
|
|
257
269
|
// Check if target file exists
|
|
258
270
|
const fileExists = await FileManager.exists(targetPath);
|
|
259
271
|
if (!fileExists) {
|
|
260
|
-
await uiManager.log(` Skipping ${targetLang}
|
|
272
|
+
await uiManager.log(` Skipping ${targetLang} (file doesn't exist)`);
|
|
261
273
|
continue;
|
|
262
274
|
}
|
|
263
275
|
// Read target file
|
|
@@ -279,12 +291,12 @@ class TranslationService {
|
|
|
279
291
|
await uiManager.log(` ${targetLang}.json: Removed ${removedFromThisFile} keys`);
|
|
280
292
|
}
|
|
281
293
|
else {
|
|
282
|
-
await uiManager.log(` No keys to remove from ${targetLang}
|
|
294
|
+
await uiManager.log(` No keys to remove from ${targetLang}`);
|
|
283
295
|
}
|
|
284
296
|
filesProcessed++;
|
|
285
297
|
}
|
|
286
298
|
catch (error) {
|
|
287
|
-
await uiManager.log(` \u274c Error processing ${targetLang}
|
|
299
|
+
await uiManager.log(` \u274c Error processing ${targetLang}: ${error.message}`);
|
|
288
300
|
}
|
|
289
301
|
}
|
|
290
302
|
await uiManager.log(`Cleanup Summary: Removed ${totalRemoved} keys from ${filesProcessed} files\n`);
|
|
@@ -300,14 +312,23 @@ class TranslationService {
|
|
|
300
312
|
const sourceContent = await FileManager.readJSON(file);
|
|
301
313
|
const flattenedSource = ObjectTransformer.flatten(sourceContent);
|
|
302
314
|
const orchestrator = new Orchestrator(options);
|
|
315
|
+
const sourceDir = path.dirname(file);
|
|
316
|
+
const isNestedStructure = path.basename(file) === "translation.json";
|
|
303
317
|
const languageResults = await Promise.all((options.targets || []).map(async (targetLang) => {
|
|
304
318
|
try {
|
|
305
|
-
|
|
319
|
+
let targetPath;
|
|
320
|
+
if (isNestedStructure) {
|
|
321
|
+
const localesRoot = path.dirname(sourceDir);
|
|
322
|
+
targetPath = path.join(localesRoot, targetLang, "translation.json");
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
targetPath = path.join(sourceDir, `${targetLang}.json`);
|
|
326
|
+
}
|
|
306
327
|
const targetContent = await FileManager.readJSON(targetPath);
|
|
307
328
|
return { targetLang, targetPath, content: targetContent, success: true };
|
|
308
329
|
}
|
|
309
330
|
catch (err) {
|
|
310
|
-
await uiManager.log(`Could not read ${targetLang}
|
|
331
|
+
await uiManager.log(`Could not read ${targetLang}: ${err.message}`);
|
|
311
332
|
return {
|
|
312
333
|
targetLang,
|
|
313
334
|
targetPath: "",
|
|
@@ -40,6 +40,12 @@ declare class FileManager {
|
|
|
40
40
|
* @returns Current configuration
|
|
41
41
|
*/
|
|
42
42
|
static getConfig(): FileOptions;
|
|
43
|
+
/**
|
|
44
|
+
* Find locale files in the specified directory
|
|
45
|
+
* @param localesDir - Directory containing locale files
|
|
46
|
+
* @param sourceLang - Source language code
|
|
47
|
+
* @returns Array of file paths
|
|
48
|
+
*/
|
|
43
49
|
/**
|
|
44
50
|
* Find locale files in the specified directory
|
|
45
51
|
* @param localesDir - Directory containing locale files
|
|
@@ -37,6 +37,12 @@ class FileManager {
|
|
|
37
37
|
static getConfig() {
|
|
38
38
|
return this.options || this.defaultOptions;
|
|
39
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Find locale files in the specified directory
|
|
42
|
+
* @param localesDir - Directory containing locale files
|
|
43
|
+
* @param sourceLang - Source language code
|
|
44
|
+
* @returns Array of file paths
|
|
45
|
+
*/
|
|
40
46
|
/**
|
|
41
47
|
* Find locale files in the specified directory
|
|
42
48
|
* @param localesDir - Directory containing locale files
|
|
@@ -45,17 +51,45 @@ class FileManager {
|
|
|
45
51
|
*/
|
|
46
52
|
static async findLocaleFiles(localesDir, sourceLang) {
|
|
47
53
|
try {
|
|
48
|
-
//
|
|
54
|
+
// Priority 1: Check for nested structure: localesDir/sourceLang/translation.json
|
|
55
|
+
// This is the standard i18next-fs-backend pattern
|
|
56
|
+
const nestedPath = path.join(localesDir, sourceLang, "translation.json");
|
|
57
|
+
if (await this.exists(nestedPath)) {
|
|
58
|
+
return [nestedPath];
|
|
59
|
+
}
|
|
60
|
+
// Priority 2: Check for flat structure: localesDir/sourceLang.json
|
|
61
|
+
const flatPath = path.join(localesDir, `${sourceLang}.json`);
|
|
62
|
+
if (await this.exists(flatPath)) {
|
|
63
|
+
return [flatPath];
|
|
64
|
+
}
|
|
65
|
+
// Priority 3: Scan directory for any file starting with sourceLang
|
|
66
|
+
// This matches "en-US.json" or "en_GB.json" if sourceLang is "en"
|
|
49
67
|
const files = await fs.readdir(localesDir);
|
|
50
68
|
const sourceFiles = files
|
|
51
|
-
.filter((file) => file.startsWith(`${sourceLang}.`))
|
|
69
|
+
.filter((file) => file.startsWith(`${sourceLang}.`) || file === sourceLang)
|
|
52
70
|
.map((file) => path.join(localesDir, file));
|
|
53
71
|
if (sourceFiles.length === 0) {
|
|
54
72
|
throw new Error(`Source language file not found for '${sourceLang}' in ${localesDir}`);
|
|
55
73
|
}
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
74
|
+
// Filter specifically for files or directories that look like the locale
|
|
75
|
+
const validFiles = [];
|
|
76
|
+
for (const file of sourceFiles) {
|
|
77
|
+
const stats = await fs.stat(file);
|
|
78
|
+
if (stats.isDirectory()) {
|
|
79
|
+
// If it's a directory, check for translation.json inside
|
|
80
|
+
const nestedTrial = path.join(file, "translation.json");
|
|
81
|
+
if (await this.exists(nestedTrial)) {
|
|
82
|
+
validFiles.push(nestedTrial);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
validFiles.push(file);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (validFiles.length > 0) {
|
|
90
|
+
return validFiles;
|
|
91
|
+
}
|
|
92
|
+
throw new Error(`Source language file not found for '${sourceLang}' in ${localesDir}`);
|
|
59
93
|
}
|
|
60
94
|
catch (err) {
|
|
61
95
|
if (err.code === "ENOENT") {
|
|
@@ -398,7 +398,7 @@ class RateLimiter {
|
|
|
398
398
|
}
|
|
399
399
|
const config = {
|
|
400
400
|
queueStrategy: process.env.QUEUE_STRATEGY || "priority",
|
|
401
|
-
queueTimeout: parseInt(process.env.QUEUE_TIMEOUT || "
|
|
401
|
+
queueTimeout: parseInt(process.env.QUEUE_TIMEOUT || "60000"),
|
|
402
402
|
adaptiveThrottling: process.env.ADAPTIVE_THROTTLING !== "false",
|
|
403
403
|
};
|
|
404
404
|
export default new RateLimiter(config);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "locai",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/core/orchestrator.js",
|
|
6
6
|
"types": "dist/core/orchestrator.d.ts",
|
|
@@ -72,35 +72,35 @@
|
|
|
72
72
|
"./translator": "./dist/commands/translator.js"
|
|
73
73
|
},
|
|
74
74
|
"dependencies": {
|
|
75
|
-
"axios": "1.13.
|
|
75
|
+
"axios": "1.13.6",
|
|
76
76
|
"c12": "^3.3.3",
|
|
77
|
-
"commander": "^14.0.
|
|
77
|
+
"commander": "^14.0.3",
|
|
78
78
|
"dot-properties": "^1.1.1",
|
|
79
|
-
"dotenv": "^17.
|
|
80
|
-
"fast-xml-parser": "^5.
|
|
81
|
-
"gettext-parser": "^9.0.
|
|
79
|
+
"dotenv": "^17.3.1",
|
|
80
|
+
"fast-xml-parser": "^5.4.2",
|
|
81
|
+
"gettext-parser": "^9.0.1",
|
|
82
82
|
"jiti": "^2.6.1",
|
|
83
83
|
"js-yaml": "^4.1.1",
|
|
84
|
-
"lru-cache": "^11.2.
|
|
85
|
-
"p-limit": "^7.
|
|
84
|
+
"lru-cache": "^11.2.6",
|
|
85
|
+
"p-limit": "^7.3.0",
|
|
86
86
|
"prompts": "^2.4.2",
|
|
87
87
|
"ts-morph": "^27.0.2",
|
|
88
|
-
"vectra": "^0.
|
|
88
|
+
"vectra": "^0.12.3"
|
|
89
89
|
},
|
|
90
90
|
"devDependencies": {
|
|
91
|
-
"@eslint/js": "^9.39.
|
|
91
|
+
"@eslint/js": "^9.39.3",
|
|
92
92
|
"@types/js-yaml": "^4.0.9",
|
|
93
|
-
"@types/node": "^25.
|
|
93
|
+
"@types/node": "^25.3.3",
|
|
94
94
|
"@types/prompts": "^2.4.9",
|
|
95
|
-
"@vitest/coverage-v8": "^4.0.
|
|
96
|
-
"eslint": "^9.39.
|
|
95
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
96
|
+
"eslint": "^9.39.3",
|
|
97
97
|
"eslint-config-prettier": "^10.1.8",
|
|
98
|
-
"globals": "^
|
|
99
|
-
"prettier": "^3.
|
|
98
|
+
"globals": "^17.4.0",
|
|
99
|
+
"prettier": "^3.8.1",
|
|
100
100
|
"std-mocks": "^2.0.0",
|
|
101
101
|
"tsx": "^4.21.0",
|
|
102
102
|
"typescript": "^5.9.3",
|
|
103
|
-
"vitest": "^4.0.
|
|
103
|
+
"vitest": "^4.0.18"
|
|
104
104
|
},
|
|
105
105
|
"files": [
|
|
106
106
|
"dist/"
|