Source code for md2star.preprocessing.language
"""Language detection and ``lang`` / ``date_format`` metadata mapping.
A best-effort guess via the ``langdetect`` package; returns ``None`` when
``langdetect`` is unavailable so the caller can skip metadata injection
without crashing.
Author
------
[Warith HARCHAOUI](https://linkedin.com/in/warith-harchaoui/)
"""
from __future__ import annotations
import re
# Detected two-letter language → Pandoc BCP-47 ``lang`` + a strftime
# ``date_format`` matching that locale's conventional long date. The format
# strings are what the Lua filter feeds to strftime for the auto-dated
# subtitle, so each is hand-tuned to the language (e.g. day-before-month in
# French/Spanish, CJK ``年月日`` ordering). Region choices (en-US, pt-BR) reflect
# md2star's most common audiences.
_LANG_MAPPING: dict[str, dict[str, str]] = {
"en": {"lang": "en-US", "date_format": "%A, %B %e, %Y"},
"fr": {"lang": "fr-FR", "date_format": "%A %e %B %Y"},
"es": {"lang": "es-ES", "date_format": "%A, %e de %B de %Y"},
"de": {"lang": "de-DE", "date_format": "%A, %e. %B %Y"},
"it": {"lang": "it-IT", "date_format": "%A %e %B %Y"},
"pt": {"lang": "pt-BR", "date_format": "%A, %e de %B de %Y"},
"nl": {"lang": "nl-NL", "date_format": "%A %e %B %Y"},
"ru": {"lang": "ru-RU", "date_format": "%A, %e %B %Y"},
"zh-cn": {"lang": "zh-CN", "date_format": "%Y年%m月%d日"},
"ja": {"lang": "ja-JP", "date_format": "%Y年%m月%d日"},
}
[docs]
def get_language_metadata(content: str) -> dict | None:
"""Return ``{'lang': ..., 'date_format': ...}`` or ``None``.
Strips fenced code blocks, HTML tags, and link targets before detection so
only prose is fed to the language guesser.
"""
# langdetect is an optional dependency: if it's absent we return None so
# the caller silently skips language metadata rather than crashing.
try:
import langdetect
except ImportError:
return None
# Strip non-prose before detection — code, HTML, and link *targets* are
# often English-ish noise that skews the guess. We keep link *text*
# (the \1 backref) because that's real prose in the document's language.
text = re.sub(r"```.*?```", "", content, flags=re.DOTALL)
text = re.sub(r"<[^>]+>", "", text)
text = re.sub(r"\[([^\]]+)\]\([^\)]+\)", r"\1", text)
# detect() raises on empty/undetectable input; treat that as "unknown".
try:
lang_code = langdetect.detect(text)
except Exception:
return None
# Known languages get their tuned mapping; anything else still gets a
# usable fallback (the raw code + a neutral long-date format) rather than
# nothing, so unusual languages don't lose their date subtitle entirely.
return _LANG_MAPPING.get(
lang_code,
{"lang": lang_code, "date_format": "%A, %e %B %Y"},
)