Tag Interpolation
Tag interpolation lets translators use tags like <link> and <bold> inside translation values. Your code maps those tag names to real UI elements or components, and the <T> component renders the result.
What this feature does:
- Lets translators move semantic tags around inside translated sentences.
- Lets developers decide what each tag renders to: a link, styled text, a framework component, or an allowed HTML tag.
- Escapes plain translation text and interpolation values by default.
- Falls back to inner text when a tag has no handler, unless strict mode is enabled.
What this feature does not do:
- It does not render arbitrary HTML from translation strings.
- It does not let translators set attributes like
href,class, or event handlers. Attributes come from your code. - It does not instantiate framework components by tag name from the translation file.
- It does not make
t()return HTML. Use<T>for UI rich text.
In Svelte, <T> internally renders a library-generated HTML string via {@html}. Translation text and attributes are escaped, unsafe attributes are blocked, and only allowed tag names are emitted.
How It Works
Section titled “How It Works”A translation value contains named tags:
{ "help": "Click <link>here</link> for help"}Your code provides a handler for each tag. Comvi i18n parses the translation, matches tags to handlers, and returns the result:
import { T } from '@comvi/react';
// React element — children auto-injected via cloneElement<T i18nKey="help" components={{ link: <a href="/help" /> }}/>// → Click <a href="/help">here</a> for help
// Function handler — full control<T i18nKey="help" components={{ link: ({ children }) => <a href="/help">{children}</a>, }}/><script setup lang="ts">import { T } from '@comvi/vue';</script>
<template> <T i18nKey="help"> <template #link="{ children }"> <a href="/help">{{ children }}</a> </template> </T> <!-- → Click <a href="/help">here</a> for help --></template>import { T } from '@comvi/next/client';
<T i18nKey="help" components={{ link: <a href="/help" /> }}/><template> <T i18nKey="help"> <template #link="{ children }"> <a href="/help">{{ children }}</a> </template> </T></template>import { T } from '@comvi/solid';
<T i18nKey="help" components={{ link: (props) => <a href="/help">{props.children}</a>, }}/><script>import { T } from '@comvi/svelte';</script>
<T i18nKey="help" components={{ link: { tag: 'a', props: { href: '/help' } } }}/>Translators move the tags freely without touching code. The developer controls what each tag renders.
Handler Types
Section titled “Handler Types”| Type | Example | Behavior |
|---|---|---|
| React element | <a href="/help" /> | Children auto-injected via cloneElement |
| Function | ({ children }) => <a>{children}</a> | Full control over rendering |
| String | "strong" | Renders as that HTML element |
<T i18nKey="demo" components={{ link: <a href="/help" />, // React element bold: ({ children }) => <b>{children}</b>, // Function highlight: "mark", // String → <mark> }}/>Vue supports named slots and the components prop. Slots receive { children }:
<T i18nKey="demo"> <template #link="{ children }"> <a href="/help">{{ children }}</a> </template> <template #bold="{ children }"> <strong>{{ children }}</strong> </template></T>Use components when handlers are reused or configured at runtime:
<T i18nKey="demo" :components="{ link: { component: 'a', props: { href: '/help' } }, bold: 'strong', }"/>Same API as React — works in both Server and Client Components:
| Type | Example | Behavior |
|---|---|---|
| React element | <a href="/help" /> | Children auto-injected via cloneElement |
| Function | ({ children }) => <a>{children}</a> | Full control over rendering |
| String | "strong" | Renders as that HTML element |
Same API as Vue — auto-imported, no import needed:
<T i18nKey="demo"> <template #link="{ children }"> <a href="/help">{{ children }}</a> </template></T>SolidJS uses function handlers:
<T i18nKey="demo" components={{ link: (props) => <a href="/help">{props.children}</a>, bold: (props) => <strong>{props.children}</strong>, }}/>Svelte uses the components prop with HTML tag mappings. It does not render Svelte component constructors from translations.
<T i18nKey="demo" components={{ link: { tag: 'a', props: { href: '/help' } }, bold: 'strong', }}/>Because Svelte renders through {@html}, only tags from a built-in safe whitelist are emitted as the requested element. Tags outside the whitelist render as <span> to prevent XSS. The default set is:
a, abbr, b, bdi, bdo, br, cite, code, data, del, dfn, em, hr, i,img, ins, kbd, mark, ol, li, p, pre, q, rp, rt, ruby, s, samp,small, span, strong, sub, sup, time, u, ul, var, wbrPass allowedTags={new Set([...])} to override it for a specific <T> call:
<T i18nKey="demo" components={{ video: { tag: 'video' } }} allowedTags={new Set(['video', 'source'])}/>Nested Tags
Section titled “Nested Tags”Tags can be nested. Each tag gets its own handler:
{ "msg": "Click <a><b>here</b></a> to continue"}<T i18nKey="msg" components={{ a: <a href="/next" />, b: <strong />, }}/>// → Click <a href="/next"><strong>here</strong></a> to continue<T i18nKey="msg"> <template #a="{ children }"> <a href="/next">{{ children }}</a> </template> <template #b="{ children }"> <strong>{{ children }}</strong> </template></T><T i18nKey="msg" components={{ a: <a href="/next" />, b: <strong />, }}/><T i18nKey="msg"> <template #a="{ children }"> <a href="/next">{{ children }}</a> </template> <template #b="{ children }"> <strong>{{ children }}</strong> </template></T><T i18nKey="msg" components={{ a: (props) => <a href="/next">{props.children}</a>, b: (props) => <strong>{props.children}</strong>, }}/><T i18nKey="msg" components={{ a: { tag: 'a', props: { href: '/next' } }, b: 'strong', }}/>Self-Closing Tags
Section titled “Self-Closing Tags”Use <tag/> for elements without children:
{ "address": "Line 1<br/>Line 2<br/>Line 3"}<T i18nKey="address" components={{ br: <br /> }} /><T i18nKey="address"> <template #br><br /></template></T><T i18nKey="address" components={{ br: <br /> }} /><T i18nKey="address"> <template #br><br /></template></T><T i18nKey="address" components={{ br: () => <br /> }} /><T i18nKey="address" components={{ br: 'br' }} />ICU Inside Tags
Section titled “ICU Inside Tags”Tags and ICU parameters work together. Use {param} and {count, plural, ...} inside tagged sections:
{ "promo": "Get <discount>{percent}% off</discount> today!", "cart": "{count, plural, one {<b># item</b>} other {<b># items</b>}}"}<T i18nKey="promo" params={{ percent: 20 }} components={{ discount: ({ children }) => <span className="text-red-500">{children}</span>, }}/>// → Get <span class="text-red-500">20% off</span> today!
<T i18nKey="cart" params={{ count: 5 }} components={{ b: <strong /> }}/>// → <strong>5 items</strong><T i18nKey="promo" :params="{ percent: 20 }"> <template #discount="{ children }"> <span class="text-red-500">{{ children }}</span> </template></T>
<T i18nKey="cart" :params="{ count: 5 }"> <template #b="{ children }"> <strong>{{ children }}</strong> </template></T><T i18nKey="promo" params={{ percent: 20 }} components={{ discount: ({ children }) => <span className="text-red-500">{children}</span>, }}/><T i18nKey="promo" :params="{ percent: 20 }"> <template #discount="{ children }"> <span class="text-red-500">{{ children }}</span> </template></T><T i18nKey="promo" params={{ percent: 20 }} components={{ discount: (props) => <span class="text-red-500">{props.children}</span>, }}/><T i18nKey="promo" params={{ percent: 20 }} components={{ discount: { tag: 'span', props: { class: 'text-red-500' } }, }}/>Basic HTML Tags Whitelist
Section titled “Basic HTML Tags Whitelist”For simple formatting tags like <strong> or <em>, you can whitelist them globally so they render as HTML without needing handlers:
const i18n = createI18n({ locale: 'en', tagInterpolation: { basicHtmlTags: ['strong', 'em', 'br', 'b', 'i', 'p', 'span'], },});{ "note": "This is <strong>important</strong> and <em>urgent</em>"}Render the translation through <T>:
<T i18nKey="note" />The whitelisted tags render as HTML elements automatically — no handler needed. Plain t('note') still returns text only ("This is important and urgent").
Handlers always take precedence over the whitelist. If you provide a handler for a whitelisted tag, the handler is used.
Strict Mode
Section titled “Strict Mode”Control what happens when a tag in the translation has no handler:
const i18n = createI18n({ locale: 'en', tagInterpolation: { strict: false, // false | 'warn' | true (default: false) },});| Mode | Behavior | Best for |
|---|---|---|
false (default) | Falls back to inner text silently | Production |
'warn' | Calls onTagWarning or reports a warning, falls back to inner text | Development |
true | Throws error | Testing / CI |
Example with strict: false (default):
{ "msg": "Click <link>here</link>"}t('msg'); // No handler for <link> → "Click here"Handling warnings with onTagWarning
Section titled “Handling warnings with onTagWarning”When strict: 'warn', the library calls onTagWarning(tagName) for each unhandled tag. If you do not provide onTagWarning, the i18n instance reports the warning through its configured onError handler.
const i18n = createI18n({ locale: 'en', tagInterpolation: { strict: 'warn', onTagWarning: (tagName) => { Sentry.captureMessage(`i18n: missing handler for <${tagName}>`, 'warning'); }, },});Important: onTagWarning is only called when strict: 'warn'. With strict: false warnings are silent, and with strict: true the library throws to the caller.
Escaping
Section titled “Escaping”Use a backslash to include literal angle brackets in translations:
{ "code": "Use the \\<div> element"}t('code'); // → "Use the <div> element"HTML entities are also supported: < → <, > → >, & → &.
Error Handling
Section titled “Error Handling”Malformed tags are handled gracefully:
- Unclosed tags (
<link>here) — logs a warning, returns the raw text - Mismatched tags (
<a><b>text</a></b>) — logs a warning, returns the raw text - Missing handlers — behavior depends on strict mode
No crashes in production. Errors are visible in development via console warnings.