Skip to content

Serializers: meta.language renders as prose (Markdown/plain text print the pydantic repr, HTML shows a "Meta" block) and HTMLParams.html_lang is never emitted #745

Description

@wittjeff

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:

  1. Markdown and plain text print the field's pydantic repr. MarkdownMetaSerializer._serialize_meta_field has no LanguageMetaField branch, so it falls through to str(field_val) (markdown.py:478 on main) and the document gains a paragraph like confidence=0.99 created_by='langdetect' code=<HumanLanguageLabel.DE: 'de'>. Plain text inherits the same serializer.
  2. HTML renders the language as visible text, a <details class="docling-meta"> block with language: de, instead of a lang="de" attribute on the element (WCAG 3.1.2 Language of Parts).
  3. HTMLParams.html_lang is 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 LanguageMetaField in _serialize_meta_field (return None) or, at most, emit it only under mark_meta=True as [Language] de. The current fallthrough to str() also means any future BasePrediction-derived field leaks its repr the same way, so a safer default for the else branch would be to skip unknown field types unless mark_meta is set.
  • HTML: emit lang="<code>" on the element that carries the text (<p lang="de">, <h2 lang="de">, <td lang="de">, …) when item.meta.language differs 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_lang on <html lang="…">. Reasonable default when html_lang is left at its default: the character-weighted majority of meta.language across 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

  1. michaelkillgta commented on Sep 25, 2026

    @michaelkillgta
    Contributor

    Hi! I'd like to work on this.

    Plan: stop serializers from dumping meta.language as prose (Markdown/plain pydantic repr / HTML "Meta" block), and emit HTMLParams.html_lang on the root element when set. I'll add regression tests for Markdown, plain text, and HTML export paths and open a PR.

  2. wittjeff commented on Sep 25, 2026

    @wittjeff
    ContributorAuthor

    @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).

  3. added a commit that references this issue on Sep 25, 2026
    0bc9457
  4. michaelkillgta commented on Sep 25, 2026

    @michaelkillgta
    Contributor

    @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.

  5. wittjeff commented on Oct 12, 2026

    @wittjeff
    ContributorAuthor

    Downstream consumer worth linking: docling-project/docling#4741 reports that a PDF's catalog /Lang and XMP metadata never reach the DoclingDocument. The slot for that data is DoclingDocument.body.meta (per #544), and attaching language there triggers point 1 above for every PDF that declares /Lang: the Markdown export gains a paragraph such as confidence=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.

  6. added 2 commits that reference this issue on Nov 3, 2026
    b56bf50
    8837555
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions