A blog that checks its own translations
This blog has 64 posts in eleven languages: 704 rows and about 2.2 million characters. No post is missing in any language, which is a claim that only means something if something enforces it.
Something does. Before a translated row is written, it is checked against the English original, and a row that fails is not written at all — it is reported and left for a person to look at.
What each check is for
The same link targets. An internal path must not change while being translated: /privacy stays /privacy in every language, and the language prefix is added at render time. A translator — human or otherwise — who helpfully localises a path produces a link that resolves nowhere.
The same placeholders. Some posts carry figures that are substituted when the page is built, so they cannot go stale. A translation that drops one of those loses a number; one that invents one produces a brace in the middle of a sentence.
The same count per tag. Not just balanced tags — the same number of them as the English. This is the check that earns its place: a lost table row, a dropped list item or a missing diagram is invisible to every other test, because the result is still valid HTML that reads perfectly well.
A length inside a band. Between half and two and a half times the English. Languages genuinely differ in length, so the band is wide; what it catches is a translation that stopped halfway.
What it caught, and what it wrongly caught
A real catch: a French translation came in with four emphasised words where the English had three, because a negation had been marked up in two pieces. Nothing looked wrong. The count did.
Two false alarms, both on the same day, both instructive.
The first: a post quotes a line of CSS, and CSS ends in }}. The brace check counted every {{ and every }}, so every translation of that post failed while nothing was wrong. A }} without a {{ cannot be a broken placeholder — placeholders start with {{ — so only the opening side is counted now.
The second: <rect/> and <path/> have no closing tag, so a balance test reports them as unbalanced forever. Empty elements are now counted rather than balanced.
Both of those cost a round of work, and both were the check being too strict rather than too loose. That is the better direction to fail in, but it is not free: a check that cries wolf gets loosened by whoever is in a hurry, and then it is not there when it matters.
The test that mattered most
When diagrams were added to the posts, the tag check was extended to cover them. Rather than trusting that, the check was tested: a diagram was deliberately deleted from one translation and the importer was run.
It reported exactly what was gone — the diagram, its ten labels, its four rectangles, its seven paths — and refused to write the row. The diagram was restored and the check passed again.
That took two minutes. A verification you have not seen fail is a verification you are assuming, and assuming is the thing all of this exists to avoid.