Formatting and locale / Language in generated text
Plurals in generated line text, and 1 items
Text assembled around a number, where the grammar depends on the number and the template usually assumes there are exactly two possibilities.
The defect
The visible version is the classic: a line reading 1 items, because the template appended an s. The fix people reach for, a conditional on whether the count is one, is correct for English and wrong as a general rule, because languages differ in how many plural categories they have and in which numbers fall into which category. A template with two branches cannot express a language with more, so a document translated into one comes out ungrammatical in a way that no amount of translation review catches, because the translator was given two strings and there was no third to translate.
What it does to the document
Generated descriptions are the least predictable text in a document because their length depends on data. A line description assembled from parts can be twice as long in one language as another, which is what turns a one-line table row into a two-line row and changes how many rows fit on a page.
The decisions
Reasons rather than a description of the code. Formatting rules are data that changes, so most of these are about taking the rule from a maintained source rather than encoding today's answer.
- Use a plural-rules facility that takes a locale and returns a category, rather than an is-it-one conditional.
- Give the translator one message per category the target language has, not two, and let the tooling determine how many that is.
- Never build a sentence by concatenating fragments around a number. Word order differs, and a fragment that is a prefix in one language is a suffix in another.
- Include the number in the message rather than gluing it on, so its position within the sentence is the translator's decision.
- Watch for zero. Some languages treat it as its own category, and in most documents a zero case reads better as a sentence than as a count anyway.
- Remember this applies to generated descriptions and notes, not just to counts. Anything a program assembles from parts has this problem.
In code
The formatting happens before the markup exists, so this is mostly code rather than CSS. The wrong version is kept in a comment where the wrong version is the thing people write.
// Ask the locale how many categories it has, rather than assuming two.
const count = 1;
const rules = new Intl.PluralRules("en-GB");
rules.select(count); // "one"
new Intl.PluralRules("ar").select(count); // a different set of categories
// One message per category the target language actually has. The number
// goes inside the message, so its position is the translator's choice.
const messages = {
one: "{n} item on this order",
other: "{n} items on this order",
};
const text = messages[rules.select(count)].replace("{n}", String(count));
/* Not this: two branches, and a number glued to a fragment.
const bad = count + " item" + (count === 1 ? "" : "s"); */What people do instead
Solving it with a ternary on whether the count is one. It fixes the visible English defect and hard-codes the assumption that two forms are enough, which is the thing that later has to be undone everywhere rather than in one place.
How to find out rather than assume
Render the document with counts of zero, one, two and a large number, in every language it is issued in. Then have a native speaker read the generated lines, because this is the class of defect that only a speaker of the language will see.
Where the API sits in this
Nowhere, and that is worth saying plainly. This API renders HTML to PDF. By the time markup reaches it, every value in it is already a string that somebody's code produced, so there is no locale option on the render because there would be nothing for one to do. What the render controls is the page: the size, the margins, and whether the formatted value still fits the column it landed in. That is why each of these pages has a paragraph about the layout consequence, because that is the half a document adds to the general problem.
Frequently asked
Should the document use the sender's conventions or the reader's?
That is a decision rather than a fact, and the only wrong answer is not making it. The issuer's conventions are defensible for a document that is a record of what the issuer did. The recipient's are defensible for a document meant to be acted on. What fails is a document that mixes them, because a reader who sees two conventions on one page cannot tell which one any given figure is in.
Is it enough to set the document language?
No. A language attribute helps with hyphenation, with the assistive technology reading order and with anything that needs to know what language the text is in, and it changes none of the values. A date formatted as 03/04/2026 is exactly as ambiguous in a document declared as English as in one declared as anything else. The values are formatted upstream.
Why does this belong in a document reference rather than a general one?
Because a document cannot be re-rendered by its reader. On a screen a wrong format is annoying and a user can often change a setting. On paper the format is final, the layout was tuned to the string lengths of one language, and a value that grew by a few characters can push a table onto another page. The formatting problem and the pagination problem are the same problem here.
Related formatting topics
The defects that travel together, then the rest of the same group.
Translated labels that no longer fit the column they were sized for
The same label in another language is frequently longer, and a layout tuned to the original wraps, clips or reflows once the translation goes in.
Units on a document, and the quantity with no unit at all
A measured quantity printed without its unit, in the wrong system for the reader, or separated from its unit by a line break.
Alphabetical order that is not alphabetical to the reader
The order a list is sorted into, which is defined by the language rather than by the byte values of the characters.
Numbers and codes inside right-to-left text, in the wrong order
A left-to-right run such as an amount, a reference or a URL embedded in right-to-left text, where the boundary between the two is resolved by an algorithm and not always the way you meant.
Line-item table, as a layout problem
The block this value lands in, and what it takes to make it fit.
Every formatting and locale topic
The full list, grouped by money, time, identity and generated language.
Writing systems in a PDF
The other half: whether the glyphs of a script come out shaped and joined correctly.
Values are formatted by your code before they reach the markup, so every fix on this page is upstream of the render. What the render decides is whether the result still fits.