Skip to content

docs: legal pages mark their English fallback and get zh-Hant from zh-Hans; one main landmark - #318

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

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

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #312

The legal pages /LOCALE/privacy and /LOCALE/terms now handle locales they are not written in correctly:

What changed

  • apps/docs/app/[lang]/{privacy,terms}/page.tsx
  • apps/docs/app/[lang]/{privacy,terms}/zh-Hans.json (new): the zh-Hans entry, moved out of page.tsx with no change (deep-equal to the HEAD entry). It is JSON because the generator converts a data file, the same reason lib/ui-text/ is JSON.
  • apps/docs/app/[lang]/{privacy,terms}/zh-Hant.json (new, generated):
  • apps/docs/scripts/gen-zh-hant.mjs: the single ui-text pair becomes FIXED_PAIRS (ui-text, privacy, terms). As a result, --check covers the legal copy.
  • .github/scripts/check-locale-surface.mjs
    • STATIC_PAGES lists zh-Hant.
    • Three new rules read the built legal pages. The English page's main-content text is the oracle.
      • legal-english-unmarked: an unlisted locale shows English text that does not resolve to en.
      • legal-translation-shows-english: a listed locale shows English text, or text marked English.
      • legal-page-unread: a page is missing, the oracle is empty, or an unlisted locale shows no English text.
    • Five self-test cases cover these rules, including the not-built case, which raises artifact-missing.
    • The reader skips the header and nav that HomeLayout puts inside its main element, and it skips script content. The fixture mirrors the real structure: HomeLayout's main holds the header and the page's article.

Branch and rebase

Verification on the rebased tip (e6bf746, real --force build)

  • pnpm install --frozen-lockfile: Lockfile is up to date, 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: Generating static pages (1038/1038), Tasks 1 successful, VERDICT command-exit 0. There are 332 OG font-fetch proxy warnings, the same count as every earlier build.
  • pnpm turbo run test --force: ✓ 10 self-test(s) passed, VERDICT command-exit 0.
  • check-locale-surface: exit 0 with ✓ every advertised URL has a source file …, on a real build. Both cards' sections are present.
  • check-locale-surface --self-test: ✓ self-test: 48 case(s) over 25 rule(s) and 3 artifact(s).
  • check-positioning ✓, check-search-locales ✓ (all 8 locales answer 200), and gen-zh-hant --check: ✓ zh-Hant: 62 generated file(s) match ….
  • Translations, using the workflow argv. origin/main...HEAD is now only this card's 8 files.
    • Ownership with actor hotlong: unset exit 0; armed ✓ 8 file(s) changed, no translation artifacts touched.
    • Bot-actor control: ✗ translation PRs may only touch translation artifacts. (exit 1, as expected).
    • Freshness ✓, Output --self-test ✓, and Output --files: ✓ … (256 pre-existing finding(s) reported).
  • check-node-floor and --self-test, check-half-states --self-test: all exit 0.
  • Built-HTML legal check with JavaScript off: 16 of 16. Live legal check at 1440 and 390: 32 of 32, 0 console errors.
  • axe 4.14.0 on /privacy and /terms in en, de and zh-Hant, at 1440 light, 1440 dark and 390 light (18 runs): 0 violations of any rule.
  • Screenshots retaken: /de/privacy and /zh-Hant/terms at 1440 and 390, and looked at. The /de/privacy PNGs are byte-identical to the pre-rebase ones (sha256 ed1644f3e921…, 9f32ffbe995e…).

Verification before the rebase (on 4da46c5; HEAD then 85b6b4b)

The rebased tip is verified again in the section above. The measurements below were taken before the rebase. 85b6b4b differs from the built c3ad871 only in a comment reflow in check-locale-surface.mjs, so its app tree is byte-identical to the build measured here.

CI case, red before, green after. The committed gate was run against a fresh --force build of the base (4da46c5): ✗ locale surface: 14 finding(s).

  • legal-english-unmarked ×10: ja, de, es, fr and ko, for both pages. "11 of the 11 English text node(s) de shows for privacy resolve to lang=de".
  • legal-translation-shows-english ×2: zh-Hant, for both pages.
  • missing-url ×2: the zh-Hant legal URLs are missing from the sitemap.

On this branch's build (c3ad871, the same app tree as HEAD) the gate exits 0 with ✓ every advertised URL has a source file …. The legal table shows wrong 0 on all 14 non-English pages, and the sitemap has 301 entries (301 expected).

Built-HTML check. Chromium parsed every built page with JavaScript off, and the DOM was asked which lang each text node resolves to (closest('[lang]')). All 16 pages (8 locales × 2) pass, on both the 297882f build and the c3ad871 build:

  • de, ja, es, fr, ko: html lang is the route locale. There are 4 lang="en" elements in the content, and every English text node resolves to en. The notice is visible, matches ui-text notTranslated, and resolves to the route locale.
  • zh-Hant: the text equals zh-Hant.json, and per-string s2twp(zh-Hans) equals zh-Hant.json. No English text, no lang="en". Canonical is the page's own zh-Hant URL.
  • hreflang: exactly en, zh-Hans, zh-Hant, x-default on every page.

The same checks also passed against next start, with JavaScript on, at 1440 and 390: 32 of 32 on each build, and 0 console errors.

axe 4.14.0 on /privacy and /terms in en, de and zh-Hant, hydrated, at 1440 light, 1440 dark and 390 light (18 runs each side):

