-
Notifications
You must be signed in to change notification settings - Fork 186
Add i18n sync-hash and repair-fences tooling #1227
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,135 @@ | ||||||||||||||||||||||||||||||||||||||
| #!/usr/bin/env bun | ||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||
| * Append missing closing ``` markers in translated MDX (structural fix only). | ||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||
| * Safe to run when `translate:check-truncation` reports `unclosed_code_fence`. | ||||||||||||||||||||||||||||||||||||||
| * Does not restore truncated code inside the block — use `translate:repair-truncated` | ||||||||||||||||||||||||||||||||||||||
| * when content was cut off, not just the closing fence. | ||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||
| * Usage: | ||||||||||||||||||||||||||||||||||||||
| * pnpm translate:repair-fences | ||||||||||||||||||||||||||||||||||||||
| * pnpm translate:repair-fences -- --lang ko | ||||||||||||||||||||||||||||||||||||||
| * pnpm translate:repair-fences -- --dry-run path/to/page.mdx | ||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| import { readFile, writeFile } from "fs/promises"; | ||||||||||||||||||||||||||||||||||||||
| import { join } from "path"; | ||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||
| loadI18nConfig, | ||||||||||||||||||||||||||||||||||||||
| REPO_ROOT, | ||||||||||||||||||||||||||||||||||||||
| parseLangArg as parseLangArgFromConfig, | ||||||||||||||||||||||||||||||||||||||
| } from "./i18n-config.mjs"; | ||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||
| hasUnclosedCodeFence, | ||||||||||||||||||||||||||||||||||||||
| repairTargetContent, | ||||||||||||||||||||||||||||||||||||||
| scanTruncationIssues, | ||||||||||||||||||||||||||||||||||||||
| writeTruncationReport, | ||||||||||||||||||||||||||||||||||||||
| } from "./check-translation-truncation.ts"; | ||||||||||||||||||||||||||||||||||||||
| import { parseFrontmatterAndBody } from "./chunked-translate.ts"; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| interface LangConfig { | ||||||||||||||||||||||||||||||||||||||
| code: string; | ||||||||||||||||||||||||||||||||||||||
| name: string; | ||||||||||||||||||||||||||||||||||||||
| dir: string; | ||||||||||||||||||||||||||||||||||||||
| snippets_dir: string; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const config = loadI18nConfig() as { | ||||||||||||||||||||||||||||||||||||||
| languages: LangConfig[]; | ||||||||||||||||||||||||||||||||||||||
| skip_paths: string[]; | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| async function readFileOr(path: string, fallback = ""): Promise<string> { | ||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||
| return await readFile(path, "utf-8"); | ||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||
| return fallback; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+42
to
+48
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value Silent error swallowing in Any read error (permissions, I/O failure, encoding issue), not just "file missing", falls back to an empty string, which is then silently skipped at line 83-86 as if the target simply didn't exist yet. A truly missing target vs. a broken read are treated identically with no diagnostic output. 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| function parseLangArg(args: string[]): LangConfig[] { | ||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||
| return parseLangArgFromConfig(args, config.languages) as LangConfig[]; | ||||||||||||||||||||||||||||||||||||||
| } catch (err: unknown) { | ||||||||||||||||||||||||||||||||||||||
| console.error(err instanceof Error ? err.message : String(err)); | ||||||||||||||||||||||||||||||||||||||
| process.exit(1); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| async function main() { | ||||||||||||||||||||||||||||||||||||||
| const args = process.argv.slice(2); | ||||||||||||||||||||||||||||||||||||||
| const dryRun = args.includes("--dry-run"); | ||||||||||||||||||||||||||||||||||||||
| const snippetsMode = args.includes("--snippets"); | ||||||||||||||||||||||||||||||||||||||
| const langs = parseLangArg(args); | ||||||||||||||||||||||||||||||||||||||
| const fileArgs = args.filter((a, i) => !a.startsWith("--") && args[i - 1] !== "--lang"); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const issues = await scanTruncationIssues({ langs, snippetsMode, fileArgs }); | ||||||||||||||||||||||||||||||||||||||
| const fenceIssues = issues.filter((i) => i.reasons.includes("unclosed_code_fence")); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if (fenceIssues.length === 0) { | ||||||||||||||||||||||||||||||||||||||
| console.log("No unclosed code fences found."); | ||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| console.log(`Found ${fenceIssues.length} file(s) with unclosed \`\`\` blocks${dryRun ? " [dry-run]" : ""}:`); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| let wouldRepair = 0; | ||||||||||||||||||||||||||||||||||||||
| let repaired = 0; | ||||||||||||||||||||||||||||||||||||||
| let skipped = 0; | ||||||||||||||||||||||||||||||||||||||
| const repairedPairs: { lang: string; enRel: string }[] = []; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| for (const issue of fenceIssues) { | ||||||||||||||||||||||||||||||||||||||
| const targetPath = join(REPO_ROOT, issue.targetRel); | ||||||||||||||||||||||||||||||||||||||
| const targetContent = await readFileOr(targetPath); | ||||||||||||||||||||||||||||||||||||||
| if (!targetContent.trim()) { | ||||||||||||||||||||||||||||||||||||||
| skipped++; | ||||||||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const { body } = parseFrontmatterAndBody(targetContent); | ||||||||||||||||||||||||||||||||||||||
| if (!hasUnclosedCodeFence(body)) { | ||||||||||||||||||||||||||||||||||||||
| console.log(` [${issue.lang}] ${issue.enRel}: already balanced, skipping`); | ||||||||||||||||||||||||||||||||||||||
| skipped++; | ||||||||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const result = repairTargetContent(targetContent); | ||||||||||||||||||||||||||||||||||||||
| if (!result.repaired) { | ||||||||||||||||||||||||||||||||||||||
| skipped++; | ||||||||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+88
to
+99
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value Redundant fence detection before repair.
♻️ Simplify by relying on repairTargetContent's own result- const { body } = parseFrontmatterAndBody(targetContent);
- if (!hasUnclosedCodeFence(body)) {
- console.log(` [${issue.lang}] ${issue.enRel}: already balanced, skipping`);
- skipped++;
- continue;
- }
-
const result = repairTargetContent(targetContent);
if (!result.repaired) {
+ console.log(` [${issue.lang}] ${issue.enRel}: already balanced, skipping`);
skipped++;
continue;
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if (dryRun) { | ||||||||||||||||||||||||||||||||||||||
| console.log(` [${issue.lang}] would repair: ${issue.targetRel}`); | ||||||||||||||||||||||||||||||||||||||
| console.log(` ${result.detail}`); | ||||||||||||||||||||||||||||||||||||||
| wouldRepair++; | ||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||
| await writeFile(targetPath, result.content); | ||||||||||||||||||||||||||||||||||||||
| console.log(` [${issue.lang}] repaired: ${issue.targetRel}`); | ||||||||||||||||||||||||||||||||||||||
| console.log(` ${result.detail}`); | ||||||||||||||||||||||||||||||||||||||
| repaired++; | ||||||||||||||||||||||||||||||||||||||
| repairedPairs.push({ lang: issue.lang, enRel: issue.enRel }); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if (!dryRun && repairedPairs.length > 0) { | ||||||||||||||||||||||||||||||||||||||
| const remaining = await scanTruncationIssues({ | ||||||||||||||||||||||||||||||||||||||
| langs, | ||||||||||||||||||||||||||||||||||||||
| snippetsMode, | ||||||||||||||||||||||||||||||||||||||
| pairs: repairedPairs.map((p) => ({ langCode: p.lang, enRel: p.enRel })), | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
| await writeTruncationReport(remaining, { replacePairs: repairedPairs }); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| console.log( | ||||||||||||||||||||||||||||||||||||||
| `\nDone: ${dryRun ? wouldRepair : repaired} repaired, ${skipped} skipped` + | ||||||||||||||||||||||||||||||||||||||
| (fenceIssues.some((i) => i.reasons.includes("missing_code_fence")) && !dryRun | ||||||||||||||||||||||||||||||||||||||
| ? "\nNote: files with missing_code_fence (fewer fences than EN) may need translate:repair-truncated." | ||||||||||||||||||||||||||||||||||||||
| : "") | ||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| main().catch((err) => { | ||||||||||||||||||||||||||||||||||||||
| console.error(err); | ||||||||||||||||||||||||||||||||||||||
| process.exit(1); | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Extra blank line inserted between frontmatter and body on every fence repair.
parseFrontmatterAndBodyalready restores the newline consumed by its\n?regex group (frontmatter:${match[1]}\n``), so plainfrontmatter + bodyconcatenation faithfully reproduces the original spacing. Here, `${frontmatter}\n${newBody}` adds one more `\n` on top of that, so every repaired file gains a spurious extra blank line between frontmatter and content (regardless of whether the source had a blank line there or not). Harmless for MDX rendering (consecutive blank lines collapse), but it needlessly bloats the diff/committed file beyond the actual fence fix.🔧 Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents