Skip to content

fix(rest): localize the canonical PLURAL /meta spelling, not just the singular (#6349) - #7372

Merged
os-help merged 2 commits into
mainfrom
claude/issue-6349-meta-plural-i18n
Aug 10, 2026
Merged

fix(rest): localize the canonical PLURAL /meta spelling, not just the singular (#6349)#7372
os-help merged 2 commits into
mainfrom
claude/issue-6349-meta-plural-i18n

Conversation

@os-help

@os-help os-help commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Fixes #6349

translateMetaItem / translateMetaItems decide "does this type translate" by asking
isTranslatableMetaType, which reads TRANSLATABLE_METADATA_TYPES — a set derived
(#3786) from METADATA_DOCUMENT_TRANSLATORS' keys, and those keys are singular-only:
view / action / object / app / dashboard / page. That matches
translateMetadataDocument's documented parameter, "Canonical metadata type string".

The /meta read handlers were handing those helpers the raw :type path segment.
Prime Directive #3 makes PLURAL the canonical REST spelling (/api/v1/meta/apps), so a
caller following the documentation missed the set, the predicate answered false, and
the entire localization was skipped. Same route, same document, same Accept-Language,
two different answers.

This is #3984's family — a per-type judgement that only ever saw the singular — landing
on the i18n predicate instead of on an authorization gate, so the cost is an English
fallback, not a leak.

Premise re-verified on origin/main, and my own census

The issue's line numbers had drifted (this file took several merges); I relocated every
passthrough by content. The card said "three handlers"; the issue listed four call sites.
Measured on my merge base: five passthrough sites across three handlers.

# handler route sites
1 list GET /meta/:type 1
2 single item GET /meta/:type/:name 3
3 compound name GET /meta/:type/:section/:name 1

The single-item handler's three are the cached-normal exit, the non-cached exit, and
the cached branch's ADR-0106 D6 "visibility undetermined" exit — which has its own
res.json and its own re-wrapped envelope. That fifth site is the one the issue did not
have; it arrived after the card was filed. All five reach translateMetaItem
(translateMetaEnvelope delegates to it).

The PM's dispatch note corrected its own claim comment, and the correction is right:
TRANSLATABLE_METADATA_TYPES is not defined in rest-server.ts. It is derived in
packages/spec/src/system/i18n-resolver.ts; rest-server.ts imports and consumes it via
a local memoised isTranslatableMetaType. packages/spec is untouched here — option 3
was ruled out of this card.

The fix — option 2, and the existing normaliser reused

Normalized inside the two helpers rather than at the five call sites, per the issue's
preference and the ruling. The reason is #6241's, written into the code: a normalization
the callers own is one a later caller forgets — eight days after #3984 a new branch in
this same file was still comparing the raw param. The helper owns the question "does this
type translate", so it owns the spelling that question is asked in, and a sixth call site
inherits the fix for free.

No new pluralisation table. The repo already has the normaliser and I reused it:
RestServer.metaTypeSingular (rest-server.ts), a one-liner over
PLURAL_TO_SINGULAR from @objectstack/spec/shared, introduced by the #3984 family fix
and already used by this file's per-type gates and by the compound handler's
compoundType. A second table would have been exactly the drift pair this family keeps
deleting.

Both helpers now also pass the normalized type on to translateMetadataDocument,
which is what its "canonical" contract asks for.

metaTypeSingular leaves an unmapped type untouched, so the translatable set is
unchanged
— only which spellings reach it.

Tests — 10 cases, packages/rest/src/meta-plural-i18n.test.ts

Every case asserts the pair: the plural body is translated, AND it toEquals the
singular body. Asserting only "the plural is translated" would stay green if a later
change translated it differently from the singular, which is precisely the
list-translated / detail-not inconsistency the issue asked not to trade for.

One case per handler exit, against a fixture zh-CN bundle, plus three controls:

  • a type with no translator and no plural mapping (widget / widgets) is
    untranslated in both spellings — and its document is deliberately named home, the
    same name the bundle translates under pages, so a fix that keyed on name rather than
    type would fail right here;
  • booksbook: a plural that does normalize but whose singular is not a
    translator key must still come back untranslated. This separates "folds the spelling"
    from "translates whatever it can fold" — a fix that folded the spelling into the
    translator lookup while dropping the predicate passes every other case and fails this
    one;
  • a request expressing no locale preference is untouched in both spellings, pinning that
    normalizing the type did not move the early extractLocale exit.

One correction I had to make to my own doubles rather than to the code: the real producer
folds the plural to the singular canonical key in the envelope it answers with (#4432), so
a getMetaItem double echoing the raw spelling back failed three toEquals on the
double's own artefact. The double now canonicalizes, as the producer does.

Reverse verification — direction predicted first

Prediction: removing the fix turns red exactly the six plural-spelling cases (§1
plural, §2's four exits, §3), and leaves green §1's singular case plus the three §4
controls — because the controls pin behaviour the fix does not change. So 6 failed |
4 passed
.

Fix removed with git checkout origin/main -- packages/rest/src/rest-server.ts (never
git stash — shared stack), restored with git apply of the saved patch.

Measured: 6 failed | 4 passed, exactly the predicted set, every red being raw English
where the singular spelling got the translation:

x translates under the canonical PLURAL spelling, identically
    expected { name: 'home', label: 'Home', ... } to match object { label: '首页' }
x cached branch: the plural spelling is translated, identically to the singular
x non-cached branch: the plural spelling is translated, identically to the singular
x cached branch, ADR-0106 D6 `undetermined` visibility exit
    expected { name: 'customer', ... } to match object { label: '客户' }
x `app` - which structurally bypasses the cache - under `apps`
    expected { name: 'crm', label: 'CRM', ... } to match object { label: '客户管理' }
x compound name: translates the plural spelling, identically

All 10 green again after restore.

Verification

Merged origin/main before the first edit and again before push; #7324 (#6877,
query-multiplicity, 24 gate sites in this same file) and #7293 (#7035) are both
ancestors of this branch, and the merge was clean.

pnpm --filter '@objectstack/rest^...' build            # build closure first
pnpm --filter @objectstack/rest test                   # 79 files, 1271 tests, ALL PASS
npx tsc --noEmit -p packages/rest/tsconfig.json        # clean (exit 0)
node scripts/check-nul-bytes.mjs                       # OK, 6742 files
node scripts/check-error-code-casing.mjs               # OK, 3399 files
node scripts/check-route-envelope.mjs                  # OK, 16 domains
pnpm check:type-check-debt                             # OK, none above its recorded number

TEST_DEBT['@objectstack/rest'] was the live hazard — 155 with zero margin. My first
draft of the test file spent 4 of a margin that does not exist: four TS2550 from
.at(-1), this package's lib target predating ES2022. Replaced with an indexed
lastBody helper and re-measured the way the gate does it (a sibling extends config
with the test globs dropped from exclude, after the full built closure): 155 errors —
exactly the ceiling, and zero of them in the new file.
.js import extension on the new
test's import (the TS2835 trap).

Wire-visible

Plural /meta spellings now return localized label / description / navigation where
they previously returned raw English. No singular-spelling response changes, and no type
becomes translatable that was not already. Changeset added
(.changeset/meta-plural-spelling-i18n.md, patch).

Scope

packages/rest only, as ruled. packages/spec untouched — option 3 (teaching
TRANSLATABLE_METADATA_TYPES / isTranslatableMetaType to accept plurals) is an
acceptance-surface change and belongs to domain:spec with its own card. No out-of-scope
findings to file.


Generated by Claude Code

claude added 2 commits August 10, 2026 08:02
… singular (#6349)

`translateMetaItem` / `translateMetaItems` decide whether a metadata type is
localizable by asking `isTranslatableMetaType`, which reads
`TRANSLATABLE_METADATA_TYPES` — a set DERIVED (#3786) from
`METADATA_DOCUMENT_TRANSLATORS`' keys, and those keys are singular-only
(view / action / object / app / dashboard / page), matching
`translateMetadataDocument`'s documented "Canonical metadata type string".

The three `/meta` read handlers handed those helpers the RAW `:type` path
segment. Prime Directive #3 makes PLURAL the canonical REST spelling, so a
caller following the documentation missed the set, the predicate answered
false, and the whole localization was skipped — same route, same document,
same Accept-Language, two different answers.

Normalized in the two helpers (the issue's option 2) rather than at the five
call sites, for the reason #6241 proved: a normalization the callers own is
one a later caller forgets. `metaTypeSingular` leaves an unmapped type
untouched, so the translatable SET is unchanged — only the spellings that
reach it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0158ZQo7LiHSxGWpYKuPq1wu
@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 10, 2026 8:07am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/rest.

9 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/connect-mcp.mdx (via @objectstack/rest)
  • content/docs/api/error-handling-server.mdx (via @objectstack/rest)
  • content/docs/api/index.mdx (via @objectstack/rest)
  • content/docs/permissions/authentication.mdx (via @objectstack/rest)
  • content/docs/permissions/system-context.mdx (via packages/rest)
  • content/docs/plugins/index.mdx (via @objectstack/rest)
  • content/docs/plugins/packages.mdx (via @objectstack/rest)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/rest)
  • content/docs/protocol/kernel/i18n-standard.mdx (via packages/rest)

3 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx (via @objectstack/rest)
  • content/docs/releases/v12.mdx (via @objectstack/rest)
  • content/docs/releases/v17.mdx (via @objectstack/rest)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants