From 5d90b266eb70d1eef27a5e877d970450261c0c05 Mon Sep 17 00:00:00 2001 From: christophe dervieux Date: Mon, 18 May 2026 17:51:50 +0200 Subject: [PATCH 1/6] fix(typst-book): localize orange-book running header when lang is set Surfaces crossref-ch-prefix, crossref-lof-title, and crossref-lot-title from format.language into Pandoc YAML metadata so they resolve in the orange-book typst-show.typ template. Adds a small surfaceParamToMeta helper in common/meta.lua for reuse by future channel-2b localized template vars. The supplement-chapter parameter on book.with(...) drives the running header via a show heading rule; without the meta surfacing it defaulted to the English literal "Chapter" regardless of lang. The existing list-of-figure-title and list-of-table-title pipes in the same template were affected by the same gap and silently rendered as empty strings. Fixes #14524 --- news/changelog-1.10.md | 1 + src/resources/filters/common/meta.lua | 15 ++++++++++++++- src/resources/filters/crossref/meta.lua | 12 ++++++++++++ .../smoke-all/typst/orange-book-lang/.gitignore | 5 +++++ .../typst/orange-book-lang/_quarto.yml | 17 +++++++++++++++++ .../typst/orange-book-lang/chapter1.qmd | 3 +++ .../smoke-all/typst/orange-book-lang/index.qmd | 16 ++++++++++++++++ 7 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 tests/docs/smoke-all/typst/orange-book-lang/.gitignore create mode 100644 tests/docs/smoke-all/typst/orange-book-lang/_quarto.yml create mode 100644 tests/docs/smoke-all/typst/orange-book-lang/chapter1.qmd create mode 100644 tests/docs/smoke-all/typst/orange-book-lang/index.qmd diff --git a/news/changelog-1.10.md b/news/changelog-1.10.md index e9f6a320ad3..7a50de5f2c8 100644 --- a/news/changelog-1.10.md +++ b/news/changelog-1.10.md @@ -23,6 +23,7 @@ All changes included in 1.10: - ([#14261](https://github.com/quarto-dev/quarto-cli/issues/14261)): Fix theorem/example block titles containing inline code producing invalid Typst markup when syntax highlighting is applied. - ([#14460](https://github.com/quarto-dev/quarto-cli/issues/14460)): Fix CSS `border` and `border-color` declarations losing tokens that precede an `rgb()`/`rgba()` color (e.g. `border: 0px solid rgb(255, 0, 0)` rendering as a 2.25pt border instead of being suppressed). Also fixes: `var(--brand-NAME)` references crashing the Typst CSS translator when `NAME` contained digits (e.g. `--brand-red-50`); a crash when an `rgba()` alpha is unparseable; the `dvmin` length unit being silently rejected (a stray space in the unit table); CSS keywords like `BOLD` not matching as `bold` (CSS keywords are case-insensitive); invalid hex colors like `#fffff` being silently accepted as 2-component colors. - ([#14511](https://github.com/quarto-dev/quarto-cli/issues/14511)): Fix brand fonts downloaded for a Typst book project not being passed to `typst compile`, causing `unknown font family` warnings and fallback to Libertinus Serif. +- ([#14524](https://github.com/quarto-dev/quarto-cli/issues/XXXX)): Fix orange-book Typst book running header not honoring `lang:` — chapter heading band stayed `Chapter N.` instead of the locale's word (e.g. `Chapitre N.` for `lang: fr`). Also fixes the orange-book `list-of-figure-title` / `list-of-table-title` template pipes which were silently rendering as empty strings. ### `revealjs` diff --git a/src/resources/filters/common/meta.lua b/src/resources/filters/common/meta.lua index fc8bfa50df7..b697db62732 100644 --- a/src/resources/filters/common/meta.lua +++ b/src/resources/filters/common/meta.lua @@ -87,7 +87,20 @@ function metaInjectHtml(meta, func) end -function readMetaOptions(meta) +-- Copy a filter param into Pandoc metadata so it resolves as a $key$ +-- variable in Pandoc templates. Skips when the user already set the key +-- (explicit metadata wins). See llm-docs/localization-architecture.md for +-- the broader pattern. +function surfaceParamToMeta(meta, key) + if meta[key] == nil then + local value = param(key, nil) + if value ~= nil then + meta[key] = value + end + end +end + +function readMetaOptions(meta) local options = {} for key,value in pairs(meta) do if type(value) == "table" and value.clone ~= nil then diff --git a/src/resources/filters/crossref/meta.lua b/src/resources/filters/crossref/meta.lua index 207f850e911..2089279705a 100644 --- a/src/resources/filters/crossref/meta.lua +++ b/src/resources/filters/crossref/meta.lua @@ -5,6 +5,18 @@ function crossrefMetaInject() return { Meta = function(meta) + -- surface localized crossref strings as Pandoc template variables + -- (consumed by Pandoc templates like orange-book typst-show.typ via + -- $crossref-ch-prefix$, $crossref-lof-title$, $crossref-lot-title$). + -- Filter params are Lua-only; templates need values in meta. + for _, key in ipairs({ + "crossref-ch-prefix", + "crossref-lof-title", + "crossref-lot-title", + }) do + surfaceParamToMeta(meta, key) + end + local function as_latex(inlines) return trim(pandoc.write(pandoc.Pandoc(quarto.utils.as_blocks(inlines)), "latex")) end diff --git a/tests/docs/smoke-all/typst/orange-book-lang/.gitignore b/tests/docs/smoke-all/typst/orange-book-lang/.gitignore new file mode 100644 index 00000000000..5d34a00bb3a --- /dev/null +++ b/tests/docs/smoke-all/typst/orange-book-lang/.gitignore @@ -0,0 +1,5 @@ +/.quarto/ +/_book/ +/index.typ +**/*.quarto_ipynb +*_files/ diff --git a/tests/docs/smoke-all/typst/orange-book-lang/_quarto.yml b/tests/docs/smoke-all/typst/orange-book-lang/_quarto.yml new file mode 100644 index 00000000000..4e819e9be7e --- /dev/null +++ b/tests/docs/smoke-all/typst/orange-book-lang/_quarto.yml @@ -0,0 +1,17 @@ +project: + type: book + +book: + title: "Livre Test" + author: "Auteur Test" + chapters: + - index.qmd + - chapter1.qmd + +lang: fr + +format: + typst: + keep-typ: true + lof: true + lot: true diff --git a/tests/docs/smoke-all/typst/orange-book-lang/chapter1.qmd b/tests/docs/smoke-all/typst/orange-book-lang/chapter1.qmd new file mode 100644 index 00000000000..937115c56e9 --- /dev/null +++ b/tests/docs/smoke-all/typst/orange-book-lang/chapter1.qmd @@ -0,0 +1,3 @@ +# Premier chapitre + +Voici le premier chapitre de ce livre de test. diff --git a/tests/docs/smoke-all/typst/orange-book-lang/index.qmd b/tests/docs/smoke-all/typst/orange-book-lang/index.qmd new file mode 100644 index 00000000000..1def8d796bf --- /dev/null +++ b/tests/docs/smoke-all/typst/orange-book-lang/index.qmd @@ -0,0 +1,16 @@ +--- +keep-typ: true +_quarto: + render-project: true + tests: + typst: + ensureTypstFileRegexMatches: + - + - 'supplement-chapter: "Chapitre"' + - 'list-of-figure-title: "Liste des Figures"' + - 'list-of-table-title: "Liste des Tables"' +--- + +# Préface {.unnumbered} + +Préface du livre de test. From 1ac882abf03b955a75ac6674f99bd88d560d088c Mon Sep 17 00:00:00 2001 From: christophe dervieux Date: Mon, 18 May 2026 17:52:13 +0200 Subject: [PATCH 2/6] docs(llm): add localization architecture overview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Maps the full lang: → rendered output pipeline across HTML, LaTeX/PDF, and Typst. Documents the three downstream surfaces from format.language (Lua filter params, explicit format.metadata copies in pandoc.ts, direct TS reads in HTML extras) and the design split between crossref: block (per-document overrides) and language: block (translations) per the crossref schema. --- llm-docs/localization-architecture.md | 222 ++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 llm-docs/localization-architecture.md diff --git a/llm-docs/localization-architecture.md b/llm-docs/localization-architecture.md new file mode 100644 index 00000000000..0d6d4651860 --- /dev/null +++ b/llm-docs/localization-architecture.md @@ -0,0 +1,222 @@ +--- +main_commit: a8d0dcfee +analyzed_date: 2026-05-18 +key_files: + - src/resources/language/_language.yml + - src/resources/language/_language-fr.yml + - src/core/language.ts + - src/command/render/render-contexts.ts + - src/command/render/pandoc.ts + - src/command/render/filters.ts + - src/resources/filters/common/meta.lua + - src/resources/filters/crossref/meta.lua + - src/resources/filters/crossref/refs.lua + - src/resources/filters/crossref/format.lua + - src/resources/filters/modules/authors.lua + - src/resources/filters/modules/callouts.lua + - src/resources/filters/layout/meta.lua + - src/resources/formats/html/pandoc/html.template + - src/resources/formats/html/pandoc/title-block.html + - src/resources/formats/html/templates/title-metadata.html + - src/resources/formats/pdf/pandoc/latex.template + - src/resources/formats/pdf/pandoc/babel-lang.tex + - src/resources/formats/pdf/pandoc/toc.tex + - src/resources/formats/beamer/pandoc/beamer.template + - src/resources/formats/typst/pandoc/typst.template + - src/resources/formats/typst/pandoc/quarto/typst-template.typ + - src/resources/formats/typst/pandoc/quarto/typst-show.typ + - src/resources/extension-subtrees/orange-book/_extensions/orange-book/typst-show.typ + - src/format/html/format-html-shared.ts + - src/format/html/format-html-bootstrap.ts + - src/format/html/format-html-appendix.ts + - src/format/pdf/format-pdf.ts + - src/project/types/book/book-chapters.ts + - src/project/types/website/listing/website-listing-template.ts + - src/project/types/website/website-search.ts +--- + +# Localization architecture + +How Quarto turns `lang: fr` (and similar) into localized strings across HTML, LaTeX/PDF, Typst, and the various extension points. Required reading before adding or wiring a new translatable string. + +## TL;DR map + +``` +_language.yml + _language-.yml + │ + ▼ +core/language.ts (formatLanguage, translationsForLang) + │ + ▼ +format.language (per-render Format object) + │ + ├─► languageFilterParams (filters.ts) + │ └─► Lua filter params (JSON) + │ └─► param("key", default) in Lua filters + │ ├─► written into Pandoc AST as text + │ └─► written into meta (becomes $var$ in templates) + │ + ├─► explicit copies into format.metadata (pandoc.ts) + │ └─► Pandoc YAML metadata + │ └─► $var$ in Pandoc templates + │ + └─► direct TS reads (HTML format extras, listings, search, + appendices) writing into rendered DOM / config +``` + +Plus format-native localization (LaTeX babel, Typst `set text(lang:)`) that Quarto does NOT control — the language just flows as a value Pandoc/Typst/babel know to interpret. + +## 1. Source of truth and resolution + +- `src/resources/language/_language.yml` — base English defaults for every Quarto-defined localized key. +- `src/resources/language/_language-.yml` — overrides per language. `_language-fr.yml` provides French; `_language-pt-BR.yml` provides Brazilian Portuguese with `_language-pt.yml` as parent. +- `src/resources/schema/definitions.yml` — `format-language` schema enumerates valid keys. Adding a new key to YAML requires also adding it here. +- `src/config/constants.ts:320-383` — `kCrossref*Title`, `kCrossref*Prefix`, `k*Title`, `kListingPage*` constants for every key, plus `kLanguageDefaultsKeys` array used as the gatekeeper. +- `src/core/language.ts:135` — `readDefaultLanguageTranslations(lang)` resolves the base file, calls `readLanguageTranslations` which walks BCP-47 subtags and merges variation files. +- `src/core/language.ts:103-130` — `readLanguageTranslations`: per subtag, loads `_language-.yml` and merges onto base. +- `src/core/language.ts:161-188` — `translationsForLang(language, lang)`: filters to `kLanguageDefaultsKeys`, `crossref-*-title`, `crossref-*-prefix`, then merges variation sub-objects (`fr-CA` falls back to `fr`). +- `src/core/language.ts:190-211` — `formatLanguage(metadata, language, flags)`: reads `kLang` from flags/metadata (default `"en"`), calls `readDefaultLanguageTranslations`, merges any user-provided `language:` block, calls `translationsForLang`. +- `src/command/render/render-contexts.ts:254-258` — assigns the resolved object onto every `Format`: + ```ts + formats[formatKey].format.language = await formatLanguage( + formats[formatKey].format.metadata, + formats[formatKey].format.language, + options.flags, + ); + ``` + +After this point, `format.language` is the in-memory canonical map of `{ "crossref-ch-prefix": "Chapitre", "toc-title-document": "Table des matières", ... }`. + +## 2. Three downstream surfaces from `format.language` + +`format.language` is not consumed directly by Pandoc. Three distinct pipelines plug into it. + +### 2a. Lua filter params (the broad channel) + +`src/command/render/filters.ts:465-496` (`languageFilterParams`) auto-spreads selected language keys into Lua filter params: + +- Always: `[kCodeSummary]`, `[kTocTitleDocument]`. +- For each crossref type (`fig`, `tbl`, `lst`, `thm`, ...): `params["crossref--prefix"] = language["crossref--title"]` — the prefix-from-title default. +- Bulk: every key in `format.language` that starts with `callout-`, `crossref-`, or `environment-` is copied verbatim. + +`src/command/render/pandoc.ts:492` separately puts the whole language table into filter params at key `"language"`: + +```ts +formatFilterParams["language"] = options.format.language; +``` + +Both routes coexist in filter params JSON. Lua filters access them via: +- `param("language", nil)["title-block-author-single"]` — convenient when reading many keys (used in `modules/authors.lua:854`). +- `param("crossref-ch-prefix", "Chapter")` — convenient with default fallback (used throughout `crossref/`). + +Same underlying data, just different access ergonomics. Pick the style that matches the file you are editing. + +### 2b. Direct `format.metadata` writes in TS + +`format.metadata` becomes Pandoc YAML metadata, so its keys resolve as `$key$` in Pandoc templates. Quarto explicitly copies a few language values across in `src/command/render/pandoc.ts`: + +- `pandoc.ts:498-503` — `format.metadata[kTocTitle]` from website or document variant of toc title. +- `pandoc.ts:520-522` — `format.metadata[kAbstractTitle]` from `kSectionTitleAbstract`. + +Both are gated "only if user did not already set the metadata key". + +### 2c. Direct TS reads (`format.language[key]` in renderer code) + +Some HTML/PDF rendering code reads `format.language[...]` directly to populate DOM nodes or JS configs rather than going through Pandoc metadata: + +- `src/format/html/format-html-shared.ts:356,372,478` — footnotes/references headings, copy-button tooltip. +- `src/format/html/format-html-bootstrap.ts:711,735,753,757,905` — dev container, Binder, Other Links, Code Links, Related Formats. +- `src/format/html/format-html-appendix.ts:240,252,282,301,318,327` — license, reuse, copyright, BibTeX attribution, cite-as, citation labels. +- `src/format/asciidoc/format-asciidoc.ts:244` — references section. +- `src/project/types/website/website-search.ts:613-617` — copies all `search-*` keys into search JS options. +- `src/project/types/website/listing/website-listing-read.ts:153-163` — listing column header labels. +- `src/project/types/website/listing/website-listing-template.ts:343,353,366,375` — listing sort UI. +- `src/project/types/book/book-chapters.ts:149` — uses `kCrossrefApxPrefix` to format appendix chapter titles ("Annexe A — ..."). +- `src/render/notebook/notebook-contributor-html.ts:120,174,208,209` — notebook preview UI labels. +- `src/command/render/codetools.ts:211,215,221,265,330` — code tools menu labels. + +These bypass both Pandoc metadata and Lua filters. The string lands directly in the rendered DOM or in a JSON blob the front-end JS reads. + +## 3. Format-by-format breakdown + +### 3a. HTML + +Localization paths used: + +- **Document `lang` attribute**: `_language.yml` lookup not involved. `lang:` flows as Pandoc-native metadata, template renders `` (`html.template:2`). +- **TOC title**: `$toc-title$` resolved via channel 2b. Used in `html.template:60`, `toc.html:3`. +- **Abstract title**: `$abstract-title$` resolved via 2b. Used in `html.template:51`, `title-block.html:15`. +- **Title block labels** (`Authors`, `Affiliations`, `Published`, `Modified`, `Doi`, `Abstract`, `Keywords`): `$labels.*$` written into meta by `modules/authors.lua:854-913` (`computeLabels`). Templates: `title-metadata.html:3,4,32,43,52,61,74,83`, `manuscript/title-metadata.html:6,7,37,48,57,66,83,92`. +- **Crossref text** (`Figure 1.1`, `Table 2.1`): assembled in Lua by `crossref/format.lua` using `title()` / `refPrefix()` which call `param("crossref--title"/"-prefix")`. Written as inline text directly into the AST. By the time Pandoc renders HTML, the localized prefix is already document content. +- **Callout titles** (`Tip`, `Note`, etc.): `modules/callouts.lua:15,185` reads `param("callout--title", default)`, writes into the callout node. +- **DOM-level UI strings** (search, listings, related formats, code tools, etc.): TS surface (2c) writes into the DOM in HTML postprocessors. + +### 3b. LaTeX / PDF + +Three localization paths layered: + +1. **babel (Pandoc-native)** — `_language-.yml` not involved. Pandoc converts `lang:` into `$babel-lang$` and friends; `pdf/pandoc/babel-lang.tex:4-23` injects `\usepackage{babel}` with the right option; `babel` then automatically localizes `\chaptername`, `\figurename`, `\tablename`, `\contentsname`, `\listfigurename`, `\listtablename`, `\proofname`. These are "free" — no Quarto code touches them. + +2. **Lua-injected `\renewcommand` overrides** — `crossref/meta.lua:32-43` calls `metaInjectLatex` (defined at `common/meta.lua:51-59`, format-gated to LaTeX), which appends raw TeX to `header-includes`. Uses `maybeRenewCommand` (`crossref/meta.lua:93-95`) which emits `\ifdefined\\renewcommand*\{arg}\else\newcommand\{arg}\fi`. This deliberately overrides babel's defaults with the Quarto-side crossref title (so user can override e.g. `figurename` via crossref options). Overridden commands: `contentsname`, `listfigurename`, `listtablename`, `figurename`, `tablename`. `crossref/theorems.lua:217` does the same for `\proofname` via raw `\renewcommand` inside `\AtBeginDocument`. + + Other Lua filters use `metaInjectLatex` for non-language LaTeX customization (`crossref/custom.lua:78`, `layout/meta.lua`, `quarto-post/landscape.lua`, etc. — they inject packages or styling, not localized strings). + +3. **Pandoc template `$var$`** — `pdf/pandoc/toc.tex:3` and `pdf/pandoc/latex.template:93-94` use `$toc-title$` to set `\contentsname`. Beamer templates (`beamer/pandoc/beamer.template:146-151`, `beamer/pandoc/toc.tex:3`) likewise. + +PDF-side TS code does not read `format.language` directly except `src/format/pdf/format-pdf.ts:242` which registers `"babel-lang"` as a Quarto partial. + +### 3c. Typst + +Localization paths: + +1. **Typst native (`set text(lang: ...)`)** — `lang:` flows as Pandoc metadata, `$lang$` resolves to `lang: "fr"` in `typst-show.typ:22-24`, then `typst-template.typ:45-47` applies `set text(lang: lang, region: region, size: fontsize)`. Typst then auto-localizes its built-in figure/table/equation/listing/appendix/bibliography/outline strings using its own translation tables. Like babel for LaTeX, Quarto does not control these — they come for free from Typst. + +2. **Pandoc template `$var$` via meta** — same channel as HTML. + - `$toc-title$` (resolved via 2b in `pandoc.ts:498`) used in `typst-show.typ:93-95` as `toc_title:`. + - `$labels.abstract$` (written by `authors.lua` `computeLabels`) used in `typst-show.typ:30` as `abstract-title:`. + - For orange-book (`extension-subtrees/orange-book/_extensions/orange-book/typst-show.typ:28-33`): `$crossref-lof-title$`, `$crossref-lot-title$`, `$crossref-ch-prefix$` used to set `list-of-figure-title`, `list-of-table-title`, `supplement-chapter` parameters on `book.with(...)`. These are surfaced into meta by `crossref/meta.lua:6-23` (the recent fix — see §4 below). + +3. **Lua-injected supplements for refs** — `crossref/refs.lua:89-91` wraps every `@fig-foo`-style reference into `#ref(