feat: language switcher, hreflang, RTL and translator credit (Phases 4–5) - #174
Conversation
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
/update-snapshots |
|
|
🎭 Refreshed visual baselines in aeb4001:
|
There was a problem hiding this comment.
🔵 Needs a closer look
It forks the upstream document shell and adds RTL layout heuristics against third-party markup that cannot be fully verified here, and it already contains one confirmed dark-mode RTL CSS defect.
Pull request overview
This PR implements Phases 4–5 of the book-theme parity plan plus the translator credit: a language switcher with hreflang alternates (#90), right-to-left support (#91), and a translators credit block (#143). It introduces a scalar-only configuration surface in template.yml (with the two lists carried as YAML block strings parsed by a new app/i18n.ts), a local fork of @myst-theme/site's Document to set lang/dir and provide Radix's DirectionProvider, and styles/rtl.css to mirror physical utilities used by upstream content markup.
Changes:
- New i18n module (
app/i18n.ts) with parsing/resolution helpers, a globeLanguageSwitcher, andhreflangalternates emitted from both page routes'meta. - RTL support: local
Documentcopy setsdir/langon<html>, plusstyles/rtl.cssmirroring accents/spacing and keeping code/maths LTR; theme components moved to logical utilities (ms-/me-/pe-). - Translator credit in the page header with per-page override/suppression, new fixtures (
fixture-rtl), unit + visual tests, and docs/changelog updates.
File summaries
| File | Description |
|---|---|
| app/i18n.ts | New multilingual helpers (parsing, people/label resolution, hreflang, dir/lang). |
| app/components/Document.tsx | Local copy of upstream 1.3.0 Document adding lang/dir + DirectionProvider. |
| app/components/toolbar/LanguageSwitcher.tsx | Globe Radix dropdown linking to same page per edition. |
| app/components/ProjectFrontmatter.tsx / PageHeaderHistory.tsx | Translator block + alignEnd handoff for the "Last changed" row. |
| app/components/PageContent.tsx | Wires page/site option resolution for translators. |
| app/routes/$.tsx, _index.tsx | Emit hreflang alternates from route meta using the base URL. |
| app/root.tsx / types.ts | Use local Document; set lang/dir; declare new option types. |
| styles/rtl.css, styles/app.css | RTL overrides for physical utilities; import ordering. |
| template.yml | Declares the six new scalar site options (no defaults). |
| tests/*, playwright.config.ts, fixtures | Unit + visual coverage and a third RTL fixture/port. |
| README.md, PLAN.md, CHANGELOG.md, UPSTREAM-CANDIDATES.yml, package*.json, .gitignore | Docs, changelog, deps (yaml, @radix-ui/react-direction), ignores. |
One concrete issue: in styles/rtl.css the dark-mode blue-accent mirror uses a descendant selector ([dir='rtl'] .dark .dark\:border-l-blue-400) that can never match, since dir="rtl" and .dark sit on the same <html>; it needs a compound [dir='rtl'].dark.
Review details
- Files reviewed: 31/61 changed files
- Comments generated: 1
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
Note for review: |
`dir` and `.dark` share the <html> element, so the descendant form could never match. No visible change: executable cells carry the light class too, which the first selector already mirrors in dark mode. Copilot review on #174. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…4-5) Brings the theme to parity with quantecon-book-theme for translated editions: Phase 4 (#90), Phase 5 (#91) and the translator credit (#143) of the parity plan, on the shape settled across the themes on QuantEcon/workspace-themes#3. Language switcher and hreflang (#90). `languages` (list of code, name, url) and `current_language` site options; with two or more editions the toolbar gains a globe-icon Radix dropdown at every width linking to the same page in each edition, the current one marked with aria-current, and the two page routes emit <link rel="alternate" hreflang> tags with the first edition as x-default. Right-to-left (#91). `enable_rtl` sets dir="rtl" on <html>, and `current_language` now sets its lang in place of the hard-coded "en". Upstream's Document offers neither attribute, so the theme carries a local copy (app/components/Document.tsx) adding the two props and Radix's DirectionProvider. The theme's own components move to logical utilities; styles/rtl.css mirrors the physical utilities upstream content markup uses, flips the drawer's slide and keeps code and maths left-to-right. Translator credit (#143). `translators` and `translators_label` site options with a per-page `site:` override (replace, never merge; an explicit empty value suppresses), rendered at the end of the "Last changed" row as the book theme does since v0.22.0, with a distinct class and no rel="author". Labels are the localisation mechanism; the list connector stays English, the same gap the book theme records. Configuration surface. The CLI drops every site.options key the template does not declare and can declare only scalar types (see #173), so the options are declared in template.yml and the two lists are YAML written inside a block string, parsed by app/i18n.ts; a real list is accepted too. No template defaults: a declared default is written into every page's validated `site:` block, which then replaces the raw block and both overrides the site-wide value and discards git_metadata. Defaults live in the code. Tests: unit tests for the parsing and resolution rules; the visual fixture configures two editions and a translator with a page-level override and a suppression; a third fixture is a Persian edition with enable_rtl for the rtl snapshot and the direction assertions. Darwin baselines refreshed. Local Document and the RTL audit are recorded on the upstream-candidates ledger. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`dir` and `.dark` share the <html> element, so the descendant form could never match. No visible change: executable cells carry the light class too, which the first selector already mirrors in dark mode. Copilot review on #174. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
c6fb042 to
9d0e262
Compare
|
Rebased onto |
🎭 Visual regression resultsDetails
Skipped testsmobile-chrome › theme.spec.ts › QuantEcon theme — visual regression › without JavaScript › drawer-opens-without-javascript |
…acing, mirrored list markers Three defects found in review of the Phase 4-5 work: - The <li> wrapping LanguageSwitcher stayed in the flow when the switcher rendered nothing, so every single-edition site gained a phantom gap after the GitHub button. `empty:hidden`, as the compute slot already does, and a stable `qe-language-slot` class so a test can assert it. - The toolbar list used `space-x-*`, a physical `margin-left` that a right-to-left edition does not mirror: the first two icons touched and the margin landed on the outer edge. `gap-x-*` is direction-neutral. - The theme's parenthesised list markers (styles/lists.css) hang in the gutter with a physical `left`, so in RTL they sat outside the wrong edge; rtl.css now mirrors them. Tests: a single-edition page (the no-thebe fixture) asserts no switcher, no hreflang tags and a hidden slot; the RTL document asserts the gap between the first two toolbar items; the RTL fixture gains a `(a)`-style list via the fancy-list stamp plugin. Darwin `rtl.png` baselines refreshed for the toolbar gap and the new list. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ocs, plan dates - `@radix-ui/react-direction` resolved to a separate instance from the one `@radix-ui/react-menu` and `react-select` pin, so the provider's context never reached them and the dependency was dead. Floating positioning reads the CSS `direction` that `dir` on <html> sets, so nothing is lost; the Document docstring, UPSTREAM-CANDIDATES.yml and PLAN.md say so. - template.yml and app/types.ts claimed every new option could be set per page; only `translators` / `translators_label` are read from page options. - PLAN.md: Phase 3 merged 2026-09-10 (#171), and Phases 4-5 land via #174 rather than on a pre-recorded date. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
/update-snapshots |
|
Applied the review findings in 8ab1141 (fixes) and 23242d1 (cleanups); details in the new "Review fixes" section of the description. Locally on the fixed tree: typecheck clean, unit 27 passed, visual suite 31 passed on both Chromium projects plus the WebKit FOUC guard, with only the two darwin |
|
🎭 Refreshed visual baselines in 5cbe651:
|
There was a problem hiding this comment.
🔵 Needs a closer look
It forks the upstream server-rendered document shell and adds broad RTL/i18n and config-validation behavior across many files, which warrants final human review despite strong test coverage.
Review details
- Files reviewed: 31/61 changed files
- Comments generated: 0 new
- Review effort level: Balanced
Phases 4 and 5 of the book-theme parity plan (#90, #91) and the translator credit (#143), gating the cutover on #147. Was stacked on #171; rebased onto
mainafter that PR squash-merged, so this PR now carries only the Phase 4–5 commits. Everything a translated edition needs from the theme, on the shape settled across the themes on QuantEcon/workspace-themes#3.What ships
Language switcher and hreflang (#90).
languages(a list ofcode,name,url) andcurrent_languageundersite.options. With two or more editions the toolbar gains a globe-icon Radix dropdown, at every width rather than in the overflow menu, since it is the one action a reader of a translated edition reaches for. Each entry links to the same page in the other edition (that edition's root plus the page path), the current one carriesaria-currentand a check mark, items are real anchors withhreflang. The two page routes emit<link rel="alternate" hreflang>tags for every edition with the first asx-default.Right-to-left (#91).
enable_rtlsetsdir="rtl"on<html>, andcurrent_languagenow setslangin place of the hard-codeden. Upstream'sDocumentoffers neither, so the theme carries a local copy (app/components/Document.tsx) that adds the two props; recorded as an upstream candidate. No RadixDirectionProvideris needed: floating positioning reads the CSSdirectionthatdirsets (a provider was tried and found inert, see the review fixes below). The theme's own components move to logical utilities (ms-,me-,pe-), andstyles/rtl.cssmirrors the physical utilities upstream content markup uses (border-l-4accents,pl-*/ml-*spacing, the copy button), flips the drawer's slide, and keeps code and maths left-to-right. The rest follows fromdiron its own: flex rows and the grid mirror, the outline column moves to the left, the typography plugin already indents withpadding-inline-start.Translator credit (#143).
translatorsandtranslators_label(default "Translated by") site options, with a per-page override undersite:in page frontmatter: the page value replaces the site value (never merges), and an explicitly empty value suppresses the credit on that page. Rendered at the end of the "Last changed" row, as the book theme places it since v0.22.0, with a distinct class and norel="author"on the links. The landing page credits translators too, so a translated edition's front door is not the shape quantecon-book-theme#432 describes. The list connector stays English, the same gap the book theme records.The configuration surface, and why the lists are strings
The MyST CLI validates
site.optionsagainst the template's declared options and drops every key the template does not declare, and it can declare only scalar types (boolean, string, number, choice, file). Until this branchtemplate.ymldeclared nothing, so nosite.optionskey has ever reached the theme; that is filed as #173 with the list of affected options. This PR declares the six new options, and the two lists are YAML written inside a block string, parsed byapp/i18n.ts; a real list is accepted too, so nothing changes if the engine ever passes lists through.Two traps found on the way, both documented in
template.ymlandapp/types.ts: a declareddefault:is written into every page's validatedsite:block and then overrides the site-wide value (so there are no template defaults; they live in the code), and a page that sets any declared key undersite:has its whole block replaced, losing an undeclaredgit_metadataon the same page (so the fixture keeps them on different pages).Verification
npm run test:unit: 27 tests, including the parsing and resolution rules (page override, suppression, unreadable value inherits, URL joining with a static base URL).aria-current, hrefs, Escape returning focus), the translator credit on all three pages, and the RTL document (dir,lang, Persian label, current edition, code left-to-right). The main fixture now configures two editions and a translator;tests/visual/fixture-rtlis a Persian edition on a third port./update-snapshotswill follow for Linux.Review fixes (2026-09-10)
Three defects from review, fixed in 8ab1141: the
<li>wrapping the switcher stayed in the flow on single-edition sites and took a toolbar gap (nowempty:hidden, with a test on the no-thebe fixture); the toolbar'sspace-x-*is a physical margin that RTL does not mirror, so the first two icons touched (nowgap-x-*, asserted on the RTL document); and the theme's parenthesised list markers were not mirrored (rule inrtl.css, list added to the RTL fixture). Cleanups in 23242d1: the@radix-ui/react-directionprovider was a separate instance from the one the menu packages pin, so it was inert and is dropped with its dependency; the per-page claim intemplate.yml/app/types.tsis narrowed to the two translator keys;PLAN.mddates corrected.Not done, on purpose
rel="author"on author links is upstream'sAuthorcomponent's to add; authors were already at parity and are untouched.myst.ymloption localises the list connector; labels are the mechanism, as in the book theme.twitter,launch_*,favicon,analytics_*,hide_*) are Every undeclared site.options key is dropped by the CLI, so twitter and launch_* never reach the theme #173, not this PR.🤖 Generated with Claude Code