# Insert values and markup

Fill {param} placeholders with t(), decide what a missing parameter renders, and turn <link> or <bold> tags in a translation into real elements with <T>.

Canonical: https://comvi.io/docs/i18n/tag-interpolation

Fill `{param}` placeholders with `t()` and render tags such as `<link>` inside a translation
with `<T>`. Parameters work on any instance; `<T>` needs one of the framework bindings on top
of the [Quick start](/docs/i18n/quick-start).

## Parameters
**React**

`src/Welcome.tsx`

```tsx
import { useI18n } from '@comvi/react';

export function Welcome({ name }: { name: string }) {
    const { t } = useI18n();
    return <h1>{t('welcome', { name })}</h1>;
}
```

**Vue**

`src/Welcome.vue`

```vue
<script setup lang="ts">
import { useI18n } from '@comvi/vue';

defineProps<{ name: string }>();
const { t } = useI18n();
</script>

<template>
    <h1>{{ t('welcome', { name }) }}</h1>
</template>
```

**Nuxt**

`nuxt.config.ts`

```ts
import { defineNuxtConfig } from 'nuxt/config';

export default defineNuxtConfig({
    modules: ['@comvi/nuxt'],
    comvi: { defaultLocale: 'en', locales: [{ code: 'en', name: 'English' }] },
});
```

`components/Welcome.vue`

```vue
<script setup lang="ts">
defineProps<{ name: string }>();
const { t } = useI18n();
</script>

<template>
    <h1>{{ t('welcome', { name }) }}</h1>
</template>
```

**Svelte**

`src/Welcome.svelte`

```svelte
<script lang="ts">
    import { useI18n } from '@comvi/svelte';

    const { name }: { name: string } = $props();
    const { t } = useI18n();
</script>

<h1>{$t('welcome', { name })}</h1>
```

**Solid**

`src/Welcome.tsx`

```tsx
import { useI18n } from '@comvi/solid';

export function Welcome(props: { name: string }) {
    const { t } = useI18n();
    return <h1>{t('welcome', { name: props.name })}</h1>;
}
```

**Vanilla**

`src/welcome.ts`

```ts
import { createI18n } from '@comvi/core';

const i18n = createI18n({ locale: 'en', messages: { en: { welcome: 'Welcome, {name}!' } } });
await i18n.init();

i18n.t('welcome', { name: 'Alice' }); // "Welcome, Alice!"
```

Every `{name}` in the message is replaced by the value under that key. Strings, numbers and
booleans are pasted in, an array item by item. A value renders as an element only when it is a
node made by the factories in `@comvi/core/rich-text`, or a framework element passed to `<T>`;
anything else goes through `String()`.

## Missing parameters

`src/i18n.ts`

```ts

export const i18n = createI18n({
locale: 'en',
missingParam: 'literal', // default; 'drop' renders an empty string instead
messages: { en: { greet: 'Hello, {name}!' } },
});
```

`src/greet.ts`

```ts

await i18n.init();

i18n.t('greet'); // "Hello, {name}!" plus one development warning
i18n.t('greet', { name: null }); // "Hello, !"
i18n.t('greet', { name: 'Alice' }); // "Hello, Alice!"
```

A parameter that is absent or `undefined` renders as its own placeholder, and development
warns once per template and parameter. `missingParam: 'drop'` renders an empty string instead.
An explicit `null` always renders as an empty string, in both modes.

## `<T>` versus plain `t()`
**React**

`src/Help.tsx`

```tsx
import { T } from '@comvi/react';

// "help": "Click <link>here</link> for help"
export function Help() {
    return <T i18nKey="help" components={{ link: <a href="/help" /> }} />;
}
```

**Vue**

`src/Help.vue`

```vue
<script setup lang="ts">
import { T } from '@comvi/vue';
</script>

<!-- "help": "Click <link>here</link> for help" -->
<template>

      <template #link="{ children }">
        <a href="/help">{{ children }}</a>
      </template>

</template>
```

**Nuxt**

`components/Help.vue`

```vue
<!-- "help": "Click <link>here</link> for help" -->
<template>

      <template #link="{ children }">
        <NuxtLinkLocale to="/help">{{ children }}</NuxtLinkLocale>
      </template>

</template>
```

**Svelte**

`src/Help.svelte`

```svelte
<script lang="ts">
    import { T } from '@comvi/svelte';
</script>

<!-- "help": "Click <link>here</link> for help" -->

```

**Solid**

`src/Help.tsx`

```tsx
import { T } from '@comvi/solid';

// "help": "Click <link>here</link> for help"
export function Help() {
    return <T i18nKey="help" components={{ link: (props) => <a href="/help">{props.children}</a> }} />;
}
```

`<T>` parses the tags on any instance and hands each one to the handler under its name. The
translator moves the tags; your code decides what they render, and no attribute or component
name ever comes from the catalog. Without a component layer there is no `<T>`:
`prepareTranslation` from `@comvi/core/rich-text` returns the parsed parts to build DOM nodes from.

Plain `t()` leaves tags in the string until the bundle registers the tag grammar:

`src/tags-on.ts`

```ts
```

With that import, `t('help')` returns `Click here for help` instead of the string with angle
brackets in it. Without it, the string renders literally with one development warning per
template. `createNextI18n()` imports the entry itself; every other binding and bare
`@comvi/core` leave it to you.

The snippet imports from `@comvi/core`. An app that installed only a binding adds
`@comvi/core` to its own `dependencies` first: it arrives as a transitive dependency, and
pnpm does not hoist one into the app's `node_modules`, so the import does not resolve there.
> **SSR: register tags in both graphs**
>
> An SSR app that imports `@comvi/core/tags` on the client only renders the string literally on
> the server and stripped on the client, which is a hydration mismatch with no error. Import it
> in the server graph too, or render the string through `<T>`. A `@comvi/next` app built with
> `createNextI18n()` is covered on both sides.

## Handlers
**React**

`src/Handlers.tsx`

```tsx
import type { ReactNode } from 'react';
import { T } from '@comvi/react';

// "demo": "Click <link>here</link> for <bold>help</bold> and <highlight>tips</highlight>"
// "msg": "Click <a><b>here</b></a> to continue"
// "address": "Line 1<br/>Line 2"
// "promo": "Get <discount>{percent}% off</discount> today!"
const Bold = ({ children }: { children?: ReactNode }) => <b>{children}</b>; // a handler is a component: define it outside render

export function Handlers() {
    return (
      <>
        <T
          i18nKey="demo"
          components={{
            link: <a href="/help" />, // element: rendered with createElement, children injected
            bold: Bold, // function: full control
            highlight: 'mark', // string: renders that HTML element
          }}
        />
        <T i18nKey="msg" components={{ a: <a href="/next" />, b: <strong /> }} />
        <T i18nKey="address" components={{ br: <br /> }} />
        <T i18nKey="promo" params={{ percent: 20 }} components={{ discount: <span className="text-red-500" /> }} />
      </>
    );
}
```

**Vue**

`src/Handlers.vue`

```vue
<script setup lang="ts">
import { T } from '@comvi/vue';
</script>

<!-- "demo": "Click <link>here</link> for <bold>help</bold> and <highlight>tips</highlight>" -->
<!-- "msg": "Click <a><b>here</b></a> to continue" -->
<!-- "address": "Line 1<br/>Line 2" -->
<!-- "promo": "Get <discount>{percent}% off</discount> today!" -->
<template>

      <template #link="{ children }"><a href="/help">{{ children }}</a></template>
      <template #bold="{ children }"><b>{{ children }}</b></template>

</template>
```

**Nuxt**

`components/Handlers.vue`

```vue
<!-- "demo": "Click <link>here</link> for <bold>help</bold> and <highlight>tips</highlight>" -->
<!-- "msg": "Click <a><b>here</b></a> to continue" -->
<!-- "address": "Line 1<br/>Line 2" -->
<!-- "promo": "Get <discount>{percent}% off</discount> today!" -->
<template>

      <template #link="{ children }"><NuxtLinkLocale to="/help">{{ children }}</NuxtLinkLocale></template>
      <template #bold="{ children }"><b>{{ children }}</b></template>

</template>
```

**Svelte**

`src/Handlers.svelte`

```svelte
<script lang="ts">
    import { T } from '@comvi/svelte';
</script>

<!-- "demo": "Click <link>here</link> for <bold>help</bold> and <highlight>tips</highlight>" -->
<!-- "msg": "Click <a><b>here</b></a> to continue" -->
<!-- "address": "Line 1<br/>Line 2" -->
<!-- "promo": "Get <discount>{percent}% off</discount> today!" -->

```

**Solid**

`src/Handlers.tsx`

```tsx
import { T } from '@comvi/solid';

// "demo": "Click <link>here</link> for <bold>help</bold> and <highlight>tips</highlight>"
// "msg": "Click <a><b>here</b></a> to continue"
// "address": "Line 1<br/>Line 2"
// "promo": "Get <discount>{percent}% off</discount> today!"
export function Handlers() {
    return (
      <>
        <T
          i18nKey="demo"
          components={{
            link: { tag: 'a', props: { href: '/help' } }, // config: element plus attributes
            bold: (props) => <b>{props.children}</b>, // function: full control
            highlight: 'mark', // string: renders that HTML element
          }}
        />

      </>
    );
}
```

