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 6 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.
|
@cderv, I haven't looked at all your comments, but since the language thing keeps coming up, I think it's worth a convention. I'd lean towards including and acknowledging LLM translations. That prevents the drift, and also exposes them to the people most likely to be able to do a better job at the translation. |
Yes I agree on that. |
Rename two language keys and their default values: navigation-main-label Main -> navigation-site-label Site navigation-secondary-label Secondary -> navigation-toolbar-label Toolbar All five landmark labels are now nouns (Site, Section, Toolbar, Page, Breadcrumbs). Bare adjectives do not survive translation: in Danish, German, Finnish, Icelandic, Dutch, Norwegian and Swedish the word for "main" is a bound prefix that cannot stand alone, and in the inflected languages a bare adjective has no noun to agree with. Docusaurus ships the same string (theme.NavBar.navAriaLabel = "Main") in 37 locales; 9 translators left it in English and 2 produced bare stems. "Toolbar" also describes the narrow-viewport strip more honestly than "Secondary" did. It holds a sidebar toggle, the breadcrumbs and search, and no navigation link of its own. Also drop the hardcoded "Breadcrumbs" fallback in makeBreadCrumbs, which was the only language lookup in website-navigation.ts not relying on the resolved defaults. Raised in review by cderv.
Every value comes from an existing human-translated interface string, not
from a machine translation:
Section, Page, Path LibreOffice Writer (sw/messages.po), all 33 locales
Toolbar Wikidata Q371434, GNOME gtk/gedit for the 9 gaps
Site Wikidata Q35127
Breadcrumbs Wikidata Q846205 where the attested term does not
contain that language's word for "navigation";
otherwise the word for "path"
Each line carries a comment naming its source. Cells that were adapted
rather than used verbatim (a genitive made nominative, a parenthetical
removed, a transliteration, a script conversion) are marked "needs
review" so a reviewer can grep for them:
grep -rn 'needs review' src/resources/language/
Two problems the sourcing pass found and fixed:
- Site and Page collided in 10 languages when both came from the
encyclopedia terms for "website" and "web page" (German gave
Website against Webseite). Taking Page from the plain word for
"page" separates them; cs and sk needed Site changed to "Web".
- Wikidata returns traditional Chinese for some labels, which would
have mixed scripts inside _language-zh.yml.
_language-fr-CA.yml is left alone: it is a 3-key partial that inherits
from fr.
A new smoke-all case asserts that a built-in language file resolves for
every landmark label.
|
Thanks for the careful review, and for running the axe pass on Windows. Your warning about The labels are now all nouns
Bare adjectives do not survive translation. Wiktionary gives main as a bound
TranslationsBased on the new nouns Claude found some nice sources:
Each line carries a comment naming its source. Of 165 cells, 134 are a verbatim You were right that breadcrumbs is the least settled column. 21 of those 31 are To be clear about what is verified: no native speaker has read any of this. What One question for you. In English The TOC duplicate idFiled as #14844 and fixed in #14853, which is stacked on this branch. The clone The nitFixed. |
…labels # Conflicts: # src/resources/editor/tools/vs-code.mjs # src/resources/editor/tools/yaml/all-schema-definitions.json # src/resources/editor/tools/yaml/web-worker.js # src/resources/editor/tools/yaml/yaml-intelligence-resources.json # src/resources/schema/definitions.yml # src/resources/schema/json-schemas.json # src/resources/types/schema-types.ts # src/resources/types/zod/schema-types.ts
The merge with main resolved these five keys into the nullable shape that #14773 gave the 60 language keys it added. That was wrong for these keys. #14773 allowed null so a key written with nothing after the colon, which YAML parses as null, stops failing validation. For a label like toggle-navigation an empty result is merely unhelpful. For a landmark label it is the bug this PR exists to fix: a null value renders aria-label="", so the navbar and toolbar become unnamed navigation landmarks and landmark-unique fails again. Rendering with 'navigation-site-label:' confirms it. The label comes out empty rather than falling back to the default. The schema does not gate this either way. A null, or even a list, renders without complaint at document and project level. So the declared type is a signal to editor tooling and to anyone reading definitions.yml, and 'string' is the honest one: null is not a meaningful value for these keys.
|
@cderv, one question from merging main: #14773 added 60 language keys to the schema and allowed null for all of them. I initially, put my five For these keys, a null would result in |
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 — now handled by Make website sidebar section toggles keyboard-operable buttons #14826, which removes both and makes the sidebar section toggles real<button>disclosures. The two PRs are complementary: this one labels the genuine<nav>landmarks, Make website sidebar section toggles keyboard-operable buttons #14826 deletes the fake ones. They conflict only innews/changelog-1.11.md, where both add an entry to## Accessibility; keep both.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