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.
Files changed (67) hide show
  1. package/README.md +112 -47
  2. package/esm/cli.js +136 -32
  3. package/esm/deno.d.ts +5 -0
  4. package/esm/deno.js +18 -6
  5. package/esm/src/config.d.ts +4 -0
  6. package/esm/src/config.d.ts.map +1 -0
  7. package/esm/src/config.js +95 -0
  8. package/esm/src/contants.d.ts +4 -0
  9. package/esm/src/contants.d.ts.map +1 -1
  10. package/esm/src/contants.js +14 -5
  11. package/esm/src/diff.d.ts +4 -0
  12. package/esm/src/diff.d.ts.map +1 -0
  13. package/esm/src/diff.js +53 -0
  14. package/esm/src/file.d.ts +4 -3
  15. package/esm/src/file.d.ts.map +1 -1
  16. package/esm/src/file.js +23 -5
  17. package/esm/src/providers/anthropic.d.ts +2 -2
  18. package/esm/src/providers/anthropic.d.ts.map +1 -1
  19. package/esm/src/providers/anthropic.js +9 -1
  20. package/esm/src/providers/gemini.d.ts +2 -2
  21. package/esm/src/providers/gemini.d.ts.map +1 -1
  22. package/esm/src/providers/gemini.js +10 -2
  23. package/esm/src/providers/openai.d.ts +2 -2
  24. package/esm/src/providers/openai.d.ts.map +1 -1
  25. package/esm/src/providers/openai.js +9 -1
  26. package/esm/src/translator.d.ts +14 -7
  27. package/esm/src/translator.d.ts.map +1 -1
  28. package/esm/src/translator.js +161 -77
  29. package/esm/src/types.d.ts +31 -3
  30. package/esm/src/types.d.ts.map +1 -1
  31. package/esm/src/utilites.d.ts +3 -4
  32. package/esm/src/utilites.d.ts.map +1 -1
  33. package/esm/src/utilites.js +40 -16
  34. package/package.json +20 -6
  35. package/schema/glotto.schema.json +87 -0
  36. package/script/cli.js +134 -30
  37. package/script/deno.d.ts +5 -0
  38. package/script/deno.js +18 -6
  39. package/script/src/config.d.ts +4 -0
  40. package/script/src/config.d.ts.map +1 -0
  41. package/script/src/config.js +132 -0
  42. package/script/src/contants.d.ts +4 -0
  43. package/script/src/contants.d.ts.map +1 -1
  44. package/script/src/contants.js +15 -6
  45. package/script/src/diff.d.ts +4 -0
  46. package/script/src/diff.d.ts.map +1 -0
  47. package/script/src/diff.js +57 -0
  48. package/script/src/file.d.ts +4 -3
  49. package/script/src/file.d.ts.map +1 -1
  50. package/script/src/file.js +28 -10
  51. package/script/src/providers/anthropic.d.ts +2 -2
  52. package/script/src/providers/anthropic.d.ts.map +1 -1
  53. package/script/src/providers/anthropic.js +9 -1
  54. package/script/src/providers/gemini.d.ts +2 -2
  55. package/script/src/providers/gemini.d.ts.map +1 -1
  56. package/script/src/providers/gemini.js +10 -2
  57. package/script/src/providers/openai.d.ts +2 -2
  58. package/script/src/providers/openai.d.ts.map +1 -1
  59. package/script/src/providers/openai.js +9 -1
  60. package/script/src/translator.d.ts +14 -7
  61. package/script/src/translator.d.ts.map +1 -1
  62. package/script/src/translator.js +168 -83
  63. package/script/src/types.d.ts +31 -3
  64. package/script/src/types.d.ts.map +1 -1
  65. package/script/src/utilites.d.ts +3 -4
  66. package/script/src/utilites.d.ts.map +1 -1
  67. package/script/src/utilites.js +43 -20
