locai 2.2.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/helpers.d.ts +2 -0
- package/dist/cli/helpers.js +344 -0
- package/dist/cli.js +38 -519
- package/dist/commands/interactive-review.d.ts +9 -0
- package/dist/commands/interactive-review.js +109 -0
- package/dist/commands/review.d.ts +1 -37
- package/dist/commands/review.js +40 -160
- package/dist/commands/translator.d.ts +1 -1
- package/dist/config/index.d.ts +1 -0
- package/dist/config/index.js +26 -0
- package/dist/config/setup.d.ts +10 -0
- package/dist/config/setup.js +79 -0
- package/dist/core/adapters/android-adapter.d.ts +12 -0
- package/dist/core/adapters/android-adapter.js +107 -0
- package/dist/core/adapters/factory.js +4 -0
- package/dist/core/adapters/ios-adapter.d.ts +10 -0
- package/dist/core/adapters/ios-adapter.js +45 -0
- package/dist/providers/openai.js +3 -3
- package/dist/services/language-processor.d.ts +22 -0
- package/dist/services/language-processor.js +239 -0
- package/dist/services/translation-service.d.ts +1 -49
- package/dist/services/translation-service.js +3 -215
- package/dist/types/index.d.ts +58 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/ui-manager.js +2 -2
- package/package.json +18 -7
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { XMLParser, XMLBuilder } from "fast-xml-parser";
|
|
2
|
+
/**
|
|
3
|
+
* Adapter for Android XML resources files (strings.xml)
|
|
4
|
+
*/
|
|
5
|
+
export class AndroidAdapter {
|
|
6
|
+
extensions = [".xml"];
|
|
7
|
+
parser;
|
|
8
|
+
builder;
|
|
9
|
+
constructor() {
|
|
10
|
+
this.parser = new XMLParser({
|
|
11
|
+
ignoreAttributes: false,
|
|
12
|
+
attributeNamePrefix: "@_",
|
|
13
|
+
textNodeName: "#text",
|
|
14
|
+
isArray: (name) => name === "string" || name === "string-array" || name === "item",
|
|
15
|
+
});
|
|
16
|
+
this.builder = new XMLBuilder({
|
|
17
|
+
ignoreAttributes: false,
|
|
18
|
+
attributeNamePrefix: "@_",
|
|
19
|
+
textNodeName: "#text",
|
|
20
|
+
format: true,
|
|
21
|
+
indentBy: " ",
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
async parse(content) {
|
|
25
|
+
try {
|
|
26
|
+
const parsed = this.parser.parse(content);
|
|
27
|
+
// Extract strings resources
|
|
28
|
+
const resources = parsed.resources || {};
|
|
29
|
+
const result = {};
|
|
30
|
+
// Handle regular strings
|
|
31
|
+
if (resources.string) {
|
|
32
|
+
resources.string.forEach((item) => {
|
|
33
|
+
const key = item["@_name"];
|
|
34
|
+
const value = item["#text"];
|
|
35
|
+
if (key) {
|
|
36
|
+
result[key] = value || "";
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
// Handle string arrays (flattened as array_name.index)
|
|
41
|
+
if (resources["string-array"]) {
|
|
42
|
+
resources["string-array"].forEach((array) => {
|
|
43
|
+
const arrayName = array["@_name"];
|
|
44
|
+
if (array.item) {
|
|
45
|
+
array.item.forEach((value, index) => {
|
|
46
|
+
result[`${arrayName}.${index}`] = value;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
throw new Error(`Failed to parse Android XML: ${error.message}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async serialize(data) {
|
|
58
|
+
const resources = {
|
|
59
|
+
string: [],
|
|
60
|
+
"string-array": [],
|
|
61
|
+
};
|
|
62
|
+
// Collect keys to handle arrays properly
|
|
63
|
+
const arrays = {};
|
|
64
|
+
const strings = {};
|
|
65
|
+
for (const [key, value] of Object.entries(data)) {
|
|
66
|
+
if (key.includes(".")) {
|
|
67
|
+
const [arrayName, indexStr] = key.split(".");
|
|
68
|
+
if (!isNaN(Number(indexStr))) {
|
|
69
|
+
if (!arrays[arrayName])
|
|
70
|
+
arrays[arrayName] = [];
|
|
71
|
+
arrays[arrayName][Number(indexStr)] = value;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
strings[key] = value;
|
|
76
|
+
}
|
|
77
|
+
// Add strings
|
|
78
|
+
for (const [key, value] of Object.entries(strings)) {
|
|
79
|
+
resources.string.push({
|
|
80
|
+
"@_name": key,
|
|
81
|
+
"#text": value,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
// Add arrays
|
|
85
|
+
for (const [key, items] of Object.entries(arrays)) {
|
|
86
|
+
resources["string-array"].push({
|
|
87
|
+
"@_name": key,
|
|
88
|
+
item: items.filter((i) => i !== undefined),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
// Sort by name for consistency
|
|
92
|
+
resources.string.sort((a, b) => a["@_name"].localeCompare(b["@_name"]));
|
|
93
|
+
resources["string-array"].sort((a, b) => a["@_name"].localeCompare(b["@_name"]));
|
|
94
|
+
if (resources["string-array"].length === 0)
|
|
95
|
+
delete resources["string-array"];
|
|
96
|
+
if (resources.string.length === 0)
|
|
97
|
+
delete resources.string;
|
|
98
|
+
const xmlObj = {
|
|
99
|
+
"?xml": {
|
|
100
|
+
"@_version": "1.0",
|
|
101
|
+
"@_encoding": "utf-8",
|
|
102
|
+
},
|
|
103
|
+
resources: resources,
|
|
104
|
+
};
|
|
105
|
+
return this.builder.build(xmlObj);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -3,6 +3,8 @@ import { JsonAdapter } from "./json-adapter.js";
|
|
|
3
3
|
import { YamlAdapter } from "./yaml-adapter.js";
|
|
4
4
|
import { PoAdapter } from "./po-adapter.js";
|
|
5
5
|
import { PropertiesAdapter } from "./properties-adapter.js";
|
|
6
|
+
import { AndroidAdapter } from "./android-adapter.js";
|
|
7
|
+
import { IosAdapter } from "./ios-adapter.js";
|
|
6
8
|
/**
|
|
7
9
|
* Factory class to manage file format adapters
|
|
8
10
|
*/
|
|
@@ -12,6 +14,8 @@ export class FormatFactory {
|
|
|
12
14
|
new YamlAdapter(),
|
|
13
15
|
new PoAdapter(),
|
|
14
16
|
new PropertiesAdapter(),
|
|
17
|
+
new AndroidAdapter(),
|
|
18
|
+
new IosAdapter(),
|
|
15
19
|
];
|
|
16
20
|
/**
|
|
17
21
|
* Get adapter for a specific file path
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { FileFormatAdapter } from "./base-adapter.js";
|
|
2
|
+
/**
|
|
3
|
+
* Adapter for iOS .strings files
|
|
4
|
+
* Format: "KEY" = "VALUE";
|
|
5
|
+
*/
|
|
6
|
+
export declare class IosAdapter implements FileFormatAdapter {
|
|
7
|
+
extensions: string[];
|
|
8
|
+
parse(content: string): Promise<Record<string, any>>;
|
|
9
|
+
serialize(data: Record<string, any>): Promise<string>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter for iOS .strings files
|
|
3
|
+
* Format: "KEY" = "VALUE";
|
|
4
|
+
*/
|
|
5
|
+
export class IosAdapter {
|
|
6
|
+
extensions = [".strings"];
|
|
7
|
+
async parse(content) {
|
|
8
|
+
const result = {};
|
|
9
|
+
const lines = content.split(/\r?\n/);
|
|
10
|
+
for (const line of lines) {
|
|
11
|
+
const trimmed = line.trim();
|
|
12
|
+
// Skip comments and empty lines
|
|
13
|
+
if (!trimmed || trimmed.startsWith("/*") || trimmed.startsWith("//")) {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
// Parse "key" = "value";
|
|
17
|
+
// Handle escaped quotes in value
|
|
18
|
+
const match = trimmed.match(/^"(.+)"\s*=\s*"(.*)";$/);
|
|
19
|
+
if (match) {
|
|
20
|
+
const key = match[1];
|
|
21
|
+
let value = match[2];
|
|
22
|
+
// Unescape quotes and newlines if needed
|
|
23
|
+
value = value.replace(/\\"/g, '"').replace(/\\n/g, "\n");
|
|
24
|
+
result[key] = value;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
async serialize(data) {
|
|
30
|
+
let output = "";
|
|
31
|
+
// Sort keys for deterministic output
|
|
32
|
+
const keys = Object.keys(data).sort();
|
|
33
|
+
for (const key of keys) {
|
|
34
|
+
let value = data[key];
|
|
35
|
+
if (typeof value !== "string") {
|
|
36
|
+
value = String(value);
|
|
37
|
+
}
|
|
38
|
+
// Escape quotes and newlines
|
|
39
|
+
const safeKey = key; // Keys usually don't have quotes, but simplistic handling
|
|
40
|
+
const safeValue = value.replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
41
|
+
output += `"${safeKey}" = "${safeValue}";\n`;
|
|
42
|
+
}
|
|
43
|
+
return output;
|
|
44
|
+
}
|
|
45
|
+
}
|
package/dist/providers/openai.js
CHANGED
|
@@ -40,7 +40,7 @@ class OpenAIProvider extends BaseProvider {
|
|
|
40
40
|
return RetryHelper.withRetry(async () => {
|
|
41
41
|
try {
|
|
42
42
|
const response = await this.client.post(this.getEndpoint(), {
|
|
43
|
-
model: config.model || "gpt-
|
|
43
|
+
model: config.model || "gpt-4o",
|
|
44
44
|
...promptData,
|
|
45
45
|
temperature: config.temperature,
|
|
46
46
|
max_completion_tokens: config.maxTokens || config.max_tokens,
|
|
@@ -65,7 +65,7 @@ class OpenAIProvider extends BaseProvider {
|
|
|
65
65
|
}
|
|
66
66
|
async analyze(prompt, options = {}) {
|
|
67
67
|
const config = this.getConfig({
|
|
68
|
-
model: options.model || "gpt-
|
|
68
|
+
model: options.model || "gpt-4o",
|
|
69
69
|
temperature: options.temperature || 0.2,
|
|
70
70
|
maxTokens: options.maxTokens || 1000,
|
|
71
71
|
});
|
|
@@ -94,7 +94,7 @@ class OpenAIProvider extends BaseProvider {
|
|
|
94
94
|
}
|
|
95
95
|
async chat(messages, options = {}) {
|
|
96
96
|
const config = this.getConfig({
|
|
97
|
-
model: options.model || "gpt-
|
|
97
|
+
model: options.model || "gpt-4o",
|
|
98
98
|
temperature: options.temperature || 0.3,
|
|
99
99
|
maxTokens: options.maxTokens || 2000,
|
|
100
100
|
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import Orchestrator from "../core/orchestrator.js";
|
|
2
|
+
import { TranslationOptions, GlobalStats, LanguageProcessResult } from "../types/index.js";
|
|
3
|
+
export declare class LanguageProcessor {
|
|
4
|
+
/**
|
|
5
|
+
* Process a single language translation.
|
|
6
|
+
* @param {string} targetLang - Target language code.
|
|
7
|
+
* @param {string} sourceFile - Path to source file.
|
|
8
|
+
* @param {Record<string, any>} flattenedSource - Flattened source content.
|
|
9
|
+
* @param {Orchestrator} orchestrator - Orchestrator instance.
|
|
10
|
+
* @param {TranslationOptions} options - Translation options.
|
|
11
|
+
* @param {GlobalStats} globalStats - Global statistics object.
|
|
12
|
+
* @param {any} comparison - State comparison result.
|
|
13
|
+
* @returns {Promise<LanguageProcessResult>} - Status and messages for this language.
|
|
14
|
+
*/
|
|
15
|
+
static processLanguage(targetLang: string, sourceFile: string, flattenedSource: Record<string, any>, orchestrator: Orchestrator, options: TranslationOptions, globalStats: GlobalStats, comparison: any): Promise<LanguageProcessResult>;
|
|
16
|
+
/**
|
|
17
|
+
* Check if text only contains placeholders and whitespace.
|
|
18
|
+
* @param {string} text - Text to check.
|
|
19
|
+
* @returns {boolean} - True if text only contains placeholders.
|
|
20
|
+
*/
|
|
21
|
+
static isPlaceholderOnlyText(text: string): boolean;
|
|
22
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { FileManager } from "../utils/file-manager.js";
|
|
3
|
+
import ObjectTransformer from "../utils/object-transformer.js";
|
|
4
|
+
import InputValidator from "../utils/input-validator.js";
|
|
5
|
+
import uiManager from "../utils/ui-manager.js";
|
|
6
|
+
import statisticsManager from "../utils/statistics-manager.js";
|
|
7
|
+
export class LanguageProcessor {
|
|
8
|
+
/**
|
|
9
|
+
* Process a single language translation.
|
|
10
|
+
* @param {string} targetLang - Target language code.
|
|
11
|
+
* @param {string} sourceFile - Path to source file.
|
|
12
|
+
* @param {Record<string, any>} flattenedSource - Flattened source content.
|
|
13
|
+
* @param {Orchestrator} orchestrator - Orchestrator instance.
|
|
14
|
+
* @param {TranslationOptions} options - Translation options.
|
|
15
|
+
* @param {GlobalStats} globalStats - Global statistics object.
|
|
16
|
+
* @param {any} comparison - State comparison result.
|
|
17
|
+
* @returns {Promise<LanguageProcessResult>} - Status and messages for this language.
|
|
18
|
+
*/
|
|
19
|
+
static async processLanguage(targetLang, sourceFile, flattenedSource, orchestrator, options, globalStats, comparison) {
|
|
20
|
+
if (!targetLang) {
|
|
21
|
+
throw new Error("Invalid target language provided");
|
|
22
|
+
}
|
|
23
|
+
const langStartTime = Date.now();
|
|
24
|
+
try {
|
|
25
|
+
if (!targetLang || typeof targetLang !== "string") {
|
|
26
|
+
throw new Error("Invalid target language provided");
|
|
27
|
+
}
|
|
28
|
+
if (!sourceFile || typeof sourceFile !== "string") {
|
|
29
|
+
throw new Error("Invalid source file path provided");
|
|
30
|
+
}
|
|
31
|
+
if (!flattenedSource || typeof flattenedSource !== "object") {
|
|
32
|
+
throw new Error("Invalid flattened source data provided");
|
|
33
|
+
}
|
|
34
|
+
if (!orchestrator || typeof orchestrator.processTranslations !== "function") {
|
|
35
|
+
throw new Error("Invalid orchestrator instance provided");
|
|
36
|
+
}
|
|
37
|
+
if (!globalStats || typeof globalStats !== "object") {
|
|
38
|
+
throw new Error("Invalid global stats object provided");
|
|
39
|
+
}
|
|
40
|
+
const safeTargetLang = InputValidator.validateLanguageCode(targetLang, "target language");
|
|
41
|
+
await uiManager.log(`Starting translations for ${safeTargetLang}`);
|
|
42
|
+
let finalStatus = null;
|
|
43
|
+
let savedMessage = undefined;
|
|
44
|
+
statisticsManager.initLanguageStats(safeTargetLang);
|
|
45
|
+
const sourceDir = path.dirname(sourceFile);
|
|
46
|
+
const safeTargetFilename = `${safeTargetLang}.json`;
|
|
47
|
+
const targetPath = InputValidator.createSafeFilePath(sourceDir, safeTargetFilename);
|
|
48
|
+
let targetContent = {};
|
|
49
|
+
try {
|
|
50
|
+
targetContent = await FileManager.readJSON(targetPath);
|
|
51
|
+
if (!targetContent || typeof targetContent !== "object") {
|
|
52
|
+
console.warn(`Invalid content in ${targetPath}, using empty object`);
|
|
53
|
+
targetContent = {};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
if (err.code === "ENOENT") {
|
|
58
|
+
await uiManager.log(`\ud83c\udd95 Creating new translation file for ${safeTargetLang}`);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
if (options.debug) {
|
|
62
|
+
console.warn(`Error reading ${targetPath}: ${err.message}, using empty object`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
targetContent = {};
|
|
66
|
+
}
|
|
67
|
+
let flattenedTarget = {};
|
|
68
|
+
try {
|
|
69
|
+
flattenedTarget = ObjectTransformer.flatten(targetContent);
|
|
70
|
+
if (!flattenedTarget || typeof flattenedTarget !== "object") {
|
|
71
|
+
flattenedTarget = {};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
console.warn(`Error flattening target content: ${err.message}, using empty object`);
|
|
76
|
+
flattenedTarget = {};
|
|
77
|
+
}
|
|
78
|
+
const missingKeys = [];
|
|
79
|
+
let hasPlaceholderOnlyChanges = false;
|
|
80
|
+
for (const [key, sourceText] of Object.entries(flattenedSource)) {
|
|
81
|
+
try {
|
|
82
|
+
InputValidator.validateKey(key, "translation key");
|
|
83
|
+
InputValidator.validateText(sourceText, "source text");
|
|
84
|
+
}
|
|
85
|
+
catch (keyError) {
|
|
86
|
+
await uiManager.log(`\u26a0\ufe0f Skipping invalid key/text: ${keyError.message}`);
|
|
87
|
+
globalStats.languages[safeTargetLang].failed++;
|
|
88
|
+
globalStats.failed++;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
globalStats.languages[safeTargetLang].processed++;
|
|
92
|
+
if (this.isPlaceholderOnlyText(sourceText)) {
|
|
93
|
+
const existingValue = flattenedTarget[key];
|
|
94
|
+
if (existingValue !== sourceText) {
|
|
95
|
+
flattenedTarget[key] = sourceText;
|
|
96
|
+
hasPlaceholderOnlyChanges = true;
|
|
97
|
+
if (options.debug) {
|
|
98
|
+
await uiManager.log(` Copying placeholder-only text: "${sourceText}"`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
globalStats.languages[safeTargetLang].skipped++;
|
|
102
|
+
globalStats.skipped++;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const isNewKey = comparison.newKeys.includes(key);
|
|
106
|
+
const isModifiedKey = comparison.modifiedKeys.includes(key);
|
|
107
|
+
const keyExistsInTarget = key in flattenedTarget;
|
|
108
|
+
if (keyExistsInTarget && !options.forceUpdate && !isModifiedKey) {
|
|
109
|
+
globalStats.languages[safeTargetLang].skipped++;
|
|
110
|
+
globalStats.skipped++;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
missingKeys.push({
|
|
114
|
+
key,
|
|
115
|
+
text: sourceText,
|
|
116
|
+
targetLang: safeTargetLang,
|
|
117
|
+
existingTranslation: flattenedTarget[key],
|
|
118
|
+
isModified: isModifiedKey,
|
|
119
|
+
isNew: isNewKey,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
if (missingKeys.length === 0 && !hasPlaceholderOnlyChanges) {
|
|
123
|
+
await uiManager.log(`All translations exist for ${safeTargetLang}`);
|
|
124
|
+
globalStats.languages[safeTargetLang].timeMs = Date.now() - langStartTime;
|
|
125
|
+
return { status: { completed: 0, total: 0, language: safeTargetLang } };
|
|
126
|
+
}
|
|
127
|
+
if (missingKeys.length > 0) {
|
|
128
|
+
await uiManager.log(`Found ${missingKeys.length} missing translations for ${safeTargetLang}`);
|
|
129
|
+
}
|
|
130
|
+
let results = [];
|
|
131
|
+
if (missingKeys.length > 0) {
|
|
132
|
+
try {
|
|
133
|
+
results = await orchestrator.processTranslations(missingKeys);
|
|
134
|
+
if (!Array.isArray(results)) {
|
|
135
|
+
console.warn("Invalid results from orchestrator, using empty array");
|
|
136
|
+
results = [];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
console.error(`Error processing translations for ${safeTargetLang}: ${err.message}`);
|
|
141
|
+
results = [];
|
|
142
|
+
if (globalStats.languages[safeTargetLang]) {
|
|
143
|
+
globalStats.languages[safeTargetLang].failed += missingKeys.length;
|
|
144
|
+
globalStats.failed += missingKeys.length;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
if (hasPlaceholderOnlyChanges) {
|
|
150
|
+
await uiManager.log(`Found placeholder-only changes for ${safeTargetLang}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
if (orchestrator?.progress &&
|
|
155
|
+
typeof orchestrator.progress.getStatus === "function") {
|
|
156
|
+
finalStatus = orchestrator.progress.getStatus();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
console.warn(`Error getting orchestrator status: ${err.message}`);
|
|
161
|
+
}
|
|
162
|
+
let validResults = [];
|
|
163
|
+
try {
|
|
164
|
+
validResults = results.filter((result) => result && result.success === true);
|
|
165
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
166
|
+
validResults = validResults.filter((result) => result.key &&
|
|
167
|
+
typeof result.key === "string" &&
|
|
168
|
+
result.translated &&
|
|
169
|
+
typeof result.translated === "string");
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
console.warn(`Error filtering results: ${err.message}`);
|
|
173
|
+
validResults = [];
|
|
174
|
+
}
|
|
175
|
+
if (validResults.length > 0) {
|
|
176
|
+
validResults.forEach(({ key, translated }) => {
|
|
177
|
+
if (translated) {
|
|
178
|
+
flattenedTarget[key] = translated;
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
const hasChangesToSave = validResults.length > 0 || hasPlaceholderOnlyChanges;
|
|
183
|
+
if (hasChangesToSave) {
|
|
184
|
+
const unflattened = ObjectTransformer.unflatten(flattenedTarget);
|
|
185
|
+
await FileManager.writeJSON(targetPath, unflattened);
|
|
186
|
+
globalStats.total += validResults.length;
|
|
187
|
+
globalStats.success += validResults.length;
|
|
188
|
+
globalStats.failed += results.length - validResults.length;
|
|
189
|
+
globalStats.languages[safeTargetLang].added += validResults.length;
|
|
190
|
+
globalStats.languages[safeTargetLang].failed +=
|
|
191
|
+
results.length - validResults.length;
|
|
192
|
+
validResults.forEach((result) => {
|
|
193
|
+
if (result.context) {
|
|
194
|
+
const category = result.context.category || "general";
|
|
195
|
+
globalStats.byCategory[category] =
|
|
196
|
+
(globalStats.byCategory[category] || 0) + 1;
|
|
197
|
+
if (!globalStats.details[category]) {
|
|
198
|
+
globalStats.details[category] = {
|
|
199
|
+
totalConfidence: 0,
|
|
200
|
+
samples: 0,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
globalStats.details[category].totalConfidence +=
|
|
204
|
+
result.context.confidence || 0;
|
|
205
|
+
globalStats.details[category].samples++;
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
savedMessage = `Translations saved: ${safeTargetLang}.json`;
|
|
209
|
+
}
|
|
210
|
+
globalStats.languages[safeTargetLang].timeMs = Date.now() - langStartTime;
|
|
211
|
+
return { status: finalStatus, savedMessage };
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
const safeError = error.message.includes("outside working directory")
|
|
215
|
+
? "Invalid file path detected"
|
|
216
|
+
: error.message;
|
|
217
|
+
await uiManager.log(`\n\u274c Error processing ${targetLang}: ${safeError}`);
|
|
218
|
+
if (globalStats.languages[targetLang]) {
|
|
219
|
+
globalStats.languages[targetLang].error = safeError;
|
|
220
|
+
globalStats.languages[targetLang].timeMs = Date.now() - langStartTime;
|
|
221
|
+
}
|
|
222
|
+
return { status: null, error: safeError };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Check if text only contains placeholders and whitespace.
|
|
227
|
+
* @param {string} text - Text to check.
|
|
228
|
+
* @returns {boolean} - True if text only contains placeholders.
|
|
229
|
+
*/
|
|
230
|
+
static isPlaceholderOnlyText(text) {
|
|
231
|
+
if (!text || typeof text !== "string") {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
// Remove all placeholders from the text
|
|
235
|
+
const placeholderRegex = /\{[^}]+\}|\$\{[^}]+\}|%[sd]/g;
|
|
236
|
+
const textWithoutPlaceholders = text.replace(placeholderRegex, "").trim();
|
|
237
|
+
return textWithoutPlaceholders.length === 0;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
@@ -1,60 +1,12 @@
|
|
|
1
1
|
import Orchestrator from "../core/orchestrator.js";
|
|
2
|
-
import { OrchestratorOptions } from "../core/orchestrator.js";
|
|
3
2
|
import StateManager from "../utils/state-manager.js";
|
|
4
|
-
|
|
5
|
-
source: string;
|
|
6
|
-
targets?: string[];
|
|
7
|
-
localesDir?: string;
|
|
8
|
-
debug?: boolean;
|
|
9
|
-
forceUpdate?: boolean;
|
|
10
|
-
syncOptions?: {
|
|
11
|
-
enabled?: boolean;
|
|
12
|
-
removeDeletedKeys?: boolean;
|
|
13
|
-
};
|
|
14
|
-
concurrencyLimit?: number;
|
|
15
|
-
saveReviewQueue?: boolean;
|
|
16
|
-
minConfidence?: number;
|
|
17
|
-
fileExtensions?: string[];
|
|
18
|
-
}
|
|
19
|
-
export interface GlobalStats {
|
|
20
|
-
total: number;
|
|
21
|
-
success: number;
|
|
22
|
-
failed: number;
|
|
23
|
-
skipped: number;
|
|
24
|
-
languages: Record<string, any>;
|
|
25
|
-
byCategory: Record<string, number>;
|
|
26
|
-
details: Record<string, {
|
|
27
|
-
totalConfidence: number;
|
|
28
|
-
samples: number;
|
|
29
|
-
}>;
|
|
30
|
-
error?: {
|
|
31
|
-
message: string;
|
|
32
|
-
time: string;
|
|
33
|
-
stack?: string;
|
|
34
|
-
};
|
|
35
|
-
orchestrators: any[];
|
|
36
|
-
endTime?: string;
|
|
37
|
-
totalDuration?: number;
|
|
38
|
-
totalTime: number;
|
|
39
|
-
startTime: string;
|
|
40
|
-
}
|
|
41
|
-
interface LanguageProcessResult {
|
|
42
|
-
status: any;
|
|
43
|
-
savedMessage?: string;
|
|
44
|
-
error?: string;
|
|
45
|
-
}
|
|
3
|
+
import { TranslationOptions, GlobalStats, LanguageProcessResult } from "../types/index.js";
|
|
46
4
|
/**
|
|
47
5
|
* Service for handling file translation operations.
|
|
48
6
|
* Orchestrates the translation process including state management,
|
|
49
7
|
* verification, and statistics collection.
|
|
50
8
|
*/
|
|
51
9
|
declare class TranslationService {
|
|
52
|
-
/**
|
|
53
|
-
* Check if text only contains placeholders and whitespace.
|
|
54
|
-
* @param {string} text - Text to check.
|
|
55
|
-
* @returns {boolean} - True if text only contains placeholders.
|
|
56
|
-
*/
|
|
57
|
-
isPlaceholderOnlyText(text: string): boolean;
|
|
58
10
|
/**
|
|
59
11
|
* Main translator function to process source file and create translations.
|
|
60
12
|
* @param {string} file - Path to source file.
|