Fonts & Internationalization

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

Baseline date: 2026-10-09. A PDF viewer can only draw a character if it has a glyph for it, either from a font that travels inside the file or from one it finds on the reader’s machine. Getting that right is the difference between an invoice that looks the same everywhere and one that shows empty boxes for the customer’s name on somebody else’s laptop. The second half of the problem is language: the same Bookshelf Billing invoice has to read "Invoice" for an English customer and "Factura" for a Spanish one, with dates and amounts written the local way. This page covers both for JasperReports 7.0.8: how fonts reach the engine, how to ship your own, what happens when a font is missing, and how resource bundles and the report locale produce a localized document.

The examples were compiled and run with Java 21 and JasperReports 7.0.8 (jasperreports, jasperreports-pdf, jasperreports-fonts, jasperreports-jdt), and every PDF was read back with Apache PDFBox 3.0.8 to list the fonts and whether they are embedded and to extract the text. Results that come from that run, including a surprising one about resource bundle lookup, are marked as such. What was not run (a Spring MVC controller, a container image) is said so where it appears.

Fonts in a PDF: standard fonts and embedded fonts

A PDF file can describe text in two ways. It can name one of the fourteen standard fonts (Helvetica, Times, Courier and a few others), which every viewer is expected to supply, or it can carry a font program inside the file, usually a subset that holds only the glyphs the document uses. The first way gives small files but only the Latin characters of the standard encodings, and different viewers may draw the glyphs slightly differently. The second way gives an identical result everywhere and is the only way to print Greek, Cyrillic, or any character outside the old Latin encodings. It is also required by the archival standard, see PDF Standards & Conformance, and the file-level mechanics (font dictionaries, subsets, ToUnicode maps) are described in PDF File Structure. The same topic from the iText side is in Fonts & Unicode.

For a PDF produced by JasperReports the decision is made per text element through three values: the font program to use, the PDF encoding, and whether to embed. The jasperreports-pdf module delegates the drawing to the OpenPDF library, so these three values are what the exporter hands to OpenPDF. A TrueType font written with the encoding Identity-H and embedded is the combination that works for any Unicode text; this is what the DejaVu families in jasperreports-fonts declare, and it is what the run below shows.

The font attributes of a text element

A text element selects its font with a few attributes. In v7 JRXML they are written directly on the <element kind="textField"> or <element kind="staticText"> element, or inherited from a style.

Attribute Meaning

fontName

The name of a font family: either one declared by a font extension (such as DejaVu Sans) or a font known to the JVM.

fontSize, bold, italic

Size in points (default 10) and the face to pick inside the family.

underline, strikeThrough

Decorations.

pdfFontName, pdfEncoding, pdfEmbedded

The PDF equivalent of the font, its encoding and the embedding flag. These are the legacy way, described below.

Inheritance works as for every other attribute: an element inherits from its style, and the style from the default style (default="true") declared in the report. A report with no default style and no explicit font uses the property net.sf.jasperreports.default.font.name, whose value in the shipped defaults of 7.0.8 is SansSerif.

The legacy route is worth recognizing because older tutorials and templates are full of it. Before font extensions existed, a template declared a named reportFont element, and each text element carried pdfFontName, pdfEncoding and isPdfEmbedded, sometimes pointing at a font file on disk or at an Acrobat Asian font pack for Chinese, Japanese or Korean text. Those books and posts are outdated on three counts. The reportFont element is gone from the v7 format in favour of styles. The boolean attribute is now pdfEmbedded. And the official sample documentation says that the pdfEncoding and pdfEmbedded attributes on the element are deprecated in favour of the same two settings inside the font family of an extension, which is the route the rest of this page follows. (The old attributes were also run against 7.0.8: an element with pdfFontName pointing at a TrueType file on the classpath, pdfEncoding="Identity-H" and pdfEmbedded="true" still produced an embedded font, so old templates keep working once converted to v7 syntax. It is a compatibility path, not one to copy.)

The reference for all of this is the fonts sample.

Font extensions

A font extension is the way to make fonts part of the application instead of part of the machine. It is a classpath resource set made of three things: the TrueType files, an XML file describing font families, and a jasperreports_extension.properties file at the root of the classpath that registers the XML. The engine finds every jasperreports_extension.properties on the classpath, so a font extension can be a separate jar, or just resources of the application.

