Blog ·
A vague plural is not a number
A guard comparing numbers across nine languages reported two defects that were not defects, and passed two languages for reasons unrelated to numbers.
By Provibr
Every article we publish exists in nine languages, and the thing most likely to go wrong in a translation is not a word. It is a number. A count that says 200 in one language and 20 in another is invisible to a reader who only sees one of them, and there is no spell checker for it.
So there is a guard. It takes the English source, extracts the numbers, and demands that each translation contains at least as many occurrences of the same value. This round it produced two red reports about two translations that were entirely correct, and the reason is worth writing down.
The guard has to read words, not digits
The obvious version of this check compares digits, and we had that one first. It is nearly useless here, because our English house style writes numbers out. Not 38 tests but thirty eight tests; not 1440 but fourteen hundred and forty.
So there is a second guard with a table in it: for every value worth checking, how each of the nine languages spells it. Count the occurrences in the English, count them in each translation, and report anything that comes up short.
Small values are deliberately excluded. In every one of these languages the words for one, two and three are also articles, prepositions or pieces of longer words, and counting them would produce hundreds of findings that mean nothing. A guard that cries wolf gets skipped, and a skipped guard is worse than no guard.
Counting by substring, and counting from the top down
Two mechanics make the rest of this possible, and both of them create the failure this article is about.
The first is that matching has to be a substring match. Dutch writes sixteen thousand as one word, and the only way to see the sixteen in it is to look inside. Several of these languages compound freely, so anchoring on word boundaries would lose most of the real hits.
The second is that values are counted from largest to smallest, and every hit is cut out of the text before the next value is counted. Without that, forty one is counted once as forty one and once as forty, and then a language that writes the same number as a single indivisible word looks like it is missing one. Seven of the first thirty seven findings the guard ever produced were exactly that, and none of them was a defect.
Substring matching plus cut out is the right design. It also means the guard is looking for letters, and letters are not numbers.
The word that is not a number
The English sentence that broke it reads: a guard on it names hundreds of non defects.
There is no number in that sentence. Hundreds of is a quantity word, the same kind of word as several or plenty, and it is not a claim that anything happened one hundred times. But it contains the letters of the numeral, so the guard counted an occurrence of one hundred in the English and then went looking for one hundred in eight translations.
What came back was this. Italian says centinaia, Romanian says sute de, and neither of those contains the language's numeral for a hundred. Two red reports, on two sentences that a native speaker would call perfect.
The other six passed, and only four of them passed for a reason. Dutch honderden contains honderd, German Hunderte contains hundert, Spanish cientos contains ciento, French centaines contains cent. Those four are the same defect as the Italian and the Romanian; the guard simply could not see it, because the vague word happens to be built from the numeral.
The last two are the uncomfortable ones. Portuguese passed because the Portuguese word for a hundred, cem, appears inside parecem, in a heading about two things that look alike. Ukrainian passed with 39 hits in one article, because the Cyrillic letters for one hundred sit inside the ordinary words for page and for guard.
So of eight languages: two failed on a non defect, four hid the same non defect behind a coincidence of spelling, and two were green on words that have nothing to do with numbers at all. A guard is only as trustworthy as the reasons behind its green results, and here only two of the eight greens were about counting.
Where the stripping has to go, and why it cannot go first
The fix is a second table: the vague plurals, per language. Hundreds, honderden, Hunderte, cientos and centenares, centaines, centinaia, centenas, sute de, and the Ukrainian forms. Remove them before counting, because they are not values.
The interesting part is when. The instinct is to strip them at the start, as a cleanup pass over the text, and that would break the guard in a way that is much harder to notice than the thing it fixes.
The Romanian for four hundred times is patru sute de ori. It contains sute de literally. Strip the vague plurals up front and you have destroyed a real four hundred while removing a fake one hundred, and now the guard reports Romanian as missing a number it plainly has.
So the stripping happens inside the loop, at the moment the loop reaches the value one hundred. By then the pass for four hundred has already consumed patru sute, and what is left of sute de is genuinely the vague word. The ordering that existed for a different reason turns out to be the thing that makes this safe, which is a good argument for keeping the reason written next to the code.
With the vague plurals removed the comparison went from 224 counts and two findings to 216 counts and none. The count drops because a value that appears zero times in the English is no longer compared at all, which is exactly right: there was never a hundred there.
Six values that were simply absent
While proving all of this the table turned out to be missing six of the values the new articles used, so those numbers were unchecked in eight languages each.
That is the more ordinary failure and it is the one that will happen again. A table of values is written for the articles of the round that created it, and the next round writes different numbers. A missing row does not fail; it silently checks nothing, which is the same shape as an allowlist entry with a typo in it, and we have been bitten by that before.
The honest description of this guard is therefore: it is a net with a known mesh, it catches the class of error it was built for, and every round has to add its own values or get nothing. Which is fine, as long as nobody reads a green result as coverage.
What we would write down
- A quantity word is not a value. Hundreds of is closer to several than to 100.
- Substring matching is correct here and it is the source of the problem. Compounding languages need it.
- Ask why each green result is green. Four of ours were coincidence and two were noise.
- A short numeral stem hides inside ordinary words, and Cyrillic 100 sits inside the word for page.
- Order the passes from large to small and cut out each hit, or compounds count twice.
- Where a cleanup happens matters as much as what it removes. Stripping vague plurals first destroys a real four hundred.
- Keep the reason next to the ordering, because the ordering is load bearing and does not look it.
- A table of values only covers the round that wrote it. Six were missing here.
- A missing row is silent, exactly like a mistyped allowlist entry.
- Say what a guard does not cover, so nobody reads green as coverage.
For clarity about what this is: the comparison ran over four articles in eight languages, 224 counts with two findings before the vague plurals were handled and 216 with none afterwards, six values added to the table, and the vague plural stripping placed inside the loop rather than before it so that the Romanian for four hundred survives. Alongside it that round produced 886 generated articles with no warnings, a content check of 887 of 887 files byte identical, the web suite at 4594 green with the other suites at 7287, the fact comparison at 1144 counts with none failing, tokens at 64 and link names at 72, seven scripts and four typography rules proven red before they were believed green, and a layout sweep of 108 checks at 390 pixels against a production build. Not proven: the substring noise in Ukrainian and Portuguese is understood but not removed, which needs a morphological reader rather than a table, and each translation still has one reader.
From the platform work on Provibr. What nine catalogues do to a fixed set of controls is in sixteen thousand keys, nine languages, the guard that distinguishes a shortened link text from a replaced one is in two names for the same article, and the reader review that started this whole series is in three faults no script could see.