I have:
Bug description
A key set under language that is not part of Quarto's own string set passes validation, is merged over the defaults, and is then discarded before templates see it.
Reading it back through $quarto.language.<key>$ yields an empty string, with no warning at any point.
translationsForLang rebuilds the language table from scratch and copies across only keys present in kLanguageDefaultsKeys or matching crossref-.*-title / crossref-.*-prefix, and formatLanguage runs it after merging the user's language object over the defaults.
So a key such as custom-key survives schema validation and the merge, then is dropped on the way out.
|
export function translationsForLang(language: FormatLanguage, lang: string) { |
|
// start with the defaults |
|
let translations = {} as FormatLanguage; |
|
Object.keys(language).forEach((key) => { |
|
// crossrefs can be custom, so be more lenient |
|
if ( |
|
kLanguageDefaultsKeys.includes(key) || |
|
key.match(/^crossref-.*-title$/) || |
|
key.match(/^crossref-.*-prefix$/) |
|
) { |
|
translations[key] = language[key]; |
|
} |
|
}); |
|
const defaultLanguage = translationsForLang( |
|
(await readDefaultLanguageTranslations(langCode)).language, |
|
langCode, |
|
); |
|
|
|
// merge any user provided language w/ the defaults |
|
language = mergeConfigs(defaultLanguage, language); |
|
|
|
// now select the correct variations based on the lang code and translations |
|
return translationsForLang(language, langCode); |
|
export const kLanguageDefaultsKeys = [ |
|
kTocTitleDocument, |
|
kTocTitleWebsite, |
This looks like a bug rather than a deliberate restriction, for four reasons:
-
The value is accepted before it is thrown away.
The format-language schema is an ordinary object with no closed: true, unlike neighbouring definitions such as website-about a few lines below, which do set it.
An unknown key is therefore valid input by the project's own schema rules.
-
The same function already supports user-defined keys.
The crossref-.*-title and crossref-.*-prefix escape hatches exist because users define their own crossref types, and the comment there reads "crossrefs can be custom, so be more lenient".
-
The documentation never describes a closed set.
https://quarto.org/docs/authoring/language.html presents language as a YAML file of translations and tells readers to copy _language.yml and translate the values.
-
The failure mode is the worst available one: neither a value nor a diagnostic.
In a template the result is an empty string, so the visible symptom is a blank heading or a missing label, at a distance from the cause.
A typo in a known key, say toc-title-documnt, behaves identically.
-
|
- id: format-language |
|
object: |
|
properties: |
|
toc-title-document: string |
|
toc-title-website: string |
-
|
- id: website-about |
|
object: |
|
closed: true |
It became more consequential with $quarto.language.*$.
Now that resolved strings are exposed to Pandoc templates under the reserved quarto.language namespace, format extensions can read localised strings but cannot supply any of their own.
An extension whose template contains wording such as "Signature", "Confidential" or "Page N of M" has no supported path to a localised value and must fall back to ordinary metadata, duplicating a mechanism that language already looks like it provides.
Steps to reproduce
issue.qmd:
---
title: "Custom `language` keys do not reach `$quarto.language.*$`"
format:
typst:
template-partials:
- typst-show.typ
keep-typ: true
language:
toc-title-document: "Overridden built-in"
custom-key: "Custom value"
---
The `language` schema accepts a free-form object, so `custom-key` above passes
validation, but only keys shipped in Quarto's `_language.yml` reach the
`quarto.language` template namespace.
typst-show.typ, next to it:
#show: doc => {
[built-in overridden: `$quarto.language.toc-title-document$`]
linebreak()
[built-in untouched: `$quarto.language.section-title-abstract$`]
linebreak()
[custom key: `$quarto.language.custom-key$`]
doc
}
Then:
quarto render issue.qmd
grep -n "built-in\|custom key" issue.typ
The same happens through the file form, language: custom.yml: the file loads, its override of a known key applies, and its custom key is dropped without an error.
Actual behavior
415: [built-in overridden: `Overridden built-in`]
417: [built-in untouched: `Abstract`]
419: [custom key: ``]
The first two lines confirm the namespace itself works and that user overrides are merged, so the empty value is specific to keys absent from _language.yml.
language entry |
Read back as |
Outcome |
toc-title-document: "Overridden built-in" |
Overridden built-in |
override honoured |
section-title-abstract (not set) |
Abstract |
default honoured |
crossref-xyz-title: "Custom crossref title" |
Custom crossref title |
custom key honoured |
custom-key: "Custom value" |
(empty) |
dropped |
Expected behavior
Line 419 to read `Custom value`.
More generally, either outcome would be fine, but the current silent empty string is neither:
- Pass unknown keys through.
Keep the filter for locale-variation subkeys but let unrecognised scalar keys survive into translations, so extension and template authors can define their own localised strings alongside Quarto's.
This is what this report proposes, and it needs one extra clause on the condition in translationsForLang.
- Reject unknown keys.
Set closed: true on format-language so an unrecognised key fails validation with a message.
This also catches typos in known keys, at the cost of ruling out extension-owned strings, and it would need the crossref patterns expressed in the schema.
Your environment
- IDE: Visual Studio Code
- OS: macOS,
darwin 25.6.0
Quarto check output
Confirmed on a local dev build of quarto-cli at abc6a78ed68f9e8bc9d54e27851093bd687a1cb7, which reports version 99.9.9.
Quarto 99.9.9
[✓] Checking environment information...
Quarto cache location: /Users/mcanouil/Library/Caches/quarto
[✓] Checking versions of quarto binary dependencies...
Pandoc version 3.10.0: OK
Dart Sass version 1.101.0: OK
Deno version 2.7.14: OK
Typst version 0.15.1: OK
[✓] Checking versions of quarto dependencies......OK
[✓] Checking Quarto installation......OK
Version: 99.9.9
commit: abc6a78ed68f9e8bc9d54e27851093bd687a1cb7
Path: /Users/mcanouil/Projects/quarto-dev/quarto-cli/package/dist/bin
[✓] Checking tools....................OK
TinyTeX: v2026.07
Chrome Headless Shell: (not installed)
VeraPDF: (not installed)
[✓] Checking LaTeX....................OK
Using: TinyTex
Path: /Users/mcanouil/Library/TinyTeX/bin/universal-darwin
Version: 2026
[✓] Checking Chrome Headless....................OK
Using: Chrome from QUARTO_CHROMIUM
Path: /Applications/Brave Browser.app/Contents/MacOS/Brave Browser
[✓] Checking basic markdown render....OK
[✓] Checking R installation...........OK
Version: 4.6.1
Path: /Library/Frameworks/R.framework/Versions/4.6/Resources
knitr: 1.51
rmarkdown: 2.31
[✓] Checking Knitr engine render......OK
[✓] Checking Python 3 installation....OK
Version: 3.14.3
Jupyter: 5.9.1
Kernels: julia-1.12, python3
[✓] Checking Jupyter engine render....OK
I have:
Bug description
A key set under
languagethat is not part of Quarto's own string set passes validation, is merged over the defaults, and is then discarded before templates see it.Reading it back through
$quarto.language.<key>$yields an empty string, with no warning at any point.translationsForLangrebuilds the language table from scratch and copies across only keys present inkLanguageDefaultsKeysor matchingcrossref-.*-title/crossref-.*-prefix, andformatLanguageruns it after merging the user'slanguageobject over the defaults.So a key such as
custom-keysurvives schema validation and the merge, then is dropped on the way out.quarto-cli/src/core/language.ts
Lines 161 to 173 in abc6a78
quarto-cli/src/core/language.ts
Lines 201 to 210 in abc6a78
quarto-cli/src/config/constants.ts
Lines 385 to 387 in abc6a78
This looks like a bug rather than a deliberate restriction, for four reasons:
The value is accepted before it is thrown away.
The
format-languageschema is an ordinaryobjectwith noclosed: true, unlike neighbouring definitions such aswebsite-abouta few lines below, which do set it.An unknown key is therefore valid input by the project's own schema rules.
The same function already supports user-defined keys.
The
crossref-.*-titleandcrossref-.*-prefixescape hatches exist because users define their own crossref types, and the comment there reads "crossrefs can be custom, so be more lenient".The documentation never describes a closed set.
https://quarto.org/docs/authoring/language.html presents
languageas a YAML file of translations and tells readers to copy_language.ymland translate the values.The failure mode is the worst available one: neither a value nor a diagnostic.
In a template the result is an empty string, so the visible symptom is a blank heading or a missing label, at a distance from the cause.
A typo in a known key, say
toc-title-documnt, behaves identically.quarto-cli/src/resources/schema/definitions.yml
Lines 1372 to 1376 in abc6a78
quarto-cli/src/resources/schema/definitions.yml
Lines 1439 to 1441 in abc6a78
It became more consequential with
$quarto.language.*$.Now that resolved strings are exposed to Pandoc templates under the reserved
quarto.languagenamespace, format extensions can read localised strings but cannot supply any of their own.An extension whose template contains wording such as "Signature", "Confidential" or "Page N of M" has no supported path to a localised value and must fall back to ordinary metadata, duplicating a mechanism that
languagealready looks like it provides.Steps to reproduce
issue.qmd:typst-show.typ, next to it:Then:
quarto render issue.qmd grep -n "built-in\|custom key" issue.typThe same happens through the file form,
language: custom.yml: the file loads, its override of a known key applies, and its custom key is dropped without an error.Actual behavior
The first two lines confirm the namespace itself works and that user overrides are merged, so the empty value is specific to keys absent from
_language.yml.languageentrytoc-title-document: "Overridden built-in"Overridden built-insection-title-abstract(not set)Abstractcrossref-xyz-title: "Custom crossref title"Custom crossref titlecustom-key: "Custom value"Expected behavior
Line 419 to read
`Custom value`.More generally, either outcome would be fine, but the current silent empty string is neither:
Keep the filter for locale-variation subkeys but let unrecognised scalar keys survive into
translations, so extension and template authors can define their own localised strings alongside Quarto's.This is what this report proposes, and it needs one extra clause on the condition in
translationsForLang.Set
closed: trueonformat-languageso an unrecognised key fails validation with a message.This also catches typos in known keys, at the cost of ruling out extension-owned strings, and it would need the crossref patterns expressed in the schema.
Your environment
darwin25.6.0Quarto check output
Confirmed on a local dev build of
quarto-cliatabc6a78ed68f9e8bc9d54e27851093bd687a1cb7, which reports version99.9.9.Quarto 99.9.9 [✓] Checking environment information... Quarto cache location: /Users/mcanouil/Library/Caches/quarto [✓] Checking versions of quarto binary dependencies... Pandoc version 3.10.0: OK Dart Sass version 1.101.0: OK Deno version 2.7.14: OK Typst version 0.15.1: OK [✓] Checking versions of quarto dependencies......OK [✓] Checking Quarto installation......OK Version: 99.9.9 commit: abc6a78ed68f9e8bc9d54e27851093bd687a1cb7 Path: /Users/mcanouil/Projects/quarto-dev/quarto-cli/package/dist/bin [✓] Checking tools....................OK TinyTeX: v2026.07 Chrome Headless Shell: (not installed) VeraPDF: (not installed) [✓] Checking LaTeX....................OK Using: TinyTex Path: /Users/mcanouil/Library/TinyTeX/bin/universal-darwin Version: 2026 [✓] Checking Chrome Headless....................OK Using: Chrome from QUARTO_CHROMIUM Path: /Applications/Brave Browser.app/Contents/MacOS/Brave Browser [✓] Checking basic markdown render....OK [✓] Checking R installation...........OK Version: 4.6.1 Path: /Library/Frameworks/R.framework/Versions/4.6/Resources knitr: 1.51 rmarkdown: 2.31 [✓] Checking Knitr engine render......OK [✓] Checking Python 3 installation....OK Version: 3.14.3 Jupyter: 5.9.1 Kernels: julia-1.12, python3 [✓] Checking Jupyter engine render....OK