Skip to content

i18n: surface uncurated English pages instead of burying them - #2002

Merged
zksquirrel merged 3 commits into
ZecHub:mainfrom
bloxster:i18n/surface-uncurated
Aug 26, 2026
Merged

i18n: surface uncurated English pages instead of burying them#2002
zksquirrel merged 3 commits into
ZecHub:mainfrom
bloxster:i18n/surface-uncurated

Conversation

@bloxster

@bloxster bloxster commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Six English pages sat untranslated in all 18 locales for up to three weeks and nothing reported it. The uncurated list was already on the dashboard, but at the bottom of a 130-line issue and missing from the summary table, so it was never seen.

uncurated is now a column in that table, with a line explaining that those pages are counted in none of the others — a page that was never curated can't be stale or missing. The all-fresh banner no longer reads "nothing to sync" when uncurated pages exist.

Also adds an advisory comment on PRs that introduce English pages missing from the curated set, so the backlog is visible while someone is still looking at the PR. It never fails a check and the comment says so. There's deliberately no rule requiring new pages to be curated: that's a lasting editorial commitment to maintain the page in 18 languages, and a blocking gate would make "add the line" the cheapest route to green CI.

It uses pull_request_target because fork PRs otherwise can't comment. The job is one github-script step with no checkout and no run: step, and the PR is read only as API data.

Currently blocked by the stale translation/menu-titles/en.json on main — #1998 is the fix.

🤖 Generated with Claude Code

rt and others added 2 commits August 25, 2026 16:54
Six English pages sat untranslated in all 18 locales for up to three weeks and
nothing said so. Two of them were noticed by a human browsing the live site.

The detector was not silent — that was my first diagnosis and it was wrong. It
already rendered an "Uncurated source pages" section. The problem is where: line
122 of a 133-line dashboard issue, below a Details list that runs one row per
stale pair (126 of them that day), and absent from the summary table at the top.
Technically visible, reliably unseen. The console line had the same shape:
`Uncurated source pages: 6 — e.g. <three of them>`, which reads as a footnote
rather than a work item.

Worse, when the corpus is otherwise clean the dashboard said:

    ✅ All 3654 curated (page × locale) pairs are fresh. Nothing to sync.

True and misleading at once. A page that was never curated cannot be stale or
missing, so it is counted in none of the columns — the all-clear is measured over
exactly the set that excludes the problem.

## Changes

* `uncurated` is now a column in the summary table, with a callout under it
  explaining that those pages are counted in none of the other columns and why.
* the all-fresh banner no longer implies there is no work when uncurated pages
  exist; it states both facts.
* the section itself now says what the state costs (untranslated in every
  locale, English served) and how to change it (add to curated-pages.txt → shows
  up as `missing` × 18 → next sync fills it).
* the console summary prints the FULL list, not `e.g.` plus three.

## New: advisory PR comment (.github/workflows/uncurated-page-advisory.yml)

Comments on a PR that adds English pages missing from the curated set, so the
debt is visible while someone is still looking at the PR. It is explicitly
ADVISORY: it never fails a check, and the comment opens by telling the
contributor nothing is wrong with their PR.

There is deliberately no rule that every new page must be curated. Curation is a
lasting editorial commitment — the wiki then maintains that page in 18 languages
— and it is not a drive-by contributor's call to make. A blocking gate would also
be worse than useless: the fix that makes CI green is to add the line, which
creates 18 items of real work with nobody having decided the page deserves them.
That is precisely how a backlog grows silently. `curated ⊆ site` is the
invariant; `site ⊆ curated` is not, by design.

Security: it uses pull_request_target, because most PRs here come from forks and
a plain `pull_request` token cannot comment on those. That is only safe if PR
code never runs, so the job has ONE step, no checkout of anything, and no `run:`
step at all. The PR is read purely as data through the API — the file list, and
the text of curated-pages.txt at the head sha. The header comment says not to add
a PR checkout; please keep it that way.

It reads curated-pages.txt at the HEAD sha, not base, so a PR that adds a page
AND curates it in the same change stays silent. It upserts a single comment, so
repeated pushes edit rather than pile up, and it withdraws its own warning if the
pages get curated later in the PR's life.

Verified against the six currently-open PRs: five stay silent (they only edit
existing pages) and exactly one fires — ZecHub#1930, which adds
Zcash_Tech/Project_Tachyon.md. That is a real 7th uncurated page about to land
with nothing today that would report it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEgDv6GG6Eybs1gu2mhcgY
Three reviewers (Fable, gpt-5.5, Zabaniya) all cleared the part I most wanted
challenged: the `pull_request_target` surface is safe as written — one
github-script step, no checkout, no `run:`, and the only `${{ }}` interpolation
anywhere is `pull_request.number` (an integer). PR content is read purely as API
data. Two of them verified independently that the getContent fallback cannot
post a wrong comment, that the >1MB `content`-omission case is ~150x away for a
7KB file and degrades safely regardless, and that the concurrency group cannot
produce duplicate or misdirected comments.

Four real defects, each fixed and re-verified by execution:

1. Renames were invisible. The filter only accepted `status === 'added'`, but a
   PR that MOVES drafts/X.md to site/X.md creates exactly the same translation
   debt and GitHub reports it as `renamed`. Now accepts added/renamed/copied.
   Confirmed against real PR data that this adds no noise: the same 5 of 6 open
   PRs stay silent, the same one fires.

2. The comment lookup matched on the marker alone, so anyone could pre-post a
   comment containing `<!-- uncurated-page-advisory -->` and the job would try to
   edit it — failing instead of advising, or rewriting someone else's comment.
   Now requires the marker AND a github-actions bot author.

3. A stale warning could stand forever. If a later push dropped the added pages
   while still touching site/**, `if (added.length === 0) return` fired BEFORE
   the branch that rewrites the comment to "nothing outstanding". The PR then
   permanently carried an advisory listing pages it no longer adds. The early
   return is gone; the flow falls through to the withdraw branch.

4. Filenames are attacker-controlled and were interpolated straight into a
   markdown code span. A backtick in a path closes the span and lets the rest
   render as markdown — an @mention in a filename would ping people. Verified:
   "x`</code>@everyone`.md" escapes the span raw and does not after stripping.

Also noted by review and deliberately left: the advisory hardcodes a link to
issue 1889, which goes stale if that dashboard issue is ever closed and
recreated. Not worth a config surface for an advisory comment.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEgDv6GG6Eybs1gu2mhcgY
@bloxster

bloxster commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

Reviewed by Fable, gpt-5.5 and Zabaniya. Fable and Zabaniya approved, gpt-5.5 rejected on two workflow gaps; all four findings fixed in cab7933.

All three independently checked the pull_request_target surface and found no way for PR code to run: one step, no checkout, no run:, and the only ${{ }} is the PR number.

Fixed: renames were invisible (only added was matched, so moving a page into site/ created debt silently); the comment lookup matched the marker without checking authorship, so anyone could pre-post one; a later push that dropped the added pages left the old warning standing; and filenames went into a markdown code span unescaped, where a backtick breaks out.

@zksquirrel
zksquirrel merged commit 0397573 into ZecHub:main Aug 26, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants