glotto 3.0.0 → 3.1.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/README.md +112 -47
- package/esm/cli.js +136 -32
- package/esm/deno.d.ts +5 -0
- package/esm/deno.js +18 -6
- package/esm/src/config.d.ts +4 -0
- package/esm/src/config.d.ts.map +1 -0
- package/esm/src/config.js +95 -0
- package/esm/src/contants.d.ts +4 -0
- package/esm/src/contants.d.ts.map +1 -1
- package/esm/src/contants.js +14 -5
- package/esm/src/diff.d.ts +4 -0
- package/esm/src/diff.d.ts.map +1 -0
- package/esm/src/diff.js +53 -0
- package/esm/src/file.d.ts +4 -3
- package/esm/src/file.d.ts.map +1 -1
- package/esm/src/file.js +23 -5
- package/esm/src/providers/anthropic.d.ts +2 -2
- package/esm/src/providers/anthropic.d.ts.map +1 -1
- package/esm/src/providers/anthropic.js +9 -1
- package/esm/src/providers/gemini.d.ts +2 -2
- package/esm/src/providers/gemini.d.ts.map +1 -1
- package/esm/src/providers/gemini.js +10 -2
- package/esm/src/providers/openai.d.ts +2 -2
- package/esm/src/providers/openai.d.ts.map +1 -1
- package/esm/src/providers/openai.js +9 -1
- package/esm/src/translator.d.ts +14 -7
- package/esm/src/translator.d.ts.map +1 -1
- package/esm/src/translator.js +161 -77
- package/esm/src/types.d.ts +31 -3
- package/esm/src/types.d.ts.map +1 -1
- package/esm/src/utilites.d.ts +3 -4
- package/esm/src/utilites.d.ts.map +1 -1
- package/esm/src/utilites.js +40 -16
- package/package.json +20 -6
- package/schema/glotto.schema.json +87 -0
- package/script/cli.js +134 -30
- package/script/deno.d.ts +5 -0
- package/script/deno.js +18 -6
- package/script/src/config.d.ts +4 -0
- package/script/src/config.d.ts.map +1 -0
- package/script/src/config.js +132 -0
- package/script/src/contants.d.ts +4 -0
- package/script/src/contants.d.ts.map +1 -1
- package/script/src/contants.js +15 -6
- package/script/src/diff.d.ts +4 -0
- package/script/src/diff.d.ts.map +1 -0
- package/script/src/diff.js +57 -0
- package/script/src/file.d.ts +4 -3
- package/script/src/file.d.ts.map +1 -1
- package/script/src/file.js +28 -10
- package/script/src/providers/anthropic.d.ts +2 -2
- package/script/src/providers/anthropic.d.ts.map +1 -1
- package/script/src/providers/anthropic.js +9 -1
- package/script/src/providers/gemini.d.ts +2 -2
- package/script/src/providers/gemini.d.ts.map +1 -1
- package/script/src/providers/gemini.js +10 -2
- package/script/src/providers/openai.d.ts +2 -2
- package/script/src/providers/openai.d.ts.map +1 -1
- package/script/src/providers/openai.js +9 -1
- package/script/src/translator.d.ts +14 -7
- package/script/src/translator.d.ts.map +1 -1
- package/script/src/translator.js +168 -83
- package/script/src/types.d.ts +31 -3
- package/script/src/types.d.ts.map +1 -1
- package/script/src/utilites.d.ts +3 -4
- package/script/src/utilites.d.ts.map +1 -1
- package/script/src/utilites.js +43 -20
package/esm/src/contants.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const DEFAULT_PROVIDER = '
|
|
1
|
+
export const DEFAULT_PROVIDER = 'openai';
|
|
2
2
|
export const DEFAULT_MODELS = {
|
|
3
3
|
gemini: 'gemini-2.5-flash',
|
|
4
4
|
openai: 'gpt-4.1-mini',
|
|
@@ -8,6 +8,10 @@ export const DEFAULT_MAX_BATCH_BYTES = 12_000;
|
|
|
8
8
|
export const MAX_RETRIES = 3;
|
|
9
9
|
export const BASE_RETRY_DELAY_MS = 2_000;
|
|
10
10
|
export const INTER_BATCH_DELAY_MS = 1_500;
|
|
11
|
+
export const INTER_LEAF_DELAY_MS = 200;
|
|
12
|
+
export const PER_LEAF_FALLBACK_RATIO = 0.5;
|
|
13
|
+
export const GITHUB_REPO_URL = 'https://github.com/ibodev1/glotto';
|
|
14
|
+
export const CONFIG_FILE_NAME = 'glotto.config.json';
|
|
11
15
|
export const HELP_TEXT = `
|
|
12
16
|
Glotto AI Translator
|
|
13
17
|
-------------------
|
|
@@ -23,10 +27,13 @@ Options:
|
|
|
23
27
|
-p, --provider AI translation provider to use (default: ${DEFAULT_PROVIDER})
|
|
24
28
|
-m, --model Model name for the selected provider (optional)
|
|
25
29
|
-i, --input Path to source JSON file (required)
|
|
26
|
-
-o, --output
|
|
30
|
+
-o, --output Target JSON file path. Comma-separated for multi-target (required)
|
|
27
31
|
-f, --from Source language (required)
|
|
28
|
-
-t, --to Target language (required)
|
|
32
|
+
-t, --to Target language. Comma-separated for multi-target (required)
|
|
29
33
|
--url Custom base URL for OpenAI/Anthropic (optional)
|
|
34
|
+
--config Path to a glotto.config.json (default: ./glotto.config.json if present)
|
|
35
|
+
--stats Print AI usage stats (input/output tokens, calls) at the end
|
|
36
|
+
--incremental Translate only missing/empty keys when the target file already exists
|
|
30
37
|
--no-limit Disable rate limit delay between batches
|
|
31
38
|
--no-timeout Disable request timeout (wait indefinitely for AI response)
|
|
32
39
|
--max-batch-size Maximum source size per batch, in KB (default: ${DEFAULT_MAX_BATCH_BYTES / 1024} KB)
|
|
@@ -36,8 +43,10 @@ Options:
|
|
|
36
43
|
Examples:
|
|
37
44
|
glotto --key {{key}} --input=en.json --output=tr.json --from=english --to=turkish
|
|
38
45
|
glotto --key {{key}} -i en.json -o tr.json -f english -t turkish -p gemini
|
|
39
|
-
glotto --key {{key}} -i en.json -
|
|
40
|
-
glotto --key {{key}} -i en.json -o tr.json -f english -t turkish
|
|
46
|
+
glotto --key {{key}} -i en.json -f english -t "turkish,french" -o "tr.json,fr.json"
|
|
47
|
+
glotto --key {{key}} -i en.json -o tr.json -f english -t turkish --incremental
|
|
48
|
+
glotto --key {{key}} -i en.json -o tr.json -f english -t turkish --stats
|
|
49
|
+
glotto --key {{key}} --config ./glotto.config.json
|
|
41
50
|
glotto --key {{key}} -i en.json -o tr.json -f english -t turkish --no-limit --no-timeout
|
|
42
51
|
glotto --key {{key}} -i en.json -o tr.json -f english -t turkish --max-batch-size 8
|
|
43
52
|
`;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { JsonValue, Leaf } from './types.js';
|
|
2
|
+
export declare function findMissingLeaves(sourceLeaves: Leaf[], existingTarget: JsonValue): Leaf[];
|
|
3
|
+
export declare function mergeTranslations(existingTarget: JsonValue, sourceLeaves: Leaf[], translations: Map<number, string>): JsonValue;
|
|
4
|
+
//# sourceMappingURL=diff.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff.d.ts","sourceRoot":"","sources":["../../src/src/diff.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,IAAI,EAAQ,MAAM,YAAY,CAAC;AAyBxD,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,IAAI,EAAE,EAAE,cAAc,EAAE,SAAS,GAAG,IAAI,EAAE,CAYzF;AASD,wBAAgB,iBAAiB,CAC/B,cAAc,EAAE,SAAS,EACzB,YAAY,EAAE,IAAI,EAAE,EACpB,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAChC,SAAS,CAYX"}
|
package/esm/src/diff.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { setPath } from './translator.js';
|
|
2
|
+
function getPath(root, path) {
|
|
3
|
+
let node = root;
|
|
4
|
+
for (const key of path) {
|
|
5
|
+
if (node === null || typeof node !== 'object') {
|
|
6
|
+
return undefined;
|
|
7
|
+
}
|
|
8
|
+
const container = node;
|
|
9
|
+
if (!(key in container)) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
node = container[key];
|
|
13
|
+
}
|
|
14
|
+
return node;
|
|
15
|
+
}
|
|
16
|
+
function isPresentTranslation(value) {
|
|
17
|
+
if (typeof value !== 'string') {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
return value.trim().length > 0;
|
|
21
|
+
}
|
|
22
|
+
export function findMissingLeaves(sourceLeaves, existingTarget) {
|
|
23
|
+
const missing = [];
|
|
24
|
+
for (const leaf of sourceLeaves) {
|
|
25
|
+
if (!leaf.translatable) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const existingValue = getPath(existingTarget, leaf.path);
|
|
29
|
+
if (!isPresentTranslation(existingValue)) {
|
|
30
|
+
missing.push(leaf);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return missing;
|
|
34
|
+
}
|
|
35
|
+
function deepClone(value) {
|
|
36
|
+
if (value === null || typeof value !== 'object') {
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
return JSON.parse(JSON.stringify(value));
|
|
40
|
+
}
|
|
41
|
+
export function mergeTranslations(existingTarget, sourceLeaves, translations) {
|
|
42
|
+
const clone = deepClone(existingTarget);
|
|
43
|
+
for (const leaf of sourceLeaves) {
|
|
44
|
+
if (!translations.has(leaf.id)) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (leaf.path.length === 0) {
|
|
48
|
+
return translations.get(leaf.id);
|
|
49
|
+
}
|
|
50
|
+
setPath(clone, leaf.path, translations.get(leaf.id));
|
|
51
|
+
}
|
|
52
|
+
return clone;
|
|
53
|
+
}
|
package/esm/src/file.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { JsonValue } from './types.js';
|
|
2
|
-
export declare
|
|
3
|
-
export declare
|
|
4
|
-
export declare
|
|
2
|
+
export declare function resolvePath(...paths: string[]): string;
|
|
3
|
+
export declare function getImportJson<T = JsonValue>(input: string): Promise<T>;
|
|
4
|
+
export declare function writeOutput(outputPath: string, content: string): Promise<void>;
|
|
5
|
+
export declare function readJsonIfExists<T = JsonValue>(path: string): Promise<T | null>;
|
|
5
6
|
//# sourceMappingURL=file.d.ts.map
|
package/esm/src/file.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file.d.ts","sourceRoot":"","sources":["../../src/src/file.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,
|
|
1
|
+
{"version":3,"file":"file.d.ts","sourceRoot":"","sources":["../../src/src/file.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,wBAAgB,WAAW,CAAC,GAAG,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,CAEtD;AAED,wBAAsB,aAAa,CAAC,CAAC,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAO5E;AAED,wBAAsB,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEpF;AAED,wBAAsB,gBAAgB,CAAC,CAAC,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAcrF"}
|
package/esm/src/file.js
CHANGED
|
@@ -1,14 +1,32 @@
|
|
|
1
1
|
import * as dntShim from "../_dnt.shims.js";
|
|
2
2
|
import { join } from '../deps/jsr.io/@std/path/1.1.4/mod.js';
|
|
3
|
-
export
|
|
4
|
-
|
|
3
|
+
export function resolvePath(...paths) {
|
|
4
|
+
return join(dntShim.Deno.cwd(), ...paths);
|
|
5
|
+
}
|
|
6
|
+
export async function getImportJson(input) {
|
|
5
7
|
const filePath = resolvePath(input);
|
|
6
8
|
const fileContent = await dntShim.Deno.readTextFile(filePath);
|
|
7
9
|
if (typeof fileContent !== 'string' || fileContent.trim() === '') {
|
|
8
10
|
throw new Error('No Content!');
|
|
9
11
|
}
|
|
10
12
|
return JSON.parse(fileContent);
|
|
11
|
-
}
|
|
12
|
-
export
|
|
13
|
+
}
|
|
14
|
+
export async function writeOutput(outputPath, content) {
|
|
13
15
|
await dntShim.Deno.writeTextFile(outputPath, content, { create: true });
|
|
14
|
-
}
|
|
16
|
+
}
|
|
17
|
+
export async function readJsonIfExists(path) {
|
|
18
|
+
let raw;
|
|
19
|
+
try {
|
|
20
|
+
raw = await dntShim.Deno.readTextFile(path);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (error instanceof dntShim.Deno.errors.NotFound) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
if (raw.trim() === '') {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return JSON.parse(raw);
|
|
32
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { TextTranslator, TranslateOptions } from '../types.js';
|
|
1
|
+
import type { TextTranslator, TranslateOptions, TranslateResult } from '../types.js';
|
|
2
2
|
declare class AnthropicModel implements TextTranslator {
|
|
3
3
|
private client;
|
|
4
4
|
private model;
|
|
5
5
|
constructor(key: string, baseUrl?: string, modelName?: string, options?: TranslateOptions);
|
|
6
|
-
translate(prompt: string): Promise<
|
|
6
|
+
translate(prompt: string): Promise<TranslateResult>;
|
|
7
7
|
}
|
|
8
8
|
export default AnthropicModel;
|
|
9
9
|
//# sourceMappingURL=anthropic.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"anthropic.d.ts","sourceRoot":"","sources":["../../../src/src/providers/anthropic.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"anthropic.d.ts","sourceRoot":"","sources":["../../../src/src/providers/anthropic.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,eAAe,EAAkB,MAAM,aAAa,CAAC;AAGrG,cAAM,cAAe,YAAW,cAAc;IAC5C,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,KAAK,CAAS;gBAGpB,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,MAAM,EAChB,SAAS,CAAC,EAAE,MAAM,EAClB,OAAO,GAAE,gBAAuD;IAU5D,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;CAoB1D;AAED,eAAe,cAAc,CAAC"}
|
|
@@ -28,10 +28,18 @@ class AnthropicModel {
|
|
|
28
28
|
temperature: 0.2,
|
|
29
29
|
messages: [{ role: 'user', content: prompt }],
|
|
30
30
|
});
|
|
31
|
-
|
|
31
|
+
const text = message.content
|
|
32
32
|
.filter((part) => part.type === 'text')
|
|
33
33
|
.map((part) => part.text)
|
|
34
34
|
.join('');
|
|
35
|
+
let usage;
|
|
36
|
+
if (message.usage) {
|
|
37
|
+
usage = {
|
|
38
|
+
inputTokens: message.usage.input_tokens ?? 0,
|
|
39
|
+
outputTokens: message.usage.output_tokens ?? 0,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return { text, usage };
|
|
35
43
|
}
|
|
36
44
|
}
|
|
37
45
|
export default AnthropicModel;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { TextTranslator, TranslateOptions } from '../types.js';
|
|
1
|
+
import type { TextTranslator, TranslateOptions, TranslateResult } from '../types.js';
|
|
2
2
|
declare class Gemini implements TextTranslator {
|
|
3
3
|
private genAI;
|
|
4
4
|
private model;
|
|
5
5
|
constructor(key: string, modelName?: string, options?: TranslateOptions);
|
|
6
|
-
translate(prompt: string): Promise<
|
|
6
|
+
translate(prompt: string): Promise<TranslateResult>;
|
|
7
7
|
}
|
|
8
8
|
export default Gemini;
|
|
9
9
|
//# sourceMappingURL=gemini.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gemini.d.ts","sourceRoot":"","sources":["../../../src/src/providers/gemini.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"gemini.d.ts","sourceRoot":"","sources":["../../../src/src/providers/gemini.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,eAAe,EAAkB,MAAM,aAAa,CAAC;AAGrG,cAAM,MAAO,YAAW,cAAc;IACpC,OAAO,CAAC,KAAK,CAAc;IAC3B,OAAO,CAAC,KAAK,CAAS;gBAGpB,GAAG,EAAE,MAAM,EACX,SAAS,CAAC,EAAE,MAAM,EAClB,OAAO,GAAE,gBAAuD;IAS5D,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;CAmB1D;AAED,eAAe,MAAM,CAAC"}
|
|
@@ -21,7 +21,7 @@ class Gemini {
|
|
|
21
21
|
});
|
|
22
22
|
}
|
|
23
23
|
async translate(prompt) {
|
|
24
|
-
const
|
|
24
|
+
const response = await this.genAI.models.generateContent({
|
|
25
25
|
model: this.model,
|
|
26
26
|
contents: prompt,
|
|
27
27
|
config: {
|
|
@@ -29,7 +29,15 @@ class Gemini {
|
|
|
29
29
|
temperature: 0.2,
|
|
30
30
|
},
|
|
31
31
|
});
|
|
32
|
-
|
|
32
|
+
const text = response.text ?? '';
|
|
33
|
+
let usage;
|
|
34
|
+
if (response.usageMetadata) {
|
|
35
|
+
usage = {
|
|
36
|
+
inputTokens: response.usageMetadata.promptTokenCount ?? 0,
|
|
37
|
+
outputTokens: response.usageMetadata.candidatesTokenCount ?? 0,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return { text, usage };
|
|
33
41
|
}
|
|
34
42
|
}
|
|
35
43
|
export default Gemini;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { TextTranslator, TranslateOptions } from '../types.js';
|
|
1
|
+
import type { TextTranslator, TranslateOptions, TranslateResult } from '../types.js';
|
|
2
2
|
declare class OpenAIModel implements TextTranslator {
|
|
3
3
|
private client;
|
|
4
4
|
private model;
|
|
5
5
|
constructor(key: string, baseUrl?: string, modelName?: string, options?: TranslateOptions);
|
|
6
|
-
translate(prompt: string): Promise<
|
|
6
|
+
translate(prompt: string): Promise<TranslateResult>;
|
|
7
7
|
}
|
|
8
8
|
export default OpenAIModel;
|
|
9
9
|
//# sourceMappingURL=openai.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openai.d.ts","sourceRoot":"","sources":["../../../src/src/providers/openai.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"openai.d.ts","sourceRoot":"","sources":["../../../src/src/providers/openai.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,eAAe,EAAkB,MAAM,aAAa,CAAC;AAGrG,cAAM,WAAY,YAAW,cAAc;IACzC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,KAAK,CAAS;gBAGpB,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,MAAM,EAChB,SAAS,CAAC,EAAE,MAAM,EAClB,OAAO,GAAE,gBAAuD;IAU5D,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;CAgB1D;AAED,eAAe,WAAW,CAAC"}
|
|
@@ -27,7 +27,15 @@ class OpenAIModel {
|
|
|
27
27
|
messages: [{ role: 'user', content: prompt }],
|
|
28
28
|
temperature: 0.2,
|
|
29
29
|
});
|
|
30
|
-
|
|
30
|
+
const text = completion.choices[0]?.message?.content ?? '';
|
|
31
|
+
let usage;
|
|
32
|
+
if (completion.usage) {
|
|
33
|
+
usage = {
|
|
34
|
+
inputTokens: completion.usage.prompt_tokens ?? 0,
|
|
35
|
+
outputTokens: completion.usage.completion_tokens ?? 0,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return { text, usage };
|
|
31
39
|
}
|
|
32
40
|
}
|
|
33
41
|
export default OpenAIModel;
|
package/esm/src/translator.d.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
import type { Batch, JsonValue, Leaf, TextTranslator, TranslateOptions } from './types.js';
|
|
2
|
-
export declare
|
|
3
|
-
export declare
|
|
4
|
-
export declare
|
|
5
|
-
export declare
|
|
6
|
-
export declare
|
|
7
|
-
export declare
|
|
1
|
+
import type { Batch, JsonValue, Leaf, Path, TextTranslator, TranslateOptions, TranslateUsage } from './types.js';
|
|
2
|
+
export declare function extractLeaves(data: JsonValue): Leaf[];
|
|
3
|
+
export declare function groupIntoBatches(leaves: Leaf[], maxBytes: number): Batch[];
|
|
4
|
+
export declare function buildBatchPrompt(from: string, to: string, leaves: Leaf[]): string;
|
|
5
|
+
export declare function buildSinglePrompt(from: string, to: string, value: string): string;
|
|
6
|
+
export declare function decodeResponse(text: string): Map<number, string>;
|
|
7
|
+
export declare function setPath(root: JsonValue, path: Path, value: JsonValue): void;
|
|
8
|
+
export declare function reconstruct(allLeaves: Leaf[], translations: Map<number, string>): JsonValue;
|
|
9
|
+
export type RunBatchesResult = {
|
|
10
|
+
translations: Map<number, string>;
|
|
11
|
+
usage: TranslateUsage;
|
|
12
|
+
calls: number;
|
|
13
|
+
};
|
|
14
|
+
export declare function runBatches(batches: Batch[], translator: TextTranslator, from: string, to: string, options: TranslateOptions): Promise<RunBatchesResult>;
|
|
8
15
|
//# sourceMappingURL=translator.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"translator.d.ts","sourceRoot":"","sources":["../../src/src/translator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,
|
|
1
|
+
{"version":3,"file":"translator.d.ts","sourceRoot":"","sources":["../../src/src/translator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,cAAc,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAWjH,wBAAgB,aAAa,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI,EAAE,CAmCrD;AAgBD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,KAAK,EAAE,CA0B1E;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,CAkBjF;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAMjF;AAQD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAWhE;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,GAAG,IAAI,CAY3E;AAED,wBAAgB,WAAW,CAAC,SAAS,EAAE,IAAI,EAAE,EAAE,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAe3F;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,KAAK,EAAE,cAAc,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AA6IF,wBAAsB,UAAU,CAC9B,OAAO,EAAE,KAAK,EAAE,EAChB,UAAU,EAAE,cAAc,EAC1B,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,gBAAgB,CAAC,CAwC3B"}
|
package/esm/src/translator.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { logger } from './logger.js';
|
|
2
2
|
import { delay, formatBytes } from './utilites.js';
|
|
3
|
-
import { BASE_RETRY_DELAY_MS, INTER_BATCH_DELAY_MS, MAX_RETRIES } from './contants.js';
|
|
3
|
+
import { BASE_RETRY_DELAY_MS, INTER_BATCH_DELAY_MS, INTER_LEAF_DELAY_MS, MAX_RETRIES, PER_LEAF_FALLBACK_RATIO } from './contants.js';
|
|
4
4
|
const encoder = new TextEncoder();
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
function byteSize(s) {
|
|
6
|
+
return encoder.encode(s).byteLength;
|
|
7
|
+
}
|
|
8
|
+
export function extractLeaves(data) {
|
|
7
9
|
const out = [];
|
|
8
10
|
let nextId = 0;
|
|
9
|
-
|
|
11
|
+
function walk(node, path) {
|
|
10
12
|
if (node === null || typeof node !== 'object') {
|
|
11
13
|
const isString = typeof node === 'string';
|
|
12
14
|
out.push({
|
|
@@ -30,17 +32,24 @@ export const extractLeaves = (data) => {
|
|
|
30
32
|
out.push({ id: ++nextId, path, value: node, translatable: false });
|
|
31
33
|
return;
|
|
32
34
|
}
|
|
33
|
-
for (const [k, v] of entries)
|
|
35
|
+
for (const [k, v] of entries) {
|
|
34
36
|
walk(v, [...path, k]);
|
|
35
|
-
|
|
37
|
+
}
|
|
38
|
+
}
|
|
36
39
|
walk(data, []);
|
|
37
40
|
return out;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
}
|
|
42
|
+
function tagOpen(id) {
|
|
43
|
+
return `≪${id}≫`;
|
|
44
|
+
}
|
|
45
|
+
function tagClose(id) {
|
|
46
|
+
return `≪/${id}≫`;
|
|
47
|
+
}
|
|
41
48
|
const TAG_OVERHEAD_CHARS = 8;
|
|
42
|
-
|
|
43
|
-
|
|
49
|
+
function wrapEntry(leaf) {
|
|
50
|
+
return `${tagOpen(leaf.id)}${String(leaf.value)}${tagClose(leaf.id)}`;
|
|
51
|
+
}
|
|
52
|
+
export function groupIntoBatches(leaves, maxBytes) {
|
|
44
53
|
const translatable = leaves.filter((l) => l.translatable);
|
|
45
54
|
const batches = [];
|
|
46
55
|
let current = [];
|
|
@@ -62,8 +71,8 @@ export const groupIntoBatches = (leaves, maxBytes) => {
|
|
|
62
71
|
batches.push({ index: batches.length, leaves: current, byteSize: currentSize });
|
|
63
72
|
}
|
|
64
73
|
return batches;
|
|
65
|
-
}
|
|
66
|
-
export
|
|
74
|
+
}
|
|
75
|
+
export function buildBatchPrompt(from, to, leaves) {
|
|
67
76
|
const entries = leaves.map(wrapEntry).join('\n');
|
|
68
77
|
return `Translate each tagged entry below from ${from} to ${to}.
|
|
69
78
|
|
|
@@ -81,24 +90,32 @@ What to write back:
|
|
|
81
90
|
|
|
82
91
|
Now translate the following entries:
|
|
83
92
|
${entries}`;
|
|
84
|
-
}
|
|
93
|
+
}
|
|
94
|
+
export function buildSinglePrompt(from, to, value) {
|
|
95
|
+
return `You are a professional ${from} to ${to} translator. Your goal is to accurately convey the meaning and nuances of the original ${from} text while adhering to ${to} grammar, vocabulary, and cultural sensitivities. Preserve every variable ({{name}}, {name}, __VAR__, $t(...), %s, %d), HTML tag, markdown token, escape sequence, and special character exactly as given.
|
|
96
|
+
Produce only the ${to} translation, without any additional explanations, quotes, labels, or commentary. Please translate the following ${from} text into ${to}:
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
${value}`;
|
|
100
|
+
}
|
|
85
101
|
const TAG_PATTERNS = [
|
|
86
102
|
/≪\s*(\d+)\s*≫([\s\S]*?)≪\s*\/\s*\1\s*≫/g,
|
|
87
103
|
/<<\s*(\d+)\s*>>([\s\S]*?)<<\s*\/\s*\1\s*>>/g,
|
|
88
104
|
/«\s*(\d+)\s*»([\s\S]*?)«\s*\/\s*\1\s*»/g,
|
|
89
105
|
];
|
|
90
|
-
export
|
|
106
|
+
export function decodeResponse(text) {
|
|
91
107
|
const map = new Map();
|
|
92
108
|
for (const pattern of TAG_PATTERNS) {
|
|
93
109
|
for (const match of text.matchAll(pattern)) {
|
|
94
110
|
const id = parseInt(match[1], 10);
|
|
95
|
-
if (!map.has(id))
|
|
111
|
+
if (!map.has(id)) {
|
|
96
112
|
map.set(id, match[2].trim());
|
|
113
|
+
}
|
|
97
114
|
}
|
|
98
115
|
}
|
|
99
116
|
return map;
|
|
100
|
-
}
|
|
101
|
-
|
|
117
|
+
}
|
|
118
|
+
export function setPath(root, path, value) {
|
|
102
119
|
let node = root;
|
|
103
120
|
for (let i = 0; i < path.length - 1; i++) {
|
|
104
121
|
const key = path[i];
|
|
@@ -110,10 +127,11 @@ const setPath = (root, path, value) => {
|
|
|
110
127
|
node = container[key];
|
|
111
128
|
}
|
|
112
129
|
node[path[path.length - 1]] = value;
|
|
113
|
-
}
|
|
114
|
-
export
|
|
115
|
-
if (allLeaves.length === 0)
|
|
130
|
+
}
|
|
131
|
+
export function reconstruct(allLeaves, translations) {
|
|
132
|
+
if (allLeaves.length === 0) {
|
|
116
133
|
return {};
|
|
134
|
+
}
|
|
117
135
|
const firstStep = allLeaves[0].path[0];
|
|
118
136
|
if (firstStep === undefined) {
|
|
119
137
|
const only = allLeaves[0];
|
|
@@ -125,10 +143,115 @@ export const reconstruct = (allLeaves, translations) => {
|
|
|
125
143
|
setPath(root, leaf.path, value);
|
|
126
144
|
}
|
|
127
145
|
return root;
|
|
128
|
-
}
|
|
129
|
-
|
|
146
|
+
}
|
|
147
|
+
function accumulateUsage(target, source) {
|
|
148
|
+
if (!source) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
target.inputTokens += source.inputTokens;
|
|
152
|
+
target.outputTokens += source.outputTokens;
|
|
153
|
+
}
|
|
154
|
+
async function translateOne(translator, from, to, value, usage, callCounter) {
|
|
155
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
156
|
+
if (attempt > 0) {
|
|
157
|
+
await delay(BASE_RETRY_DELAY_MS * Math.pow(2, attempt - 1));
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
callCounter.count += 1;
|
|
161
|
+
const response = await translator.translate(buildSinglePrompt(from, to, value));
|
|
162
|
+
accumulateUsage(usage, response.usage);
|
|
163
|
+
const trimmed = response.text.trim();
|
|
164
|
+
if (trimmed.length > 0) {
|
|
165
|
+
return trimmed;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
if (attempt === MAX_RETRIES) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
async function runBatchAttempts(batchLeaves, translator, from, to, label, translations, usage, callCounter) {
|
|
177
|
+
let remaining = [...batchLeaves];
|
|
178
|
+
let firstAttempt = true;
|
|
179
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
180
|
+
if (attempt > 0) {
|
|
181
|
+
const backoffMs = BASE_RETRY_DELAY_MS * Math.pow(2, attempt - 1);
|
|
182
|
+
logger.warn(`${label} Retry ${attempt}/${MAX_RETRIES} after ${backoffMs}ms backoff (${remaining.length} entries)...`);
|
|
183
|
+
await delay(backoffMs);
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
const prompt = buildBatchPrompt(from, to, remaining);
|
|
187
|
+
callCounter.count += 1;
|
|
188
|
+
const response = await translator.translate(prompt);
|
|
189
|
+
accumulateUsage(usage, response.usage);
|
|
190
|
+
const text = response.text;
|
|
191
|
+
if (!text || text.trim().length === 0) {
|
|
192
|
+
if (attempt === MAX_RETRIES) {
|
|
193
|
+
return remaining;
|
|
194
|
+
}
|
|
195
|
+
logger.warn(`${label} Attempt ${attempt + 1} returned empty response`);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const decoded = decodeResponse(text);
|
|
199
|
+
const stillMissing = [];
|
|
200
|
+
for (const leaf of remaining) {
|
|
201
|
+
const value = decoded.get(leaf.id);
|
|
202
|
+
if (!value || value.trim().length === 0) {
|
|
203
|
+
stillMissing.push(leaf);
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
translations.set(leaf.id, value);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (stillMissing.length === 0) {
|
|
210
|
+
return [];
|
|
211
|
+
}
|
|
212
|
+
if (firstAttempt && stillMissing.length >= remaining.length * PER_LEAF_FALLBACK_RATIO) {
|
|
213
|
+
logger.warn(`${label} Batch mode unreliable for this model (${stillMissing.length}/${remaining.length} missing on first attempt), switching to per-entry mode`);
|
|
214
|
+
return stillMissing;
|
|
215
|
+
}
|
|
216
|
+
logger.warn(`${label} ${stillMissing.length}/${remaining.length} entries missing, will retry`);
|
|
217
|
+
firstAttempt = false;
|
|
218
|
+
remaining = stillMissing;
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
222
|
+
if (attempt === MAX_RETRIES) {
|
|
223
|
+
logger.warn(`${label} Batch attempt failed after retries: ${message}`);
|
|
224
|
+
return remaining;
|
|
225
|
+
}
|
|
226
|
+
logger.warn(`${label} Attempt ${attempt + 1} failed: ${message}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return remaining;
|
|
230
|
+
}
|
|
231
|
+
async function runPerLeafFallback(leaves, translator, from, to, options, label, translations, usage, callCounter) {
|
|
232
|
+
logger.info(`${label} Per-entry fallback for ${leaves.length} entries`);
|
|
233
|
+
const failed = [];
|
|
234
|
+
for (let i = 0; i < leaves.length; i++) {
|
|
235
|
+
if (i > 0 && !options.noLimit) {
|
|
236
|
+
await delay(INTER_LEAF_DELAY_MS);
|
|
237
|
+
}
|
|
238
|
+
const leaf = leaves[i];
|
|
239
|
+
const result = await translateOne(translator, from, to, String(leaf.value), usage, callCounter);
|
|
240
|
+
if (result) {
|
|
241
|
+
translations.set(leaf.id, result);
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
logger.warn(`${label} Per-entry failed for id=${leaf.id}`);
|
|
245
|
+
failed.push(leaf);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return failed;
|
|
249
|
+
}
|
|
250
|
+
export async function runBatches(batches, translator, from, to, options) {
|
|
130
251
|
const translations = new Map();
|
|
131
252
|
const failedIds = [];
|
|
253
|
+
const usage = { inputTokens: 0, outputTokens: 0 };
|
|
254
|
+
const callCounter = { count: 0 };
|
|
132
255
|
for (let i = 0; i < batches.length; i++) {
|
|
133
256
|
const batch = batches[i];
|
|
134
257
|
const label = `[Batch ${batch.index + 1}/${batches.length}]`;
|
|
@@ -136,58 +259,19 @@ export const runBatches = async (batches, translator, from, to, options) => {
|
|
|
136
259
|
logger.info(`Waiting ${INTER_BATCH_DELAY_MS}ms before next batch (rate limit protection)...`);
|
|
137
260
|
await delay(INTER_BATCH_DELAY_MS);
|
|
138
261
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
for (const leaf of remaining)
|
|
153
|
-
failedIds.push(leaf.id);
|
|
154
|
-
break;
|
|
155
|
-
}
|
|
156
|
-
logger.warn(`${label} Attempt ${attempt + 1} returned empty response`);
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
159
|
-
const decoded = decodeResponse(response);
|
|
160
|
-
const stillMissing = [];
|
|
161
|
-
for (const leaf of remaining) {
|
|
162
|
-
const value = decoded.get(leaf.id);
|
|
163
|
-
if (!value || value.trim().length === 0) {
|
|
164
|
-
stillMissing.push(leaf);
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
translations.set(leaf.id, value);
|
|
168
|
-
}
|
|
169
|
-
if (stillMissing.length === 0) {
|
|
170
|
-
logger.info(`${label} Translated successfully (${formatBytes(batch.byteSize)}, ${batch.leaves.length} entries)`);
|
|
171
|
-
break;
|
|
172
|
-
}
|
|
173
|
-
if (attempt === MAX_RETRIES) {
|
|
174
|
-
logger.error(`${label} Failed after ${MAX_RETRIES + 1} attempts: ${stillMissing.length} entries missing`);
|
|
175
|
-
for (const leaf of stillMissing)
|
|
176
|
-
failedIds.push(leaf.id);
|
|
177
|
-
break;
|
|
178
|
-
}
|
|
179
|
-
logger.warn(`${label} ${stillMissing.length}/${remaining.length} entries missing, will retry`);
|
|
180
|
-
remaining = stillMissing;
|
|
181
|
-
}
|
|
182
|
-
catch (error) {
|
|
183
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
184
|
-
if (attempt === MAX_RETRIES) {
|
|
185
|
-
logger.error(`${label} Failed after ${MAX_RETRIES + 1} attempts: ${message}`);
|
|
186
|
-
for (const leaf of remaining)
|
|
187
|
-
failedIds.push(leaf.id);
|
|
188
|
-
break;
|
|
189
|
-
}
|
|
190
|
-
logger.warn(`${label} Attempt ${attempt + 1} failed: ${message}`);
|
|
262
|
+
const missingAfterBatch = await runBatchAttempts(batch.leaves, translator, from, to, label, translations, usage, callCounter);
|
|
263
|
+
let stillFailed = [];
|
|
264
|
+
if (missingAfterBatch.length > 0) {
|
|
265
|
+
stillFailed = await runPerLeafFallback(missingAfterBatch, translator, from, to, options, label, translations, usage, callCounter);
|
|
266
|
+
}
|
|
267
|
+
const succeeded = batch.leaves.length - stillFailed.length;
|
|
268
|
+
if (stillFailed.length === 0) {
|
|
269
|
+
logger.info(`${label} Translated successfully (${formatBytes(batch.byteSize)}, ${succeeded} entries)`);
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
logger.error(`${label} ${stillFailed.length}/${batch.leaves.length} entries failed permanently`);
|
|
273
|
+
for (const leaf of stillFailed) {
|
|
274
|
+
failedIds.push(leaf.id);
|
|
191
275
|
}
|
|
192
276
|
}
|
|
193
277
|
}
|
|
@@ -196,5 +280,5 @@ export const runBatches = async (batches, translator, from, to, options) => {
|
|
|
196
280
|
const more = failedIds.length > 20 ? `, …(+${failedIds.length - 20})` : '';
|
|
197
281
|
throw new Error(`Translation failed for ${failedIds.length} entry/entries (ids: ${preview}${more})`);
|
|
198
282
|
}
|
|
199
|
-
return translations;
|
|
200
|
-
}
|
|
283
|
+
return { translations, usage, calls: callCounter.count };
|
|
284
|
+
}
|