From 34ccf293cce17244b5591917eec24ac4ecdf1f8d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 09:33:22 +0000 Subject: [PATCH 1/4] Install hyphenation package when babel fails with an undefined language With `lang: de-CH` and no hyphen-german installed, babel-german checks `\iflanguage{ngerman}` at begin document, and babel raises a fatal "You haven't defined the language 'ngerman' yet" error. Missing hyphenation patterns were only handled as babel warnings on a successful compile, so the failure branch found nothing to install and gave up. Detect this error on the failure path and install the matching `hyphen-` package directly by name, reusing the language-to-package mapping of findMissingHyphenationFiles (now a shared helper). Fixes rstudio/tinytex#515 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PPRhHuCKXMd1s1wZgEe1M4 --- news/changelog-1.11.md | 1 + src/command/render/latexmk/parse-error.ts | 70 ++++++++++++------ src/command/render/latexmk/pdf.ts | 13 +++- .../docs/smoke-all/2026/10/12/tinytex-515.qmd | 26 +++++++ tests/unit/latexmk/parse-error.test.ts | 72 ++++++++++++++++++- 5 files changed, 155 insertions(+), 27 deletions(-) create mode 100644 tests/docs/smoke-all/2026/10/12/tinytex-515.qmd diff --git a/news/changelog-1.11.md b/news/changelog-1.11.md index 4944dc36ba8..2b27fcd1d24 100644 --- a/news/changelog-1.11.md +++ b/news/changelog-1.11.md @@ -45,6 +45,7 @@ All changes included in 1.11: ### `pdf` - ([#14982](https://github.com/quarto-dev/quarto-cli/issues/14982)): Fix a footnote in document metadata, such as `title` or `author`, making PDF and Typst output shift heading levels, which turned the first level-1 heading into the document title. +- ([rstudio/tinytex#515](https://github.com/rstudio/tinytex/issues/515)): Fix PDF rendering with regional German variants such as `lang: de-CH` failing when German hyphenation patterns are not installed, instead of installing them automatically. ### `revealjs` diff --git a/src/command/render/latexmk/parse-error.ts b/src/command/render/latexmk/parse-error.ts index 2b760243231..6407816d391 100644 --- a/src/command/render/latexmk/parse-error.ts +++ b/src/command/render/latexmk/parse-error.ts @@ -156,36 +156,62 @@ export function findPdfAccessibilityWarnings( return result; } -// Finds missing hyphenation files (these appear as warnings in the log file) -export function findMissingHyphenationFiles(logText: string) { - //ngerman gets special cased - const filterLang = (lang: string) => { - // It seems some languages have no hyphenation files, so we just filter them out - // e.g. `lang: zh` has no hyphenation files - // https://github.com/quarto-dev/quarto-cli/issues/10291 - const noHyphen = ["chinese-hans", "chinese"]; - if (noHyphen.includes(lang)) { - return; - } +// Maps a babel language name to the TeX Live package providing its +// hyphenation patterns (undefined when there is no such package) +function hyphenationPackageForLanguage(lang: string) { + // It seems some languages have no hyphenation files, so we just filter them out + // e.g. `lang: zh` has no hyphenation files + // https://github.com/quarto-dev/quarto-cli/issues/10291 + const noHyphen = ["chinese-hans", "chinese"]; + if (noHyphen.includes(lang)) { + return; + } + + // NOTE Although the names of the corresponding lfd files match those in this list, + // there are some exceptions, particularly in German and Serbian. So, ngerman is + // called here german, which is the name in the CLDR and, actually, the most logical. + // + // See https://ctan.math.utah.edu/ctan/tex-archive/macros/latex/required/babel/base/babel.pdf + if (lang === "ngerman") { + return "hyphen-german"; + } + return `hyphen-${lang.toLowerCase()}`; +} - // NOTE Although the names of the corresponding lfd files match those in this list, - // there are some exceptions, particularly in German and Serbian. So, ngerman is - // called here german, which is the name in the CLDR and, actually, the most logical. - // - // See https://ctan.math.utah.edu/ctan/tex-archive/macros/latex/required/babel/base/babel.pdf - if (lang === "ngerman") { - return "hyphen-german"; +// babel raises a fatal error (rather than the warning handled by +// findMissingHyphenationFiles) when a language is referenced but has neither +// hyphenation patterns in the format nor a dialect fallback. For instance, +// babel-german checks `\iflanguage{ngerman}` at begin document, which fails for +// `lang: de-CH` or `lang: de-AT` when hyphen-german is not installed: +// ! Package babel Error: You haven't defined the language 'ngerman' yet. +// https://github.com/rstudio/tinytex/issues/515 +// Older babel versions use `ngerman' or no quotes at all. +const kBabelUndefinedLanguageRegex = + /Package babel Error: You haven't defined the language [`']?([^`'\s]+)[`']? yet/g; + +// Finds hyphenation packages whose absence makes the compilation fail +// (to be installed directly by name, as opposed to the file search terms +// returned by findMissingFontsAndPackages) +export function findMissingHyphenationPackages(logText: string): string[] { + const packages: string[] = []; + for (const match of logText.matchAll(kBabelUndefinedLanguageRegex)) { + const pkg = hyphenationPackageForLanguage(match[1]); + if (pkg) { + packages.push(pkg); } - return `hyphen-${lang.toLowerCase()}`; - }; + } + return ld.uniq(packages); +} +// Finds missing hyphenation files (these appear as warnings in the log file) +export function findMissingHyphenationFiles(logText: string) { const babelWarningRegex = /^Package babel Warning:/m; const hasWarning = logText.match(babelWarningRegex); if (hasWarning) { const languageRegex = /^\(babel\).* language [`'](\S+)[`'].*$/m; const languageMatch = logText.match(languageRegex); if (languageMatch) { - return filterLang(languageMatch[1]); + return hyphenationPackageForLanguage(languageMatch[1]); } } @@ -196,7 +222,7 @@ export function findMissingHyphenationFiles(logText: string) { if (match) { const language = match[1]; if (language) { - return filterLang(language); + return hyphenationPackageForLanguage(language); } } } diff --git a/src/command/render/latexmk/pdf.ts b/src/command/render/latexmk/pdf.ts index 2e7d5010de2..756906d21e3 100644 --- a/src/command/render/latexmk/pdf.ts +++ b/src/command/render/latexmk/pdf.ts @@ -21,6 +21,7 @@ import { findLatexError, findMissingFontsAndPackages, findMissingHyphenationFiles, + findMissingHyphenationPackages, findPdfAccessibilityWarnings, kMissingFontLog, needsRecompilation, @@ -28,6 +29,7 @@ import { import { error, info, warning } from "../../../deno_ral/log.ts"; import { logProgress } from "../../../core/log.ts"; import { isWindows } from "../../../deno_ral/platform.ts"; +import * as ld from "../../../core/lodash.ts"; export async function generatePdf(mkOptions: LatexmkOptions): Promise { if (!mkOptions.quiet) { @@ -470,8 +472,15 @@ async function findAndInstallPackages( const logText = Deno.readTextFileSync(logFile); const searchTerms = findMissingFontsAndPackages(logText, dirname(logFile)); - if (searchTerms.length > 0) { - const packages = await pkgMgr.searchPackages(searchTerms); + // Packages known by name (no tlmgr file search needed) + const namedPackages = findMissingHyphenationPackages(logText); + if (searchTerms.length > 0 || namedPackages.length > 0) { + const packages = ld.uniq([ + ...namedPackages, + ...(searchTerms.length > 0 + ? await pkgMgr.searchPackages(searchTerms) + : []), + ]); if (packages.length > 0) { const packagesInstalled = await pkgMgr.installPackages( packages, diff --git a/tests/docs/smoke-all/2026/10/12/tinytex-515.qmd b/tests/docs/smoke-all/2026/10/12/tinytex-515.qmd new file mode 100644 index 00000000000..5efccd305b3 --- /dev/null +++ b/tests/docs/smoke-all/2026/10/12/tinytex-515.qmd @@ -0,0 +1,26 @@ +--- +title: "Swiss German hyphenation auto-install (rstudio/tinytex#515)" +format: pdf +lang: de-CH +_quarto: + tests: + pdf: null +--- + +```{r} +#| eval: !expr isFALSE(as.logical(Sys.getenv("CI", "false"))) +#| include: false +#| messages: "NA" + +# Remove the German babel and hyphenation packages to test auto-installation. +# Without hyphen-german, babel fails with a fatal error for `lang: de-CH`: +# "You haven't defined the language 'ngerman' yet." +for (pkg in c("hyphen-german", "babel-german")) { + if (tinytex::check_installed(pkg)) { + message("Removing '", pkg, "' package for the render") + tinytex::tlmgr_remove(pkg) + } +} +``` + +Dieses Dokument prüft, dass Quarto `babel-german` und `hyphen-german` automatisch installiert, wenn `lang: de-CH` verwendet wird. diff --git a/tests/unit/latexmk/parse-error.test.ts b/tests/unit/latexmk/parse-error.test.ts index 2410e00a66b..8acc8b121d7 100644 --- a/tests/unit/latexmk/parse-error.test.ts +++ b/tests/unit/latexmk/parse-error.test.ts @@ -5,9 +5,9 @@ * */ -import { findLatexError, findMissingFontsAndPackages, findMissingHyphenationFiles } from "../../../src/command/render/latexmk/parse-error.ts" +import { findLatexError, findMissingFontsAndPackages, findMissingHyphenationFiles, findMissingHyphenationPackages } from "../../../src/command/render/latexmk/parse-error.ts" import { unitTest } from "../../test.ts"; -import { assert } from "testing/asserts"; +import { assert, assertEquals } from "testing/asserts"; function fontSearchTerm(font: string): string { const fontPattern = font.replace(/\s+/g, '\\s*'); @@ -163,4 +163,70 @@ unitTest("Detect missing hyphenation with babel warnings", async () => { ); }, { cwd: () => "unit/latexmk/" -}) \ No newline at end of file +}) + +unitTest("Detect missing hyphenation causing a fatal babel error", async () => { + // `lang: de-CH` without hyphen-german (https://github.com/rstudio/tinytex/issues/515): + // babel-german defines nswissgerman as a dialect of the null language, but then + // checks `\iflanguage{ngerman}`, which is a fatal error, so compilation fails. + const logSwissGerman = `Package babel Warning: No hyphenation patterns were preloaded for +(babel) the language 'German (current orthography), needed by Swiss Standard German (current orthography)' into the format. +(babel) Please, configure your TeX system to add them and +(babel) rebuild the format. Now I will use the patterns +(babel) preloaded for \\language=0 instead on input line 1260. + +Package babel Info: Hyphen rules for 'nswissgerman' set to \\l@nil +(babel) (\\language5). Reported on input line 1260. + +! Package babel Error: You haven't defined the language 'ngerman' yet. +(babel) Perhaps you misspelled it or your installation +(babel) is not complete. + +See the babel package documentation for explanation. +Type H for immediate help.`; + assertEquals( + findMissingHyphenationPackages(logSwissGerman), + ["hyphen-german"], + "Should map the undefined ngerman language to hyphen-german", + ); + // The file-based search finds nothing for this error + assertEquals(findMissingFontsAndPackages(logSwissGerman, "."), []); + + // Backtick-quote format (older babel) + assertEquals( + findMissingHyphenationPackages( + "! Package babel Error: You haven't defined the language `ngerman' yet.", + ), + ["hyphen-german"], + ); + // Unquoted format (older babel) + assertEquals( + findMissingHyphenationPackages( + "! Package babel Error: You haven't defined the language ngerman yet.", + ), + ["hyphen-german"], + ); + // Other languages are lowercased and prefixed + assertEquals( + findMissingHyphenationPackages( + "! Package babel Error: You haven't defined the language 'Spanish' yet.", + ), + ["hyphen-spanish"], + ); + // Languages without hyphenation package are ignored + assertEquals( + findMissingHyphenationPackages( + "! Package babel Error: You haven't defined the language 'chinese-hans' yet.", + ), + [], + ); + // Only the fatal error is considered, not babel warnings + assertEquals( + findMissingHyphenationPackages(`Package babel Warning: No hyphenation patterns were preloaded for +(babel) the language 'Spanish' into the format.`), + [], + ); + assertEquals(findMissingHyphenationPackages("asdf qwer"), []); +}, { + cwd: () => "unit/latexmk/" +}) From 04f41d4e850c8c4c9c346f6fcd51902fc6c9b9a9 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Mon, 12 Oct 2026 15:00:09 +0200 Subject: [PATCH 2/4] Run #15027 smoke test package removal only on CI The test now removes hyphen-german and babel-german only on CI, so the CI run exercises the auto-installation path, and local runs no longer uninstall packages from the developer's TinyTeX. The document and the changelog entry now point at the quarto-cli issue. --- news/changelog-1.11.md | 2 +- .../smoke-all/2026/10/12/{tinytex-515.qmd => 15027.qmd} | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) rename tests/docs/smoke-all/2026/10/12/{tinytex-515.qmd => 15027.qmd} (77%) diff --git a/news/changelog-1.11.md b/news/changelog-1.11.md index 2b27fcd1d24..9f5bba7c53e 100644 --- a/news/changelog-1.11.md +++ b/news/changelog-1.11.md @@ -45,7 +45,7 @@ All changes included in 1.11: ### `pdf` - ([#14982](https://github.com/quarto-dev/quarto-cli/issues/14982)): Fix a footnote in document metadata, such as `title` or `author`, making PDF and Typst output shift heading levels, which turned the first level-1 heading into the document title. -- ([rstudio/tinytex#515](https://github.com/rstudio/tinytex/issues/515)): Fix PDF rendering with regional German variants such as `lang: de-CH` failing when German hyphenation patterns are not installed, instead of installing them automatically. +- ([#15027](https://github.com/quarto-dev/quarto-cli/issues/15027)): Fix PDF rendering with regional German variants such as `lang: de-CH` failing when German hyphenation patterns are not installed, instead of installing them automatically. ### `revealjs` diff --git a/tests/docs/smoke-all/2026/10/12/tinytex-515.qmd b/tests/docs/smoke-all/2026/10/12/15027.qmd similarity index 77% rename from tests/docs/smoke-all/2026/10/12/tinytex-515.qmd rename to tests/docs/smoke-all/2026/10/12/15027.qmd index 5efccd305b3..9ca84f6fa5e 100644 --- a/tests/docs/smoke-all/2026/10/12/tinytex-515.qmd +++ b/tests/docs/smoke-all/2026/10/12/15027.qmd @@ -1,5 +1,5 @@ --- -title: "Swiss German hyphenation auto-install (rstudio/tinytex#515)" +title: "Swiss German hyphenation auto-install (#15027)" format: pdf lang: de-CH _quarto: @@ -7,8 +7,10 @@ _quarto: pdf: null --- +**Only remove packages on CI** + ```{r} -#| eval: !expr isFALSE(as.logical(Sys.getenv("CI", "false"))) +#| eval: !expr isTRUE(as.logical(Sys.getenv("CI", "false"))) #| include: false #| messages: "NA" @@ -23,4 +25,4 @@ for (pkg in c("hyphen-german", "babel-german")) { } ``` -Dieses Dokument prüft, dass Quarto `babel-german` und `hyphen-german` automatisch installiert, wenn `lang: de-CH` verwendet wird. +Dieses Dokument prüft, dass Quarto `babel-german` und `hyphen-german` automatisch installiert, wenn `lang: de-CH` verwendet wird (siehe auch rstudio/tinytex#515). From a038fe12a9753998132717498d00ed9a50f791b8 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Mon, 12 Oct 2026 15:02:10 +0200 Subject: [PATCH 3/4] Only install hyphenation packages with a valid package name The language name in babel messages comes from the LaTeX log and ends up as a tlmgr argument. On Windows tlmgr.bat is called through a temporary .bat file, and requireQuoting() does not handle `"`, `%`, `^` or `!`, so a crafted name in a document's log could be interpreted by cmd. Every hyphenation name in texlive.tlpdb (102 names and synonyms) is made of lowercase letters, digits and hyphens, so names that don't match ^hyphen-[a-z0-9-]+$ are dropped. This applies to both the babel error and the older babel warning detection, which share the mapping function. Same validation as rstudio/tinytex#516. --- src/command/render/latexmk/parse-error.ts | 9 ++++--- tests/unit/latexmk/parse-error.test.ts | 30 +++++++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/command/render/latexmk/parse-error.ts b/src/command/render/latexmk/parse-error.ts index 6407816d391..c3358b52989 100644 --- a/src/command/render/latexmk/parse-error.ts +++ b/src/command/render/latexmk/parse-error.ts @@ -172,10 +172,13 @@ function hyphenationPackageForLanguage(lang: string) { // called here german, which is the name in the CLDR and, actually, the most logical. // // See https://ctan.math.utah.edu/ctan/tex-archive/macros/latex/required/babel/base/babel.pdf - if (lang === "ngerman") { - return "hyphen-german"; + const pkg = lang === "ngerman" ? "hyphen-german" : `hyphen-${lang.toLowerCase()}`; + // The name comes from the log and is passed to tlmgr (through a .bat file + // on Windows), so only keep what looks like a package name (all + // hyphenation names in texlive.tlpdb are lowercase letters, digits and hyphens) + if (/^hyphen-[a-z0-9-]+$/.test(pkg)) { + return pkg; } - return `hyphen-${lang.toLowerCase()}`; } // babel raises a fatal error (rather than the warning handled by diff --git a/tests/unit/latexmk/parse-error.test.ts b/tests/unit/latexmk/parse-error.test.ts index 8acc8b121d7..80058e37930 100644 --- a/tests/unit/latexmk/parse-error.test.ts +++ b/tests/unit/latexmk/parse-error.test.ts @@ -230,3 +230,33 @@ Type H for immediate help.`; }, { cwd: () => "unit/latexmk/" }) + +unitTest("Drop hyphenation package names that are not valid package names", async () => { + // Language names come from the log and end up as tlmgr arguments, which + // go through a .bat file on Windows + for (const lang of ['x"&calc', "ngerman;id", "%PATH%", "a|b", "a^b", "--repository=x"]) { + assertEquals( + findMissingHyphenationPackages( + `! Package babel Error: You haven't defined the language '${lang}' yet.`, + ), + [], + `Should drop '${lang}' from the babel error`, + ); + assertEquals( + findMissingHyphenationFiles(`Package babel Warning: No hyphenation patterns were preloaded for +(babel) the language '${lang}' into the format.`), + undefined, + `Should drop '${lang}' from the babel warning`, + ); + } + // Valid names are kept + assertEquals( + findMissingHyphenationPackages( + `! Package babel Error: You haven't defined the language 'x"&calc' yet. +! Package babel Error: You haven't defined the language 'french' yet.`, + ), + ["hyphen-french"], + ); +}, { + cwd: () => "unit/latexmk/" +}) From 2d0a54d30bd65abb8315bff3bbb41840dbe595b7 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Mon, 12 Oct 2026 15:05:36 +0200 Subject: [PATCH 4/4] Map hyphenation language names to the package that provides them Only ngerman was mapped, so any other pattern name whose package is not hyphen- asked tlmgr for a package that doesn't exist, e.g. swissgerman (hyphen-german), nynorsk (hyphen-norwegian) or hindi (hyphen-indic). The table lists every such name from the `execute AddHyphen` lines (name= and synonyms=) of the current texlive.tlpdb: 16 pattern names, 9 older synonyms and 12 Indic languages, 37 in total. That's the same table as rstudio/tinytex#516. The 4 german-x-* names provided by dehyph-exptl are left out, as they are not hyphen-* packages. Names are lowercased before the lookup, since language.dat uses mixed case names such as UKenglish. --- src/command/render/latexmk/parse-error.ts | 52 +++++++++++++++- tests/unit/latexmk/parse-error.test.ts | 76 +++++++++++++++++++++++ 2 files changed, 127 insertions(+), 1 deletion(-) diff --git a/src/command/render/latexmk/parse-error.ts b/src/command/render/latexmk/parse-error.ts index c3358b52989..a9c1588b0c0 100644 --- a/src/command/render/latexmk/parse-error.ts +++ b/src/command/render/latexmk/parse-error.ts @@ -156,9 +156,56 @@ export function findPdfAccessibilityWarnings( return result; } +// Hyphenation pattern names (in language.dat) whose package is not +// hyphen-, according to `execute AddHyphen` in texlive.tlpdb +const kHyphenationPackageNames: Record = { + ngerman: "german", + swissgerman: "german", + pinyin: "chinese", + ukenglish: "english", + british: "english", + usenglishmax: "english", + bokmal: "norwegian", + norsk: "norwegian", + nynorsk: "norwegian", + schoolfinnish: "finnish", + monogreek: "greek", + ibycus: "ancientgreek", + classiclatin: "latin", + liturgicallatin: "latin", + mongolianlmc: "mongolian", + serbianc: "serbian", + // older names (synonyms in texlive.tlpdb) + amharic: "ethiopic", + geez: "ethiopic", + persian: "farsi", + patois: "french", + francais: "french", + polygreek: "greek", + portuges: "portuguese", + slovene: "slovenian", + espanol: "spanish", + // all of these are in the hyphen-indic package + assamese: "indic", + bengali: "indic", + gujarati: "indic", + hindi: "indic", + kannada: "indic", + malayalam: "indic", + marathi: "indic", + oriya: "indic", + pali: "indic", + panjabi: "indic", + tamil: "indic", + telugu: "indic", +}; + // Maps a babel language name to the TeX Live package providing its // hyphenation patterns (undefined when there is no such package) function hyphenationPackageForLanguage(lang: string) { + // Names in language.dat may be in mixed case (e.g. UKenglish) + lang = lang.toLowerCase(); + // It seems some languages have no hyphenation files, so we just filter them out // e.g. `lang: zh` has no hyphenation files // https://github.com/quarto-dev/quarto-cli/issues/10291 @@ -172,7 +219,10 @@ function hyphenationPackageForLanguage(lang: string) { // called here german, which is the name in the CLDR and, actually, the most logical. // // See https://ctan.math.utah.edu/ctan/tex-archive/macros/latex/required/babel/base/babel.pdf - const pkg = lang === "ngerman" ? "hyphen-german" : `hyphen-${lang.toLowerCase()}`; + const name = Object.hasOwn(kHyphenationPackageNames, lang) + ? kHyphenationPackageNames[lang] + : lang; + const pkg = `hyphen-${name}`; // The name comes from the log and is passed to tlmgr (through a .bat file // on Windows), so only keep what looks like a package name (all // hyphenation names in texlive.tlpdb are lowercase letters, digits and hyphens) diff --git a/tests/unit/latexmk/parse-error.test.ts b/tests/unit/latexmk/parse-error.test.ts index 80058e37930..0c991a0b56c 100644 --- a/tests/unit/latexmk/parse-error.test.ts +++ b/tests/unit/latexmk/parse-error.test.ts @@ -260,3 +260,79 @@ unitTest("Drop hyphenation package names that are not valid package names", asyn }, { cwd: () => "unit/latexmk/" }) + +unitTest("Map hyphenation language names to their TeX Live package", async () => { + // Names whose package is not hyphen-, according to + // `execute AddHyphen` (name= and synonyms=) in texlive.tlpdb + const expected: Record = { + ngerman: "hyphen-german", + swissgerman: "hyphen-german", + pinyin: "hyphen-chinese", + ukenglish: "hyphen-english", + british: "hyphen-english", + usenglishmax: "hyphen-english", + bokmal: "hyphen-norwegian", + norsk: "hyphen-norwegian", + nynorsk: "hyphen-norwegian", + schoolfinnish: "hyphen-finnish", + monogreek: "hyphen-greek", + polygreek: "hyphen-greek", + ibycus: "hyphen-ancientgreek", + classiclatin: "hyphen-latin", + liturgicallatin: "hyphen-latin", + mongolianlmc: "hyphen-mongolian", + serbianc: "hyphen-serbian", + amharic: "hyphen-ethiopic", + geez: "hyphen-ethiopic", + persian: "hyphen-farsi", + patois: "hyphen-french", + francais: "hyphen-french", + portuges: "hyphen-portuguese", + slovene: "hyphen-slovenian", + espanol: "hyphen-spanish", + assamese: "hyphen-indic", + bengali: "hyphen-indic", + gujarati: "hyphen-indic", + hindi: "hyphen-indic", + kannada: "hyphen-indic", + malayalam: "hyphen-indic", + marathi: "hyphen-indic", + oriya: "hyphen-indic", + pali: "hyphen-indic", + panjabi: "hyphen-indic", + tamil: "hyphen-indic", + telugu: "hyphen-indic", + }; + for (const [lang, pkg] of Object.entries(expected)) { + assertEquals( + findMissingHyphenationPackages( + `! Package babel Error: You haven't defined the language '${lang}' yet.`, + ), + [pkg], + `Should map '${lang}' to ${pkg} from the babel error`, + ); + assertEquals( + findMissingHyphenationFiles(`Package babel Warning: No hyphenation patterns were preloaded for +(babel) the language '${lang}' into the format.`), + pkg, + `Should map '${lang}' to ${pkg} from the babel warning`, + ); + } + // Names in language.dat may be in mixed case + assertEquals( + findMissingHyphenationPackages( + "! Package babel Error: You haven't defined the language 'UKenglish' yet.", + ), + ["hyphen-english"], + ); + // Several languages from the same package are installed once + assertEquals( + findMissingHyphenationPackages( + `! Package babel Error: You haven't defined the language 'hindi' yet. +! Package babel Error: You haven't defined the language 'tamil' yet.`, + ), + ["hyphen-indic"], + ); +}, { + cwd: () => "unit/latexmk/" +})