The jasperreports-fonts artifact is the extension that JasperReports itself ships. It contains the DejaVu fonts, in the families DejaVu Sans, DejaVu Sans Condensed, DejaVu Serif, DejaVu Serif Condensed and DejaVu Sans Mono (read from the XML file of the 7.0.8 jar), together with the logical names SansSerif, Serif and Monospaced, which are declared only to give the HTML exporter a CSS font stack, and its registration file. Adding the dependency is enough to make fontName="DejaVu Sans" work in any template:

<dependency>
  <groupId>net.sf.jasperreports</groupId>
  <artifactId>jasperreports-fonts</artifactId>
  <version>7.0.8</version>
</dependency>

Its registration file, read from the 7.0.8 jar, is two lines:

net.sf.jasperreports.extension.registry.factory.simple.font.families=net.sf.jasperreports.engine.fonts.SimpleFontExtensionsRegistryFactory
net.sf.jasperreports.extension.simple.font.families.dejavu=net/sf/jasperreports/fonts/dejavu/jasperreports-fonts.xml

The first line names the factory class that turns font-family XML files into registry entries. The second says "here is one XML file", and its last segment (dejavu) is only a unique name, so each extension uses a different one. A family declared there looks like this (the DejaVu Sans entry of the jar, shortened):

<fontFamilies>
  <fontFamily name="DejaVu Sans">
    <normal>net/sf/jasperreports/fonts/dejavu/DejaVuSans.ttf</normal>
    <bold>net/sf/jasperreports/fonts/dejavu/DejaVuSans-Bold.ttf</bold>
    <italic>net/sf/jasperreports/fonts/dejavu/DejaVuSans-Oblique.ttf</italic>
    <boldItalic>net/sf/jasperreports/fonts/dejavu/DejaVuSans-BoldOblique.ttf</boldItalic>
    <pdfEncoding>Identity-H</pdfEncoding>
    <pdfEmbedded>true</pdfEmbedded>
    <exportFonts>
      <export key="net.sf.jasperreports.html">'DejaVu Sans', Arial, Helvetica, sans-serif</export>
    </exportFonts>
  </fontFamily>
</fontFamilies>

Each part has a job:

  • name and <normal> are the only required parts. The name is what a template writes in fontName.

  • <bold>, <italic> and <boldItalic> are the other three faces, picked from the bold and italic attributes of the element. If a face is not declared, the normal one is used, so a family that lacks a bold file shows bold text in regular weight, without an error.

  • <pdfEncoding> and <pdfEmbedded> hold the PDF settings once for the family, instead of on every element. Identity-H with true is the right pair for a TrueType font.

  • <exportFonts> maps the family to a CSS font stack for the HTML exporter, so a report exported to HTML falls back to fonts a browser has.

  • <locales> (commented out in the DejaVu file) restricts a family to a list of locales. Several families may share a name if they differ in their locales, which is how one report can use a Japanese font for the Japanese locale and a Chinese font for the Chinese locale, since no single file covers both.

Fonts that the application owns follow the same recipe. The Bookshelf company font (here the four DejaVu Serif files from the same jar stand in for it, so the example runs without a licence question) is registered in three files:

# src/main/resources/jasperreports_extension.properties
net.sf.jasperreports.extension.registry.factory.simple.font.families=net.sf.jasperreports.engine.fonts.SimpleFontExtensionsRegistryFactory
net.sf.jasperreports.extension.simple.font.families.bookshelf=fonts/bookshelf-fonts.xml
<!-- src/main/resources/fonts/bookshelf-fonts.xml -->
<fontFamilies>
  <fontFamily name="Bookshelf Serif">
    <normal>fonts/ttf/DejaVuSerif.ttf</normal>
    <bold>fonts/ttf/DejaVuSerif-Bold.ttf</bold>
    <italic>fonts/ttf/DejaVuSerif-Italic.ttf</italic>
    <boldItalic>fonts/ttf/DejaVuSerif-BoldItalic.ttf</boldItalic>
    <pdfEncoding>Identity-H</pdfEncoding>
    <pdfEmbedded>true</pdfEmbedded>
  </fontFamily>
</fontFamilies>

