Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# CHANGES IN tinytex VERSION 0.62

- `latexmk()` can automatically install missing `hyphen-*` packages when the missing hyphenation patterns lead to the babel error `You haven't defined the language 'xxx' yet` instead of a warning, e.g., for the babel option `nswissgerman` (from `lang: de-CH` in R Markdown or Quarto), which needs the patterns of `ngerman` from **hyphen-german** (thanks, @stibu81, #515). Language names found in the log are now only used if they look like valid package names, and more of them are mapped to the package that provides them, e.g., `hindi` and the other Indic languages to **hyphen-indic**, and `slovene` to **hyphen-slovenian**.

# CHANGES IN tinytex VERSION 0.61

Expand Down
60 changes: 51 additions & 9 deletions R/latex.R
Original file line number Diff line number Diff line change
Expand Up @@ -426,11 +426,51 @@ check_extra = function(file) {

check_babel = function(text) {
r = "^(\\(babel\\).* |Package babel Warning: No hyphenation patterns were preloaded for the )language [`']([^']+)'.*$"
if (length(m <- grep_sub(r, 'hyphen-\\2', text)) == 0) return(FALSE)
if (length(m <- grep_sub(r, '\\2', text)) == 0) return(FALSE)
length(pkgs <- hyphen_pkgs(m)) > 0 && tlmgr_install(pkgs) == 0
}

# map language names in babel/polyglossia messages to TeX Live hyphen-* packages
hyphen_pkgs = function(x) {
x = tolower(gsub("[`']", '', x))
# (babel) the language `German (new orthography)' into the format
m = gsub('\\s.*', '', m)
m[m == 'hyphen-pinyin'] = 'hyphen-chinese'
tlmgr_install(tolower(m)) == 0
x = gsub('\\s.*', '', x)
# hyphenation pattern names (in language.dat) that differ from the package
# names, according to `execute AddHyphen` in texlive.tlpdb
m = c(
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'
)
Comment on lines +440 to +455

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to retrieve these names programmatically (from language.dat/texlive.tlpdb) so we don't need to hardcode them here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I did get all them from texlive.tlpdb but I think I needed to download it and it was not locally available in my install. Though it was quick.

So I can see if I can extract the logic to either generate this list on a schedule basis or before each release we do, instead of dynamically.

but you're right - this list came from an analysis of some data I made.

@cderv cderv Oct 13, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked again at where these names came from, and I don't think we can read them from the local files.

language.dat and the local texlive.tlpdb only contain the packages that are installed. The package we are looking for is the missing one, so it is never in there. The full list is only in the remote texlive.tlpdb, in the execute AddHyphen name=... synonyms=... lines of each package.

tlmgr does keep a cached copy of the remote db (tlpkg/texlive.tlpdb.main.<md5>, ~20MB) and it has all the names. But we delete these files ourselves when building the bundles, and also with options(tinytex.delete_tlpdb = TRUE):

tinytex/R/tlmgr.R

Lines 258 to 262 in 0997537

delete_tlpdb_files = function() {
if ((root <- tinytex_root(FALSE)) != '') file.remove(list.files(
file.path(root, 'tlpkg'), '^texlive[.]tlpdb.*[.][0-9a-f]{32}$', full.names = TRUE
))
}

So we can't count on it, and we would still need the table as a fallback. Using tlmgr dump-tlpdb --remote instead would download the full db each time this error happens, which seems too much for this.

What we could do is generate the table instead: a script in tools/ that downloads texlive.tlpdb.xz from CTAN, parses the AddHyphen lines, and writes the table as R code, keeping only the names whose package is not hyphen-<name>. We could run it before each release, or in a scheduled workflow that opens a PR when the table changes. I tried a quick version and it gives the same 37 entries as here, plus the 4 german-x-* names from dehyph-exptl that I listed as not handled in the PR description.

I would do this in a follow-up PR, and we can discuss there the best way ?

i = x %in% names(m)
x[i] = m[x[i]]
x = unique(paste0('hyphen-', x))
# names come from a log file, so only keep what looks like a package name
# (all names in language.dat/texlive.tlpdb are lowercase letters, digits, and
# hyphens), since package names are passed to tlmgr via system2()
x[grepl('^hyphen-[a-z0-9-]+$', x)]
}

# missing hyphenation patterns may also lead to babel errors (instead of
# warnings), e.g., with the babel option nswissgerman (#515):
# ! Package babel Error: You haven't defined the language 'ngerman' yet.
# (babel) Perhaps you misspelled it or your installation
# (babel) is not complete.
detect_hyphen = function(text) {
r = ".*Package babel Error: You haven't defined the language [`']([^'`]+)' yet.*"
if (length(m <- grep_sub(r, '\\1', text)) == 0) return(character())
hyphen_pkgs(m)
}

# Package glossaries Warning: No language module detected for `english'.
Expand All @@ -448,9 +488,7 @@ check_glossaries = function(text) {
check_polyglossia = function(text) {
r = "^Package polyglossia Warning: No hyphenation patterns were loaded for ([`'][^']+'|British English).*"
if (length(m <- grep_sub(r, '\\1', text)) == 0) return(FALSE)
m[m == 'British English'] = 'english'
m = gsub("[`']", '', m)
tlmgr_install(paste0('hyphen-', m)) == 0
length(pkgs <- hyphen_pkgs(m)) > 0 && tlmgr_install(pkgs) == 0
}

# Package datetime2 Warning: Date-Time Language Module `english' not installed on
Expand Down Expand Up @@ -505,9 +543,13 @@ exist_files = function(files) {
parse_packages = function(
log, text = read_lines(log), files = detect_files(text), quiet = rep(FALSE, 3)
) {
pkgs = character(); quiet = rep_len(quiet, length.out = 3)
quiet = rep_len(quiet, length.out = 3)
# hyphen-* packages are inferred from language names instead of file names
# (`text` may be unavailable, e.g., parse_packages(files = 'bibtex'))
pkgs = if (missing(log) && missing(text)) character() else detect_hyphen(text)
x = unique(c(files, miss_font()))
if (length(x) == 0) {
if (length(pkgs)) return(pkgs)
if (!quiet[1]) message(
'I was unable to find any missing LaTeX packages from the error log',
if (missing(log)) '.' else c(' ', log, '.')
Expand All @@ -520,7 +562,7 @@ parse_packages = function(
# Known packages where tlmgr search returns results but no file path ends
# with the exact package name (e.g., no file ends with "/latex-lab").
# These packages are returned directly without further tlmgr search parsing.
if (x[j] %in% c('fandol', 'latex-lab')) return(x[j])
if (x[j] %in% c('fandol', 'latex-lab')) return(unique(c(pkgs, x[j])))
# why $? e.g. searching for mf returns a list like this
# metafont.x86_64-darwin:
# bin/x86_64-darwin/mf <- what we want
Expand Down
55 changes: 55 additions & 0 deletions tests/test-cran/test-latex.R
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,61 @@ assert('detect_files() can detect filenames from LaTeX log', {
(detect_files("(pdf backend): cannot open file for embedding") %==% 'colorprofiles.sty')
})

assert('hyphen_pkgs() maps babel/polyglossia language names to hyphen-* packages', {
(hyphen_pkgs('french') %==% 'hyphen-french')
(hyphen_pkgs("`hungarian'") %==% 'hyphen-hungarian')
(hyphen_pkgs('German (new orthography)') %==% 'hyphen-german')
(hyphen_pkgs('British English') %==% 'hyphen-english')
(hyphen_pkgs('pinyin') %==% 'hyphen-chinese')
(hyphen_pkgs(c('ngerman', 'swissgerman', 'german')) %==% 'hyphen-german')
(hyphen_pkgs('nynorsk') %==% 'hyphen-norwegian')
# all Indic languages are in a single package
(hyphen_pkgs(c('hindi', 'tamil', 'bengali', 'Panjabi')) %==% 'hyphen-indic')
(hyphen_pkgs(c(
'assamese', 'bengali', 'gujarati', 'hindi', 'kannada', 'malayalam',
'marathi', 'oriya', 'pali', 'panjabi', 'tamil', 'telugu'
)) %==% 'hyphen-indic')
# older names that are synonyms of other languages
(hyphen_pkgs(c('amharic', 'geez')) %==% 'hyphen-ethiopic')
(hyphen_pkgs('persian') %==% 'hyphen-farsi')
(hyphen_pkgs(c('patois', 'francais', 'french')) %==% 'hyphen-french')
(hyphen_pkgs(c('polygreek', 'monogreek', 'greek')) %==% 'hyphen-greek')
(hyphen_pkgs('portuges') %==% 'hyphen-portuguese')
(hyphen_pkgs('slovene') %==% 'hyphen-slovenian')
(hyphen_pkgs('espanol') %==% 'hyphen-spanish')
# names in language.dat may be in mixed case
(hyphen_pkgs('UKenglish') %==% 'hyphen-english')
})

assert('hyphen_pkgs() drops names that are not valid package names', {
# names come from the log, and they must not be interpreted by the shell
(hyphen_pkgs('ngerman;id') %==% character())
(hyphen_pkgs('$(id)') %==% character())
(hyphen_pkgs('a|b') %==% character())
(hyphen_pkgs('a&b') %==% character())
(hyphen_pkgs('--repository=x') %==% character())
(hyphen_pkgs(character()) %==% character())
# valid names are kept
(hyphen_pkgs(c('french', 'x;id', 'swissgerman')) %==% c('hyphen-french', 'hyphen-german'))
# nothing to install, so tlmgr must not be called (and no rerun is needed)
(detect_hyphen("! Package babel Error: You haven't defined the language 'x;id' yet.") %==% character())
(!check_babel("Package babel Warning: No hyphenation patterns were preloaded for the language `x;id'."))
(!check_polyglossia("Package polyglossia Warning: No hyphenation patterns were loaded for `x;id'"))
})

assert('missing hyphenation patterns are detected from babel errors (#515)', {
x = c(
"! Package babel Error: You haven't defined the language 'ngerman' yet.",
"(babel) Perhaps you misspelled it or your installation",
"(babel) is not complete."
)
(detect_hyphen(x) %==% 'hyphen-german')
(detect_hyphen("! Package babel Error: You haven't defined the language `french' yet.") %==% 'hyphen-french')
(detect_hyphen("! Package babel Error: Unknown option 'ngerman'.") %==% character())
# the error is not about a missing file, so no tlmgr search is needed
(length(detect_files(x)) == 0)
(parse_packages(text = x, quiet = rep(TRUE, 3)) %==% 'hyphen-german')
})

assert('rerun are correctly detected', {
(needs_rerun(text = "Package biblatex Warning: Please rerun LaTeX."))
Expand Down
Loading