Files
suaveplan/packages/localization/i18n/i18n-icu-parser/src/interpolation/interpolation.md
2026-05-27 23:47:53 +01:00

4.5 KiB

InterpolationEngine

(no submodules)

Purpose

InterpolationEngine replaces placeholder tokens (default: {{ name }}) in message template strings with caller-supplied values. It is the rendering workhorse behind MessageCatalog.t() and standalone template usage. The engine is designed to be instantiated once and reused across many render calls, with all configuration captured in the constructor.

The engine handles the common i18n case of named variable substitution, and additionally supports pluggable per-slot value formatters (e.g., number, date, currency) via a named formatter registry. Values can be HTML-escaped on output to prevent XSS when templates are rendered into HTML.

Features

  • Named placeholder substitution: {{ name }}, {{ count, number }}, etc.
  • Custom delimiter pairs (e.g., %{ / } for Ruby-style templates)
  • Per-slot ValueFormatter registry — applied when a placeholder contains a format name
  • Optional defaultFormatter called for all slots without an explicit formatter
  • HTML escaping of rendered values (escapeHtml: true) via escapeHtmlChars
  • Recursive interpolation with configurable depth limit (maxDepth)
  • missingVariables tracking in the result — non-throwing for production resilience

Usage: Basic Example

import { InterpolationEngine } from "@suaveplan/i18n-icu-parser/interpolation";

const engine = new InterpolationEngine();
const { text } = engine.render(
  "Hello, {{ name }}! You have {{ count }} messages.",
  { name: "Alice", count: 3 }
);
// text === "Hello, Alice! You have 3 messages."

Usage: Advanced Example

import { InterpolationEngine } from "@suaveplan/i18n-icu-parser/interpolation";

const engine = new InterpolationEngine({
  escapeHtml: true,
  formatters: {
    upper: (v) => String(v).toUpperCase(),
    number: (v, locale) =>
      new Intl.NumberFormat(locale).format(Number(v)),
  },
});

const { text, missingVariables } = engine.render(
  "Welcome, {{ user, upper }}! Balance: {{ amount, number }}.",
  { user: "alice", amount: 1234567 },
  "en-US"
);
// text === "Welcome, ALICE! Balance: 1,234,567."
// missingVariables === []

// HTML escaping
const { text: safe } = engine.render(
  "Comment: {{ comment }}",
  { comment: "<script>alert('xss')</script>" }
);
// safe === "Comment: &lt;script&gt;alert(&#039;xss&#039;)&lt;/script&gt;"

API Reference

new InterpolationEngine(options?: InterpolationOptions)

Option Type Default Description
prefix string "{{" Placeholder open delimiter
suffix string "}}" Placeholder close delimiter
escapeHtml boolean false HTML-escape rendered values
maxDepth number 10 Maximum recursion depth
formatters Record<string, ValueFormatter>? {} Named formatter map
defaultFormatter ValueFormatter? — Applied when no named formatter matches

.render(template, values?, locale?): InterpolationResult

Renders template replacing all placeholders. Returns { text: string; missingVariables: string[] }.

.addFormatter(name, formatter): void

Registers or replaces a named formatter at runtime.

.removeFormatter(name): boolean

Removes a named formatter; returns true if it existed.

.hasPlaceholders(template): boolean

Returns true if the template contains at least one placeholder.

.extractVariables(template): string[]

Returns all unique variable names referenced in the template.

escapeHtmlChars(str: string): string

Standalone export. Escapes &, <, >, ", '.

escapeRegex(str: string): string

Standalone export. Escapes all regex metacharacters for safe use in RegExp.

Implementation Notes

  • The placeholder regex is rebuilt on each call to replaceValues rather than cached, because options.prefix / suffix could theoretically contain characters whose escaped forms would produce a different pattern — rebuilding is correct and the cost is negligible for the expected template sizes
  • Recursive interpolation is capped at maxDepth (default 10) to prevent infinite loops when a value itself contains placeholder syntax
  • HTML escaping is applied after formatting so that formatter output (e.g., "$1,234") is escaped as a unit, not before
  • missingVariables accumulates in-place via the closure; the engine never throws on a missing variable — it leaves the placeholder in place and records the name