Give every website and book nav landmark a distinct, localizable label - #14813
Give every website and book nav landmark a distinct, localizable label#14813cwickham wants to merge 2 commits into
nav landmark a distinct, localizable label#14813Conversation
#14376) New language keys navigation-{main,section,secondary,page,breadcrumbs}-label label the navbar, sidebar (adaptive), secondary nav, prev/next page nav, and breadcrumbs; the TOC nav is labelled by its localized heading via aria-labelledby (html + revealjs). Defaults omit the word "navigation" per the APG landmark-regions practice, since screen readers announce the role after the label. Note: all-schema-definitions.json also picks up the lagged cache-globals schema from #14735 (regenerated artifacts trail schema changes by one build).
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
cderv
left a comment
There was a problem hiding this comment.
LGTM. I checked out the branch and ran both new smoke-all suites, they pass here on Windows. I also ran axe-core against the rendered fixture site in Chrome, landmark-unique, landmark-one-main, region and duplicate-id-aria are all clean at 1400x900 and at 500x900, including with the mobile sidebar expanded. The shared breadcrumbs label holds up, .quarto-secondary-nav is display: none from lg up and .quarto-title-breadcrumbs carries d-none d-lg-block, so only one of the two is ever in the accessibility tree.
One thing I noticed while looking at the TOC change. With toc-location: left-body or right-body we clone the whole TOC nav into the body and only rename the nav id:
quarto-cli/src/format/html/format-html-bootstrap.ts
Lines 353 to 362 in 4715317
The <h2 id="toc-title"> inside is cloned as is, so the page ends up with two elements sharing that id, and now both navs point at it with aria-labelledby. The name resolves to the same title for both either way, and I measured landmark-unique already failing on that page before your change, so it is not a regression from this PR. Still, the ARIA reference now depends on a duplicated id. Would you rather fold a fix in here, or should I file it separately ?
Nit, no need to act on it: language[kNavigationBreadcrumbsLabel] || "Breadcrumbs" is the only language lookup in website-navigation.ts with a hardcoded fallback, the others rely on the resolved defaults.
The new keys and the translation files
This part is not really about your change. The five new keys land in _language.yml only, so every translated site gets English landmark names sitting next to otherwise translated chrome. I looked at the wider picture and 25 of the 30 base language files are already behind _language.yml, so I filed #14822 for that separately.
For these five keys I produced a candidate set for all 30 base language files. To be explicit about what that is: it is LLM generated. I do not speak most of these languages and no native speaker has looked at any of it. What I verified is only the mechanics, lang: fr renders aria-label="Principale" and lang: zh renders aria-label="主要", so the keys resolve and the encoding is fine. That says nothing about whether the wording is right.
Every row wants a review before it ships. The de, is, tr, ko and eu ones are the ones I trust least, Main and Secondary are bare adjectives and those languages want a head noun after them. The breadcrumbs column is the least settled everywhere, there is no single agreed term.
So it is your call. We could take this as a starting point here and be clear in the commit that the strings are machine generated and unreviewed, or leave the five keys English in this PR and let #14822 collect translations from people who actually speak the languages. Which do you prefer ?
Candidate translations, LLM generated and unreviewed (30 base languages)
These are LLM generated and have not been checked by a native speaker. The warning sign marks the rows I trust least, where "main" and "secondary" do not stand alone as adjectives.
| main | section | secondary | page | breadcrumbs | |
|---|---|---|---|---|---|
| bg | Основна | Раздел | Второстепенна | Страница | Път |
| ca | Principal | Secció | Secundària | Pàgina | Ruta |
| cs | Hlavní | Sekce | Sekundární | Stránka | Cesta |
| da | Primær | Sektion | Sekundær | Side | Brødkrummesti |
| de |
Primär | Abschnitt | Sekundär | Seite | Brotkrümelpfad |
| el | Κύρια | Ενότητα | Δευτερεύουσα | Σελίδα | Διαδρομή |
| es | Principal | Sección | Secundaria | Página | Ruta |
| eu |
Nagusia | Atala | Bigarren mailakoa | Orria | Ibilbidea |
| fi | Ensisijainen | Osio | Toissijainen | Sivu | Polku |
| fr | Principale | Section | Secondaire | Page | Fil d'Ariane |
| he | ראשי | מקטע | משני | עמוד | נתיב |
| id | Utama | Bagian | Sekunder | Halaman | Jejak |
| is |
Aðal | Undirkafli | Auka | Síða | Slóð |
| it | Principale | Sezione | Secondaria | Pagina | Percorso |
| ja | メイン | セクション | サブ | ページ | パンくずリスト |
| ko |
주요 | 섹션 | 보조 | 페이지 | 이동 경로 |
| lt | Pagrindinė | Skiltis | Antrinė | Puslapis | Kelias |
| nb | Primær | Seksjon | Sekundær | Side | Brødsmulesti |
| nl | Primair | Sectie | Secundair | Pagina | Kruimelpad |
| nn | Primær | Bolk | Sekundær | Side | Brødsmulesti |
| pl | Główna | Sekcja | Dodatkowa | Strona | Ścieżka |
| pt | Principal | Seção | Secundária | Página | Trilha |
| ru | Основная | Раздел | Дополнительная | Страница | Путь |
| sk | Hlavná | Sekcia | Sekundárna | Stránka | Cesta |
| sl | Glavna | Razdelek | Drugotna | Stran | Pot |
| sr-Latn | Glavna | Odeljak | Sekundarna | Stranica | Putanja |
| sv | Primär | Sektion | Sekundär | Sida | Brödsmulor |
| tr |
Ana | Bölüm | İkincil | Sayfa | Yol |
| ua | Основна | Розділ | Додаткова | Сторінка | Шлях |
| zh | 主要 | 章节 | 次要 | 页面 | 面包屑 |
All five avoid that language's own word for "navigation", following the rule you documented in _language.yml. That comment only lives in _language.yml though, so a translator editing _language-de.yml later will not see it.
Description
Closes #14376.
Website and book pages emit up to six
<nav>landmarks. Before this PR, only the breadcrumbs had a label, and it was hardcoded English (aria-label="breadcrumb"). Assistive technology listed the rest as identical "navigation" entries, and axe flagged the pages underlandmark-unique.This PR gives each landmark a distinct label from a new set of language keys:
navigation-main-label("Main")navigation-secondary-label("Secondary")navigation-page-label("Page")navigation-breadcrumbs-label("Breadcrumbs")aria-labelledbyon its existing localized heading ("On this page") — no new keyAuthors can override any key with
language:metadata, per document or site-wide.Label wording
The default labels do not contain the word "navigation". Screen readers announce the landmark role after the label, so a label like "Main navigation" reads as "Main navigation, navigation". The ARIA Authoring Practices Guide says not to use the role as part of the label, and the WAI page-structure tutorial uses the bare label "Main" for a nav region. VoiceOver testing confirmed the double announcement with the longer wording.
Design notes
lgand up, the other below). The APG also asks identical navigation sets to share a label.aria-labelledby, which is the pattern the WAI tutorial recommends. The TOC nav can also hold the Other Formats / code-links / other-links sections. The label stays acceptable: everything in that nav is page-scoped.revealjs(same partial pattern,toc-slide.html).Out of scope
<a>elements carry an invalidrole="navigation"(secondary-nav toggles, sidebar section toggles). Unchanged here; to be filed separately.Documentation
No documentation PR: the only user-facing change is the new language keys, and the docs already refer users to
_language.ymlitself as the reference for available keys (Document Language).Testing
language:override, and a customtoc-titlein bothhtmlandrevealjs.landmark-uniquepasses on navbar-only, sidebar-only, navbar+sidebar, and book sites, at desktop and narrow viewports, including with the mobile sidebar open. Also reviewed the in-page report fromaxe: output: documenton the rendered site. These browser checks were manual; this PR does not add a Playwright case for them.Checklist
I have (if applicable):
AI-assisted PR