@aws/nx-plugin 1.0.0-rc.67 → 1.0.0-rc.68
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/migrations.json +6 -1
- package/package.json +1 -1
- package/src/migrations/latest/translate-script-whole-file-writes/files/build-system-prompt.ts.fixture +23 -0
- package/src/migrations/latest/translate-script-whole-file-writes/files/build-user-prompt.ts.fixture +96 -0
- package/src/migrations/latest/translate-script-whole-file-writes/files/config-path.ts.fixture +1 -0
- package/src/migrations/latest/translate-script-whole-file-writes/files/file-to-translate.ts.fixture +10 -0
- package/src/migrations/latest/translate-script-whole-file-writes/files/get-files-to-translate.ts.fixture +96 -0
- package/src/migrations/latest/translate-script-whole-file-writes/files/header.ts.fixture +15 -0
- package/src/migrations/latest/translate-script-whole-file-writes/files/log.ts.fixture +18 -0
- package/src/migrations/latest/translate-script-whole-file-writes/files/main-call.ts.fixture +6 -0
- package/src/migrations/latest/translate-script-whole-file-writes/files/main.ts.fixture +79 -0
- package/src/migrations/latest/translate-script-whole-file-writes/files/project-root.ts.fixture +1 -0
- package/src/migrations/latest/translate-script-whole-file-writes/files/run-with-concurrency.ts.fixture +57 -0
- package/src/migrations/latest/translate-script-whole-file-writes/files/scripts-dir.ts.fixture +1 -0
- package/src/migrations/latest/translate-script-whole-file-writes/files/translate-file-for-language.ts.fixture +61 -0
- package/src/migrations/latest/translate-script-whole-file-writes/grit/already-migrated.grit +1 -0
- package/src/migrations/latest/translate-script-whole-file-writes/grit/build-system-prompt.grit +6 -0
- package/src/migrations/latest/translate-script-whole-file-writes/grit/build-user-prompt.grit +6 -0
- package/src/migrations/latest/translate-script-whole-file-writes/grit/config-path.grit +1 -0
- package/src/migrations/latest/translate-script-whole-file-writes/grit/file-to-translate.grit +11 -0
- package/src/migrations/latest/translate-script-whole-file-writes/grit/get-files-to-translate.grit +7 -0
- package/src/migrations/latest/translate-script-whole-file-writes/grit/log.grit +3 -0
- package/src/migrations/latest/translate-script-whole-file-writes/grit/main-call.grit +1 -0
- package/src/migrations/latest/translate-script-whole-file-writes/grit/main.grit +8 -0
- package/src/migrations/latest/translate-script-whole-file-writes/grit/project-root.grit +1 -0
- package/src/migrations/latest/translate-script-whole-file-writes/grit/run-with-concurrency.grit +6 -0
- package/src/migrations/latest/translate-script-whole-file-writes/grit/translate-file-for-language.grit +6 -0
- package/src/migrations/latest/translate-script-whole-file-writes/metadata.json +3 -0
- package/src/migrations/latest/translate-script-whole-file-writes/migration.d.ts +2 -0
- package/src/migrations/latest/translate-script-whole-file-writes/migration.js +103 -0
- package/src/migrations/latest/translate-script-whole-file-writes/migration.js.map +1 -0
- package/src/migrations/latest/translate-script-whole-file-writes/test-fixtures/released-script-first.ts.fixture +430 -0
- package/src/migrations/latest/translate-script-whole-file-writes/test-fixtures/released-script.ts.fixture +431 -0
- package/src/ts/astro-docs/files/translation/scripts/translate.ts.template +269 -148
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translate documentation files using a Strands agent powered by Claude on
|
|
3
|
+
* Amazon Bedrock.
|
|
4
|
+
*
|
|
5
|
+
* Configuration lives in ./translate.config.json (sibling to this file).
|
|
6
|
+
* Run from the project root:
|
|
7
|
+
*
|
|
8
|
+
* <%= pkgMgrCmd %> nx translate <%= fullyQualifiedName %> -- --all
|
|
9
|
+
* <%= pkgMgrCmd %> nx translate <%= fullyQualifiedName %> # only files changed since last translate commit
|
|
10
|
+
*
|
|
11
|
+
* The driver gathers the set of changed source docs + their git diffs and hands
|
|
12
|
+
* them to an agent that has read/write access (scoped to the docs directory)
|
|
13
|
+
* via a wrapped version of the built-in `fileEditor` tool. The agent decides
|
|
14
|
+
* how to translate each file and writes the output itself.
|
|
15
|
+
*/
|
|
16
|
+
import { Command } from 'commander';
|
|
17
|
+
import fs from 'fs-extra';
|
|
18
|
+
import path from 'path';
|
|
19
|
+
import { simpleGit } from 'simple-git';
|
|
20
|
+
import glob from 'fast-glob';
|
|
21
|
+
import { Agent, BeforeToolCallEvent } from '@strands-agents/sdk';
|
|
22
|
+
import { BedrockModel } from '@strands-agents/sdk/models/bedrock';
|
|
23
|
+
import { fileEditor } from '@strands-agents/sdk/vended-tools/file-editor';
|
|
24
|
+
|
|
25
|
+
interface TranslateConfig {
|
|
26
|
+
sourceLanguage: string;
|
|
27
|
+
targetLanguages: string[];
|
|
28
|
+
docsDir: string;
|
|
29
|
+
include: string[];
|
|
30
|
+
exclude: string[];
|
|
31
|
+
modelId: string;
|
|
32
|
+
awsRegion: string;
|
|
33
|
+
/**
|
|
34
|
+
* Maximum number of (file x language) translations running in parallel. Each one
|
|
35
|
+
* is a fresh agent invocation, so this caps concurrent Bedrock requests.
|
|
36
|
+
* Defaults to 5 when omitted.
|
|
37
|
+
*/
|
|
38
|
+
concurrency?: number;
|
|
39
|
+
/**
|
|
40
|
+
* Commit message marker used to identify previous translation commits, so
|
|
41
|
+
* incremental runs only re-translate changes since the last translation.
|
|
42
|
+
* Defaults to "docs: update translations".
|
|
43
|
+
*/
|
|
44
|
+
translationCommitMessage?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const SCRIPTS_DIR = <% if (esm) { %>import.meta.dirname<% } else { %>__dirname<% } %>;
|
|
48
|
+
const PROJECT_ROOT = path.resolve(SCRIPTS_DIR, '..');
|
|
49
|
+
const CONFIG_PATH = path.resolve(SCRIPTS_DIR, 'translate.config.json');
|
|
50
|
+
const config: TranslateConfig = JSON.parse(
|
|
51
|
+
fs.readFileSync(CONFIG_PATH, 'utf-8'),
|
|
52
|
+
);
|
|
53
|
+
const DOCS_DIR = path.resolve(PROJECT_ROOT, config.docsDir);
|
|
54
|
+
const TRANSLATION_COMMIT_MESSAGE =
|
|
55
|
+
config.translationCommitMessage ?? 'docs: update translations';
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Reject any `fileEditor` call whose `path` resolves outside the configured
|
|
59
|
+
* docs directory. Runs as a `BeforeToolCallEvent` hook so we reuse the
|
|
60
|
+
* built-in tool's schema/description verbatim — nothing else uses fileEditor in
|
|
61
|
+
* this script, so the check is unconditional.
|
|
62
|
+
*/
|
|
63
|
+
function rejectOutsideDocsDir(event: BeforeToolCallEvent): void {
|
|
64
|
+
if (event.toolUse.name !== 'fileEditor') return;
|
|
65
|
+
const input = event.toolUse.input as { path?: unknown };
|
|
66
|
+
if (typeof input.path !== 'string') return;
|
|
67
|
+
const resolved = path.resolve(input.path);
|
|
68
|
+
const docsDirWithSep = DOCS_DIR.endsWith(path.sep)
|
|
69
|
+
? DOCS_DIR
|
|
70
|
+
: DOCS_DIR + path.sep;
|
|
71
|
+
if (resolved !== DOCS_DIR && !resolved.startsWith(docsDirWithSep)) {
|
|
72
|
+
event.cancel = `Path ${resolved} is outside the docs directory (${DOCS_DIR}); refusing access.`;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface FileToTranslate {
|
|
77
|
+
relativePath: string;
|
|
78
|
+
sourceAbsPath: string;
|
|
79
|
+
sourceContent: string;
|
|
80
|
+
/** Empty when this is a newly-added file. */
|
|
81
|
+
diff: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const program = new Command();
|
|
85
|
+
program
|
|
86
|
+
.name('translate')
|
|
87
|
+
.description('Translate documentation files using a Strands agent')
|
|
88
|
+
.option('-a, --all', 'Translate all source documentation files')
|
|
89
|
+
.option(
|
|
90
|
+
'-l, --languages <languages>',
|
|
91
|
+
'Comma-separated list of target languages (overrides translate.config.json)',
|
|
92
|
+
)
|
|
93
|
+
.option(
|
|
94
|
+
'-d, --dry-run',
|
|
95
|
+
'Show what would be translated without invoking the agent',
|
|
96
|
+
)
|
|
97
|
+
.option('-v, --verbose', 'Show verbose output')
|
|
98
|
+
.parse(process.argv);
|
|
99
|
+
const options = program.opts();
|
|
100
|
+
|
|
101
|
+
const log = {
|
|
102
|
+
info: (m: string) => console.log(`[translate] ${m}`),
|
|
103
|
+
warn: (m: string) => console.warn(`[translate] ${m}`),
|
|
104
|
+
error: (m: string) => console.error(`[translate] ERROR ${m}`),
|
|
105
|
+
verbose: (m: string) =>
|
|
106
|
+
options.verbose && console.log(`[translate] ${m}`),
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Gather the set of source-language files to translate, with their diffs.
|
|
111
|
+
*/
|
|
112
|
+
async function getFilesToTranslate(): Promise<FileToTranslate[]> {
|
|
113
|
+
const sourceLangRoot = `${DOCS_DIR}/${config.sourceLanguage}`;
|
|
114
|
+
const includePatterns = config.include.map(
|
|
115
|
+
(p) => `${sourceLangRoot}/${p}`,
|
|
116
|
+
);
|
|
117
|
+
const ignorePatterns = config.exclude.map(
|
|
118
|
+
(p) => `${sourceLangRoot}/${p}`,
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
if (options.all) {
|
|
122
|
+
log.info('Translating all source documentation files');
|
|
123
|
+
const files = await glob(includePatterns, { ignore: ignorePatterns });
|
|
124
|
+
return Promise.all(
|
|
125
|
+
files.map(async (file) => ({
|
|
126
|
+
relativePath: path.relative(sourceLangRoot, file),
|
|
127
|
+
sourceAbsPath: file,
|
|
128
|
+
sourceContent: await fs.readFile(file, 'utf-8'),
|
|
129
|
+
diff: '',
|
|
130
|
+
})),
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const git = simpleGit();
|
|
135
|
+
|
|
136
|
+
let currentBranch: string;
|
|
137
|
+
let mainBranch: string;
|
|
138
|
+
if (process.env.GITHUB_HEAD_REF) {
|
|
139
|
+
currentBranch = `origin/${process.env.GITHUB_HEAD_REF}`;
|
|
140
|
+
mainBranch = 'origin/main';
|
|
141
|
+
} else {
|
|
142
|
+
currentBranch = (await git.branch()).current;
|
|
143
|
+
mainBranch = 'main';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
await git.raw(['rev-parse', '--verify', mainBranch]);
|
|
148
|
+
} catch {
|
|
149
|
+
log.warn(
|
|
150
|
+
`Could not find "${mainBranch}"; falling back to --all behaviour`,
|
|
151
|
+
);
|
|
152
|
+
options.all = true;
|
|
153
|
+
return getFilesToTranslate();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const mergeBase = (
|
|
157
|
+
await git.raw(['merge-base', mainBranch, currentBranch])
|
|
158
|
+
).trim();
|
|
159
|
+
|
|
160
|
+
const translationCommits = (
|
|
161
|
+
await git.log({ from: mergeBase, to: 'HEAD' })
|
|
162
|
+
).all.filter((c) => c.message.includes(TRANSLATION_COMMIT_MESSAGE));
|
|
163
|
+
|
|
164
|
+
const baseCommit =
|
|
165
|
+
translationCommits.length > 0
|
|
166
|
+
? translationCommits[0].hash
|
|
167
|
+
: mergeBase;
|
|
168
|
+
|
|
169
|
+
log.info(
|
|
170
|
+
translationCommits.length > 0
|
|
171
|
+
? `Detecting changed files since last translation commit ${baseCommit.substring(0, 7)}`
|
|
172
|
+
: `Detecting changed files since branch creation ${baseCommit.substring(0, 7)}`,
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const diffNames = (
|
|
176
|
+
await git.diff([
|
|
177
|
+
`${baseCommit}..HEAD`,
|
|
178
|
+
'--name-only',
|
|
179
|
+
'--diff-filter=d',
|
|
180
|
+
])
|
|
181
|
+
)
|
|
182
|
+
.split('\n')
|
|
183
|
+
.filter(Boolean);
|
|
184
|
+
|
|
185
|
+
const { files: uncommitted } = await git.status();
|
|
186
|
+
const uncommittedNames = uncommitted.map((f) => f.path);
|
|
187
|
+
|
|
188
|
+
const allCandidates = [
|
|
189
|
+
...new Set([...diffNames, ...uncommittedNames]),
|
|
190
|
+
].map((p) => path.resolve(process.cwd(), p));
|
|
191
|
+
|
|
192
|
+
// Filter to files inside the source language dir that match include/exclude
|
|
193
|
+
const includedGlob = await glob(includePatterns, {
|
|
194
|
+
ignore: ignorePatterns,
|
|
195
|
+
});
|
|
196
|
+
const includedSet = new Set(includedGlob.map((p) => path.resolve(p)));
|
|
197
|
+
|
|
198
|
+
const changed = allCandidates.filter((abs) => includedSet.has(abs));
|
|
199
|
+
|
|
200
|
+
if (changed.length === 0) {
|
|
201
|
+
log.warn('No changed source documentation files detected');
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return Promise.all(
|
|
206
|
+
changed.map(async (file) => {
|
|
207
|
+
const sourceContent = await fs.readFile(file, 'utf-8');
|
|
208
|
+
let diff = '';
|
|
209
|
+
try {
|
|
210
|
+
diff = await git.diff([
|
|
211
|
+
`${baseCommit}..HEAD`,
|
|
212
|
+
'--',
|
|
213
|
+
path.relative(process.cwd(), file),
|
|
214
|
+
]);
|
|
215
|
+
} catch {
|
|
216
|
+
// treat as new file
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
relativePath: path.relative(sourceLangRoot, file),
|
|
220
|
+
sourceAbsPath: file,
|
|
221
|
+
sourceContent,
|
|
222
|
+
diff,
|
|
223
|
+
};
|
|
224
|
+
}),
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function buildSystemPrompt(targetLang: string): string {
|
|
229
|
+
return `You are an expert technical-documentation translator. Your job is to translate a single MDX documentation file from the source locale \`${config.sourceLanguage}\` into the target locale \`${targetLang}\`.
|
|
230
|
+
|
|
231
|
+
Both values are locale codes (e.g. ISO 639-1 / BCP-47 style, or common short forms like \`jp\`, \`zh\`, \`pt\`). Interpret them yourself and translate naturally into the language they identify. If a code is ambiguous, prefer the most widely used written form.
|
|
232
|
+
|
|
233
|
+
You have one tool available: \`fileEditor\`. Use it to:
|
|
234
|
+
- Read the source file (\`command: "view"\`). For large files you can read a portion at a time using \`view_range\`, then request the next range.
|
|
235
|
+
- Read the existing translation if one is provided (\`command: "view"\`).
|
|
236
|
+
- Write the translated file (\`command: "create"\` — it overwrites).
|
|
237
|
+
|
|
238
|
+
Translation rules:
|
|
239
|
+
1. Translate natural-language prose into the target language. Keep technical accuracy.
|
|
240
|
+
2. DO NOT translate:
|
|
241
|
+
- Code blocks and inline code (text inside backticks)
|
|
242
|
+
- URLs, link paths, and HTML/JSX/MDX tag names and attributes
|
|
243
|
+
- \`import\` statements, component names, and frontmatter keys
|
|
244
|
+
- Proper names of people, products, or AWS services
|
|
245
|
+
3. Preserve every aspect of the MDX structure exactly: frontmatter delimiters, headings, lists, code blocks, MDX components, JSX, whitespace, blank lines.
|
|
246
|
+
4. For frontmatter:
|
|
247
|
+
- Translate only string values for the \`title\` and \`description\` keys.
|
|
248
|
+
- Leave \`date\`, \`authors\`, \`template\`, \`slug\`, and all other keys untouched.
|
|
249
|
+
5. If any localised link paths embed the source locale (e.g. \`/${config.sourceLanguage}/foo\`), rewrite them to the target locale (\`/${targetLang}/foo\`).
|
|
250
|
+
6. Efficiency rule: if an existing translation is provided AND a diff is provided, reuse the existing translation verbatim for any section of the document that is NOT touched by the diff. Only retranslate sections that actually changed. Still emit the complete final file.
|
|
251
|
+
7. If no existing translation exists, translate the whole file.
|
|
252
|
+
8. Never wrap the file output in triple backticks.
|
|
253
|
+
9. Always use absolute paths with \`fileEditor\`.
|
|
254
|
+
|
|
255
|
+
When the translated file has been written, reply with a one-line summary and stop.`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function buildUserPrompt(
|
|
259
|
+
file: FileToTranslate,
|
|
260
|
+
targetLang: string,
|
|
261
|
+
): string {
|
|
262
|
+
const targetLangRoot = path.join(DOCS_DIR, targetLang);
|
|
263
|
+
const targetAbsPath = path.join(targetLangRoot, file.relativePath);
|
|
264
|
+
const existingTranslationExists = fs.existsSync(targetAbsPath);
|
|
265
|
+
|
|
266
|
+
const diffBlock = file.diff
|
|
267
|
+
? `Git diff showing what changed in the source since the last translation:\n\`\`\`diff\n${file.diff.slice(0, 40_000)}\n\`\`\``
|
|
268
|
+
: 'There is no prior diff for this file — treat it as new content and translate in full.';
|
|
269
|
+
|
|
270
|
+
const existingBlock = existingTranslationExists
|
|
271
|
+
? `An existing translation for locale \`${targetLang}\` already lives at:\n \`${targetAbsPath}\`\nRead it with \`fileEditor\` (\`command: "view"\`) before writing so you can reuse any sections that have not changed.`
|
|
272
|
+
: `No existing translation exists yet — you will create it fresh.`;
|
|
273
|
+
|
|
274
|
+
return `Translate one file from source locale \`${config.sourceLanguage}\` into target locale \`${targetLang}\`.
|
|
275
|
+
|
|
276
|
+
- Source file (read from here): \`${file.sourceAbsPath}\`
|
|
277
|
+
- Target file (write the translation here, absolute path): \`${targetAbsPath}\`
|
|
278
|
+
|
|
279
|
+
${existingBlock}
|
|
280
|
+
|
|
281
|
+
${diffBlock}
|
|
282
|
+
|
|
283
|
+
Steps:
|
|
284
|
+
1. Read the source file. If it is long, read it in slices using \`view_range\`.
|
|
285
|
+
2. If an existing translation is present, read it first so you can reuse unchanged sections.
|
|
286
|
+
3. Write the full translated file to the target path using \`command: "create"\`.
|
|
287
|
+
4. Reply with a single short confirmation line.`;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function translateFileForLanguage(
|
|
291
|
+
file: FileToTranslate,
|
|
292
|
+
targetLang: string,
|
|
293
|
+
): Promise<void> {
|
|
294
|
+
const targetAbsPath = path.join(
|
|
295
|
+
DOCS_DIR,
|
|
296
|
+
targetLang,
|
|
297
|
+
file.relativePath,
|
|
298
|
+
);
|
|
299
|
+
const beforeMtimeMs = fs.existsSync(targetAbsPath)
|
|
300
|
+
? (await fs.stat(targetAbsPath)).mtimeMs
|
|
301
|
+
: 0;
|
|
302
|
+
|
|
303
|
+
const agent = new Agent({
|
|
304
|
+
model: new BedrockModel({
|
|
305
|
+
modelId: config.modelId,
|
|
306
|
+
region: process.env.AWS_REGION ?? config.awsRegion,
|
|
307
|
+
maxTokens: 64_000,
|
|
308
|
+
temperature: 0.2,
|
|
309
|
+
}),
|
|
310
|
+
systemPrompt: buildSystemPrompt(targetLang),
|
|
311
|
+
tools: [fileEditor],
|
|
312
|
+
printer: !!options.verbose,
|
|
313
|
+
});
|
|
314
|
+
agent.addHook(BeforeToolCallEvent, rejectOutsideDocsDir);
|
|
315
|
+
|
|
316
|
+
const result = await agent.invoke(buildUserPrompt(file, targetLang));
|
|
317
|
+
|
|
318
|
+
if (result.stopReason !== 'endTurn') {
|
|
319
|
+
log.warn(
|
|
320
|
+
`agent stopped with reason=${result.stopReason} while translating ${file.relativePath} → ${targetLang} — inspect output above`,
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Sanity check: the target file should now exist and have been written during this run.
|
|
325
|
+
if (!fs.existsSync(targetAbsPath)) {
|
|
326
|
+
throw new Error(
|
|
327
|
+
`agent did not write target file ${targetAbsPath} for ${file.relativePath} → ${targetLang}`,
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
const afterMtimeMs = (await fs.stat(targetAbsPath)).mtimeMs;
|
|
331
|
+
if (afterMtimeMs <= beforeMtimeMs) {
|
|
332
|
+
log.warn(
|
|
333
|
+
`target ${file.relativePath} → ${targetLang} was not updated (mtime unchanged) — the agent may have decided no change was needed`,
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Simple concurrency-limited runner.
|
|
340
|
+
*/
|
|
341
|
+
async function runWithConcurrency<T>(
|
|
342
|
+
tasks: Array<() => Promise<T>>,
|
|
343
|
+
limit: number,
|
|
344
|
+
): Promise<T[]> {
|
|
345
|
+
const results: T[] = new Array(tasks.length);
|
|
346
|
+
let next = 0;
|
|
347
|
+
async function worker() {
|
|
348
|
+
while (true) {
|
|
349
|
+
const i = next++;
|
|
350
|
+
if (i >= tasks.length) return;
|
|
351
|
+
results[i] = await tasks[i]();
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
const workers = Array.from({ length: Math.min(limit, tasks.length) }, worker);
|
|
355
|
+
await Promise.all(workers);
|
|
356
|
+
return results;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async function main() {
|
|
360
|
+
const requestedLanguages: string[] = options.languages
|
|
361
|
+
? options.languages
|
|
362
|
+
.split(',')
|
|
363
|
+
.map((l: string) => l.trim())
|
|
364
|
+
.filter(Boolean)
|
|
365
|
+
: config.targetLanguages;
|
|
366
|
+
|
|
367
|
+
const targetLanguages = requestedLanguages.filter(
|
|
368
|
+
(l) => l !== config.sourceLanguage,
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
if (targetLanguages.length === 0) {
|
|
372
|
+
log.error('No target languages configured');
|
|
373
|
+
process.exit(1);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
log.info(`Source: ${config.sourceLanguage}`);
|
|
377
|
+
log.info(`Targets: ${targetLanguages.join(', ')}`);
|
|
378
|
+
|
|
379
|
+
const files = await getFilesToTranslate();
|
|
380
|
+
|
|
381
|
+
if (files.length === 0) {
|
|
382
|
+
log.info('Nothing to translate.');
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
log.info(`Files to translate: ${files.length}`);
|
|
387
|
+
for (const f of files) {
|
|
388
|
+
log.verbose(
|
|
389
|
+
` - ${f.relativePath}${f.diff ? ' (changed)' : ' (full)'}`,
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (options.dryRun) {
|
|
394
|
+
for (const lang of targetLanguages) {
|
|
395
|
+
for (const f of files) {
|
|
396
|
+
log.info(`[dry-run] would translate ${f.relativePath} → ${lang}`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
log.info('Done (dry-run).');
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Build one task per (file × target language) — each is a fresh agent invocation,
|
|
404
|
+
// so context windows stay small no matter how big the docs site is.
|
|
405
|
+
const tasks = targetLanguages.flatMap((lang) =>
|
|
406
|
+
files.map((file) => async () => {
|
|
407
|
+
log.info(` ${file.relativePath} → ${lang}`);
|
|
408
|
+
try {
|
|
409
|
+
await translateFileForLanguage(file, lang);
|
|
410
|
+
} catch (err) {
|
|
411
|
+
log.error(
|
|
412
|
+
`${file.relativePath} → ${lang}: ${err instanceof Error ? err.message : String(err)}`,
|
|
413
|
+
);
|
|
414
|
+
throw err;
|
|
415
|
+
}
|
|
416
|
+
}),
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
const concurrency = Math.max(1, config.concurrency ?? 5);
|
|
420
|
+
log.info(
|
|
421
|
+
`Running ${tasks.length} translation(s) with concurrency=${concurrency}`,
|
|
422
|
+
);
|
|
423
|
+
await runWithConcurrency(tasks, concurrency);
|
|
424
|
+
|
|
425
|
+
log.info('Done.');
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
main().catch((err) => {
|
|
429
|
+
log.error(err instanceof Error ? err.message : String(err));
|
|
430
|
+
process.exit(1);
|
|
431
|
+
});
|