Articles

Typesetting across languages

What to prepare when a document mixes scripts: fonts, shaping, text direction, CJK and vertical writing, and how to test each.

By Palatine · Published · Updated

Shaping and fonts

Imagine a visitor guide printed in English, Arabic and Japanese. The Arabic text cannot simply be drawn letter by letter: letters change shape depending on their neighbours, and some combine. Palatine uses HarfBuzz to shape text, which is the step that turns a string of characters into the right glyphs from the font: joined Arabic letters, Devanagari conjuncts, accents stacked on Vietnamese vowels, ligatures and kerning in Latin text.

Shaping only works if the font covers the script. You must provide the fonts yourself, and you need an appropriate one for each script in the document. Palatine does not download fonts for you; the hosted API adds Noto Sans as a last resort, which covers Latin, Greek and Cyrillic but not Arabic or CJK. Pass every font with the render, and list them in font-family in order of preference:

await renderer.render({
  html, css, format: "pdf", width: 794, height: 1123,
  fonts: [
    { family: "Body",   data: await readFile("SourceSerif4-Regular.ttf") },
    { family: "Arabic", data: await readFile("NotoNaskhArabic-Regular.ttf") },
    { family: "Japanese", data: await readFile("NotoSerifJP-Regular.ttf") }
  ],
  options: { pagination: true }
});
body { font-family: "Body", "Arabic", "Japanese", serif; }
:lang(ar) { font-family: "Arabic", "Body", serif; line-height: 1.7; }
:lang(ja) { font-family: "Japanese", "Body", serif; }

When the first font lacks a character, the next one in the list is tried, character by character. That is what lets a hotel name in Latin letters sit inside an Arabic sentence. The per-language rules matter too: Arabic needs more line spacing for its tall ascenders and dots below, and an Arabic font's own Latin glyphs rarely match your body face, so name the font you want first for each language. If no loaded font has a character, the result may be blank or a placeholder, so test each language with the exact fonts you plan to ship.

CJK fonts are large, often 10 to 20 MB. That is a cost at render time, not in the output: Palatine embeds only the glyphs the document uses, so a PDF with a page of Japanese stays small. TrueType and WOFF fonts embed compactly; CFF-based .otf fonts are embedded as outlines, which works but makes larger files, so prefer the TrueType build of a family when one exists.

Mixed directions

Arabic and Hebrew run right to left, while English runs left to right. In the guide, an Arabic sentence will often contain a hotel name in Latin letters, or a phone number. This is bidirectional text, and the order of runs on the line follows rules from Unicode. Mark the language and direction in your HTML so the renderer does not have to guess:

<section lang="ar" dir="rtl">
  <h2>الفنادق</h2>
  <p>يقع فندق <span lang="en" dir="ltr">Grand Hotel</span> قرب المحطة.
     الهاتف: <span dir="ltr">+33 1 23 45 67 89</span></p>
</section>

Wrapping the Latin name and the phone number in their own dir="ltr" span keeps the plus sign and the spaces of the number in the right place. Look closely at punctuation and numbers at the edges of mixed lines, because that is where order goes wrong.

dir="rtl" flips more than text: lists put their markers on the right, and multicolumn text starts with the right-hand column. Check your own page furniture as well: a running header placed with @top-left stays on the left, so mirror it yourself for a right-to-left edition. Have someone who reads the language look at the proof.

Line breaking and hyphenation

Every script breaks lines differently. Chinese and Japanese can break between most characters, except before closing punctuation and a few other marks; most other scripts break at spaces. Palatine follows the Unicode line breaking rules, so you do not need to insert break opportunities by hand in CJK text. Long URLs and identifiers are another story: they break at slashes, or not at all, so insert a zero-width space (&#8203;) where a break is acceptable.

Hyphenation is another matter. Automatic hyphenation with hyphens: auto covers English only for now. For German, French or Dutch body text set justified, add soft hyphens (&shy;) to long words in your source, or set the text ragged right; otherwise a long compound word can leave a very loose line. The optional Knuth–Plass composer applies to left-to-right horizontal paragraphs; right-to-left and vertical paragraphs use the standard line breaker.

CJK and vertical text

Chinese, Japanese and Korean text brings different concerns. There are rules about which punctuation may start or end a line: a closing bracket or a full stop should not begin one. Choose a font that has the characters your content uses. A name with a rare character is the kind of thing that can show up as a blank.

Vertical writing runs columns top to bottom, and lines from right to left. It is set with writing-mode:

.tategaki {
  writing-mode: vertical-rl;
  text-orientation: mixed;          /* Latin on its side, CJK upright */
}
.tategaki .date { text-combine-upright: digits 2; }   /* "12" fits in one cell */

Palatine turns CJK characters upright and Latin words on their side, as Unicode recommends, and switches to the font's vertical forms for brackets, commas and the long vowel mark, which are rotated or moved in vertical text. text-combine-upright sets a short number across a single column, the usual way to write a date or a page number. In the PDF, vertical text stays searchable and can be copied.

Some parts are not there yet. Ruby annotations (<rt>, the small reading aids above or beside kanji) are not laid out: the reading stays inline and Palatine emits a layout.ruby warning. Emphasis dots and tables in vertical flow are also limited. If you need vertical text, try it with a real page, not a single line: check where punctuation lands, how Latin letters and digits are handled inside vertical lines, and how page margins behave.

A test page per language

Before you ship, build one test page per language with the hardest content you have:

  • every character set you might meet, including names with rare characters and diacritics;
  • a mixed-direction paragraph with a number, a URL and punctuation at the start and end of a line;
  • a justified paragraph, to see how lines break without hyphenation;
  • copy text from the PDF and paste it, to check that the text layer is right for search.

Then have a native reader check it. Shaping errors and wrong mirroring are invisible to someone who does not read the script, and obvious to everyone who does.

Explore the rendering documentation


All articles