Repository navigation
Description
Summary
BaseMeta.language (LanguageMetaField, "the detected human language of the node content, expressed as a BCP 47 code") is the right slot for per-element language, and we now populate it downstream for PDF/UA /Lang tagging. But once it is set, every text serializer treats it as content:
- Markdown and plain text print the field's pydantic repr.
MarkdownMetaSerializer._serialize_meta_fieldhas noLanguageMetaFieldbranch, so it falls through tostr(field_val)(markdown.py:478on main) and the document gains a paragraph likeconfidence=0.99 created_by='langdetect' code=<HumanLanguageLabel.DE: 'de'>. Plain text inherits the same serializer. - HTML renders the language as visible text, a
<details class="docling-meta">block withlanguage: de, instead of alang="de"attribute on the element (WCAG 3.1.2 Language of Parts). HTMLParams.html_langis declared (html.py:111) but never used: the root element is the hard-coded string"<html>"(html.py:1150), so there is no way to set<html lang>(WCAG 3.1.1 Language of Page) through the API.
Checked against v2.81.0 (installed) and main as of 2026-09-03 (v2.94.1); I found no existing issue or PR on these.
Reproduction
from docling_core.types.doc.base import BoundingBox, CoordOrigin, Size
from docling_core.types.doc.document import BaseMeta, DoclingDocument, LanguageMetaField, ProvenanceItem
from docling_core.types.doc.labels import DocItemLabel, HumanLanguageLabel
from docling_core.transforms.serializer.html import HTMLDocSerializer, HTMLParams
doc = DoclingDocument(name="demo")
doc.add_page(page_no=1, size=Size(width=612, height=792))
item = doc.add_text(
label=DocItemLabel.PARAGRAPH,
text="Dieser Absatz ist auf Deutsch.",
prov=ProvenanceItem(page_no=1, bbox=BoundingBox(l=0, t=100, r=100, b=0, coord_origin=CoordOrigin.BOTTOMLEFT), charspan=(0, 0)),
)
item.meta = BaseMeta(language=LanguageMetaField(code=HumanLanguageLabel.DE, confidence=0.99, created_by="langdetect"))
print(repr(doc.export_to_markdown()))
# "Dieser Absatz ist auf Deutsch.\n\nconfidence=0.99 created_by='langdetect' code=<HumanLanguageLabel.DE: 'de'>"
print(repr(doc.export_to_text()))
# same repr appended as a paragraph
html = HTMLDocSerializer(doc=doc, params=HTMLParams(html_lang="de")).serialize().text
print("lang=" in html) # False — <html> is emitted without the attribute
# body contains: <details class="docling-meta">…<span class="docling-meta-field-value">de</span>…Expected
- Markdown / plain text: a language code is metadata, not content. Either skip
LanguageMetaFieldin_serialize_meta_field(returnNone) or, at most, emit it only undermark_meta=Trueas[Language] de. The current fallthrough tostr()also means any futureBasePrediction-derived field leaks its repr the same way, so a safer default for theelsebranch would be to skip unknown field types unlessmark_metais set. - HTML: emit
lang="<code>"on the element that carries the text (<p lang="de">,<h2 lang="de">,<td lang="de">, …) whenitem.meta.languagediffers from the document language, and drop the language row from the meta<details>block (or keep it only when the language is the same as the document's, which is never useful). - HTML root: honour
HTMLParams.html_langon<html lang="…">. Reasonable default whenhtml_langis left at its default: the character-weighted majority ofmeta.languageacross the body, falling back to"en"as today.
Why it matters
Accessible HTML output needs the page language on the root and the language of parts on the elements; screen readers switch pronunciation on these attributes. Today the only way to get clean text output once meta.language is populated is blocked_meta_names={"language"}, which callers have to know to pass on every serializer, and <html lang> cannot be set at all without post-processing the string.
Offer
Happy to open a PR with: the LanguageMetaField branch in the Markdown meta serializer (skip unless mark_meta), lang attributes in the HTML element serializers keyed off meta.language, the <html lang> emission from html_lang with a majority-language default, and tests for the three cases above. Downstream context: YRA's yra-doc-pipeline populates meta.language from a deterministic detector and maps it to PDF/UA /Lang; the same data should reach the HTML export.
🤖 Generated with Claude Code
Activity
michaelkillgta commentedon Sep 25, 2026
Hi! I'd like to work on this.
Plan: stop serializers from dumping
meta.languageas prose (Markdown/plain pydantic repr / HTML "Meta" block), and emitHTMLParams.html_langon the root element when set. I'll add regression tests for Markdown, plain text, and HTML export paths and open a PR.wittjeff commentedon Sep 25, 2026
@michaelkillgta I'm interested in how your PR might differ from the one I submitted earlier -- particularly re MD & TXT exports (as I'm just now turning attention to that in my own product).
fix(serializer): stop rendering meta.language as prose; emit lang att…
michaelkillgta commentedon Sep 25, 2026
@wittjeff — you’re right to ask. I claimed this before noticing your open PR #746. I’ll step back and won’t open a competing PR. Your approach on MD/TXT/HTML is the one to land; if anything looks missing after review I can leave notes on #746 instead of opening a second PR.
wittjeff commentedon Oct 12, 2026
Downstream consumer worth linking: docling-project/docling#4741 reports that a PDF's catalog
/Langand XMP metadata never reach theDoclingDocument. The slot for that data isDoclingDocument.body.meta(per #544), and attachinglanguagethere triggers point 1 above for every PDF that declares/Lang: the Markdown export gains a paragraph such asconfidence=None created_by='pdf:/Lang' code=<HumanLanguageLabel.EN: 'en'>.The docling side will therefore ship behind an opt-in pipeline option and flip the default once this is fixed. The PR offered above is still on; I will open it next.
fix(serializer): keep meta.language out of text exports and emit lang…
fix(serializer): keep meta.language out of text exports and emit lang…