The TrueType files go in src/main/resources/fonts/ttf/. In the run, a template using fontName="Bookshelf Serif" produced a PDF whose font list contained an embedded DejaVuSerif subset, so the extension was found with nothing but the classpath. Two practical points follow from how the registration works. The resource names in the XML are classpath resource names, with no leading slash. And because every extension uses the same file name, jasperreports_extension.properties, a build that merges several jars into one (a shaded jar, for example) must merge those files instead of keeping one of them, or the other extensions silently disappear; a Spring Boot executable jar keeps its dependency jars nested and does not have this problem.

The official walkthrough, including a second extension based on the open-source Lobster Two font and a tool that prints the fonts.xml of all installed extensions, is in the fonts sample (section "Font Extensions").

Always check the licence of a font before bundling it in a deployable. The DejaVu fonts are distributed under a permissive free licence, which is why JasperReports can bundle them. A corporate or purchased font usually has terms about embedding in documents and about redistribution inside an application, and the answer is in its licence file, not in this page.

Defaults and the properties that govern them

When a template does not say, the engine falls back on properties. The relevant ones and their values in the shipped default.jasperreports.properties of 7.0.8 (read from the jar) are:

Property Default Effect

net.sf.jasperreports.default.font.name

SansSerif

Font family of an element that has no font and inherits none.

net.sf.jasperreports.default.font.size

10

Its size.

net.sf.jasperreports.default.pdf.font.name

Helvetica

PDF font used when an element has no pdfFontName and its family has no PDF settings.

net.sf.jasperreports.default.pdf.encoding

Cp1252

PDF encoding in that case.

net.sf.jasperreports.default.pdf.embedded

false

Embedding flag in that case.

net.sf.jasperreports.awt.ignore.missing.font

false

Whether a fontName that no extension and no JVM font provides is tolerated.

A property can be set on a SimpleJasperReportsContext, in a jasperreports.properties file on the classpath, or on a report or element as a property, and the reference page lists where each applies: net.sf.jasperreports.default.pdf.embedded and net.sf.jasperreports.awt.ignore.missing.font.

The defaults are the reason a template written with the logical font SansSerif produces a PDF that is not self-contained. Three runs against 7.0.8 show the consequences (the text was extracted back from the PDF with PDFBox):

Template PDF font in the file Text of "Ήλιος Ελλάδα" read back

fontName="DejaVu Sans"

DejaVu Sans subset, embedded

Correct.

fontName="SansSerif" (defaults)

Helvetica, not embedded

Nothing: the Greek letters have no glyph in the Cp1252 Helvetica.

fontName="SansSerif" and default.pdf.embedded=true

Helvetica, not embedded

Nothing, with no change from the line above.

The last row is worth understanding: the embedding flag asks for a font file to be embedded, but Helvetica is a standard font with no file behind it, so the flag changes nothing for it. default.pdf.embedded=true helps only when the font that reaches the exporter is a real TrueType file. The robust rule is to avoid the logical font names in templates meant for PDF, give every text element (usually through the default style) a family from an extension, and set the encoding and the embedding in that family.

The old route of choosing a different single-byte encoding is still possible: an element with pdfFontName="Helvetica" and pdfEncoding="Cp1253" printed the Greek text correctly in the run, without embedding. That depends on the viewer having a suitable Greek font to substitute and on the text being limited to that one code page, so it is no substitute for embedding, and it does not help once a document mixes scripts.

Missing fonts, on servers and in containers

A template that names a font nobody provides fails by default. With net.sf.jasperreports.awt.ignore.missing.font at its default of false, filling the report raised a JRFontNotFoundException, with the message that the font is not available to the JVM, and logged a warning with a pointer to the Javadoc of that exception. With the property set to true the fill continues, a warning is logged, the missing font is replaced by Helvetica, and the PDF contained a non-embedded Helvetica and none of the Greek text. Both outcomes were observed in the run. The failing behaviour is the better one for a service: it turns a silent font substitution into an error that shows up in the first test, and true is a choice for a report that must be produced whatever happens.

