Skip to content

Architecture

Version 1.3 is about 1,100 lines of Python in ten modules. This page follows one document through them.

Module map

src/vexy_markliff/
├── __init__.py          # lazy VexyMarkliff wrapper, lazy imports, __version__
├── __version__.py       # generated by hatch-vcs at build time (not in Git)
├── cli.py               # VexyMarkliffCLI + main(), Fire entry point
├── config.py            # ConversionConfig, ConversionConfig.load()
├── exceptions.py        # VexyMarkliffError and subclasses
├── utils.py             # whitespace, language-code and logging helpers
├── core/
│   ├── __init__.py      # exports VexyMarkliff, MarkdownParser, HTMLParser
│   ├── converter.py     # VexyMarkliff: the four conversion methods
│   └── parser.py        # MarkdownParser, HTMLParser
└── models/
    ├── __init__.py      # lazy exports of the models
    └── xliff.py         # TranslationUnit, XLIFFFile, XLIFFDocument (Pydantic)

The console script vexy-markliff maps to vexy_markliff.cli:main in pyproject.toml.

Markdown to XLIFF, step by step

Take "# Hello\n\nThis is **bold** text.\n" with source_lang="en" and target_lang="fr".

  1. Entry. VexyMarkliff.markdown_to_xliff in __init__.py imports core.converter.VexyMarkliff on first use and forwards the call.
  2. Validation. The core converter rejects empty content and checks both language codes with utils.validate_language_code.
  3. Markdown rendering. MarkdownParser.parse renders with markdown-it-py (CommonMark preset, html and breaks on, table and strikethrough enabled):

    <h1>Hello</h1>
    <p>This is <strong>bold</strong> text.</p>
    
  4. HTML parsing. HTMLParser.parse calls lxml.html.fromstring, which wraps the two top-level elements in a <div>. _extract_segments walks the tree and yields:

    [
        {"content": "Hello", "translatable": True, "element": "h1"},
        {"content": "This is", "translatable": True, "element": "p"},
        {"content": "bold", "translatable": True, "element": "strong"},
        {"content": "text.", "translatable": True, "element": "text"},
    ]
    
  5. Model building. XLIFFDocument(source_lang="en", target_lang="fr", content=...) creates one XLIFFFile with id file_1 and a TranslationUnit per segment (unit_1 … unit_4, state new).

  6. Serialization. XLIFFDocument.to_xml() builds the <xliff> tree with lxml.etree and returns a pretty-printed string.

The element names collected in step 4 are not written to the XLIFF; the models have no field for them.

XLIFF back to text

  1. XLIFFDocument.from_xml parses the string and reads trans-unit elements in the …:document:2.1 namespace.
  2. The content property turns units back into segment dictionaries using the source text.
  3. HTMLParser.reconstruct wraps each segment in <p>…</p> and joins them with newlines. MarkdownParser.reconstruct calls the same method.

Lazy imports

markdown-it-py, lxml and Pydantic are imported only inside conversion methods. The top-level VexyMarkliff class lives in __init__.py so that from vexy_markliff import VexyMarkliff does not import core.converter. models/__init__.py and the package __getattr__ do the same for the models, ConversionConfig and VexyMarkliffCLI.

Error flow

ValidationError (empty input, bad language code)   ──► raised directly
anything raised inside the conversion try-block    ──► logged, re-raised as ConversionError

The CLI catches every exception from a conversion and calls sys.exit(1). It also exits with 1 if the input path does not exist, before reading it.

Dependencies

Package Used for
markdown-it-py Markdown to HTML
lxml HTML parsing, XLIFF building and parsing
pydantic XLIFF data models
fire CLI
pyyaml ConversionConfig.load
rich Declared in pyproject.toml; not imported by the 1.3 source

Planned architecture

The design overview describes the intended pipeline: skeleton files with ###uN### placeholders, Format Style (fs:fs, fs:subFs) annotations, inline <mrk> and <ph> codes, sentence segmentation and a two-document alignment mode.