A handler is an HTML tag name, a component of the binding's own kind, or a config with props;
the binding's API reference lists the exact shapes. Tags nest, each with its own handler, and
`<tag/>` is a self-closing tag with no children. `params` fills `{percent}` inside a tag. A
`plural` inside a tag needs `compiler: icuCompiler` on the instance, from
[Pluralize with ICU](/docs/i18n/pluralization#compose-the-icu-compiler).

## Whitelist and strict mode

`src/i18n.ts`

```ts

declare const Sentry: { captureMessage(message: string, level: string): void };

export const i18n = createI18n({
locale: 'en',
tagInterpolation: {
    basicHtmlTags: ['strong', 'em', 'br', 'b', 'i', 'p', 'span'],
    strict: 'warn', // false (default) | 'warn' | true
    onTagWarning: (tagName) => {
      Sentry.captureMessage(`i18n: missing handler for <${tagName}>`, 'warning');
    },
},
});
```

Tags in `basicHtmlTags` render as HTML elements with no handler, so `<T i18nKey="note" />`
renders `This is <strong>important</strong>` as bold text. A handler for the same tag name
wins over the whitelist.

`strict` decides what a tag with no handler does:

| Mode | Behavior |
| --- | --- |
| `false` | Renders the inner text silently. |
| `'warn'` | Calls `onTagWarning(tagName)`, then renders the inner text. Without your callback, the constructor routes the warning to `onError`. |
| `true` | Throws `E_MISSING_TAG_HANDLER`, in production too. |

## Escaping

`src/locales/en.json`

```json
{
"code": "Use the \\<div> element",
"legal": "a &lt;b&gt; &amp; c"
}
```

A backslash before `<`, or the entities `&lt;`, `&gt;` and `&amp;`, put a literal angle
bracket or ampersand into a message that goes through the tag pipeline: `<T i18nKey="code" />`
renders `Use the <div> element`. Without a tag extension in the bundle these are ordinary
characters, so plain `t('legal')` on a bare instance returns `a &lt;b&gt; &amp; c` as written.
ICU apostrophe quoting is a separate rule that works on every instance, on the
[ICU reference](/docs/i18n/icu-reference#apostrophes-and-quoted-literals).

## How it works

Every message goes through one parser. A template is scanned once for `{`, `<`, `&` and `'`;
a message with none of them is static and is returned as written. Anything else is parsed into
tokens, text, parameters and tags, and cached by the template text plus the grammar that parsed
it, so the first call pays for the parse and every later call with different values pays for a
lookup and a concatenation. In development the same parse also runs at ingestion, where a
broken message fails before anything renders.

Tags are a grammar the parser does not know by default. `import '@comvi/core/tags'` registers
the tag syntax as a process-wide extension, and the registration is retroactive: every
instance in the bundle starts stripping tags, because the cache is keyed by the active grammar
and a parse made before the import is never reused after it. The same registration is `registerTagSyntax()` from
`@comvi/core/rich-text`, which returns an unregister function for tests and multi-tenant
servers. `<T>` does not depend on the import: it passes the tag grammar into the call it makes,
so a `<T>` in a bundle with no tag import still renders markup while a plain `t()` next to it
returns the angle brackets. That difference is what the SSR mismatch is made of: a server
bundle without the import and a client bundle with it parse the same message two ways.

When `<T>` meets a tag, it looks for a handler in this order: an entry under that tag's name
in `components`, then the `basicHtmlTags` whitelist, then `strict`. A tag's children are rendered
first, parameters and nested tags included, and the finished children are what the handler
receives; it returns the element or component the binding knows how to mount. That is why
`{percent}` inside `<discount>` fills like any other parameter, and why a `plural` inside a tag
still needs the ICU compiler: the compiler decides what a braced argument means, the tag
extension only what the angle brackets mean. Unclosed or mismatched tags never throw:
development logs a warning and the raw text is returned. `strict` governs the tag pipeline only,
so plain `t()` on an instance without the tag import is unaffected by it.

## Read next

- [Translate text with t()](/docs/i18n/translation-function): call options, defaults and missing keys.
- [Pluralize with ICU](/docs/i18n/pluralization): `plural` and `select` inside and outside tags.
- [`@comvi/core/rich-text`](/docs/i18n/api/core-rich-text): `prepareTranslation` and `registerTagSyntax`.
