Skip to content

feat: language switcher, hreflang, RTL and translator credit (Phases 4–5) - #174

Merged
mmcky merged 7 commits into
mainfrom
feat/phase-4-5-i18n-rtl-translators
Sep 10, 2026
Merged

feat: language switcher, hreflang, RTL and translator credit (Phases 4–5)#174
mmcky merged 7 commits into
mainfrom
feat/phase-4-5-i18n-rtl-translators

Conversation

@mmcky

@mmcky mmcky commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

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 main after 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 of code, name, url) and current_language under site.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 carries aria-current and a check mark, items are real anchors with hreflang. The two page routes emit <link rel="alternate" hreflang> tags for every edition with the first as x-default.

Right-to-left (#91). enable_rtl sets dir="rtl" on <html>, and current_language now sets lang in place of the hard-coded en. Upstream's Document offers neither, so the theme carries a local copy (app/components/Document.tsx) that adds the two props; recorded as an upstream candidate. No Radix DirectionProvider is needed: floating positioning reads the CSS direction that dir sets (a provider was tried and found inert, see the review fixes below). The theme's own components move to logical utilities (ms-, me-, pe-), and styles/rtl.css mirrors the physical utilities upstream content markup uses (border-l-4 accents, pl-* / ml-* spacing, the copy button), flips the drawer's slide, and keeps code and maths left-to-right. The rest follows from dir on its own: flex rows and the grid mirror, the outline column moves to the left, the typography plugin already indents with padding-inline-start.

Translator credit (#143). translators and translators_label (default "Translated by") site options, with a per-page override under site: 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 no rel="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.options against 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 branch template.yml declared nothing, so no site.options key 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 by app/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.yml and app/types.ts: a declared default: is written into every page's validated site: 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 under site: has its whole block replaced, losing an undeclared git_metadata on 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).
  • Visual suite on both Chromium projects: 29 passed. New assertions cover the hreflang tags, the switcher (items, 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-rtl is a Persian edition on a third port.
  • FOUC guard (WebKit): 2 passed, since the document shell changed.
  • Darwin baselines refreshed; /update-snapshots will 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 (now empty:hidden, with a test on the no-thebe fixture); the toolbar's space-x-* is a physical margin that RTL does not mirror, so the first two icons touched (now gap-x-*, asserted on the RTL document); and the theme's parenthesised list markers were not mirrored (rule in rtl.css, list added to the RTL fixture). Cleanups in 23242d1: the @radix-ui/react-direction provider 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 in template.yml / app/types.ts is narrowed to the two translator keys; PLAN.md dates corrected.

Not done, on purpose

🤖 Generated with Claude Code

Copilot AI balanced review requested due to automatic review settings September 7, 2026 03:43
mmcky added a commit that referenced this pull request Sep 7, 2026
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@mmcky

mmcky commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

/update-snapshots

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-09-10 07:16 UTC

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

🎭 Refreshed visual baselines in aeb4001:

  • tests/visual/snapshots/desktop-chrome-linux/features.png
  • tests/visual/snapshots/desktop-chrome-linux/history-open.png
  • tests/visual/snapshots/desktop-chrome-linux/intro.png
  • tests/visual/snapshots/desktop-chrome-linux/lists.png
  • tests/visual/snapshots/desktop-chrome-linux/notebook.png
  • tests/visual/snapshots/desktop-chrome-linux/rtl.png
  • tests/visual/snapshots/desktop-chrome-linux/sidebar-open.png
  • tests/visual/snapshots/mobile-chrome-linux/features.png
  • tests/visual/snapshots/mobile-chrome-linux/history-open.png
  • tests/visual/snapshots/mobile-chrome-linux/intro.png
  • tests/visual/snapshots/mobile-chrome-linux/lists.png
  • tests/visual/snapshots/mobile-chrome-linux/notebook.png
  • tests/visual/snapshots/mobile-chrome-linux/rtl.png
  • tests/visual/snapshots/mobile-chrome-linux/sidebar-open.png

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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 globe LanguageSwitcher, and hreflang alternates emitted from both page routes' meta.
  • RTL support: local Document copy sets dir/lang on <html>, plus styles/rtl.css mirroring 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.

Comment thread styles/rtl.css
@mmcky

mmcky commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Note for review: ci.yml triggers on pull requests against main only, so this stacked PR shows no CI until #171 merges and GitHub retargets it. The Linux baselines have already been refreshed by the comment-triggered workflow (commit aeb4001). Locally against the built bundle: typecheck clean, unit tests 27 passed, visual suite 29 passed on both Chromium projects, FOUC guard 2 passed.

mmcky and others added 4 commits September 10, 2026 16:27
…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>
@mmcky
mmcky force-pushed the feat/phase-4-5-i18n-rtl-translators branch from c6fb042 to 9d0e262 Compare September 10, 2026 06:32
@mmcky

mmcky commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto main after #171 squash-merged (cea4352), dropping the five Phase 3 commits this branch carried from the stack; the four Phase 4–5 commits replayed with no conflicts and the resulting tree is the previous head (c6fb042) plus #175 exactly. Verified locally on the rebased head against the built bundle: typecheck clean, unit tests 27 passed, visual suite 31 passed on desktop-chrome, mobile-chrome and the WebKit FOUC guard, with no baseline changes. This is the first push since GitHub retargeted the PR to main, so ci.yml runs for the first time here.

@github-actions

github-actions Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

🎭 Visual regression results

passed  31 passed
skipped  5 skipped

Details

stats  36 tests across 1 suite
duration  58.6 seconds
commit  5cbe651

Skipped tests

mobile-chrome › theme.spec.ts › QuantEcon theme — visual regression › without JavaScript › drawer-opens-without-javascript
mobile-chrome › theme.spec.ts › QuantEcon theme — visual regression › drawer-closes-when-search-opens
mobile-chrome › theme.spec.ts › QuantEcon theme — visual regression › launch-colab
mobile-chrome › theme.spec.ts › QuantEcon theme — visual regression › live-compute-toggle
mobile-chrome › theme.spec.ts › QuantEcon theme — visual regression › live-compute-toggle-absent-without-thebe

…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>
@mmcky

mmcky commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

/update-snapshots

@mmcky

mmcky commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

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 rtl.png baselines moving (toolbar gap restored, new parenthesised list). The Linux rtl.png baselines need the same refresh, requested above.

@github-actions

Copy link
Copy Markdown
Contributor

🎭 Refreshed visual baselines in 5cbe651:

  • tests/visual/snapshots/desktop-chrome-linux/rtl.png
  • tests/visual/snapshots/mobile-chrome-linux/rtl.png

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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

@mmcky
mmcky merged commit fc15361 into main Sep 10, 2026
5 checks passed
@mmcky
mmcky deleted the feat/phase-4-5-i18n-rtl-translators branch September 10, 2026 07:16
mmcky added a commit that referenced this pull request Sep 10, 2026
Move the Unreleased entries (Phase 3 via #171, Phases 4-5 and the
translator credit via #174) under a dated 2.6.0 heading, add the compare
link, bump package.json, and mark the three phases as shipped in PLAN.md.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
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.

2 participants