In the previous posts of our Advanced PDF Templates series we covered branding, FreeMarker, debugging, and integrating saved searches. To close the series, we tackle one of the most-requested capabilities: producing localized, multi-language Advanced PDF Templates from a single NetSuite template. Whether you’re invoicing a French customer, sending a statement to Japan, or printing a purchase order for a German vendor, this guide shows how to do it cleanly without maintaining a separate template per language.
Why Localization Matters
NetSuite is global by design β multi-subsidiary, multi-currency, multi-book β but PDF output is often still hard-coded in English. A localized template improves customer experience, satisfies legal requirements in regions like the EU (mandatory invoice language) and LATAM, and reduces the support burden of maintaining one template per region.
Determining the Right Language
Inside the template, you have several sources to decide which language to render in. Pick the one closest to the recipient:
- Customer language β
record.entity.language(most accurate for customer-facing documents). - Subsidiary language β
record.subsidiary.language(good fallback). - Country β
record.billaddresslist.countryfor invoices when the customer language is blank. - User preference β useful for internal documents like internal POs.
Step 1 β Define a Locale Variable
At the top of your template, derive a locale once and reuse it everywhere:
<#assign locale = (record.entity.language)!"en_US" /> <#if !locale?has_content> <#assign locale = (record.subsidiary.language)!"en_US" /> </#if>
Step 2 β Build a Translation Map
Instead of repeating <#if locale == ...> for every label, define a single translation map and look up keys:
<#assign labels = {
"en_US": {
"invoice": "Invoice",
"billTo": "Bill To",
"dueDate": "Due Date",
"total": "Total Due"
},
"fr_FR": {
"invoice": "Facture",
"billTo": "Facturer Γ ",
"dueDate": "Date d'Γ©chΓ©ance",
"total": "Total Γ payer"
},
"de_DE": {
"invoice": "Rechnung",
"billTo": "Rechnungsadresse",
"dueDate": "FΓ€lligkeitsdatum",
"total": "Gesamtbetrag"
},
"ja_JP": {
"invoice": "θ«ζ±ζΈ",
"billTo": "θ«ζ±ε
",
"dueDate": "ζ―ζζζ₯",
"total": "εθ¨ιι‘"
}
} />
<#assign t = labels[locale]!labels["en_US"] />
Now your template body becomes language-agnostic:
<h1>${t.invoice}</h1>
<p>${t.billTo}: ${record.billaddress}</p>
<p>${t.dueDate}: ${record.duedate?date}</p>
<p>${t.total}: ${record.total?string.currency}</p>
Step 3 β Format Numbers, Dates, and Currency by Locale
Localizing labels is only half the story. FreeMarker’s built-ins respect the template’s locale setting, so set it explicitly and your numbers and dates fall into line:
<#setting locale = locale /> <#setting number_format = ",##0.00" /> <#setting date_format = "dd/MM/yyyy" />
- fr_FR formats
1 234,56 β¬with a comma decimal and space thousands separator. - de_DE formats
1.234,56 β¬with a period thousands separator. - en_US formats
$1,234.56. - ja_JP formats
Β₯1,235with no decimals.
Step 4 β Right-to-Left and CJK Considerations
Languages like Arabic and Hebrew require right-to-left layout, and Chinese/Japanese/Korean (CJK) need fonts that include the right glyphs.
- Wrap the document in
<html dir="${(locale == 'ar_AR' || locale == 'he_IL')?then('rtl','ltr')}">. - Set
font-familyto a CJK-capable font (e.g., NotoSans CJK) for ja_JP, zh_CN, ko_KR. - Upload the font to the File Cabinet and reference it via
@font-facein inline CSS. - Test with actual sample data β placeholder English text will not catch wrapping issues.
Step 5 β Translate Dynamic Lists (Item Types, Statuses)
Item types and status values come from NetSuite in English by default. Use a secondary lookup map for those values too:
<#assign statusLabels = {
"en_US": { "Open": "Open", "Paid In Full": "Paid In Full" },
"fr_FR": { "Open": "Ouvert", "Paid In Full": "PayΓ© intΓ©gralement" }
} />
${(statusLabels[locale][record.status])!record.status}
Performance and Maintenance Tips
- Externalize translations. For large vocabularies, store the map in a JSON file in the File Cabinet and load it via SuiteScript before rendering β far easier to maintain than editing the template directly.
- Fall back gracefully. Always default to en_US (or your primary language) when a key is missing.
- Use locale codes consistently. Stick to BCP 47 codes (
en_US,fr_FR) and normalize NetSuite’s values if needed. - QA per language. Build a saved search that surfaces sample transactions per language so testers can preview each variant.
Common Pitfalls
- Hard-coding currency symbols (
$) instead of letting FreeMarker’s?string.currencyresolve them per locale. - Forgetting that
record.entity.languagecan be empty for one-off customers β always fall back. - Embedding non-ASCII characters without ensuring the font supports them, which results in blank squares in the PDF.
- Mixing 24-hour and 12-hour time formats across locales; standardize with
?timebuilt-ins.
Final Thoughts
Localizing Advanced PDF Templates is one of the highest-leverage improvements you can make for a global NetSuite footprint. With a single locale variable, a translation map, and locale-aware formatting, you can deliver invoices, statements, and purchase orders that feel native to every customer β all from one template. This wraps our Advanced PDF Templates series; next, we’ll start a new series on NetSuite Workflows for Approval Automation.
Discover more from The NetSuite Pro
Subscribe to get the latest posts sent to your email.
Leave a Reply