PDFPipe

Formatting and locale / Language in generated text

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.

The defect

Sorting by code point is not alphabetical order in any language, it is byte order, and the two coincide only for unaccented Latin text in one case. Accented characters sort after all the unaccented ones, uppercase sorts before lowercase so a list splits into two alphabets, and languages that treat a letter pair as a single letter, or that place a letter somewhere other than where its code point falls, come out wrong throughout. In a document this is more damaging than on a screen: a printed list is searched by eye, and a reader who cannot find an entry where the alphabet says it should be concludes it is absent rather than misfiled.

What it does to the document

A printed list cannot be re-sorted by the reader, which is the difference from a screen. If the order is wrong, or is right for a different language, the reader's only recourse is to read the whole list. That is why an index, a glossary and an abbreviations list all state their order and why all three need the collation to be right.

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.

  • Sort with a locale-aware collator rather than by comparing strings directly. The rules are per-language data.
  • Use the reader's locale for the sort, not the system's, since the list is being scanned by them.
  • Configure the collator to ignore case for a display list, so the alphabet does not split into an uppercase run and a lowercase one.
  • Sort names with the same care as words, and remember the part you sort on is a separate question from the part you display.
  • Sort numbers as numbers. A numeric collator puts 10 after 9, which string comparison does not.
  • State the sort order in the document where a reader has to rely on it, such as an index or a glossary, because a list whose order is not stated is a list a reader has to infer the order of.

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.

js
const names = ["Zeta", "Ångström", "apple", "Apple", "Ärger", "Zulu"];

// Locale-aware, case-insensitive for a display list, so the alphabet
// does not split into an uppercase run and a lowercase one.
const collator = new Intl.Collator("sv", { sensitivity: "base" });
const sorted = [...names].sort(collator.compare);

// Numbers as numbers: puts 10 after 9, which string comparison does not.
const numeric = new Intl.Collator("en", { numeric: true });
["Item 10", "Item 9"].sort(numeric.compare);

/* Not this: byte order, which is not alphabetical order in any language
   and splits the list into two alphabets by case.
   names.sort();                                                       */

What people do instead

Sorting in the language of the system rather than of the reader. It produces a list that is correctly sorted for somebody who is not holding it, and the defect is invisible to the developer because their own locale is the one it was sorted for.

How to find out rather than assume

Sort a list containing accented characters, mixed case and numbers, render it, and have a native reader scan it for an entry. If they look in the wrong place first, the collation is wrong. Then confirm 10 comes after 9.

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.

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.