diff --git a/.claude/rules/changelog.md b/.claude/rules/changelog.md index dc8e02472b..e94b85e7ae 100644 --- a/.claude/rules/changelog.md +++ b/.claude/rules/changelog.md @@ -32,6 +32,12 @@ Use H3 headings with backtick-wrapped names: ### `pdf` ``` +Use `### All Formats` (no backticks) for cross-format features that apply regardless of output format. Place it first among the H3 subsections, before format-specific ones. Example: `changelog-1.9.md` uses it for a `syntax-highlighting` option change that affects all formats. + +### Major Feature Sections + +Large new features that deserve prominence can get a dedicated H2 section instead of being buried in `## Other fixes and improvements`. Examples from past releases: `## Shortcodes` (1.9), `## Crossrefs` (1.8), `## Languages` (1.8), `## YAML validation` (1.7). Place these sections in the hierarchy where they fit logically — before `## Formats` if format-agnostic, or after `## Formats` if they extend format concerns. Highlights (promotional) still go in quarto-web, not here. + ## Entry Format ```markdown diff --git a/.claude/rules/typescript/config-merging.md b/.claude/rules/typescript/config-merging.md new file mode 100644 index 0000000000..fd08c09a69 --- /dev/null +++ b/.claude/rules/typescript/config-merging.md @@ -0,0 +1,27 @@ +--- +paths: + - "src/**/*.ts" +--- + +# Config and metadata merging + +When merging nested config-like objects (Pandoc defaults, `variables`, +`metadata`, `format.language`, `format.pandoc`, project YAML), use the +canonical in-tree helpers — never raw `lodash.merge` or shallow spread. + +| Helper | File | Use when | +|---|---|---| +| `mergeConfigs(config, ...configs): T` | `src/core/config.ts` | General nested-object merge. Deep-merges objects; **arrays union-concat with dedup by `JSON.stringify`** (not last-wins); scalars last-wins. Deep-clones inputs. | +| `mergeFormatMetadata(config, ...configs): T` | `src/config/metadata.ts` | Format-level objects (`.pandoc`, `.metadata`, `.language`, `.render`). Adds `kTblColwidths` replace, `kVariant` pandoc-extension merge, and `kCodeLinks`/`kOtherLinks` `false→[]` semantics on top of `mergeConfigs`. | +| `mergeProjectMetadata(config, ...configs): T` | `src/config/metadata.ts` | Project YAML trees. Adds string-vs-string replace for `contents:` to avoid spurious glob concat. | +| `mergeConfigsCustomized(customizer, config, ...configs): T` | `src/config/metadata.ts` | Escape hatch — supply a per-key customizer that runs before `mergeArrayCustomizer`. | + +Shallow `{ ...a, ...b }` is correct **only** when both sides are flat +records with no shared keys that hold nested objects. If `a.x` and `b.x` +are both objects you want their leaves merged, reach for `mergeConfigs`. + +The array-union default surprises people. Want arrays to replace? +`mergeConfigsCustomized` with a customizer that returns `srcValue` for +the specific key (see how `mergeFormatMetadata` handles `kTblColwidths`). + +Architectural reference: `llm-docs/config-merging.md`. diff --git a/llm-docs/config-merging.md b/llm-docs/config-merging.md new file mode 100644 index 0000000000..4b6ae4b0a4 --- /dev/null +++ b/llm-docs/config-merging.md @@ -0,0 +1,159 @@ +--- +main_commit: 4aa86e524 +analyzed_date: 2026-05-20 +key_files: + - src/core/config.ts + - src/config/metadata.ts + - src/core/language.ts + - src/project/project-context.ts + - src/command/render/render-contexts.ts + - src/command/render/defaults.ts +--- + +# Configuration and metadata merging + +How Quarto deep-merges nested config trees — Pandoc defaults, +`format.metadata`, `format.language`, `variables`, project YAML. +Required reading before adding any helper that combines multiple +sources of structured config. + +## TL;DR + +``` +mergeConfigs (src/core/config.ts) + └─► lodash.mergeWith + mergeArrayCustomizer (array union-concat, dedup) + ▲ + ├── mergeConfigsCustomized (src/config/metadata.ts) + │ └─► caller-supplied per-key customizer, then array customizer + │ ▲ + │ ├── mergeFormatMetadata (kTblColwidths, kVariant, + │ │ kCodeLinks/kOtherLinks) + │ └── mergeProjectMetadata (string-vs-string contents:) + └── (call directly for plain nested merges with leaf-key precedence) +``` + +Never reach for raw `lodash.merge` or `Object.assign` for nested +configuration. Shallow spread `{ ...a, ...b }` is **only** safe when both +sides are flat records and you have verified there are no shared keys +that hold nested objects. + +## The canonical primitive: `mergeConfigs` + +`src/core/config.ts:10` exports `mergeConfigs(config: T, ...configs: Array): T`. +Wraps `lodash.mergeWith` with `mergeArrayCustomizer` and deep-clones every +input before merging (non-mutating). + +```ts +// usage shape +const merged = mergeConfigs(defaults, userOverrides); +``` + +**Semantics** + +- Objects: recursively deep-merged. Nested maps survive when not overridden + on the same path. +- Scalars (string, number, boolean, null): last-in wins on collision. +- Arrays: **union-concat with dedup by `JSON.stringify`** (not last-in + wins). Functions in arrays are kept and each given a fresh + `crypto.randomUUID()` key to bypass dedup. This is the lodash-uncommon + behavior — it surprises people coming from `_.merge`. +- Inputs are deep-cloned before merging — argument objects are not + mutated. + +**Array-replacement (rather than union)** requires going through +`mergeConfigsCustomized` with a per-key customizer that returns +`srcValue` for the keys you want to replace. See `mergeFormatMetadata` +handling of `kTblColwidths` for an in-tree example. + +## Format-level: `mergeFormatMetadata` + +`src/config/metadata.ts:266` wraps `mergeConfigsCustomized` for `Format` +objects (the `.pandoc`, `.metadata`, `.language`, `.render`, `.execute`, +etc. subtrees on a per-format basis). + +Adds three semantics on top of `mergeConfigs`: + +- `kTblColwidths`: `srcValue` always wins (replace, not array union). +- `kVariant`: Pandoc extension strings (`+yaml_metadata_block-tex_math_dollars`) + are merged via a dedicated extension-aware combiner so partial overrides + do not lose previously-enabled extensions. +- `kCodeLinks` / `kOtherLinks` (`kBooleanDisableArrays`): explicit `false` + becomes an empty array (the disable signal), not the default array-union. + +Use this — not raw `mergeConfigs` — anywhere two `Format` objects need to +combine (project default + directory `_metadata.yml` + document +frontmatter). + +## Project-level: `mergeProjectMetadata` + +`src/config/metadata.ts:294` wraps `mergeConfigsCustomized` for the project +config tree. Adds one rule: + +- `contents` key holding a string: `srcValue` replaces. Avoids spurious + concat of glob-string lists (`["docs/*"]` merged with `["src/*"]` would + produce `["docs/*", "src/*"]` under the default array-union, often + unintended for `contents:`). + +Reach for this when merging project YAML layers (project root, parent +project, includes). + +## The escape hatch: `mergeConfigsCustomized` + +`src/config/metadata.ts:317` — +`mergeConfigsCustomized(customizer, config, ...configs): T`. + +Use directly only when none of the named wrappers fit. The customizer +runs first; return `srcValue` (or `objValue`, or a computed value) to +short-circuit, or return `undefined` to fall through to +`mergeArrayCustomizer` and then to lodash's default object recursion. + +## In-tree call sites (representative) + +| Site | What's merged | +|---|---| +| `src/core/language.ts:formatLanguage` | `_language.yml` defaults ← user-supplied `language:` block. Same FormatLanguage shape Quarto exposes under `$quarto.language.$`. | +| `src/project/project-context.ts:resolveLanguageTranslations` | Project `kLanguageDefaults` ← `translations.language` (user overrides win on project language defaults). | +| `src/command/render/render-contexts.ts:resolveFormats` | Three-layer `mergeFormatMetadata(projFormat, directoryFormat, inputFormat)` — project / `_metadata.yml` / frontmatter, leaf-key precedence. | +| `src/command/render/defaults.ts:generateDefaults` | `variables.quarto.*` builder output ← user-supplied `variables.quarto.*` escape hatch. Internal namespace; user-set leaf keys win at any depth. | +| `src/command/render/pandoc.ts` | Multiple layered metadata merges across format / theme / extension contributions. | + +## Common pitfalls + +- **Shallow spread silently drops nested keys.** `{ ...a, ...b }` where + both have `b.language` → `a.language` is dropped entirely. Tests that + only probe the overridden leaf pass under this bug; the dropped sibling + keys silently resolve empty downstream. The fix is `mergeConfigs(a, b)`. +- **Array-union, not replacement.** A list-typed config key (filter + chain, contents glob list, format list) under `mergeConfigs` will grow + on each merge rather than replace. If replacement is intended, route + through `mergeConfigsCustomized` and return `srcValue` for that key, + or use the named wrapper that already does it (see + `mergeFormatMetadata` / `kTblColwidths`). +- **Order matters: defaults first, user last.** `mergeConfigs(defaults, user)` + — last argument wins on scalar collision. Reversing the argument order + silently makes Quarto-shipped defaults win over user values, which + almost never the intended direction. +- **Non-plain-object src values.** Passing a string, number, or array as + a sibling source to `mergeConfigs` exercises lodash's surprising + behavior of iterating the value's enumerable index keys. Guard with + `ld.isPlainObject(value)` before passing user-controlled values from + YAML. +- **`mergeFormatMetadata` is not `mergeConfigs`.** It deep-clones, merges + arrays the same way, **and** layers extra rules. If you are merging + `Format` objects, use the named wrapper — don't drop down to + `mergeConfigs` and re-invent the extra semantics. + +## Adding a new merge wrapper + +Only justified when there is a per-key behavior that none of the +existing wrappers handle. Steps: + +1. Add an export to `src/config/metadata.ts` (the natural home for + merge wrappers). +2. Wrap `mergeConfigsCustomized` with a customizer that handles the + per-key rule and returns `undefined` for everything else. +3. Add a unit test in `tests/unit/` covering: the new per-key rule + (positive), a non-affected key (regression — confirms the default + `mergeConfigs` behavior still flows through), and an empty/undefined + input (resilience). +4. Update this doc with the new wrapper row. diff --git a/llm-docs/localization-architecture.md b/llm-docs/localization-architecture.md new file mode 100644 index 0000000000..a366b43560 --- /dev/null +++ b/llm-docs/localization-architecture.md @@ -0,0 +1,252 @@ +--- +main_commit: 4aa86e524 +analyzed_date: 2026-05-20 +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/command/render/defaults.ts + - src/command/render/quarto-template-variables.ts + - 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) [2a] + │ └─► Lua filter params (JSON) + │ └─► param("key", default) in Lua filters + │ └─► written into Pandoc AST or meta + │ + ├─► explicit copies into format.metadata (pandoc.ts) [2b] + │ └─► Pandoc YAML metadata + │ └─► $key$ in Pandoc templates + │ └─► leaked into +yaml_metadata_block writers + │ + ├─► direct TS reads (HTML format extras, listings, [2c] + │ search, appendices) writing into rendered DOM + │ + └─► defaults-file variables.quarto.language [2d] + (defaults.ts:generateDefaults) + └─► Pandoc template variables + └─► $quarto.language.$ in any template + └─► NEVER reaches format.metadata, + so no +yaml_metadata_block leakage +``` + +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. Four downstream surfaces from `format.language` + +`format.language` is not consumed directly by Pandoc. Four 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. + +### 2d. Pandoc defaults-file `variables:` section (the bulk template channel) + +`src/command/render/quarto-template-variables.ts` defines the `QuartoTemplateVariables` interface and the `buildQuartoTemplateVariables(options)` builder that collects every contribution to the reserved `quarto.*` Pandoc template-variable namespace. Today the only field is `language: FormatLanguage`, sourced verbatim from `options.format.language`. `src/command/render/defaults.ts:generateDefaults` calls the builder and merges the result onto `allDefaults.variables.quarto`. When `writeDefaultsFile` serializes `allDefaults` to YAML, the nested map lands in the `variables:` section of the defaults file. Pandoc reads structured `variables:` values natively (per the Pandoc manual under `--variable`: *"Structured values (lists, maps) … can be assigned in the variables section of a defaults file"*). + +`quarto.*` is an internal namespace. The user-facing override path for localized strings is the top-level `language:` YAML key (resolved by `formatLanguage`, already merged into `options.format.language` before the builder runs). The schema does not advertise `variables.quarto.*` as a user option, but the merge in `generateDefaults` honors a user-set value on collision: it deep-merges via `mergeConfigs` (`src/core/config.ts`) so user-supplied keys win at any nesting depth while all other localized values remain available. For example, a user setting `variables.quarto.language.crossref-ch-prefix: Bouquin` overrides only that one leaf — `$quarto.language.toc-title-document$` still resolves to the localized fallback. (Same helper is used in `src/core/language.ts:formatLanguage` to merge user-supplied `language:` onto `_language.yml` defaults.) A non-object `variables.quarto` (string, number, array) is ignored defensively. New contributors to this file should design around the `language:` path, not around the escape hatch. + +From any Pandoc template — built-in (`html.template`, `latex.template`, `typst-template.typ`), extension partial, or custom user template — every localized key is then accessible as: + +```pandoc +$quarto.language.$ +``` + +For example, `$quarto.language.crossref-ch-prefix$` resolves to `Chapitre` when `lang: fr` is set. Hyphens within the leaf segment are valid Pandoc template-variable syntax (manual: *"Variable names can include letters, numbers, underscores, hyphens, and periods"*). + +This is the channel of choice for new template consumers because: +- It is bulk — every key in `format.language` is exposed, no per-key wiring. +- It does **not** touch `format.metadata`, so writers with `+yaml_metadata_block` (markdown, native, json) never serialize the values back into the document YAML header. +- It does not depend on Lua filter ordering or AST traversal. +- Pandoc's last-write-wins rule between defaults-file variables and `--variable` on the CLI lets users override individual values without breaking the bulk channel. + +It does **not** replace 2a — Lua filters still need `param("key")` because Pandoc templates and Lua filter params are independent surfaces. It does not replace 2b for the two specific keys that ship via `pandoc.ts` today (`toc-title`, `abstract-title`) — those are kept so existing built-in templates that read the flat `$toc-title$` and `$abstract-title$` keep working without a template change. New consumers should reach for 2d. + +## 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 (`src/resources/extension-subtrees/orange-book/_extensions/orange-book/typst-show.typ`): as of this writing the template still uses the legacy channel-2b form `$if(crossref.lof-title)$$crossref.lof-title$$else$$crossref-lof-title$$endif$` (and similarly for `lot-title`) to set `list-of-figure-title` and `list-of-table-title` on `book.with(...)`. The fallback `$crossref-lof-title$` resolves against `format.metadata` (channel 2b), so on writers like `+yaml_metadata_block` it can leak into the rendered markdown YAML header. There is no `supplement-chapter` argument wired today. Migration of orange-book's `typst-show.typ` to the `$quarto.language.*$` form (channel 2d), including adding `supplement-chapter`, is tracked at GitHub #14524 — once that lands the leakage path disappears. + +3. **Lua-injected supplements for refs** — `crossref/refs.lua:89-91` wraps every `@fig-foo`-style reference into `#ref(