cliff-python
Python parser, serializer, validator and converter for CLIFF 1.0 / 1.1 — the Contextual Localization Integrated File Format.
CLIFF keeps translation context — namespace, clan, group path, entry id, type, emotion, width — next to every string, in a line-oriented format that stays cheap for translators and LLMs alike. cliff-python is the reference implementation of that format for Python: one typed data model, one parser, and every converter a localization pipeline needs.
| Parse | Strict mode rejects exactly what the grammar rejects; tolerant mode applies the seven documented repairs of Appendix C and reports each one. |
| Serialize | Canonical output that round-trips, preserving the spec version the document declared. |
| Validate | Required fields, closed vocabularies, status rules, duplicate ids, ICU brace balance, layout, display width, and style. |
| Convert | JSON, YAML, CSV, PO, XLIFF, Fluent, Android strings.xml, iOS Localizable.strings — in both directions. |
| Typed | Ships py.typed; the public API is annotated and checked with mypy. |
The parser, serializer, validator and converters target CLIFF 1.1 and are checked against the normative examples of both 1.0 and 1.1 plus the cliff-test conformance fixtures.
Installation
pip install cliff-python
Optional converter support (YAML) is a separate extra, so the core package has no dependencies:
pip install "cliff-python[converters]"
Requires Python 3.11 or newer.
Quick start
import cliff_format
text = '''CLIFF 1.1
namespace: demo
clan: settings
source-language: en-US
target-language: zh-CN
[Video]
type: label
<Resolution>
source: "Resolution"
target: "分辨率"
status: final
'''
doc = cliff_format.parse(text)
print(doc.spec_version) # 1.1 — the version the document declared
print(doc.header.namespace) # demo
print(doc.groups[0].path) # Video
print(doc.groups[0].entries[0].id) # Resolution
print(cliff_format.serialize(doc)) # canonical CLIFF
for issue in cliff_format.validate(text):
print(issue.line, issue.category, issue.message)
Strict and tolerant parsing
CLIFF 1.1 defines two readings of the same syntax, and cliff_format exposes both. Strict is
the default; the mode is never chosen for you.
Strict (parse, load) rejects everything the grammar rejects, and accepts everything it
accepts:
- identifiers may use
A-Z a-z 0-9 _ -and never., are case-sensitive, and are never rewritten —bad_idandBadVersionstay as written, because an identifier is a translation match key; - tags (
type,emotion,status,variant) are the one thing 1.1 did not relax: lowercase words from a closed vocabulary; - one optional trailing
,or;per line is standard syntax, means nothing, and is never re-emitted by the serializer; - a file-layout mismatch is a warning — the layout is a recommendation;
- resource limits are enforced.
Tolerant (parse(..., tolerant=True), parse_tolerant) adds the seven relaxations of
Appendix C and reports every repair:
| Relaxation | Input | Result |
|---|---|---|
| C.2.1 bare scalar in a list-typed field | emotion: objective |
["objective"] |
| C.2.2 repeated field in one scope | two target: lines |
concatenated / appended in order |
| C.2.3 quoted tag | status: "final" |
final |
| C.2.4 quoted entry id | <"resolution"> |
resolution |
| C.2.5 identifier with a reserved character | <Save & Load> |
Save-Load (+ collision handling) |
| C.2.6 version-line spelling | cliff 1.1.0 |
CLIFF 1.1 |
| C.2.7 quoted key | "context": "…" |
context: "…" |
C.2.7 removes the quotes and nothing else: the enclosed text must be a name, no escape is
processed, and the key is then checked against the legal keys of its scope exactly as a bare key
is. A quoted word that is not a legal key, or a key quoted in a scope that does not allow it, is
still an error (§C.5) — the relaxation cannot legalize a word.
C.2.5 applies to an identifier that begins with a name character (ignoring surrounding
whitespace), or that uses the quoted form of C.2.4. Angle brackets that enclose anything else are
not an entry marker: a stray closing tag </terms> — what a model writes when it treats a
section like an XML element — or <.hidden> is rejected under C.5 rather than normalized.
Without that limit the reading turned /terms into the entry terms and then failed the
document on a required field of an entry the file never contained, which is the guessing C.5
forbids. An identifier that is empty or whitespace-only is unchanged: C.3 step 7 still gives the
scope's fallback name.
Every repair is recorded, so nothing is fixed silently:
doc, corrections = cliff_format.parse_tolerant(text)
for c in corrections:
print(c.line, c.category, c.message, c.before, "->", c.after)
Categories: list-shape, field-repeat, tag-quote, name-quote, name-normalized,
id-collision, version, casing. A category names the operation, not the clause, so
name-quote covers both a quoted entry id (C.2.4) and a quoted key (C.2.7).
The mode never guesses. Appendix C.5 lists what a tolerant parser MUST refuse — a missing
required field, a word outside a closed vocabulary, an unbalanced ICU brace, a malformed line, an
unsupported version — and those raise CliffParseError with a line number and a category, exactly
as in strict mode.
Two guarantees make the mode usable in a pipeline:
# 1. the repaired document is valid under the strict grammar
once = cliff_format.serialize(doc)
assert not [i for i in cliff_format.validate(once) if i.category not in ("warning", "extension")]
# 2. canonical output is a fixed point
assert cliff_format.serialize(cliff_format.parse(once)) == once
Command line
cliff-format parse path/to/file.cliff
cliff-format serialize path/to/file.cliff
cliff-format validate path/to/file.cliff
cliff-format convert path/to/file.cliff --format po
cliff-format convert path/to/file.po --from po --format cliff
# Tolerant reading of a model's output, with the repair report on stderr
cliff-format parse path/to/model-output.cliff --tolerant
cliff-format validate path/to/model-output.cliff --tolerant
cliff-format convert path/to/model-output.cliff --tolerant --format json
# Opt-in checks: a layout mismatch as an error, style deviations as warnings
cliff-format validate path/to/file.cliff --check-layout
cliff-format validate path/to/file.cliff --style
cliff-format validate path/to/file.cliff --check-width
# Canonical output for a specific version, or with a canonical id style applied
cliff-format serialize path/to/file.cliff --spec-version 1.0
cliff-format serialize path/to/file.cliff --style
Supported convert formats: cliff, json, yaml, csv, po, xliff, fluent,
android, ios.
validate exits 1 when the file has errors and 0 when it only has warnings, extension
notes, corrections, or style findings. Every diagnostic carries a line number, a category, and the
offending line.
Converters
Bidirectional for JSON, YAML, CSV, PO, XLIFF 2.x, Fluent, Android strings.xml and iOS Localizable.strings:
po_text = cliff_format.to_po(doc)
doc = cliff_format.from_po(po_text)
# XLIFF 2.0 / 2.1 / 2.2; 2.2 also writes the glossary module for variant: glossary
xml = cliff_format.to_xliff(doc, version="2.2")
CLIFF carries context as first-class data, and every converter moves that data through the target format's own documented metadata channel — never through an invented one:
| Format | Channel used for CLIFF attributes | Channel used for context |
|---|---|---|
| JSON / YAML / CSV | native fields of the CLIFF-shaped document | native field |
| XLIFF 2.x | metadata module (mda:metadata / mda:metaGroup category="cliff" / mda:meta), plus state on segment for status |
mda:meta type="context" and a plain note |
| XLIFF 2.2 glossary | additionally the glossary module (gls:glossEntry) for variant: glossary |
gls:definition |
| gettext PO | extracted comments #. cliff:<key>: <value>; #: for references; msgctxt for the group path and entry id |
plain #. comment |
| Fluent | message comment # cliff:<key> = <value> (attributes are translatable content, so they are not used for metadata) |
plain # comment |
| Android strings.xml | XML comment above the resource, cliff:<key>: <value> |
first line of that comment |
| iOS Localizable.strings | /* ... */ block comment above the pair, cliff:<key>: <value> |
first line of that comment |
Because formats without a group level cannot express inheritance, the exporters fold the group values into each entry (the effective context, type, emotion and max-width) so nothing is lost.
The reverse direction is deliberately conservative. An importer reads only the metadata the source
format actually defines: namespaced cliff: entries become CLIFF attributes, an ordinary comment
becomes context, and everything else falls back to a documented default (type: sentence,
status derived from whether a translation exists). A converter never invents context, emotion,
references, widths or reviewers that the source file did not contain.
Round-trip fidelity
JSON, YAML, CSV and XLIFF preserve the whole CLIFF data model, so cliff -> format -> cliff is
lossless. The remaining formats have no slot for some CLIFF fields:
| Format | Fidelity | Dropped on the way back |
|---|---|---|
| JSON / YAML / CSV | yes | — |
| XLIFF 2.0 / 2.1 / 2.2 | yes | — (CLIFF-only fields travel as cliff:-categorised notes) |
| PO | entry level | header prose (variant, version, title, info, standard, dependency) and the group structure |
| Fluent | entry level | header prose and the group structure; Fluent attributes of foreign files cannot be represented |
| Android strings.xml | entry level | header prose and the group structure |
| iOS Localizable.strings | entry level | header prose and the group structure |
Importers keep foreign identifiers that are already valid CLIFF 1.1 names — InvSwordIron and
bad_id are valid names, and rewriting one silently would break a translation memory. Only
characters outside the name set are replaced, by the documented normalization of specification
Appendix C.3, and duplicates are disambiguated with a numeric suffix so every generated document
validates.
API
| Function | Description |
|---|---|
cliff_format.parse(text, path=None, tolerant=False, corrections=None, **limits) |
Parse CLIFF text into CliffDocument. |
cliff_format.parse_tolerant(text, path=None) |
Parse in tolerant mode; returns (document, corrections). |
cliff_format.load(path, tolerant=False) |
Read and parse a .cliff file. |
cliff_format.serialize(doc, style=False) |
Serialize a CliffDocument to canonical CLIFF text. |
cliff_format.validate(text, check_width=False, check_layout=False, style=False, tolerant=False) |
Validate CLIFF text and return list[ValidationIssue]. |
cliff_format.validate_document(doc, ...) |
Validate an already parsed document. |
cliff_format.effective_context / effective_type / effective_emotion / effective_max_width |
Resolve a group-inherited value for one entry. |
cliff_format.is_name / is_tag / is_style_identifier / normalize_identifier / strip_line_terminator |
The identifier rules of §5.5 and Appendix C.3, and the terminator rule of §5.6. |
cliff_format.to_dict / from_dict |
Convert between CliffDocument and a JSON-shaped dict. |
cliff_format.to_json / from_json |
Convert between CliffDocument and JSON text. |
Diagnostics
| Category | Meaning |
|---|---|
syntax, semantic, vocabulary, icu, id |
Errors: the file does not conform. |
warning |
A recommendation (layout mismatch, glossary shape, width overflow). |
extension |
An x- field was ignored, as the specification requires. |
correction |
A tolerant parse repaired something; reports before → after. |
style |
A deviation from the style guide (--style); never an error. |
Resource limits
parse and load accept max_lines, max_line_length and max_string_length; the
defaults bound a document at 1 000 000 lines, 1 MiB per line and 1 MiB per decoded string.
Exceeding a limit raises a located error instead of exhausting memory. The limits apply in strict
and tolerant mode alike (§19).
Repository layout
cliff-python/
├── pyproject.toml
├── src/
│ └── cliff_format/
│ ├── identifiers.py # name / tag / terminator rules (§5.5, §5.6, Appendix C.3)
│ ├── vocabulary.py # the closed tag vocabularies (§12, §13)
│ ├── model.py # dataclasses for the CLIFF data model
│ ├── parser.py # single-pass line parser, strict and tolerant
│ ├── serializer.py # canonical serializer
│ ├── validator.py # semantic validation, inheritance helpers, style and layout checks
│ ├── converter.py # bidirectional format converters
│ └── cli.py # cliff-format command line
├── tools/
│ └── regenerate_examples.py
├── examples/
└── tests/
Conversion samples live in examples/cliff_to_<format>/ and examples/<format>_to_cliff/.
The generated side is produced by tools/regenerate_examples.py; run it after changing a
converter, and python tools/regenerate_examples.py --check verifies the samples are current.
Links
License
MIT — see LICENSE. The CLIFF wordmark under docs/assets/ comes from the CLIFF
project and is licensed under the MIT License as well.
Metadata
Release files for cliff-python 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| cliff_python-0.2.0.tar.gz | 77.5 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| cliff_python-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 132.3 kB
Release files / cliff_python-0.2.0.tar.gz
| Download URL | cliff_python-0.2.0.tar.gz |
|---|---|
| Size | 77.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
933bcd288a6cec2f43bebd9deeed589d74f19f7f85bded2567a3d5c18825b3d1
|
|
BLAKE2b-256 checksum How to use checksums |
987c326253be5e22203c010bc4784a91aa9cd15060cda145ae92d850ca1ca35f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.
Transparency logRelease files / cliff_python-0.2.0-py3-none-any.whl
| Download URL | cliff_python-0.2.0-py3-none-any.whl |
|---|---|
| Size | 54.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3f2403648c6588441c4fc5eaf1558cf0edc46e222bf9e96f83289b07a99a9fb2
|
|
BLAKE2b-256 checksum How to use checksums |
ccfeb6498013eee86d52fbdbfc9b9a9ef0a32fe37a205dbad934048dd61de5a2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.
Transparency logSecurity for cliff-python 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for cliff_python-0.2.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.