Skip to content

Translation content QA

The checks below run from Python or as localizzy qa; the command and its optional layers are described at the end of this page.

qa.check_text(source, target, policy=TextPolicy(), unit_key=...) returns typed findings without modifying either string. qa.validate_batch(batch, result) is a TranslationCache callback: it rejects major/critical findings before caching and on cache reopen. Version validation_identity whenever a rule or policy changes.

The default policy is Qt: exact %1, %L1, %n and %Ln token multiplicity, ampersand mnemonics, and automatic recognition of common HTML tags. Numbered arguments and mnemonic letters can move. && is a literal ampersand. In HTML, attribute ampersands and character entities do not introduce mnemonics.

One exception is form-aware. A Qt numerus form that exactly one count selects may spell its number out and leave out %n (or %Ln), as Qt's own catalogs do: "One file" beside "%n files", and in Arabic the zero, one and two forms. formats.qt_numerus.single_number_forms(lang) derives those forms from Qt's rule for the language: Arabic 0, 1 and 2; the singular of English, German, Polish or Czech; none for Russian or Ukrainian, whose first form also serves 21 and 101; none for French or Brazilian Portuguese, whose first form serves 0 and 1; none for a language with one form. A form that covers several counts must keep %n, every other argument is required in every form, and a form may always keep the count. check_catalog applies the exception only to index-keyed plurals whose forms are the ones Qt counts for the language; validate_batch reads the form from the batch item id and trusts the rule of the batch's target language. The rule accepts more than before and rejects nothing new, so the cache validation identities did not change.

HTML checks compare ordered tags and protected attributes, validate nesting, and preserve stylesheet/script contents. Attribute order, quote style, tag case and void-tag serialization may differ. alt and title text may be translated. Empty visible translations fail even when tags remain. An existing source nesting defect is reported separately; an identical retained defect is a review finding. Optional HTML closing tags are not inferred. Choose markup="html" for explicit HTML fragments or markup="none" for literal angle-bracket examples; automatic recognition is a convenience, not a complete HTML-versus-prose classifier.

Choose formatting styles explicitly. TextPolicy(placeholder_styles=("python_brace",)) uses Python's Formatter.parse, retaining fields, repeated occurrences, conversion and format specifications, including nested widths and implicit indices. It does not evaluate field expressions. Do not apply it to CSS or ICU message syntax. TextPolicy(placeholder_styles=("printf",), accelerators=False) delegates C printf checking to installed GNU msgfmt, supporting positional parameters, star widths and length modifiers. A negative control ensures malformed source formats cannot silently disable checking. Missing tools/timeouts are errors. This is native argument compatibility checking, not byte-identical formatting or Python % syntax. No printf parser is vendored; Qt-only checks require no external executable.

qa.catalog.check_catalog(catalog, policy=..., required_plural_forms=("0", "1")) checks every active nonblank-source message, even if it is marked untranslated. Every native plural and length variant is visited. Missing/extra plural forms, empty targets and inconsistent primary/variant aliases fail. Supply the exact native positions or categories for the target locale; absence of a plural rule is an explicit finding. The API never substitutes CLDR decimal categories for Qt integer plural positions. Determine these using the target application's rules and verify candidates using its native compiler.

TARGET-UNCHANGED and SOURCE-MARKUP remain visible review findings. They do not automatically approve an invariant or prove a translation correct. The final catalog pipeline must reconcile these findings, message eligibility, reviewed invariants, locale coverage and native compilation separately. These checks do not implement ICU semantic validation or linguistic quality scoring.

The qa command

localizzy qa i18n/app_de.ts --plural-forms auto --format table
localizzy qa i18n/app_de.ts --plural-forms auto --fail-on critical --format sarif --out qa-de.sarif
localizzy qa i18n/app_xx.ts --plural-forms 0,1

The command loads any supported catalog, runs check_catalog with the Qt policy and changes nothing. --plural-forms names the required native forms; auto derives them from the target language: positions 0 to n-1 from Qt's numerus count for TS and PO (falling back to the gettext table where Qt has no rule), CLDR categories for category-keyed catalogs. A language with no known rule, such as the xx-pseudo of a pseudo catalog, needs the forms spelled out; auto stops there with exit 2. Without the flag, a plural message is reported as needing a rule.

Every active message counts, so a freshly extracted catalog fails with one TARGET-EMPTY critical finding per empty slot. Gate a catalog once it is supposed to be complete.

--fail-on sets the lowest blocking severity: info, minor, major (default) or critical. --format table, json or sarif renders the findings, to --out when given (an --out that is the catalog itself is refused); SARIF maps critical to error, major to warning, the rest to note, and gives every result the catalog file as its location, so code scanning accepts it. Without --format the command returns one object with the catalog, unit count, findings and blocking count.

Optional layers

The deterministic checks always run. --layers adds the ones that cost something, in any combination; each raises an error when its dependency is missing and never skips silently.

Layer Needs Reports
pofilter the pofilter extra and the pofilter command POFILTER-<TEST> (major) from Translate Toolkit's variables, xmltags, escapes, printf, nplurals, newlines and accelerators tests, run with --gnome on a PO projection with the correct Plural-Forms header, mapped back to messages by context and source
qe unbabel-comet, installed by hand; downloads a model on first use QE-LOW-SCORE (major) where the reference-free COMET score is below --qe-threshold (0.7)
judge the llm extra, --endpoint, --model and the key in --api-key-env MQM-LOW-SCORE where a model's MQM score is below --mqm-threshold (80); below 50 it is critical

The PO projection takes its Plural-Forms rule from the gettext table. For a language the table lacks, a one-form or two-form catalog gets the common rule; any other count exits 2 and asks for the declaration.

The judge scores a seeded sample (--judge-sample, default 0.1) of translated messages that are not yet approved, skips --ignore-keys, and tells the model not to count --brand-terms left in the source language as errors. Its findings carry confidence: "heuristic" and the model's error list. Use it nightly on a sample, not as a merge gate: a judge also makes mistakes, and its findings send a message to a person, not to the bin.

Exit Meaning
0 No finding at or above --fail-on
1 Blocking findings
2 Unknown layer, the judge without endpoint, model or key, no plural rule for auto, a catalog a layer cannot convert
3 A layer's dependency is missing

Implementation references: Python Formatter, HTMLParser, GNU msgfmt checks, and Qt plural rules.