Skip to content

ci(i18n): let a protected term be satisfied by a declared equivalent form - #1966

Open
bloxster wants to merge 2 commits into
ZecHub:mainfrom
bloxster:i18n/plural-satisfied-by-singular
Open

ci(i18n): let a protected term be satisfied by a declared equivalent form#1966
bloxster wants to merge 2 commits into
ZecHub:mainfrom
bloxster:i18n/plural-satisfied-by-singular

Conversation

@bloxster

Copy link
Copy Markdown
Contributor

Why

Some protected terms are the same word in two shapes — a singular and its plural. Whether an English loanword takes an -s is a decision each language makes for itself, not one English makes for it. Italian writes "gli Unified Address", because Italian does not pluralize borrowed nouns; Japanese, Korean and Chinese do not inflect plurals at all.

Demanding one exact shape therefore forces English grammar into 18 languages that do not share it, to assert a term the other shape already carries.

This is not hypothetical. ZK-SNARKs is protected and its singular was not, so on all four English pages using it, ja/ko/zh/it carry the English plural verbatim. They had no choice — the gate required it.

What this does

A translation may now satisfy a protected term with any form declared equivalent to it, via a new optional key in translation/protected-terms.json:

"equivalentForms": [
  ["ZK-SNARK", "ZK-SNARKs"]
]

A group means "these are one term", so membership is symmetric — any member asserts it. The source side still tests the exact term, because the English page really does use that form; only the translation side accepts an equivalent.

Declared, never derived

An earlier draft inferred the pairing by stripping a trailing s/es. Two reviewers independently rejected that, and they were right:

  • it makes Argos interchangeable with Argo, and zec.rocks with zec.rock — different things that merely look related;
  • it breaks on acronyms (CSSCS);
  • it silently misses irregular plurals such as Technologies / Technology;
  • and it got its own motivating case wrong, deriving Unified Addresse from Unified Addresses, so the relaxation would never have fired for the term that prompted it.

A maintainer now states which forms are one word; the checker never guesses.

Every malformed declaration is fatal: a member absent from preserveVerbatim, a term in two groups, a duplicate within a group, a group of fewer than two, a non-array, a blank or whitespace-padded entry. A group that was quietly ignored would weaken this gate in the way least likely to be noticed — and a blank entry is the one shape that could do real damage, since a term of " " matches nearly every page.

It also closes a false negative

satisfiedIn is applied to classify()'s baseline test, and that tightens the gate rather than merely preserving it. If a base translation carries only the singular and a change strips it, the previous exact-match baseline made violatedAtBase true — bucketing the result pre-existing and exiting 0, silently passing a change that removed the last protected form. It is now correctly introduced, exit 1.

Base content is judged by the base declaration, read from git at the merge base, not by this change's config. Otherwise a PR that removed an equivalence group could never fail the gate: the base would look as though it had already been violating. A base config that cannot be read or parsed falls back to the head declaration rather than failing a PR over history it did not write.

Verification

check result
unit tests 51 pass / 0 fail (38 existing + 13 new)
whole-tree violations 72 before and after
scoped gate on this branch exit 0
remove the group and strip the last form exit 1, reported as introduced
malformed declaration fatal, exit 1, names the offending entry

