Skip to content

translate:sync-hash never converges: reports 71/84 files as drifted and appends a blank line every run #1358

Description

@christian-byrne

Summary

pnpm translate:sync-hash never converges. It reports 71 of 84 files as needing a hash sync when their hashes are already correct, and every run it appends one more blank line after the frontmatter of each translated file. The two are the same bug.

Found while adding a section to tutorials/image/qwen/qwen-image-layered.mdx (#1357), where the tool claimed drift on a page whose English source had not changed in seven months.

Reproduction

On a clean main:

$ bun .github/scripts/i18n/sync-hash-i18n.ts --dry-run tutorials/image/qwen/qwen-image-layered.mdx
  [ja] would sync hash: ja/tutorials/image/qwen/qwen-image-layered.mdx
  [zh] would sync hash: zh/tutorials/image/qwen/qwen-image-layered.mdx
  [ko] would sync hash: ko/tutorials/image/qwen/qwen-image-layered.mdx
Done: 3 updated, 0 already in sync, 0 missing target(s)

But the hashes are already right. Computing them with the repo's own functions:

computed block hashes: { _intro: "19900234", "Qwen-Image-Layered workflow": "ba937275",
                         "Model links": "98d12555", "FP8 version": "6bffdd17",
                         "Workflow settings": "b0f81aa2" }
computed aggregate:    bf2f2000
recorded in zh/ja/ko:  bf2f2000, same five block hashes

Running it for real produces this and nothing else, in all three languages:

@@ -13,6 +13,7 @@ translationBlockHashes:
 ---

+
 import UpdateReminder from '/snippets/ja/tutorials/update-reminder.mdx'

Run it three times, get three blank lines. It is unbounded.

Repo-wide on clean main:

$ bun .github/scripts/i18n/sync-hash-i18n.ts --dry-run
Done: 71 updated, 13 already in sync, 0 missing target(s)

Cause

syncOneFile only reports "unchanged" when both the hash and the full re-serialized text match:

const existingHash = getExistingHash(targetContent);
if (existingHash === expectedFileHash && output === targetContent) {
  return { status: "unchanged", warnings: [] };
}

output comes from syncChunkedHashes, which rebuilds the file as:

const { frontmatter, body } = parseFrontmatterAndBody(targetContent);
const bodyText = body.endsWith("\n") ? body : `${body}\n`;
const raw = `${frontmatter}\n${bodyText}`;

parseFrontmatterAndBody matches /^(---\n[\s\S]*?\n---)\n?([\s\S]*)$/ and returns frontmatter with a trailing \n already appended, then consumes one more \n into the separator. raw then adds its own \n back on top of a body that still begins with the remaining newlines. Round-tripping a file therefore gains one \n every time, output !== targetContent forever, and the unchanged branch is unreachable for chunked files.

Because the blank line is inside the body, it does not change any hash, which is why the recorded hashes stay correct while the tool keeps insisting they are not.

Impact

  • --dry-run is not usable as a drift signal. "71 files need a hash sync" currently means "71 chunked files exist", not "71 files drifted". A genuine drift is indistinguishable from the noise.
  • Every pnpm translate:sync-hash run dirties 71 files with a whitespace-only diff.
  • It is already in the history: 6a4aa99 ("Sync translation hash metadata for ja/zh/ko docs.", Sync ja/zh/ko translation hash metadata #1228) added exactly this blank line to the qwen page along with the intended frontmatter change.

Suggested fix

Make the round-trip idempotent, in syncChunkedHashes:

const raw = `${frontmatter}${bodyText}`;

and have parseFrontmatterAndBody own the single separating newline, or normalise the leading blank run of body before reassembly. Then the output === targetContent guard starts working and --dry-run becomes meaningful again. Worth a test that asserts sync(sync(x)) === sync(x).

.github/scripts/i18n/sync-hash-i18n.ts and .github/scripts/i18n/chunked-translate.ts.

Note on a claim this disproves

It looked at first as though the zh/ja/ko mirrors were drifting because they carry an English-derived fingerprint. They are not. translationSourceHash is documented as "SHA-256 of its English source", so carrying the English hash is correct by design, and for this page the recorded value is exactly the value the current English source produces. The drift is in the tool's own reporting, not in the data.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions