DEV Community

Pavel Gajvoronski
Pavel Gajvoronski

Posted on

Generating PDFs in 7 languages including RTL Arabic with @react-pdf/renderer

Hook

Our first Arabic PDF looked perfect in the browser previews. Then we opened it in Acrobat: boxes instead of letters, text running left-to-right, and header columns reversed. It took us 3 days to understand why — and about 47 lines of code to fix it. This is what we learned.


Context

Complyance generates compliance documents: technical reports, gap analysis summaries, risk assessments. Our users are in the EU, UAE, and US. The UAE market requires Arabic. Arabic is RTL. The documents must be legally credible — a broken layout or garbled text is not acceptable.

We use @react-pdf/renderer because it lets us write PDF templates as React components, which fits our stack. But Arabic RTL exposed a set of problems that took us several days to resolve.


The problem in detail

Three distinct issues, all interacting:

1. Font rendering. react-pdf uses PDFKit under the hood. Arabic requires a font that includes Arabic glyphs with proper ligature support. The default fonts don't have it. Loading the wrong font produces boxes (☐☐☐☐) or mangled character sequences.

2. Text direction. Arabic is right-to-left but react-pdf doesn't have a native RTL mode. CSS direction: rtl doesn't apply here — this is a layout engine, not a browser.

3. Layout mirroring. In an RTL document, the entire layout flips. Header alignment, column order, margin sides, icon placement — everything that's left-right in LTR becomes right-left in RTL. If you don't mirror the layout, the text renders RTL but the structure looks wrong.


Naive approach / what didn't work

First attempt: set textAlign: "right" on text elements and call it done.

<Text style={{ textAlign: "right" }}>{arabicText}</Text>
Enter fullscreen mode Exit fullscreen mode

Result: text was right-aligned but character order was wrong. Arabic characters need bidirectional text shaping — each character's visual form depends on its neighbors. textAlign is a visual property; it doesn't handle bidirectional rendering.

Second attempt: found a mention of direction in the PDFKit docs. Tried to pass it through react-pdf's style prop. It was silently ignored — react-pdf doesn't pass unknown style properties to the underlying engine.


Actual solution

Step 1: Load an Arabic font

// src/lib/pdf-fonts.ts
import path from "path";
import { Font } from "@react-pdf/renderer";

Font.register({
  family: "NotoSansArabic",
  fonts: [
    {
      src: path.join(process.cwd(), "public/fonts/NotoSansArabic-Regular.ttf"),
      fontWeight: "normal",
    },
    {
      src: path.join(process.cwd(), "public/fonts/NotoSansArabic-Bold.ttf"),
      fontWeight: "bold",
    },
  ],
});
Enter fullscreen mode Exit fullscreen mode

Noto Sans Arabic is the reliable choice — complete glyph coverage, open license, proper ligature support. Download both weights. Register before rendering any document.

Step 2: Locale-aware font selection in components

function getDocumentFont(locale: string): string {
  return locale === "ar" ? "NotoSansArabic" : "Inter";
}

// In the document template:
const font = getDocumentFont(locale);

<Text style={{ fontFamily: font, fontSize: 12 }}>
  {content}
</Text>
Enter fullscreen mode Exit fullscreen mode

Step 3: Layout mirroring via conditional styles

react-pdf doesn't support RTL natively, so we built a small utility that flips layout props:

function rtl(locale: string) {
  return locale === "ar";
}

function directedStyle(locale: string, ltrStyle: Style, rtlStyle: Style): Style {
  return rtl(locale) ? rtlStyle : ltrStyle;
}

// Usage in template:
<View
  style={{
    flexDirection: directedStyle(locale,
      { flexDirection: "row" },
      { flexDirection: "row-reverse" }
    ).flexDirection,
    textAlign: rtl(locale) ? "right" : "left",
  }}
>
Enter fullscreen mode Exit fullscreen mode

For most layout components, we pass locale as a prop and derive direction inline. It's verbose but explicit — you can see exactly what changes for RTL.

Step 4: Handling bidirectional text with unicode markers

For text content that mixes Arabic and Latin characters (product names, URLs, numbers), we inject Unicode bidirectional markers:

const RTL_MARK = "\u200F"; // RIGHT-TO-LEFT MARK
const LTR_MARK = "\u200E"; // LEFT-TO-RIGHT MARK

function wrapForLocale(text: string, locale: string): string {
  if (locale !== "ar") return text;
  return `${RTL_MARK}${text}${RTL_MARK}`;
}
Enter fullscreen mode Exit fullscreen mode

This tells PDFKit's text engine to treat the enclosed text as RTL, which triggers proper bidirectional algorithm handling.

Step 5: The page itself

<Page
  size="A4"
  style={{
    fontFamily: font,
    // react-pdf doesn't have a page-level direction prop,
    // so all RTL is handled via component-level styles above
  }}
>
Enter fullscreen mode Exit fullscreen mode

The full Arabic PDF template conditionally applies all the above. It's about 40 more lines than the English version — mostly the directedStyle calls and the font family threading.


What we learned

  1. Font is the first problem. If you don't have a valid Arabic font registered, nothing else matters. Test font rendering with a simple "hello world" in Arabic before building the layout.

  2. react-pdf has no native RTL. Don't look for a direction prop or an RTL mode. It doesn't exist. You handle it manually through flexDirection: "row-reverse", textAlign: "right", and unicode markers.

  3. flexDirection: "row-reverse" is your main tool. Most RTL layout issues come down to element order. Reversing flex direction handles headers, icon+text pairs, and multi-column layouts cleanly.

  4. Numbers and URLs are always LTR. Even in an Arabic document, version numbers, URLs, and code snippets should render left-to-right. Wrap them in LTR_MARK markers. Forgetting this looks wrong and confuses readers.

  5. Test with actual Arabic text, not lorem ipsum. Lorem ipsum transliterated into Arabic characters won't trigger the same rendering issues as real Arabic text with proper ligatures and bidirectional content.

  6. Font file paths are relative to process.cwd(), not the source file. This bit us in Railway deployment. Use path.join(process.cwd(), "public/fonts/...") not __dirname-relative paths.


What's next

The current implementation handles A4 documents. We haven't tested with A3 or letter size in Arabic. We also haven't handled Farsi (another RTL language with a different character set) — that would require a separate font registration.

The bigger gap: we're generating PDFs server-side in Next.js, which means every render is a cold start for the font registration. Caching the registered fonts across requests would help with document generation latency.


Community questions

  1. Has anyone built a react-pdf template with full RTL support without reverting to a separate RTL-specific template file? We ended up with conditional styles throughout — curious if there's a cleaner abstraction.

  2. What font do you use for Arabic PDF generation? Noto works but it's large. Are there lighter alternatives with comparable coverage?

  3. For teams supporting multiple RTL languages (Arabic, Hebrew, Farsi) — do you maintain one template with locale conditions or separate templates per language family?

Top comments (0)