@@ -1,4 +1,4 @@
1
- export const DEFAULT_PROVIDER = 'gemini';
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 Path to target JSON file (required)
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 -o tr.json -f english -t turkish -p openai
40
- glotto --key {{key}} -i en.json -o tr.json -f english -t turkish -p anthropic
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"}
@@ -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 const resolvePath: (...paths: string[]) => string;
3
- export declare const getImportJson: <T = JsonValue>(input: string) => Promise<T>;
4
- export declare const writeOutput: (outputPath: string, content: string) => Promise<void>;
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
@@ -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,eAAO,MAAM,WAAW,GAAI,GAAG,OAAO,MAAM,EAAE,KAAG,MAA4C,CAAC;AAE9F,eAAO,MAAM,aAAa,GAAU,CAAC,GAAG,SAAS,EAAE,OAAO,MAAM,KAAG,OAAO,CAAC,CAAC,CAO3E,CAAC;AAEF,eAAO,MAAM,WAAW,GAAU,YAAY,MAAM,EAAE,SAAS,MAAM,KAAG,OAAO,CAAC,IAAI,CAEnF,CAAC"}
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 const resolvePath = (...paths) => join(dntShim.Deno.cwd(), ...paths);
4
- export const getImportJson = async (input) => {
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 const writeOutput = async (outputPath, content) => {
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<string>;
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;AAGpE,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,MAAM,CAAC;CAYjD;AAED,eAAe,cAAc,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
- return message.content
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<string>;
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;AAGpE,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,MAAM,CAAC;CAWjD;AAED,eAAe,MAAM,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 { text } = await this.genAI.models.generateContent({
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
- return text ?? '';
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<string>;
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;AAGpE,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,MAAM,CAAC;CAQjD;AAED,eAAe,WAAW,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
- return completion.choices[0]?.message?.content ?? '';
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;
@@ -1,8 +1,15 @@
1
- import type { Batch, JsonValue, Leaf, TextTranslator, TranslateOptions } from './types.js';
2
- export declare const extractLeaves: (data: JsonValue) => Leaf[];
3
- export declare const groupIntoBatches: (leaves: Leaf[], maxBytes: number) => Batch[];
4
- export declare const buildBatchPrompt: (from: string, to: string, leaves: Leaf[]) => string;
5
- export declare const decodeResponse: (text: string) => Map<number, string>;
6
- export declare const reconstruct: (allLeaves: Leaf[], translations: Map<number, string>) => JsonValue;
7
- export declare const runBatches: (batches: Batch[], translator: TextTranslator, from: string, to: string, options: TranslateOptions) => Promise<Map<number, string>>;
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,EAAQ,cAAc,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAQjG,eAAO,MAAM,aAAa,GAAI,MAAM,SAAS,KAAG,IAAI,EAiCnD,CAAC;AAQF,eAAO,MAAM,gBAAgB,GAAI,QAAQ,IAAI,EAAE,EAAE,UAAU,MAAM,KAAG,KAAK,EA0BxE,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAAI,MAAM,MAAM,EAAE,IAAI,MAAM,EAAE,QAAQ,IAAI,EAAE,KAAG,MAkB3E,CAAC;AAQF,eAAO,MAAM,cAAc,GAAI,MAAM,MAAM,KAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAS/D,CAAC;AAgBF,eAAO,MAAM,WAAW,GAAI,WAAW,IAAI,EAAE,EAAE,cAAc,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,KAAG,SAalF,CAAC;AAEF,eAAO,MAAM,UAAU,GACrB,SAAS,KAAK,EAAE,EAChB,YAAY,cAAc,EAC1B,MAAM,MAAM,EACZ,IAAI,MAAM,EACV,SAAS,gBAAgB,KACxB,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAgF7B,CAAC"}
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"}
@@ -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
- const byteSize = (s) => encoder.encode(s).byteLength;
6
- export const extractLeaves = (data) => {
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
- const walk = (node, path) => {
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
- const TAG_OPEN = (id) => `≪${id}≫`;
40
- const TAG_CLOSE = (id) => `≪/${id}≫`;
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
- const wrapEntry = (leaf) => `${TAG_OPEN(leaf.id)}${String(leaf.value)}${TAG_CLOSE(leaf.id)}`;
43
- export const groupIntoBatches = (leaves, maxBytes) => {
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 const buildBatchPrompt = (from, to, leaves) => {
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 const decodeResponse = (text) => {
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
- const setPath = (root, path, value) => {
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 const reconstruct = (allLeaves, translations) => {
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
- export const runBatches = async (batches, translator, from, to, options) => {
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
- let remaining = [...batch.leaves];
140
- for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
141
- if (attempt > 0) {
142
- const backoffMs = BASE_RETRY_DELAY_MS * Math.pow(2, attempt - 1);
143
- logger.warn(`${label} Retry ${attempt}/${MAX_RETRIES} after ${backoffMs}ms backoff (${remaining.length} entries)...`);
144
- await delay(backoffMs);
145
- }
146
- try {
147
- const prompt = buildBatchPrompt(from, to, remaining);
148
- const response = await translator.translate(prompt);
149
- if (!response || response.trim().length === 0) {
150
- if (attempt === MAX_RETRIES) {
151
- logger.error(`${label} Failed after ${MAX_RETRIES + 1} attempts: empty response`);
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
+ }