The logic lives in translation/lib/term-forms.mjs — pure, no filesystem, process or clock — which puts its tests where the existing hash-lib-tests job already globs (translation/lib/*.test.mjs), so no workflow change is needed. They cover the declared pair in both directions, the -es case the old derivation got wrong, look-alikes that must not pair, and each validation failure.

termRegExp now memoizes; it was recompiling per (page, locale, term).

Note on scope

This changes nothing on the current corpus — 72 violations before and after — because the harm already landed: the locales were forced to carry the plural, so no violations remain to clear. The value is preventive, and it is why the companion PR adds ZK-SNARK rather than removing ZK-SNARKs.

Merge order

Stacked on #1965, which adds the ZK-SNARK singular that the declared group refers to. Merge that first; the diff here collapses to the checker change once it lands.

Known limitation

A PR that edits equivalentForms while also changing translations is judged partly by its own declaration, so removing a group cannot itself fail the gate in every case. Reviewers should treat a change to equivalentForms as needing a whole-tree audit (node scripts/check-protected-terms.mjs with no --base) rather than trusting the scoped result alone.

Three gaps found while auditing this list against the categorized list in
the frontend repo.

1. Key and address compounds are only half protected. `Unified Address` and
   `Viewing Key` are enforced, the other expanded forms are not, so a
   translation can localize them and nothing complains:

     Full Viewing Key       5 English pages, 0 translations missing it
     Incoming Viewing Key   2 English pages, 0 translations missing it

2. The check is case-sensitive, so a term protected in one casing is
   unprotected in the other. The list carries `Free2Z`, `Zcash.Me`,
   `MetaMask Snap` and `Zingo`, while the variants that actually appear in
   the wiki went unenforced:

     Zcash.me    1 English page,  0 translations missing it
     Zingo!     20 English pages, 0 translations missing it
     ZGo        27 English pages, 0 translations missing it
     Zgo         9 English pages, 0 translations missing it

   Same trap as `Zodl` vs `ZODL`, which is still open.

3. `ZK-SNARKs` is enforced but its singular is not:

     ZK-SNARK    1 English page,  0 translations missing it

All seven are free: every existing translation already keeps them verbatim,
so this locks in current behaviour and prevents a future regression.
Verified — the whole-tree audit reports 72 violations both before and after,
and none names a newly added term. (Those 72 are pre-existing drift from
ZecHub#1948 and ZecHub#1957, cleared by the pending backfill/re-sync PR.)

WHAT IS NOT HERE, AND WHY IT SHOULD NOT BE ADDED LATER EITHER

An earlier draft of this change listed `Unified Addresses` as merely
expensive — 94 violations, 113 after the pending re-sync. Measuring what
those violations actually are changed the conclusion: protecting an English
PLURAL is wrong in principle, not just costly.

Of the 113, 70% already carry the protected singular `Unified Address`.
They did not lose the concept; they declined to pluralize an English
loanword. English "for inspecting Unified Addresses" becomes Italian "per
ispezionare gli Unified Address", which is correct Italian — Italian does
not add -s to English loanwords. Japanese writes `Unified Addressを調べる`
because Japanese does not inflect plurals at all, and 23 of the 113 sit in
ja/ko/zh, none of which do.

Enforcing the plural would push a grammatical error into Italian, French,
Spanish and others, and a meaningless string into three more, to satisfy a
check the singular already satisfies conceptually.

The principle: protect singulars, not plurals. Languages differ in whether
and how they pluralize borrowed nouns, so a protected plural imposes English
grammar on all 18 locales.

This is not hypothetical. `ZK-SNARKs` is enforced today, and on all four
English pages that use it, ja/ko/zh/it carry the English plural verbatim —
they had no choice. A follow-up will teach the checker that a plural is
satisfied by its protected singular and retire the plural entries; this
change adds the singular so that follow-up has something to fall back to.

Also still held back, each priced against both main and the pending re-sync
tree, because adding them creates violations no queued sync repairs (the
terms list is not part of staleness detection, so the debt is invisible to
the dashboard and unreachable by the sync agent): TEX Address 39,
Dev Fund 25/29, Diversified Address 14, Spending Key 7, Metamask Snap 2,
zk-SNARK 2, Free2z 1.

Skipped entirely: `Unified Viewing Key` and `Outgoing Viewing Key` are in
the categorized list but appear on no English page.
…form

Some protected terms are the same word in two shapes — a singular and its
plural. Whether an English loanword takes an -s is a decision each language
makes for itself, not one English makes for it: Italian writes "gli Unified
Address" because Italian does not pluralize borrowed nouns, and Japanese,
Korean and Chinese do not inflect plurals at all. Demanding one exact shape
forces English grammar into 18 languages that do not share it, to assert a
term the other shape already carries.

Not hypothetical: `ZK-SNARKs` is protected and its singular was not, so on
all four English pages using it, ja/ko/zh/it carry the English plural
verbatim. They had no choice — the gate required it.

A translation may now satisfy a term with any form DECLARED equivalent to it,
via a new optional `equivalentForms` key:

  "equivalentForms": [
    ["ZK-SNARK", "ZK-SNARKs"]
  ]

The pairing is declared, never inferred from spelling. An earlier draft
derived it by stripping a trailing "s"/"es", which two reviewers independently
rejected: that makes `Argos` interchangeable with `Argo` and `zec.rocks` with
`zec.rock` — different things that merely look related — breaks on acronyms
(`CSS` -> `CS`), and silently misses irregular plurals like `Technologies` /
`Technology`. It also got its own motivating case wrong, deriving "Unified
Addresse" from "Unified Addresses". A maintainer states which forms are one
word; the checker never guesses.

Membership is symmetric because a group asserts "these are one term", so any
member satisfies it. That is now a declared property rather than an accident
of the derivation.

The source side still tests the exact term — the English page really does use
that form. Only the translation side accepts an equivalent.

Every malformed declaration is fatal: a member absent from preserveVerbatim,
a term in two groups, a duplicate, a group of one, a non-array. A group that
was quietly ignored would weaken this gate in the way least likely to be
noticed.

`satisfiedIn` is applied to classify()'s baseline test as well, and that is
load-bearing in the opposite direction from the obvious one: it TIGHTENS the
gate. If a base translation carries only the singular and a change strips it,
the previous exact-match baseline made violatedAtBase true, bucketing the
result "pre-existing" and exiting 0 — silently passing a change that removed
the last protected form. It is now correctly "introduced", exit 1.

Logic lives in translation/lib/term-forms.mjs (pure, no fs/process/clock) so
the existing hash-lib-tests job picks up its 11 unit tests with no workflow
change: the declared pair in both directions, the -es case the derivation got
wrong, look-alikes that must NOT pair, and each validation failure.

termRegExp now memoizes; it was recompiling per (page, locale, term).

Note this changes NOTHING on the current corpus — 72 violations before and
after — because the harm already landed: the locales were forced to carry the
plural, so no violations remain to clear. The value is preventive, and it is
why the companion terms PR adds `ZK-SNARK` rather than removing `ZK-SNARKs`.
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.

1 participant