Tiny Template String Formatter
Sometimes you need a templating helper that does not pull in handlebars or eta, just enough to substitute `{name}` placeholders against a values object. This snippet starts with a 5-line interpolator, adds escape support so literal braces survive, and ends with a tagged-template-literal version that gives you compile-time placeholder safety. Drop it into i18n strings, log formatters, or email subject lines.
428 views
14
function format(template, values) {
return template.replace(/\{(\w+)\}/g, (match, key) => {
return key in values ? String(values[key]) : match;
});
}
console.log(format('Hello, {name}!', { name: 'Ada' }));
// Hello, Ada!
console.log(format('Order #{id} costs ${amount}', { id: 42, amount: 19.99 }));
// Order #42 costs $19.99
console.log(format('Missing {key}', {}));
// Missing {key} (unknown placeholders left alone)The pattern \{(\w+)\} captures one identifier-like token between braces, and replace runs once per match. Falling back to the original match when the key is absent leaves the placeholder visible, which is usually what you want during development because it surfaces missing translation keys instead of silently emitting 'undefined'. Numbers, booleans, and other primitives get coerced to strings via String(values[key]), so passing { amount: 19.99 } works without ceremony. Reach for this whenever you need template-style strings in a CLI tool, an i18n shim, or a log formatter.
function formatWithEscape(template, values) {
// Pass 1: escape {{ to a placeholder, } -> }} to another, then substitute.
const ESC_OPEN = '\u0001';
const ESC_CLOSE = '\u0002';
const masked = template
.replace(/\{\{/g, ESC_OPEN)
.replace(/\}\}/g, ESC_CLOSE);
const replaced = masked.replace(/\{(\w+)\}/g, (m, key) =>
key in values ? String(values[key]) : m
);
return replaced.replace(new RegExp(ESC_OPEN, 'g'), '{').replace(new RegExp(ESC_CLOSE, 'g'), '}');
}
console.log(formatWithEscape('Use {{name}} to keep the literal braces', { name: 'Ada' }));
// Use {name} to keep the literal braces
console.log(formatWithEscape('Real value: {name}', { name: 'Ada' }));
// Real value: Ada
console.log(formatWithEscape('Mix: {{key}} = {key}', { key: 'X' }));
// Mix: {key} = XThe minimal version cannot output literal { and } characters, which matters when your template is itself a code sample, a CSS rule, or a JSON snippet. The classic fix is the same trick handlebars uses: doubled braces {{ and }} mean literal output. Masking them with control characters first (\u0001, \u0002) means the substitution pass cannot accidentally re-process them, then we unmask after substitution. The control-character technique avoids the harder approach of building one mega-regex with negative lookbehinds, which is also slower in V8.
function fmt(strings, ...values) {
return strings.reduce((out, part, i) => {
const v = i < values.length ? values[i] : '';
return out + part + (v == null ? '' : String(v));
}, '');
}
const name3 = 'Ada';
const price = 9.99;
console.log(fmt`Hello, ${name3}! Your total is $${price}.`);
// Hello, Ada! Your total is $9.99.
// Auto-escapes nullish values to '' (so missing data doesn't leak 'null'):
const maybe = null;
console.log(fmt`Title: "${maybe}"`);
// Title: ""
// As a sanitising tag, easy to extend with HTML escape:
const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
function safeHtml(strings, ...values) {
return strings.reduce((out, part, i) => out + part + (i < values.length ? esc(values[i]) : ''), '');
}
const userInput = '<script>';
console.log(safeHtml`<p>${userInput}</p>`);
// <p><script></p>Tagged template literals are JavaScript's built-in templating: a function tag receives the static string parts and the interpolated values as separate arrays. The fmt tag keeps it simple by stringifying each value and treating null/undefined as empty, which avoids the surprise of 'null' or 'undefined' showing up in production strings. The safeHtml extension shows the bigger win: the tag function sees the values BEFORE concatenation, so it can sanitise each one independently and you cannot accidentally bypass it by changing the template. Variables are renamed (name3, etc.) to avoid collision with earlier accordions in this snippet's combined sandbox. Use this approach for typed templates, sanitising tags (safeHtml, safeSql), or DSLs like styled-components.
