Europe/London
Jan 2026
Career
2 min read

Building a multilingual PDF generation system in Drupal and React

Drupal internationalisation, server-side PDF rendering, and multi-language email notifications in one grant application flow.

A grant application required a six-language PDF generation system — applicants submit in their language, reviewers get a PDF in that language, and the grant's secretariat gets an English summary. Here's how we built it.

The challenge

Drupal's built-in PDF generation options (Print module, Entity Print) are good for simple cases but struggle with right-to-left languages, custom fonts, and the complex table layouts the grant form required. We ended up using mPDF via a custom PHP class, called from a WebformHandlerBase.

Language detection and routing

The webform submission carries the language code from the active interface language at submission time. The handler reads this and loads the appropriate string translations before generating the PDF.

class GrantApplicationHandler extends WebformHandlerBase {
  public function postSave(
    WebformSubmissionInterface $submission,
    bool $update = TRUE
  ): void {
    $langcode = $submission->get('langcode')->value ?? 'en';
    $data     = $submission->getData();

    // Generate applicant PDF in submission language
    $applicant_pdf = $this->pdfGenerator->generate($data, $langcode);

    // Generate secretariat PDF always in English
    $secretariat_pdf = $this->pdfGenerator->generate($data, 'en');

    $this->mailer->sendToApplicant($data['email'], $applicant_pdf, $langcode);
    $this->mailer->sendToSecretariat($secretariat_pdf);
  }
}

Font handling: mPDF needs the font files for each language explicitly declared. Arabic and French use different font stacks. Add them to your mPDF config, not inline in the template — otherwise every PDF generation reloads the font files from disk.

Testing across languages

We created a test submission endpoint that let us trigger PDF generation for any language without filling in the full form. This was essential — the form had 47 fields and testing all language combinations manually would have taken days.