rule before (297882f build) after (c3ad871 build)
landmark-no-duplicate-main 18 0
landmark-main-is-top-level 18 0
landmark-unique 18 0
svg-img-alt (the header GitHub icon; 0 after the rebase onto #308) 12 12
  • No rule appears after the change that was absent before.
  • document.querySelectorAll('main') returns 2 before and 1 after.

Screenshots, before and after the article change: /de/privacy at 1440 and 390. The before and after PNGs are byte-identical (sha256 ed1644f3e921… at 1440, 9f32ffbe995e… at 390), so nothing moved, and both were looked at.

Screenshots (both widths looked at): /de/privacy and /zh-Hant/terms at 1440 and 390.

  • /de/privacy: the German notice sits above the English policy.
  • /zh-Hant/terms: 服務條款 / 許可 / 託管服務 / 返回首頁, and the header search reads 搜尋.

Reverse verification, run once against committed HEAD. Renaming back in privacy/zh-Hans.json makes tsc exit 2 with TS2339 (Property 'back' does not exist). The file was then restored byte-identical (blob 983f34dec0cc == HEAD, git diff HEAD empty).

Gate ablation. With the header and nav skip removed, the self-test turns red: the clean baseline fires legal-english-unmarked and legal-translation-shows-english on the brand text in the header. Restored byte-identical (blob 9183f0ddcd6e).

Gates (type-check, build and the post-build gates at c3ad871; turbo test, check-locale-surface and its self-test again at 85b6b4b)

  • pnpm turbo run type-check --continue --force: Tasks 1 successful, VERDICT command-exit 0.
  • NEXT_PRIVATE_STANDALONE=true pnpm turbo run build --force: Generating static pages (1038/1038), Tasks 1 successful, VERDICT command-exit 0. There are 332 OG font-fetch proxy warnings, the same count as the base build.
  • pnpm turbo run test --force: ✓ 10 self-test(s) passed, VERDICT command-exit 0.
  • check-locale-surface: exit 0 (line above). --self-test: ✓ self-test: 36 case(s) over 18 rule(s) and 3 artifact(s).
  • check-positioning: ✓ positioning: 4 copies equal their constants; … the en entry of 2 legal pages.
  • check-search-locales: ✓ search locales: all 8 locales answer 200 ….
  • gen-zh-hant --check: ✓ zh-Hant: 62 generated file(s) match the zh-Hans sources byte for byte. (60 before this change, plus the 2 legal files.)
  • Translations Ownership, using the workflow argv (git diff --name-status --no-renames origin/main...HEAD; origin/main is cf449fa, so the merge base is still 5d2f837 and the list has 23 files, Docs chrome still speaks English inside localized pages: fumadocs' hard-coded aria-labels, English page-tree names without lang="en", no zh-Hant 404 copy #305's included; after the rebase it is this card's 8), with actor hotlong:
    • Unset bot login: exit 0.
    • Armed: ✓ 23 file(s) changed, no translation artifacts touched.
    • Control with actor = bot: ✗ translation PRs may only touch translation artifacts. (exit 1, as expected).
    • This card's 8 files alone (4da46c5...HEAD), armed: ✓ 8 file(s) changed, no translation artifacts touched.
  • Translations Freshness: ✓ translations gate passed.
  • Translations Output: --self-test ✓. --files: ✓ translation output gate passed (256 pre-existing finding(s) reported).
  • check-node-floor and --self-test, check-half-states --self-test: all exit 0.
  • Not measured here, and left to CI: the Worker package, size budget and local-preview smoke. They were not in this card's gate list.

Acceptance notes

Pushed head

7c17974 on claude/pm-dispatch-objectos-ju9td1, a fast-forward with no force. It starts at the remote branch head fa2390f, merges origin/main @ 9d3ff08 (#317; after the merge the diff against main is empty), and cherry-picks 39b76e1 and e6bf746. git diff origin/main 7c17974 is byte-identical to git diff origin/main e6bf746.


Generated by Claude Code

objectstack-fleet Bot and others added 21 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
…ce; zh-Hant generated from zh-Hans

- privacy and terms on de, ja, es, fr and ko: the English entry is marked
  lang="en" (title, date, body, back link) and introduced by the #298 notice
  (ui-text notTranslated) in the route locale. en and zh-Hans render as before.
- The zh-Hans entry of each page moves, unchanged, into a zh-Hans.json beside
  it; gen-zh-hant converts it to zh-Hant.json like lib/ui-text, so
  /zh-Hant/privacy and /zh-Hant/terms show Traditional text and gen-zh-hant
  --check covers it. contentLocales, hreflang and the sitemap gain zh-Hant.
- check-locale-surface: STATIC_PAGES lists zh-Hant, and three rules read the
  built legal pages (legal-english-unmarked, legal-translation-shows-english,
  legal-page-unread), with self-test cases.

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

HomeLayout already renders main#nd-home-layout around its header and the page,
so the page's own main was a nested, duplicate landmark on /privacy and /terms
in every locale (axe: landmark-no-duplicate-main, landmark-main-is-top-level,
landmark-unique). Same classes, so nothing moves on screen. The
check-locale-surface legal fixture now mirrors that structure.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 6, 2026 16:54
@hotlong
hotlong merged commit 94ce848 into main Oct 6, 2026
4 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

2 participants