The same reasoning is why fonts should come from an extension on any machine that is not a developer laptop. A font that is only "installed" is a property of the machine, and a build server, a Kubernetes node image and a developer’s Mac have different fonts. Two points apply to containers:

  • The fonts the report uses must be on the classpath through an extension, never assumed to be on the image. A minimal base image ships almost no fonts, so a fontName that only resolves on the developer’s machine fails the first time the report runs in the cluster.

  • The JVM itself uses its AWT font classes to measure text for the layout, even when the final PDF is written by OpenPDF. Slim Linux images without the font-configuration library are commonly reported to break that part (typically with a NullPointerException or an error mentioning font configuration), and the usual remedy is to install fontconfig and a base font package in the image. No container was run for this page, so treat that point as a known pattern to check on your own image, not as something verified here; the JVM requirements of AWT fonts on your JDK distribution are documented by the distribution.

An integration test that fills and exports every template with the production classpath, and then checks the PDF for embedded fonts the way the next section does, catches both problems before deployment. How to run such tests is covered in Testing & Performance.

Unicode, scripts and text direction

Report text is Unicode inside the engine, and the PDF can carry any of it as long as a font with the glyphs is embedded. Three rules cover most cases:

  1. Keep the JRXML in UTF-8, which is the default, and declare the encoding in the XML header only if you must use another one. The characters of the template itself are decoded with that encoding when it is compiled.

  2. Do not trust the DejaVu fonts for every script. They cover Latin, Greek and Cyrillic well, and in the run the Greek and Cyrillic book titles came out right. They do not contain Chinese, Japanese or Korean characters, so CJK text needs a font family with those glyphs registered as an extension (usually a Noto Sans CJK file or another CJK family you are licensed to embed), and as the sample shows, a separate family per locale when no single file covers all of them. The old Acrobat Asian font packs, which the older books describe for CJK output, are not the way to do it today.

  3. For text written right to left, such as Arabic and Hebrew, the documentation of the i18n sample states that the library keeps the text run direction in the generated document, so these documents render properly. That needs a font with the Arabic or Hebrew glyphs, again through an extension. No Arabic or Hebrew report was run for this page.

Output formats that are text (CSV, HTML, XML) have their own encoding, set at export time with net.sf.jasperreports.export.character.encoding, which defaults to UTF-8. It does not apply to PDF, where the font encoding is the one discussed above. The unicode sample shows a template with Greek, Cyrillic, Chinese, Arabic and Hebrew text written as \uXXXX escapes in expressions, which is also a way to keep a JRXML pure ASCII.

Internationalizing a report

JasperReports separates the text that changes with the language from the layout. The texts live in java.util.ResourceBundle files, the template refers to them by key, and the engine chooses the right file from the locale of the fill. The mechanism has four parts.

Resource bundles and $R{}

A resource bundle is a set of .properties files with the same base name and a language suffix. For the invoice, under src/main/resources/messages/:

# invoice.properties (default, used when nothing more specific exists)
invoice.title=Invoice
invoice.greeting=Dear {0}, thank you for your order.
invoice.column.title=Title
invoice.column.quantity=Qty
invoice.column.amount=Amount
invoice.total=Total
# invoice_es.properties
invoice.title=Factura
invoice.greeting=Estimado/a {0}, gracias por su pedido.
invoice.column.title=T\u00edtulo
invoice.column.quantity=Cant.
invoice.column.amount=Importe
invoice.total=Total

The template names the bundle with the resourceBundle attribute of the report, using the base name as a classpath resource name with dots (messages.invoice), and writes $R{invoice.title} wherever a key is needed. The $R{} syntax is an expression like the ones of Expressions, Parameters, Fields & Variables, so it can be combined with other values. Since Java 9 the standard library reads properties bundles as UTF-8, so the accented characters can be written directly in the file; the \u00ed escape form above also works on any version.

The text with a parameter is formatted with the msg() method, which behaves like java.text.MessageFormat and has three signatures for one, two or three arguments: msg($R{invoice.greeting}, $P{CUSTOMER}). The str() method is the function form of $R{}: str("invoice.title") is the same as $R{invoice.title}, useful when the key is built at run time. The bundle can also be given when the report is filled instead of in the template, by passing a ResourceBundle in the built-in REPORT_RESOURCE_BUNDLE parameter; in the run both str() and $R{} read the keys of a bundle passed this way and returned "Invoice" and "Total" for English and "Factura" and "Total" for Spanish.

The report locale

