Skip to content

docs: a main landmark, a switcher named by its text, code blocks off the landmark list, buttons that keep their own language, a localized Open-in prompt, and a CI rule for the locale chrome - #317

Merged
hotlong merged 18 commits into
mainfrom
claude/pm-dispatch-objectos-ju9td1
Oct 6, 2026
Merged

hotlong merged 18 commits into
mainfrom
claude/pm-dispatch-objectos-ju9td1

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #308

The docs accessibility baseline. axe 4.14.0 reported four failures on every docs page in every locale, English included. This PR fixes all four, localizes the "Open in ChatGPT / Claude" prompt, and adds a CI rule for the #298 and #305 results. It also fixes the inverse language-of-parts defect that #305 introduced. It builds on #305's fumadocs-ui patch and ui-text keys, which are on main since #314.

Branch and rebase

What changed

Finding Fix Where
landmark-one-main: the docs layout had no main landmark DocsPage passes its other props to the article element it renders, so the docs page sets role="main" there. This is a fumadocs option; no patch hunk. The landmark covers the breadcrumb, title, body and footer; the sidebar and the table of contents stay outside it. app/[lang]/docs/[[...slug]]/page.tsx
label-content-name-mismatch: the language switcher shows "English" but was named "Choose a language" Named "English — Choose a language", "Deutsch — Sprache wählen", "简体中文 — 选择语言". The visible text comes first, and both halves come from the i18n context, so no new key is needed. patch hunk, layouts/shared/slots/language-select.js
svg-img-alt: the GitHub icon was an unnamed svg role="img" Every link that renders it is already named "GitHub", so the icon is decorative and gets aria-hidden. patch hunk, layouts/shared/index.js
landmark-unique: every code block's scroll area was an unnamed role="region" The role is removed. tabIndex: 0 stays, so keyboard scrolling still works. patch hunk, components/codeblock.js
landmark-unique at phone width: the table-of-contents bar was a second banner header The bar is now a div element. patch hunk, layouts/docs/page/slots/toc.js
The Open-in prompt sentence was English in every locale It is now the ui-text key pageActionsOpenInLLMPrompt, with upstream 16.9.0's key name, {url} placeholder and English text. It is translated in six locales and generated for zh-Hant. table() holds every locale's placeholders to English. lib/ui-text.ts, lib/ui-text/*.json, components/ai/page-actions.tsx, page.tsx
No CI gate for #298/#305's locale-chrome results Four rules over the prerendered HTML: english-chrome-name, untranslated-entry-unmarked, translated-entry-marked-english and localized-name-marked-english. Guards and a live control keep them from passing when they measured nothing. 12 new self-test cases. .github/scripts/check-locale-surface.mjs
Localized heading-anchor and code-copy names were read with English rules inside a fallback page's body See "The inverse language-of-parts fix" below. patch hunks components/heading.js, components/codeblock.js

No new ci.yml step is needed. The existing "Locale surface" step already runs this gate after the build, and pnpm turbo run test runs its self-test.

Decisions

  • Main landmark through a DocsPage prop, not the patch. Upstream 16.16.2 wraps the article in a main element of its own. On that upgrade, delete role="main", or the page will have two main landmarks. The page comment and the patch header both say this.
  • Language switcher in the patch, not slots.languageSelect.root. A slot would have to be passed to every layout instance; the patch covers every trigger.
  • Code blocks lose their landmark role instead of getting names. Untitled blocks have nothing to be named by, titles repeat within a page, and ten code landmarks per page would bury the real ones.
  • Prompt through ViewOptions' labels prop, like its neighbours, not through the fumadocs i18n context. No fumadocs component reads it.
  • How the CI rule finds what it compares. The English chrome set and each locale's own set are read from the built pages, so a name fumadocs adds later is covered with no edit. Page-tree entries are links outside the page body whose text contains the target page's title. The oracle for "untranslated" is the content tree.

The inverse language-of-parts fix

#305 localized the names of the heading-anchor and code-copy buttons. On a fallback page both buttons sit in the page body, which #298 marks lang="en", so "Ankerlink kopieren" was read with English rules. That is WCAG 3.1.2, the inverse of what #305 fixed.

Both buttons now take locale from the useI18n() they already read and render it as their own lang. This is in the two #305 hunks of patches/fumadocs-ui@16.8.12.patch. Inside an English body, the button's own lang keeps its name in the locale's language.

The new rule, localized-name-marked-english, fails when one of a locale's own names sits inside a lang="en" region with no lang of its own. "Own names" means the names that resolve to the locale on its built pages, minus the English set.

Built tree localized-name-marked-english Gate exit
main at cf449fa 3963: per Latin-script locale 520 anchor + 115 copy names over 55 fallback pages; zh-Hans and zh-Hant 336 + 58 over 31. Those two names only. 1
This branch at 33d9a32 / 42da2b1 0 (10 own names per locale compared) 0

These counts are higher than the 453 + 101 and 276 + 44 measured before the rebase. #301's content pass on main left more fallback pages (55 and 31, up from 53 and 29) and more headings.

The hydrated DOM confirms it: on /de/docs/operate/backup, 7 anchor buttons and 1 copy button resolve to de inside the en body. English pages are unchanged in what they announce.

Self-test cases:

  • red: the ja fallback anchor button with its own lang removed;
  • green: the same name reaching the body through a wrapper's lang.

The live control now feeds this shape too. With a reader blind to lang on names, the self-test shows it red.

Evidence

The before state is main at cf449fa, built CI-shaped. The after state is this branch at 33d9a32. git diff 33d9a32 42da2b1 touches only the gate script.

axe 4.14.0. One page per section (quickstart, use/records, build/data, configure/authentication, deploy/docker, operate/backup, resources/faq, reference/cli), in en, zh-Hans and de, light and dark. That is 48 runs per width.

Rule 1440 main 1440 after 390 main 390 after
landmark-one-main 48 runs / 48 nodes 0 48 / 48 0
label-content-name-mismatch 48 / 48 0 0 * 0 *
svg-img-alt 48 / 48 0 0 * 0 *
landmark-unique 24 / 24 0 48 / 72 0

* At 390px the switcher and the GitHub icon sit in the closed drawer. With the drawer open (en, zh-Hans, de quickstart):

  • on main: label-content-name-mismatch, svg-img-alt and landmark-unique fire;
  • on this branch: none of the four.

No new violations. Compared run by run, no rule has more nodes on this branch than on main, at either width. Removed:

  • at 1440px: region 2416 nodes, plus the four rules above;
  • at 390px: landmark-no-duplicate-banner 48, landmark-unique 72, landmark-one-main 48, region 2416.

color-contrast (9 runs, 48 nodes) and scrollable-region-focusable at 390px (16 runs, 18 nodes) are unchanged.

CI rule, red and green.

Built tree english-chrome-name untranslated-entry-unmarked localized-name-marked-english Exit
main built at cec227a (before #305) 6239 5974 n/a (rule added later) 1
#305's tree with searchOpen mapped to "Open Search" and the page-tree transform bypassed (ablation, restored) 1120 5974 n/a 1
main at cf449fa 0 0 3963 1
this branch, 42da2b1 0 0 (717 marked in zh-Hans and zh-Hant, 950 in each other locale) 0 0

Screenshots. en and zh-Hans at 1440 and 390, main versus this branch:

  • the switcher;
  • the open drawer;
  • a code block;
  • the 390px table-of-contents bar.

All 12 before/after pairs are byte-identical PNGs. Nothing visible changed.

Browser. next start, JS on, 0 console or hydration errors.

  • The Open-in prompt was read from the live popover in all eight locales, on a fallback page and a translated page each. The English prompt is unchanged.
  • gen-zh-hant --check goes red on a one-character drift in the new zh-Hant string.
  • The {url} guard throws on a locale string that drops the placeholder.

Gates (HEAD 42da2b1)

  • pnpm install --frozen-lockfile: exit 0
  • pnpm turbo run type-check --continue --force: Tasks 1 successful; VERDICT command-exit 0
  • NEXT_PRIVATE_STANDALONE=true pnpm turbo run build --force: 1038/1038 static pages; Tasks 1 successful; VERDICT command-exit 0 (332 OG font-fetch proxy warnings, the same count as main's build)
  • pnpm turbo run test --force: ✓ 10 self-test(s) passed; VERDICT command-exit 0
  • check-locale-surface.mjs: exit 0. --self-test: ✓ 43 case(s) over 22 rule(s)
  • check-positioning.mjs: ✓ 4 copies equal their constants; the brand is right in 659 pages and 2 llms bodies
  • check-search-locales.mjs: ✓ all 8 locales answer 200, find "permissions", find every own page by its title within the first 3 pages, and find nothing for a nonce
  • gen-zh-hant.mjs --check: ✓ zh-Hant: 60 generated file(s) match the zh-Hans sources byte for byte
  • Translations:
    • Ownership (workflow argv git diff --name-status --no-renames origin/main...HEAD, --actor hotlong): exit 0 (inert with no bot login); armed: ✓ 13 file(s) changed, no translation artifacts touched
    • Bot-actor control (armed, --actor = the bot login): exit 1, "English sources and site code are authored by humans. Split them out."
    • Freshness: ✓
    • Output --files: ✓ (256 pre-existing findings; the same 256 on main with an empty file list)
    • Output --self-test: ✓
  • check-node-floor.mjs and --self-test: ✅; check-half-states.mjs --self-test: ✓ 1551 cases pass

Not run here: the Worker packaging and size step, and smoke-docs.mjs. They are not in this card's gate list; CI runs both.

Acceptance notes

Pushed head

fa2390f on claude/pm-dispatch-objectos-ju9td1, a fast-forward with no force. It starts at the remote branch head 69f7f1e, merges origin/main @ 553ef7b (#315, which is #307; after the merge the diff against main is empty), and cherry-picks the five commits fa284b5..42da2b1, which were rebased onto cf449fa. git diff origin/main fa2390f is byte-identical to git diff cf449fa 42da2b1; no path is shared with #315.


Generated by Claude Code

objectstack-fleet Bot and others added 18 commits October 6, 2026 10:36
…ds or fewer

Adds a `seoTitle:` frontmatter line to every English page whose built
`<title>` read as one or two words plus the ` | ObjectOS` suffix, so the
tab/result title carries the terms a reader searches for while the H1,
sidebar and breadcrumb keep the short noun (#166's mechanism).

- 67 pages: each gains exactly one line; no `title`, `description` or
  body line changes.
- Every title leads with the page's own specific term, lifted from its
  description and headings, and renders at 52–60 characters including the
  suffix (measured on the built HTML).
- The three duplicated titles (Approvals, Dashboards, Notifications —
  each used by a build/configure page and a use page) are now distinct.
- English only. Locale siblings are translation artifacts that a
  non-translator commit may not modify (AGENTS.md, Translation workflow;
  check-translation-ownership.mjs); the translation pass carries the new
  key over, and the output report lists the 163 siblings now missing it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
…AQ headings, title-weighted per-locale search, consistency pass

- Light-mode muted foreground to hsl(0 0% 40%); code comments recoloured in
  both shiki themes.
- Tables get an always-drawn scrollbar and a scroll-driven trailing fade.
- FAQ and License FAQ questions become headings.
- /api/search builds one locale's index on that locale's first search and
  weights title > heading > text; check-search-locales gains own-title-buried;
  smoke-docs asks /api/search in every locale with a nonce control.
- Consistency: one data-residency table, one license-validation sentence,
  ObjectSchema.create, "license" spelling, Configure title, glossary order plus
  AI seat and Position, logo to the docs home, a translated llms.txt example,
  three unsourced configure/ai claims removed (with their locale siblings),
  release-following lines pointed at a populated feed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
…comments

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
…ix apiMethods primitives

The Views page declared views inside a defineObject call under an
object-level `view` key. ObjectStack provides no defineObject, and
ObjectSchema.create rejects `view` as an unknown key. Its views are a
defineView container ({ list, listViews, form, formViews }, each view
bound through `data`) registered on the stack with `views: [...]`, which
both samples now show; both parse with @objectstack/spec 17.6.0.

The REST API page's allowed apiMethods values drop the eight retired ones
the spec strips at parse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
…ype-check

`next typegen` loads next.config.mjs, and fumadocs-mdx 15.0.7's createMDX()
starts init() without awaiting it (dist/next/index.js:14-20). init rewrites
every .source/*.ts with fs.writeFile (dist/core-DlDe_Eze.js:232-236), which
truncates first. typegen ends in process.exit(0), so it can exit inside that
window and leave .source/server.ts empty for tsc: TS2306, CI run 37478051451.

Running the fumadocs-mdx CLI after typegen makes the CLI, which awaits its
writes, the last writer before tsc. typegen does not read .source.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
…ries, zh-Hant 404, Open-in links follow the page shown

- patches/fumadocs-ui@16.8.12.patch: the nine accessible names fumadocs-ui
  16.8.12 hard-codes in English (Open Search, Toggle Theme, Open/Collapse
  Sidebar, Copy Anchor Link, Copy/Copied Text, Toggle Menu, and Radix's
  "Main") read from its i18n context with the old literal as default. Eight
  keys are a backport of upstream 16.9.0's own names; lib/ui-text supplies
  all nine through RootProvider in every locale.
- app/[lang]/docs/layout.tsx: page-tree entries (sidebar, breadcrumb,
  prev/next footer) for pages a locale has no translation of carry
  lang="en", using the docs page's own translatedLocales detection.
- app/not-found.tsx: the 404 copy moves into lib/ui-text (notFound), so the
  zh-Hant string is generated from zh-Hans by gen-zh-hant and checked by
  --check like every other Traditional string.
- Open in ChatGPT / Claude on a translated page sends the assistant to the
  translated page itself; English pages and fallbacks keep the English .mdx.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
…pec 17.7.0

views.mdx: every list-view fragment carries its required top-level
columns, fontWeight is a string, the list view types table states the
required keys the spec declares, and each fragment says what it omits.

flows.mdx: every sample is rewritten from the old trigger/steps shape to
FlowSchema's nodes and edges. Email goes through a notify node with
channels: ['email']; requires lists automation and triggers.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
…ative GitHub icon, code blocks off the landmark list, a localized Open-in prompt

axe 4.14.0 reported four failures on every docs page in every locale,
English included (#308):

- landmark-one-main: the docs layout has no main landmark. DocsPage passes
  its other props to the article it renders, so the docs page sets
  role="main" there; no patch needed.
- label-content-name-mismatch: the language switcher shows the current
  language but was named "Choose a language" only. It is now named
  "English — Choose a language", "Deutsch — Sprache wählen", visible text
  first, both halves from the i18n context.
- svg-img-alt: the GitHub icon was an svg role="img" with no name inside a
  link already named "GitHub"; it is aria-hidden now.
- landmark-unique: every code block's scroll viewport was an unnamed
  role="region". The role goes; tabIndex 0 stays for keyboard scrolling.

The last three are hunks added to patches/fumadocs-ui@16.8.12.patch; its
header says why each is a hunk and not a slot, and what an upgrade must
re-check. None of the three is fixed upstream as of 16.16.2.

The "Open in ChatGPT / Claude" prompt sentence was English in every locale.
It is now the ui-text key pageActionsOpenInLLMPrompt, upstream 16.9.0's key
name, placeholder and English, translated in six locales and generated for
zh-Hant. ui-text's table() now also holds every locale's placeholders to
English, so a translation that drops {url} fails the build.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
…ree entries

#298 and #305 made two promises about every localized docs page that no
gate checked afterwards: no control is named in English, and every
page-tree entry for an untranslated page carries lang="en". The locale
surface gate now reads them off the prerendered HTML under
apps/docs/.next/server/app/<locale>/, script bodies (the RSC payload)
skipped:

- english-chrome-name: an accessible name (aria-label, title, alt,
  placeholder, svg title, .sr-only text) on a localized page that is one of
  the English pages' names, outside lang="en". The English set is read off
  the built English pages, minus GitHub and www.objectos.ai.
- untranslated-entry-unmarked: a sidebar item or previous/next card for a
  page the locale has no source file for, whose text does not resolve to
  lang="en". The oracle is the content tree, not the app's detection.
- translated-entry-marked-english: the over-marking direction.

Guards keep it from passing over nothing (a page missing from a locale's
build, no English name to compare, no entry of a kind recognised in a
locale), and a live control feeds both #305 shapes, built from the run's
real oracle values, through the same reader on every run. Ten self-test
cases, and the control shown red with a reader blinded to names or to lang.

On the HTML main built at cec227a it reports 6239 English names and 5974
unmarked entries; on #305's tree, 0 and 0. No ci.yml change: the existing
Locale surface step runs the gate after the build, and pnpm turbo run test
runs its self-test.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
Below 1280px fumadocs-ui 16.8.12 shows the page's table of contents as a
bar rendered in a <header> outside every sectioning element, so the page
had two banner landmarks: the site header (#nd-subnav) and this bar. axe
4.14.0 at 390px reported landmark-unique and landmark-no-duplicate-banner on
all 48 runs of the #308 sample (8 sections, en/zh-Hans/de, light/dark), on
the tree that already carried the other #308 fixes. The bar is a disclosure
for the page's headings, so the patch renders it as a <div>; no CSS selects
header by element, and the 390px screenshots do not change. Upstream 16.16.2
still renders it as a <header>.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
…e inside an English body

#305 localized the names of the heading-anchor and code-copy buttons. On
a fallback page both buttons sit in the page body, which #298 marks
lang="en", so "Ankerlink kopieren" was read with English rules: WCAG 3.1.2,
the inverse of what #305 fixed. On main at cf449fa that was 520 + 115
names per Latin-script locale over 55 fallback pages, and 336 + 58 in
zh-Hans and zh-Hant over 31.

Both buttons now take locale from the useI18n() they already read and
render it as their own lang (patches/fumadocs-ui@16.8.12.patch, the two
#305 hunks). On a page in the route locale this repeats what html lang
says; inside an English body it keeps the name in its own language.

check-locale-surface.mjs gains the inverse rule,
localized-name-marked-english: one of a locale's own names (read off its
built pages, the names that resolve to the locale, minus the English set)
inside a lang="en" region with no lang of its own fails. Two self-test
cases (red as #305 shipped it, green through an inherited lang), the
fixture body now carries a localized anchor button with its own lang, and
the live control feeds the inverse shape too, shown red with a reader
blind to lang on names.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
The "nothing built" guard in check-locale-surface.mjs fired artifact-missing
only when no locale directory held any HTML at all. The legal pages and the
locale roots live in the same directories, so a build (or a fixture tree)
holding them but no docs page fell through to one docs-page-html-missing per
page plus the blind-reader guards. #312 adds legal-page fixtures to every
self-test case, and with them this file's "docs pages not built" case read
exactly that way. Measured on this tip with #312's own two commits applied in
memory (git merge-tree --merge-base 4da46c5): before this change 1 self-test
case failed, after it 48 cases over 25 rules pass. The guard now asks whether
any <locale>/docs page was built.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants