feat: cherry-pick translation script improvements from PR #250
Add caching, parallel processing, and tier-based translation scripts: - Caching system via .translation-cache.json to skip unchanged content - --force flag to override cache and re-translate - --parallel flag for concurrent translations - Tier-based npm scripts (translate:tier1-4, translate:all) - Better markdown wrapper stripping - Translation disclaimer at top of files - Uses Bun for better performance Changes cherry-picked from PR #250 while preserving current version (7.2.0) and worker scripts. Does not include translated README files. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
+6
-1
@@ -47,7 +47,12 @@
|
|||||||
"changelog:generate": "node scripts/generate-changelog.js",
|
"changelog:generate": "node scripts/generate-changelog.js",
|
||||||
"usage:analyze": "node scripts/analyze-usage.js",
|
"usage:analyze": "node scripts/analyze-usage.js",
|
||||||
"usage:today": "node scripts/analyze-usage.js $(date +%Y-%m-%d)",
|
"usage:today": "node scripts/analyze-usage.js $(date +%Y-%m-%d)",
|
||||||
"translate-readme": "npx tsx scripts/translate-readme/cli.ts -v README.md zh ko ja",
|
"translate-readme": "bun scripts/translate-readme/cli.ts -v -o docs/i18n README.md",
|
||||||
|
"translate:tier1": "npm run translate-readme -- zh ja pt-br ko es de fr",
|
||||||
|
"translate:tier2": "npm run translate-readme -- he ar ru pl cs nl tr uk",
|
||||||
|
"translate:tier3": "npm run translate-readme -- vi id th hi bn ro sv",
|
||||||
|
"translate:tier4": "npm run translate-readme -- it el hu fi da no",
|
||||||
|
"translate:all": "npm run translate:tier1 && npm run translate:tier2 && npm run translate:tier3 && npm run translate:tier4",
|
||||||
"bug-report": "npx tsx scripts/bug-report/cli.ts"
|
"bug-report": "npx tsx scripts/bug-report/cli.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env npx tsx
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
import { translateReadme, SUPPORTED_LANGUAGES } from "./index.ts";
|
import { translateReadme, SUPPORTED_LANGUAGES } from "./index.ts";
|
||||||
|
|
||||||
@@ -11,6 +11,8 @@ interface CliArgs {
|
|||||||
model?: string;
|
model?: string;
|
||||||
maxBudget?: number;
|
maxBudget?: number;
|
||||||
verbose: boolean;
|
verbose: boolean;
|
||||||
|
force: boolean;
|
||||||
|
parallel: number;
|
||||||
help: boolean;
|
help: boolean;
|
||||||
listLanguages: boolean;
|
listLanguages: boolean;
|
||||||
}
|
}
|
||||||
@@ -39,6 +41,8 @@ OPTIONS:
|
|||||||
-m, --model <model> Claude model to use (default: sonnet)
|
-m, --model <model> Claude model to use (default: sonnet)
|
||||||
--max-budget <usd> Maximum budget in USD
|
--max-budget <usd> Maximum budget in USD
|
||||||
-v, --verbose Show detailed progress
|
-v, --verbose Show detailed progress
|
||||||
|
-f, --force Force re-translation ignoring cache
|
||||||
|
--parallel <n> Run n translations concurrently (default: 1)
|
||||||
-h, --help Show this help message
|
-h, --help Show this help message
|
||||||
--list-languages List all supported language codes
|
--list-languages List all supported language codes
|
||||||
|
|
||||||
@@ -59,40 +63,46 @@ SUPPORTED LANGUAGES:
|
|||||||
|
|
||||||
function printLanguages(): void {
|
function printLanguages(): void {
|
||||||
const LANGUAGE_NAMES: Record<string, string> = {
|
const LANGUAGE_NAMES: Record<string, string> = {
|
||||||
ar: "Arabic",
|
// Tier 1 - No-brainers
|
||||||
bg: "Bulgarian",
|
zh: "Chinese (Simplified)",
|
||||||
cs: "Czech",
|
|
||||||
da: "Danish",
|
|
||||||
de: "German",
|
|
||||||
el: "Greek",
|
|
||||||
es: "Spanish",
|
|
||||||
et: "Estonian",
|
|
||||||
fi: "Finnish",
|
|
||||||
fr: "French",
|
|
||||||
he: "Hebrew",
|
|
||||||
hi: "Hindi",
|
|
||||||
hu: "Hungarian",
|
|
||||||
id: "Indonesian",
|
|
||||||
it: "Italian",
|
|
||||||
ja: "Japanese",
|
ja: "Japanese",
|
||||||
ko: "Korean",
|
|
||||||
lt: "Lithuanian",
|
|
||||||
lv: "Latvian",
|
|
||||||
nl: "Dutch",
|
|
||||||
no: "Norwegian",
|
|
||||||
pl: "Polish",
|
|
||||||
pt: "Portuguese",
|
|
||||||
"pt-br": "Brazilian Portuguese",
|
"pt-br": "Brazilian Portuguese",
|
||||||
ro: "Romanian",
|
ko: "Korean",
|
||||||
|
es: "Spanish",
|
||||||
|
de: "German",
|
||||||
|
fr: "French",
|
||||||
|
// Tier 2 - Strong tech scenes
|
||||||
|
he: "Hebrew",
|
||||||
|
ar: "Arabic",
|
||||||
ru: "Russian",
|
ru: "Russian",
|
||||||
sk: "Slovak",
|
pl: "Polish",
|
||||||
sl: "Slovenian",
|
cs: "Czech",
|
||||||
sv: "Swedish",
|
nl: "Dutch",
|
||||||
th: "Thai",
|
|
||||||
tr: "Turkish",
|
tr: "Turkish",
|
||||||
uk: "Ukrainian",
|
uk: "Ukrainian",
|
||||||
|
// Tier 3 - Emerging/Growing fast
|
||||||
vi: "Vietnamese",
|
vi: "Vietnamese",
|
||||||
zh: "Chinese (Simplified)",
|
id: "Indonesian",
|
||||||
|
th: "Thai",
|
||||||
|
hi: "Hindi",
|
||||||
|
bn: "Bengali",
|
||||||
|
ro: "Romanian",
|
||||||
|
sv: "Swedish",
|
||||||
|
// Tier 4 - Why not
|
||||||
|
it: "Italian",
|
||||||
|
el: "Greek",
|
||||||
|
hu: "Hungarian",
|
||||||
|
fi: "Finnish",
|
||||||
|
da: "Danish",
|
||||||
|
no: "Norwegian",
|
||||||
|
// Other supported
|
||||||
|
bg: "Bulgarian",
|
||||||
|
et: "Estonian",
|
||||||
|
lt: "Lithuanian",
|
||||||
|
lv: "Latvian",
|
||||||
|
pt: "Portuguese",
|
||||||
|
sk: "Slovak",
|
||||||
|
sl: "Slovenian",
|
||||||
"zh-tw": "Chinese (Traditional)",
|
"zh-tw": "Chinese (Traditional)",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -112,6 +122,8 @@ function parseArgs(argv: string[]): CliArgs {
|
|||||||
languages: [],
|
languages: [],
|
||||||
preserveCode: true,
|
preserveCode: true,
|
||||||
verbose: false,
|
verbose: false,
|
||||||
|
force: false,
|
||||||
|
parallel: 1,
|
||||||
help: false,
|
help: false,
|
||||||
listLanguages: false,
|
listLanguages: false,
|
||||||
};
|
};
|
||||||
@@ -134,6 +146,10 @@ function parseArgs(argv: string[]): CliArgs {
|
|||||||
case "--verbose":
|
case "--verbose":
|
||||||
args.verbose = true;
|
args.verbose = true;
|
||||||
break;
|
break;
|
||||||
|
case "-f":
|
||||||
|
case "--force":
|
||||||
|
args.force = true;
|
||||||
|
break;
|
||||||
case "--no-preserve-code":
|
case "--no-preserve-code":
|
||||||
args.preserveCode = false;
|
args.preserveCode = false;
|
||||||
break;
|
break;
|
||||||
@@ -152,6 +168,13 @@ function parseArgs(argv: string[]): CliArgs {
|
|||||||
case "--max-budget":
|
case "--max-budget":
|
||||||
args.maxBudget = parseFloat(argv[++i]);
|
args.maxBudget = parseFloat(argv[++i]);
|
||||||
break;
|
break;
|
||||||
|
case "--parallel":
|
||||||
|
args.parallel = parseInt(argv[++i], 10);
|
||||||
|
if (isNaN(args.parallel) || args.parallel < 1) {
|
||||||
|
console.error("Error: --parallel must be a positive integer");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
if (arg.startsWith("-")) {
|
if (arg.startsWith("-")) {
|
||||||
console.error(`Unknown option: ${arg}`);
|
console.error(`Unknown option: ${arg}`);
|
||||||
@@ -215,6 +238,8 @@ async function main(): Promise<void> {
|
|||||||
model: args.model,
|
model: args.model,
|
||||||
maxBudgetUsd: args.maxBudget,
|
maxBudgetUsd: args.maxBudget,
|
||||||
verbose: args.verbose,
|
verbose: args.verbose,
|
||||||
|
force: args.force,
|
||||||
|
parallel: args.parallel,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Exit with error code if any translations failed
|
// Exit with error code if any translations failed
|
||||||
|
|||||||
@@ -1,6 +1,34 @@
|
|||||||
import { query, type SDKMessage, type SDKResultMessage } from "@anthropic-ai/claude-agent-sdk";
|
import { query, type SDKMessage, type SDKResultMessage } from "@anthropic-ai/claude-agent-sdk";
|
||||||
import * as fs from "fs/promises";
|
import * as fs from "fs/promises";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
|
import { createHash } from "crypto";
|
||||||
|
|
||||||
|
interface TranslationCache {
|
||||||
|
sourceHash: string;
|
||||||
|
lastUpdated: string;
|
||||||
|
translations: Record<string, {
|
||||||
|
hash: string;
|
||||||
|
translatedAt: string;
|
||||||
|
costUsd: number;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hashContent(content: string): string {
|
||||||
|
return createHash("sha256").update(content).digest("hex").slice(0, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readCache(cachePath: string): Promise<TranslationCache | null> {
|
||||||
|
try {
|
||||||
|
const data = await fs.readFile(cachePath, "utf-8");
|
||||||
|
return JSON.parse(data);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeCache(cachePath: string, cache: TranslationCache): Promise<void> {
|
||||||
|
await fs.writeFile(cachePath, JSON.stringify(cache, null, 2), "utf-8");
|
||||||
|
}
|
||||||
|
|
||||||
export interface TranslationOptions {
|
export interface TranslationOptions {
|
||||||
/** Source README file path */
|
/** Source README file path */
|
||||||
@@ -19,6 +47,10 @@ export interface TranslationOptions {
|
|||||||
maxBudgetUsd?: number;
|
maxBudgetUsd?: number;
|
||||||
/** Verbose output */
|
/** Verbose output */
|
||||||
verbose?: boolean;
|
verbose?: boolean;
|
||||||
|
/** Force re-translation even if cached */
|
||||||
|
force?: boolean;
|
||||||
|
/** Number of concurrent translations (default: 1) */
|
||||||
|
parallel?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TranslationResult {
|
export interface TranslationResult {
|
||||||
@@ -27,6 +59,8 @@ export interface TranslationResult {
|
|||||||
success: boolean;
|
success: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
costUsd?: number;
|
costUsd?: number;
|
||||||
|
/** Whether this was served from cache */
|
||||||
|
cached?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TranslationJobResult {
|
export interface TranslationJobResult {
|
||||||
@@ -37,40 +71,46 @@ export interface TranslationJobResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const LANGUAGE_NAMES: Record<string, string> = {
|
const LANGUAGE_NAMES: Record<string, string> = {
|
||||||
ar: "Arabic",
|
// Tier 1 - No-brainers
|
||||||
bg: "Bulgarian",
|
zh: "Chinese (Simplified)",
|
||||||
cs: "Czech",
|
|
||||||
da: "Danish",
|
|
||||||
de: "German",
|
|
||||||
el: "Greek",
|
|
||||||
es: "Spanish",
|
|
||||||
et: "Estonian",
|
|
||||||
fi: "Finnish",
|
|
||||||
fr: "French",
|
|
||||||
he: "Hebrew",
|
|
||||||
hi: "Hindi",
|
|
||||||
hu: "Hungarian",
|
|
||||||
id: "Indonesian",
|
|
||||||
it: "Italian",
|
|
||||||
ja: "Japanese",
|
ja: "Japanese",
|
||||||
ko: "Korean",
|
|
||||||
lt: "Lithuanian",
|
|
||||||
lv: "Latvian",
|
|
||||||
nl: "Dutch",
|
|
||||||
no: "Norwegian",
|
|
||||||
pl: "Polish",
|
|
||||||
pt: "Portuguese",
|
|
||||||
"pt-br": "Brazilian Portuguese",
|
"pt-br": "Brazilian Portuguese",
|
||||||
ro: "Romanian",
|
ko: "Korean",
|
||||||
|
es: "Spanish",
|
||||||
|
de: "German",
|
||||||
|
fr: "French",
|
||||||
|
// Tier 2 - Strong tech scenes
|
||||||
|
he: "Hebrew",
|
||||||
|
ar: "Arabic",
|
||||||
ru: "Russian",
|
ru: "Russian",
|
||||||
sk: "Slovak",
|
pl: "Polish",
|
||||||
sl: "Slovenian",
|
cs: "Czech",
|
||||||
sv: "Swedish",
|
nl: "Dutch",
|
||||||
th: "Thai",
|
|
||||||
tr: "Turkish",
|
tr: "Turkish",
|
||||||
uk: "Ukrainian",
|
uk: "Ukrainian",
|
||||||
|
// Tier 3 - Emerging/Growing fast
|
||||||
vi: "Vietnamese",
|
vi: "Vietnamese",
|
||||||
zh: "Chinese (Simplified)",
|
id: "Indonesian",
|
||||||
|
th: "Thai",
|
||||||
|
hi: "Hindi",
|
||||||
|
bn: "Bengali",
|
||||||
|
ro: "Romanian",
|
||||||
|
sv: "Swedish",
|
||||||
|
// Tier 4 - Why not
|
||||||
|
it: "Italian",
|
||||||
|
el: "Greek",
|
||||||
|
hu: "Hungarian",
|
||||||
|
fi: "Finnish",
|
||||||
|
da: "Danish",
|
||||||
|
no: "Norwegian",
|
||||||
|
// Other supported
|
||||||
|
bg: "Bulgarian",
|
||||||
|
et: "Estonian",
|
||||||
|
lt: "Lithuanian",
|
||||||
|
lv: "Latvian",
|
||||||
|
pt: "Portuguese",
|
||||||
|
sk: "Slovak",
|
||||||
|
sl: "Slovenian",
|
||||||
"zh-tw": "Chinese (Traditional)",
|
"zh-tw": "Chinese (Traditional)",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -107,6 +147,7 @@ Guidelines:
|
|||||||
- Preserve technical accuracy
|
- Preserve technical accuracy
|
||||||
- Use appropriate technical terminology for ${languageName}
|
- Use appropriate technical terminology for ${languageName}
|
||||||
- Keep proper nouns (product names, company names) unchanged unless they have official translations
|
- Keep proper nouns (product names, company names) unchanged unless they have official translations
|
||||||
|
- Add a small note at the very top of the document (before any other content) in ${languageName}: "🌐 This is an automated translation. Community corrections are welcome!"
|
||||||
|
|
||||||
Here is the README content to translate:
|
Here is the README content to translate:
|
||||||
|
|
||||||
@@ -114,7 +155,12 @@ Here is the README content to translate:
|
|||||||
${content}
|
${content}
|
||||||
---
|
---
|
||||||
|
|
||||||
Output ONLY the translated README content, nothing else. Do not include any preamble or explanation.`;
|
CRITICAL OUTPUT RULES:
|
||||||
|
- Output ONLY the raw translated markdown content
|
||||||
|
- Do NOT wrap output in \`\`\`markdown code fences
|
||||||
|
- Do NOT add any preamble, explanation, or commentary
|
||||||
|
- Start directly with the translation note, then the content
|
||||||
|
- The output will be saved directly to a .md file`;
|
||||||
|
|
||||||
let translation = "";
|
let translation = "";
|
||||||
let costUsd = 0;
|
let costUsd = 0;
|
||||||
@@ -182,7 +228,21 @@ Always output only the translated content without any surrounding explanation.`,
|
|||||||
process.stdout.write("\r" + " ".repeat(60) + "\r");
|
process.stdout.write("\r" + " ".repeat(60) + "\r");
|
||||||
}
|
}
|
||||||
|
|
||||||
return { translation: translation.trim(), costUsd };
|
// Strip markdown code fences if Claude wrapped the output
|
||||||
|
let cleaned = translation.trim();
|
||||||
|
if (cleaned.startsWith("```markdown")) {
|
||||||
|
cleaned = cleaned.slice("```markdown".length);
|
||||||
|
} else if (cleaned.startsWith("```md")) {
|
||||||
|
cleaned = cleaned.slice("```md".length);
|
||||||
|
} else if (cleaned.startsWith("```")) {
|
||||||
|
cleaned = cleaned.slice(3);
|
||||||
|
}
|
||||||
|
if (cleaned.endsWith("```")) {
|
||||||
|
cleaned = cleaned.slice(0, -3);
|
||||||
|
}
|
||||||
|
cleaned = cleaned.trim();
|
||||||
|
|
||||||
|
return { translation: cleaned, costUsd };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function translateReadme(
|
export async function translateReadme(
|
||||||
@@ -197,6 +257,8 @@ export async function translateReadme(
|
|||||||
model,
|
model,
|
||||||
maxBudgetUsd,
|
maxBudgetUsd,
|
||||||
verbose = false,
|
verbose = false,
|
||||||
|
force = false,
|
||||||
|
parallel = 1,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
// Read source file
|
// Read source file
|
||||||
@@ -207,6 +269,12 @@ export async function translateReadme(
|
|||||||
const outDir = outputDir ? path.resolve(outputDir) : path.dirname(sourcePath);
|
const outDir = outputDir ? path.resolve(outputDir) : path.dirname(sourcePath);
|
||||||
await fs.mkdir(outDir, { recursive: true });
|
await fs.mkdir(outDir, { recursive: true });
|
||||||
|
|
||||||
|
// Compute content hash and load cache
|
||||||
|
const sourceHash = hashContent(content);
|
||||||
|
const cachePath = path.join(outDir, ".translation-cache.json");
|
||||||
|
const cache = await readCache(cachePath);
|
||||||
|
const isHashMatch = cache?.sourceHash === sourceHash;
|
||||||
|
|
||||||
const results: TranslationResult[] = [];
|
const results: TranslationResult[] = [];
|
||||||
let totalCostUsd = 0;
|
let totalCostUsd = 0;
|
||||||
|
|
||||||
@@ -214,24 +282,28 @@ export async function translateReadme(
|
|||||||
console.log(`📖 Source: ${sourcePath}`);
|
console.log(`📖 Source: ${sourcePath}`);
|
||||||
console.log(`📂 Output: ${outDir}`);
|
console.log(`📂 Output: ${outDir}`);
|
||||||
console.log(`🌍 Languages: ${languages.join(", ")}`);
|
console.log(`🌍 Languages: ${languages.join(", ")}`);
|
||||||
|
if (parallel > 1) {
|
||||||
|
console.log(`⚡ Parallel: ${parallel} concurrent translations`);
|
||||||
|
}
|
||||||
console.log("");
|
console.log("");
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const lang of languages) {
|
// Worker function for a single language
|
||||||
// Check budget
|
async function translateLang(lang: string): Promise<TranslationResult> {
|
||||||
if (maxBudgetUsd && totalCostUsd >= maxBudgetUsd) {
|
|
||||||
results.push({
|
|
||||||
language: lang,
|
|
||||||
outputPath: "",
|
|
||||||
success: false,
|
|
||||||
error: "Budget exceeded",
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const outputFilename = pattern.replace("{lang}", lang);
|
const outputFilename = pattern.replace("{lang}", lang);
|
||||||
const outputPath = path.join(outDir, outputFilename);
|
const outputPath = path.join(outDir, outputFilename);
|
||||||
|
|
||||||
|
// Check cache (unless --force)
|
||||||
|
if (!force && isHashMatch && cache?.translations[lang]) {
|
||||||
|
const outputExists = await fs.access(outputPath).then(() => true).catch(() => false);
|
||||||
|
if (outputExists) {
|
||||||
|
if (verbose) {
|
||||||
|
console.log(` ✅ ${outputFilename} (cached, unchanged)`);
|
||||||
|
}
|
||||||
|
return { language: lang, outputPath, success: true, cached: true, costUsd: 0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (verbose) {
|
if (verbose) {
|
||||||
console.log(`🔄 Translating to ${getLanguageName(lang)} (${lang})...`);
|
console.log(`🔄 Translating to ${getLanguageName(lang)} (${lang})...`);
|
||||||
}
|
}
|
||||||
@@ -240,37 +312,81 @@ export async function translateReadme(
|
|||||||
const { translation, costUsd } = await translateToLanguage(content, lang, {
|
const { translation, costUsd } = await translateToLanguage(content, lang, {
|
||||||
preserveCode,
|
preserveCode,
|
||||||
model,
|
model,
|
||||||
verbose,
|
verbose: verbose && parallel === 1, // Only show progress spinner for sequential
|
||||||
});
|
});
|
||||||
|
|
||||||
await fs.writeFile(outputPath, translation, "utf-8");
|
await fs.writeFile(outputPath, translation, "utf-8");
|
||||||
totalCostUsd += costUsd;
|
|
||||||
|
|
||||||
results.push({
|
|
||||||
language: lang,
|
|
||||||
outputPath,
|
|
||||||
success: true,
|
|
||||||
costUsd,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (verbose) {
|
if (verbose) {
|
||||||
console.log(` ✅ Saved to ${outputFilename} ($${costUsd.toFixed(4)})`);
|
console.log(` ✅ Saved to ${outputFilename} ($${costUsd.toFixed(4)})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return { language: lang, outputPath, success: true, costUsd };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
results.push({
|
|
||||||
language: lang,
|
|
||||||
outputPath,
|
|
||||||
success: false,
|
|
||||||
error: errorMessage,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (verbose) {
|
if (verbose) {
|
||||||
console.log(` ❌ Failed: ${errorMessage}`);
|
console.log(` ❌ ${lang} failed: ${errorMessage}`);
|
||||||
}
|
}
|
||||||
|
return { language: lang, outputPath, success: false, error: errorMessage };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run with concurrency limit
|
||||||
|
async function runWithConcurrency<T>(items: T[], limit: number, fn: (item: T) => Promise<TranslationResult>): Promise<TranslationResult[]> {
|
||||||
|
const results: TranslationResult[] = [];
|
||||||
|
const executing: Promise<void>[] = [];
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
// Check budget before starting new translation
|
||||||
|
if (maxBudgetUsd && totalCostUsd >= maxBudgetUsd) {
|
||||||
|
results.push({
|
||||||
|
language: String(item),
|
||||||
|
outputPath: "",
|
||||||
|
success: false,
|
||||||
|
error: "Budget exceeded",
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const p = fn(item).then((result) => {
|
||||||
|
results.push(result);
|
||||||
|
if (result.costUsd) {
|
||||||
|
totalCostUsd += result.costUsd;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
executing.push(p.then(() => {
|
||||||
|
executing.splice(executing.indexOf(p.then(() => {})), 1);
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (executing.length >= limit) {
|
||||||
|
await Promise.race(executing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(executing);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
const translationResults = await runWithConcurrency(languages, parallel, translateLang);
|
||||||
|
results.push(...translationResults);
|
||||||
|
|
||||||
|
// Save updated cache
|
||||||
|
const newCache: TranslationCache = {
|
||||||
|
sourceHash,
|
||||||
|
lastUpdated: new Date().toISOString(),
|
||||||
|
translations: {
|
||||||
|
...(isHashMatch ? cache?.translations : {}),
|
||||||
|
...Object.fromEntries(
|
||||||
|
results.filter(r => r.success && !r.cached).map(r => [
|
||||||
|
r.language,
|
||||||
|
{ hash: sourceHash, translatedAt: new Date().toISOString(), costUsd: r.costUsd || 0 }
|
||||||
|
])
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await writeCache(cachePath, newCache);
|
||||||
|
|
||||||
const successful = results.filter((r) => r.success).length;
|
const successful = results.filter((r) => r.success).length;
|
||||||
const failed = results.filter((r) => !r.success).length;
|
const failed = results.filter((r) => !r.success).length;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user