The locale of a fill is the built-in REPORT_LOCALE parameter (JRParameter.REPORT_LOCALE). It drives three things: which bundle file is loaded, how dates and numbers are formatted by text fields that have a pattern, and the locale available to expressions. Without it the engine uses the default locale of the JVM. A time zone is separate and is set with REPORT_TIME_ZONE, which matters for any report that prints a date and time.

The same pattern="#,##0.00" for an amount and pattern="dd MMMM yyyy" for a date produced these results from the run, filled with Locale.ENGLISH and es-ES:

Item English Spanish

Title

Invoice

Factura

Date

21 September 2026

21 septiembre 2026

Amount

1,234.50

1.234,50

Total

1,278.40

1.278,40

The pattern is the same, the grouping and decimal separators and the month name come from the locale. Because of this, the rule for a localized report is to keep the patterns in the template free of hard-coded separators and to let the locale decide.

When a key is missing

An absent key can be handled in four ways, selected with the whenResourceMissingType attribute of the report: Null, Empty, Key and Error (the values of WhenResourceMissingTypeEnum). A text field with the expression "<" + $R{no.such.key} + ">" printed this under each setting:

whenResourceMissingType Text printed (and what happens)

Null (the default; also used when the attribute is omitted)

<null>: the expression value is null, which string concatenation prints as null.

Empty

<>: an empty string.

Key

<no.such.key>: the key itself.

Error

No document: the fill fails with a JRExpressionEvalException whose root cause is a MissingResourceException naming the key.

For development and tests Error is the setting that finds a forgotten translation at once. For production Key is the usual friendly choice, because a visible key tells support what is missing, whereas Null can print the word "null" inside a sentence. Empty suits optional lines. If the whole bundle cannot be found, as opposed to one key, the lookup is the one of java.util.ResourceBundle, described next.

Which file is chosen: a trap with the default locale

Resource bundle lookup in Java does not go straight from the requested locale to the default file. For a request for English it first looks for invoice_en, and if there is none it tries the bundle of the JVM’s default locale, and only then the base file. This was the one surprise of the run. On a machine whose default locale was Spanish, the invoice was filled with Locale.ENGLISH and, because the project had invoice.properties and invoice_es.properties but no invoice_en.properties, the English invoice came out in Spanish ("Factura", "Estimado/a …​"), while its dates and amounts were still formatted in English. With the JVM default set to German the same code printed the English texts from the base file. The result therefore depends on the server’s settings, which is exactly what an internationalized service must not do.

The cure is to give every supported language its own file, including the one the base file is written in, so the base file is only a last resort. In the run, adding invoice_en.properties (a copy of the base file) made the English invoice English on both configurations. The other half is to choose the locale explicitly for each request and not rely on the default of the JVM, as the next section does.

A complete example

The pieces fit together in one small program. It uses the invoice-i18n.jrxml below, the font extension and the two bundles from the sections above (add an invoice_en.properties copy of the base file, for the reason just given), a plain bean for the lines (a class with getters: a Java record does not work as a bean data source, see Data Sources & Query Executers), and prints the invoice in two languages.

The Maven dependencies, with the Java 21 release and UTF-8 for the source file with Greek and Cyrillic titles:

<properties>
  <maven.compiler.release>21</maven.compiler.release>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  <jasperreports.version>7.0.8</jasperreports.version>
</properties>

<dependencies>
  <dependency>
    <groupId>net.sf.jasperreports</groupId>
    <artifactId>jasperreports</artifactId>
    <version>${jasperreports.version}</version>
  </dependency>
  <dependency>
    <groupId>net.sf.jasperreports</groupId>
    <artifactId>jasperreports-pdf</artifactId>
    <version>${jasperreports.version}</version>
  </dependency>
  <dependency>
    <groupId>net.sf.jasperreports</groupId>
    <artifactId>jasperreports-fonts</artifactId>
    <version>${jasperreports.version}</version>
  </dependency>
  <dependency>
    <groupId>net.sf.jasperreports</groupId>
    <artifactId>jasperreports-jdt</artifactId>
    <version>${jasperreports.version}</version>
  </dependency>
</dependencies>

The template, in v7 JRXML (src/main/resources/reports/invoice-i18n.jrxml). The default style sets the family once, the title uses the application font, title and summary are bands directly under the root, and every visible text comes from the bundle:

