PDFPipe

Formatting and locale / Language in generated text

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.

The defect

Text with both directions in it is laid out by the bidirectional algorithm, which decides the visual order of each run from the characters themselves. That works for prose and it is unreliable at the edges of an embedded run, because characters like brackets, slashes, colons and trailing punctuation are directionally neutral and get absorbed into whichever run the algorithm decides. The symptom is that a reference number displays with its punctuation at the wrong end, or an amount and its currency code swap, or a trailing full stop jumps to the front of a line. The value is intact in the file and wrong on the page, and nobody who cannot read the script will spot it.

What it does to the document

Alignment inverts as well as order: in a right-to-left document the amount column belongs on the left and the label on the right, which is the mirror of the layout every totals and table page in the section cluster assumes. Mirror the layout with logical properties rather than by rewriting the rules per direction.

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.

  • Isolate every embedded opposite-direction run rather than relying on the algorithm to guess the boundary. Isolation is what tells it where the run ends.
  • Use the CSS isolation property or the Unicode isolate characters, and prefer whichever your pipeline handles consistently, but use one of them.
  • Isolate identifiers, amounts, dates, URLs and file names specifically. Those are the runs with neutral characters at their edges.
  • Set the direction on the container from the content's language rather than globally, so a document with mixed content gets it right per block.
  • Never reorder characters yourself to make the display look right. You will have corrupted the value to fix the rendering, and it will then be wrong everywhere else.
  • Have it read by somebody who reads the script. This is not checkable by inspection if you do not.

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.

html
<!-- Isolate the embedded left-to-right run so the algorithm knows
     where it ends. Without this the punctuation at the edges can be
     absorbed into the surrounding run and displayed at the wrong end. -->
<p class="rtl">
  ... <span class="ltr-run" dir="ltr">WH-2026-0418</span> ...
</p>

<style>
  .rtl { direction: rtl; text-align: right; }

  .ltr-run {
    /* isolate, not embed: isolate tells the algorithm this run is a
       self-contained unit and stops the neutral characters at its
       edges from being pulled in. */
    unicode-bidi: isolate;
    direction: ltr;
  }

  /* Amounts, references, dates, URLs and file names all need this: they
     are the runs with neutral characters at their boundaries. */
  .amount, .ref, .url, .filename { unicode-bidi: isolate; direction: ltr; }
</style>

What people do instead

Fixing a wrong-looking reference by reordering the characters in the data. The page then looks right, the stored value is corrupted, and every other consumer of that value, including search and the counterparty's system, now has a string that does not match.

How to find out rather than assume

Render a document with embedded identifiers and amounts and have somebody who reads the script check them, character by character, against the source values. There is no substitute: the failure is a rendering-order problem and inspecting the file will show you correct data.

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.