For the complete documentation index, see llms.txt. This page is also available as Markdown.

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 SVG

The 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 options in 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 block

  • options.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:

Key
What it is

$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.ts runs in the browser.


Limitations

Constraint
Detail

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

Task
Pattern

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