<jasperReport name="invoice-i18n" language="java" pageWidth="595" pageHeight="842"
              columnWidth="515" leftMargin="40" rightMargin="40" topMargin="40" bottomMargin="40"
              resourceBundle="messages.invoice" whenResourceMissingType="Key">
  <style name="Base" default="true" fontName="DejaVu Sans" fontSize="10"/>
  <parameter name="CUSTOMER" class="java.lang.String"/>
  <parameter name="ISSUE_DATE" class="java.util.Date"/>
  <field name="title" class="java.lang.String"/>
  <field name="quantity" class="java.lang.Integer"/>
  <field name="amount" class="java.math.BigDecimal"/>
  <variable name="total" calculation="Sum" class="java.math.BigDecimal">
    <expression><![CDATA[$F{amount}]]></expression>
  </variable>
  <title height="80">
    <element kind="textField" x="0" y="0" width="300" height="30" fontName="Bookshelf Serif" fontSize="20" bold="true">
      <expression><![CDATA[$R{invoice.title}]]></expression>
    </element>
    <element kind="textField" x="315" y="0" width="200" height="30" hTextAlign="Right" pattern="dd MMMM yyyy">
      <expression><![CDATA[$P{ISSUE_DATE}]]></expression>
    </element>
    <element kind="textField" x="0" y="40" width="515" height="30">
      <expression><![CDATA[msg($R{invoice.greeting}, $P{CUSTOMER})]]></expression>
    </element>
  </title>
  <columnHeader height="22">
    <element kind="textField" x="0" y="0" width="300" height="20" bold="true">
      <expression><![CDATA[$R{invoice.column.title}]]></expression>
    </element>
    <element kind="textField" x="300" y="0" width="60" height="20" bold="true" hTextAlign="Right">
      <expression><![CDATA[$R{invoice.column.quantity}]]></expression>
    </element>
    <element kind="textField" x="365" y="0" width="150" height="20" bold="true" hTextAlign="Right">
      <expression><![CDATA[$R{invoice.column.amount}]]></expression>
    </element>
  </columnHeader>
  <detail>
    <band height="20">
      <element kind="textField" x="0" y="0" width="300" height="18">
        <expression><![CDATA[$F{title}]]></expression>
      </element>
      <element kind="textField" x="300" y="0" width="60" height="18" hTextAlign="Right">
        <expression><![CDATA[$F{quantity}]]></expression>
      </element>
      <element kind="textField" x="365" y="0" width="150" height="18" hTextAlign="Right" pattern="#,##0.00">
        <expression><![CDATA[$F{amount}]]></expression>
      </element>
    </band>
  </detail>
  <summary height="40">
    <element kind="textField" x="0" y="10" width="360" height="20" bold="true">
      <expression><![CDATA[$R{invoice.total}]]></expression>
    </element>
    <element kind="textField" x="365" y="10" width="150" height="20" bold="true" hTextAlign="Right" pattern="#,##0.00">
      <expression><![CDATA[$V{total}]]></expression>
    </element>
  </summary>
</jasperReport>

The line bean and the service that compiles once, fills with an explicit locale and exports to PDF bytes:

package bookshelf;

import java.math.BigDecimal;

public class InvoiceLine {
    private final String title;
    private final int quantity;
    private final BigDecimal amount;

    public InvoiceLine(String title, int quantity, BigDecimal amount) {
        this.title = title;
        this.quantity = quantity;
        this.amount = amount;
    }

    public String getTitle() { return title; }
    public int getQuantity() { return quantity; }
    public BigDecimal getAmount() { return amount; }
}
package bookshelf;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JRParameter;
import net.sf.jasperreports.engine.JasperCompileManager;
import net.sf.jasperreports.engine.JasperFillManager;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.engine.JasperReport;
import net.sf.jasperreports.engine.data.JRBeanCollectionDataSource;
import net.sf.jasperreports.export.SimpleExporterInput;
import net.sf.jasperreports.export.SimpleOutputStreamExporterOutput;
import net.sf.jasperreports.pdf.JRPdfExporter;

public class LocalizedInvoice {

    private final JasperReport report;

    public LocalizedInvoice() throws JRException, IOException {
        try (InputStream in = getClass().getResourceAsStream("/reports/invoice-i18n.jrxml")) {
            this.report = JasperCompileManager.compileReport(in);
        }
    }

