Custom Handlebars.js helpers
How to add custom certificate helpers
Custom helpers let your country config add new Handlebars functions to certificate templates — anything from simple text formatting to complex multi-language address rendering. They are the escape hatch when the built-in helpers ($lookup, $join, $intl, etc.) aren't enough.
What Custom Helpers Are
Custom helpers are country-specific Handlebars functions that you define in TypeScript and that become available to your certificate SVG templates as new template tags.
For example, if you define a helper called formatNationalId, your SVG template can call:
{{formatNationalId ($lookup $declaration "applicant.nationalId")}}Built-in helpers ($lookup, $intl, $join, etc.) are defined in opencrvs-core and are always available. Custom helpers extend that set with anything your country's certificates need that the core doesn't provide.
How They Work End-to-End
Country config server starts
→ handler.ts reads helpers.ts
→ esbuild compiles TypeScript → JavaScript (ESM)
→ compiled JS is served at GET /handlebars.js
Client loads the app
→ referenceApi.importHandlebarHelpers() fetches /handlebars.js
→ all named exports are stored in memory as the helpers registry
User prints or reviews a certificate
→ compileSvg() is called
→ for each helper name in the registry:
customHelpers[helperName]({ intl }) ← factory called with intl
Handlebars.registerHelper(name, result)
→ Handlebars.compile(svgTemplate)(data) runs
→ custom helper is called wherever {{helperName ...}} appears in SVGThe key line in core (pdfUtils.ts:289-298):
Every export from your helpers.ts is called with { intl } and expected to return a Handlebars.HelperDelegate. This is the factory pattern — your export is not the helper itself, it's a function that receives context and returns the helper.
Where to Write Them
The handler compiles helpers.ts to JavaScript via esbuild and serves it at the /handlebars.js route. You never need to change the handler.
The Factory Pattern
Every export must be a factory function: a function that accepts { intl } (optionally) and returns the actual Handlebars helper function.
Why the factory? Core needs to inject intl (the active locale's translation engine) into your helper at render time. Because intl is created fresh per certificate render (with the correct locale), it can't be imported statically — it must be passed in. The factory pattern is how core hands it to you.
Type definition from core:
How Many Parameters You Can Pass
The Handlebars HelperDelegate type signature allows up to 6 positional arguments before the mandatory options object:
In practice context acts as your first positional argument. So the count is:
Your helper function receives them in order, with options silently appended as the final argument by Handlebars:
Practical advice: 3–4 positional arguments is the comfortable ceiling before templates become hard to read. If you find yourself needing more, consider splitting into multiple helpers or passing a structured value via $lookup and reading the rest from options.data.root.
You don't have to declare
optionsin your function signature — Handlebars passes it regardless. Only declare it when you actually need to use it.
The options Object
Handlebars always passes an options object as the very last argument to every helper call, whether you declare it or not. Its type is:
options.fn and options.inverse
Used only for block helpers — helpers that wrap a content block like {{#helper}}...{{/helper}}.
options.fn(this)renders the inner blockoptions.inverse(this)renders the{{else}}block
In template:
options.hash
Named key=value arguments passed in the template call. Useful when you want optional, named configuration instead of positional args.
In template:
options.data.root
The most commonly used property. It gives you the entire top-level template context from inside a helper, regardless of how deeply nested the call is.
In V2 templates, options.data.root contains:
$declaration
Stringified form field data (same as what $lookup $declaration accesses)
$metadata
Resolved event metadata (same as what $lookup $metadata accesses)
$review
Boolean — true during review/preview, false during print
$references
{ locations: Map, users: Array } — raw reference data
options.data iteration keys
When your helper is called from inside a {{#each}} block, options.data also carries iteration context:
Example of custom handlebar helpers
1. Simple Helper (no intl)
When your helper doesn't need translation, just ignore the factory argument.
In template:
2. Helper with i18n translation
When you need to translate something that the built-in $intl helper can't handle — for example, translating a country code with a language-specific fallback, or combining multiple translation lookups with custom logic.
In template:
3. Helper with multiple arguments
Positional arguments come before options. Declare options only if you need it.
In template:
Another example — time formatting with a single argument:
4. Debug helper — inspect template context
When building or debugging a template, add a debug helper to dump all available variables to the browser console.
In template — call it anywhere in the SVG:
options.data.root will show { $declaration, $metadata, $review, $references } for V2 templates.
Remove this before going to production.
5. Accessing all template variables via options.data.root
For helpers that need to read multiple fields at once — for example, composing a full address from several declaration fields — use options.data.root instead of passing each value as an argument.
In template:
6. Block helpers with options.fn and options.inverse
Block helpers wrap content and control whether to render it, render the {{else}}, or both.
In template:
7. Named hash parameters
Hash parameters let callers pass optional named configuration:
In template:
8. Generating SVG tspan elements
The most advanced pattern. When a certificate paragraph is too long for a single <tspan> — or needs to be assembled from many data fields with computed layout — the helper generates SVG markup and returns it as a string.
Use triple braces {{{ }}} in the template to inject raw SVG without HTML escaping.
In template — triple braces for raw SVG:
Using Custom Helpers in SVG Templates
Once a helper is exported from helpers.ts, it's available by name in any SVG template, alongside all built-in helpers.
TypeScript Imports
You can import utilities from within your own package — but be careful: helpers.ts is compiled as a standalone browser ESM bundle by esbuild. Anything you import must be resolvable and safe to run in the browser.
Do not import server-only Node.js modules (e.g.
fs,path,crypto) —helpers.tsruns in the browser.
Limitations
Browser-only runtime
The compiled JS runs in the browser. No Node.js APIs.
Only { intl } is injected by the factory
Core only passes intl to your factory. If you need locations, users, or administrativeAreas, read them from options.data.root.$references inside the helper.
All exports become helpers
Every export function in helpers.ts is registered as a Handlebars helper. Keep private utility functions unexported.
No async helpers
Handlebars helpers are synchronous. Don't use async/await or return Promises.
Max 6 positional arguments
The HelperDelegate type signature supports up to 6 positional args before options.
esbuild target is browser ESM
Imports work, but only for packages that have browser-safe builds.
Helper names are global
If you name a helper the same as a built-in (e.g. $lookup), yours will override it. Avoid names starting with $.
Real-World Examples
These are patterns commonly needed in country certificate templates:
Age calculation from two dates
Useful on death certificates to show the deceased's age in years and remaining months.
In template:
Nationality / country code translation with locale override
When the built-in $intl helper isn't flexible enough — for example, when certain country codes need custom overrides that differ from your i18n message file.
Ternary — conditional value inline
More flexible than ifCond for inline expressions where you want a value, not a block.
First non-empty value
When the same logical piece of data might be stored under different field IDs depending on which form was used.
In template:
Localized gender string
Generate a multi-line certificate paragraph
Combines multiple declaration fields and produces formatted SVG <tspan> elements. Output is injected raw with {{{ }}}.
In template:
Summary
Format a single value
Simple helper — export function x(): HelperDelegate { return fn }
Translate with locale
intl helper — export function x({ intl }: FactoryProps): HelperDelegate { ... }
Compare / choose between values
Multi-arg with ternary or custom comparison
Optional named config
Hash parameters via options.hash
Conditional block rendering
Block helper using options.fn / options.inverse
Read multiple fields at once
options.data.root for full context access
Generate multi-line SVG text
Return SVG string, use {{{ }}} in template
Debug what's available
Export debug(), call {{debug}}, check browser console
Last updated