    public byte[] render(Locale locale, String customer, List<InvoiceLine> lines)
            throws JRException {
        Map<String, Object> params = new HashMap<>();
        params.put(JRParameter.REPORT_LOCALE, locale);
        params.put("CUSTOMER", customer);
        params.put("ISSUE_DATE", new Date());

        JasperPrint print = JasperFillManager.fillReport(
                report, params, new JRBeanCollectionDataSource(lines));

        ByteArrayOutputStream out = new ByteArrayOutputStream();
        JRPdfExporter exporter = new JRPdfExporter();
        exporter.setExporterInput(new SimpleExporterInput(print));
        exporter.setExporterOutput(new SimpleOutputStreamExporterOutput(out));
        exporter.exportReport();
        return out.toByteArray();
    }

    public static void main(String[] args) throws Exception {
        LocalizedInvoice invoice = new LocalizedInvoice();
        List<InvoiceLine> lines = List.of(
                new InvoiceLine("Don Quijote de la Mancha", 1, new BigDecimal("1234.50")),
                new InvoiceLine("Η Οδύσσεια", 2, new BigDecimal("19.90")),
                new InvoiceLine("Война и мир", 1, new BigDecimal("24.00")));
        for (Locale locale : List.of(Locale.ENGLISH, Locale.forLanguageTag("es-ES"))) {
            byte[] pdf = invoice.render(locale, "Ana Pérez", lines);
            Files.write(Path.of("target/invoice-" + locale.getLanguage() + ".pdf"), pdf);
        }
    }
}

Run it with mvn compile exec:java -Dexec.mainClass=bookshelf.LocalizedInvoice (the exec plugin resolves from the command line). With the extension and DejaVu Sans on the classpath, both PDFs listed DejaVuSerif-Bold, DejaVuSans and DejaVuSans-Bold, all as embedded Type 0 (Identity-H) fonts, and the extracted text contained the Greek and Cyrillic titles intact. The compile step used the Eclipse JDT compiler of jasperreports-jdt. The approach is the one of the official i18n sample, applied to the Bookshelf scenario.

In a Spring Boot 4 service the locale of each request becomes the REPORT_LOCALE of the fill. Spring MVC resolves a Locale controller argument from the Accept-Language header by default, so the controller only passes it on. This controller was not compiled for this page, but it uses only standard Spring MVC types; the service above is registered as a bean. How to return the bytes with the right headers and how to stream a large PDF are covered in Getting Started with Spring Boot.

package bookshelf;

import java.math.BigDecimal;
import java.util.List;
import java.util.Locale;

import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class InvoiceController {

    private final LocalizedInvoice invoices;

    public InvoiceController(LocalizedInvoice invoices) {
        this.invoices = invoices;
    }

    @GetMapping(value = "/invoices/sample", produces = MediaType.APPLICATION_PDF_VALUE)
    public ResponseEntity<byte[]> sample(Locale locale) throws Exception {
        List<InvoiceLine> lines = List.of(
                new InvoiceLine("Don Quijote de la Mancha", 1, new BigDecimal("1234.50")));
        byte[] pdf = invoices.render(locale, "Ana Pérez", lines);
        return ResponseEntity.ok()
                .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"invoice.pdf\"")
                .body(pdf);
    }
}

Given the trap above, the locales a service supports should be an explicit list: map the requested locale to the nearest supported one before the fill, so a request for French does not end up in whichever bundle the server’s default locale selects.

A checklist for fonts and languages

  • Give the default style a family from a font extension, and never rely on SansSerif for PDF output.

  • Put the font files in the application (a jasperreports_extension.properties, a fonts XML, the TrueType files), with Identity-H and pdfEmbedded set in the family, and check the font licence.

  • Leave awt.ignore.missing.font at false so a missing font fails the build instead of changing the document.

  • Cover every script the data can contain; DejaVu does not cover CJK, Arabic or Hebrew.

  • Use $R{} and msg() for every visible text, one properties file per supported language including English, and whenResourceMissingType="Error" in tests and Key in production.

  • Pass REPORT_LOCALE explicitly on every fill, and keep patterns free of hard-coded separators.

  • After the export, read the PDF back in a test: list its fonts and assert that they are embedded, and extract a non-Latin string.

Where to go next