Why the Trans Component Exists#

The t() function is the workhorse of react-i18next — but it returns a plain string. The moment you need bold text, a link, or any React component inside a translation, t() breaks down. You'd have to split the string into fragments and stitch JSX together manually.

The Trans component solves this. It lets you write translations like this:

json
{
  "welcome": "Read our <link>getting started guide</link> to begin."
}

And render them with real React components — no string splitting, no raw HTML injection.

When to use Trans vs t():

ScenarioUse
Plain text, variables onlyt()
Bold, italic, or styled text inside a sentenceTrans
Links (React Router or <a>) embedded in textTrans
Icons or images inline with textTrans
Complex nested JSX structuresTrans

If your translation is pure text with {{variables}}, stick with t(). The moment HTML or JSX enters the picture, switch to Trans.

Setup & Import#

bash
npm install react-i18next i18next
tsx
import { Trans, useTranslation } from 'react-i18next';

Trans works in any React component. It reads from the same i18next instance as useTranslation(). No additional configuration required.

Basic Usage: HTML Tags in Translations#

Bold, Italic, and Styled Text#

json
// en.json
{
  "warning": "This action is <bold>permanent</bold> and <italic>cannot be undone</italic>."
}
tsx
import { Trans } from 'react-i18next';

function Warning() {
  return (
    <Trans
      i18nKey="warning"
      components={{
        bold: <strong />,
        italic: <em />
      }}
    />
  );
}
// Renders: This action is <strong>permanent</strong> and <em>cannot be undone</em>.

The components prop maps tag names in your translation to real React elements. The text between <bold> and </bold> becomes the children of <strong>.

Self-Closing Tags (Icons, Line Breaks)#

json
{
  "verified": "<icon/> Verified account",
  "address": "Line 1<br/>Line 2"
}
tsx
<Trans
  i18nKey="verified"
  components={{
    icon: <CheckCircleIcon className="h-5 w-5 text-green-500 inline" />,
  }}
/>

<Trans
  i18nKey="address"
  components={{ br: <br /> }}
/>

Self-closing tags (<icon/>, <br/>) map to components that don't wrap any text.

One of the most common use cases — links that need real click handling.

json
{
  "terms": "By continuing, you agree to our <termsLink>Terms</termsLink> and <privacyLink>Privacy Policy</privacyLink>."
}
tsx
<Trans
  i18nKey="terms"
  components={{
    termsLink: <a href="https://example.com/terms" target="_blank" rel="noopener noreferrer" />,
    privacyLink: <a href="https://example.com/privacy" target="_blank" rel="noopener noreferrer" />
  }}
/>
tsx
import Link from 'next/link';

<Trans
  i18nKey="navigation"
  components={{
    homeLink: <Link href="/" />,
    docsLink: <Link href="/docs" />
  }}
/>

This works with any routing library — React Router's <Link>, Next.js <Link>, Gatsby <Link>, etc.

json
{
  "docs": "Read the <link>documentation <icon/></link>"
}
tsx
<Trans
  i18nKey="docs"
  components={{
    link: <a href="/docs" className="inline-flex items-center gap-1" />,
    icon: <ExternalLinkIcon className="h-4 w-4" />
  }}
/>

Combining Trans with Variables (Interpolation)#

You can mix {{variable}} placeholders with component tags:

json
{
  "greeting": "Hello <bold>{{name}}</bold>, you have <badge>{{count}}</badge> new messages."
}
tsx
<Trans
  i18nKey="greeting"
  values={{ name: 'Sarah', count: 12 }}
  components={{
    bold: <strong />,
    badge: <span className="bg-red-500 text-white px-2 py-0.5 rounded-full text-sm" />
  }}
/>
// Renders: Hello <strong>Sarah</strong>, you have <span class="...">12</span> new messages.

The values prop works exactly like the second argument to t(). Variables are interpolated first, then components are applied.

Dynamic Components Based on Data#

tsx
function NotificationBanner({ level, message }) {
  return (
    <Trans
      i18nKey="alert"
      values={{ message }}
      components={{
        wrapper: <div className={level === 'error' ? 'bg-red-100' : 'bg-blue-100'} />
      }}
    />
  );
}

Translate your React JSON files

Every {{placeholder}} stays intact — count and order are checked before anything is written.

Create accountfrom €19 per month

Nested Components#

Tags can be nested inside each other:

json
{
  "help": "For assistance, <link>contact <bold>our support team</bold></link> or check the <faqLink>FAQ</faqLink>."
}
tsx
<Trans
  i18nKey="help"
  components={{
    link: <a href="/support" />,
    bold: <strong />,
    faqLink: <Link href="/faq" className="underline" />
  }}
/>
// Renders: For assistance, <a href="/support">contact <strong>our support team</strong></a> or check the <Link href="/faq">FAQ</Link>.

This works up to arbitrary nesting depth. React-i18next's parser handles the tree structure correctly.

Pluralization with Trans#

Combine plural forms with rich components:

json
{
  "cart_one": "You have <bold>{{count}}</bold> item in your cart.",
  "cart_other": "You have <bold>{{count}}</bold> items in your cart.",
  "cart_zero": "Your cart is <italic>empty</italic>."
}
tsx
<Trans
  i18nKey="cart"
  count={itemCount}
  values={{ count: itemCount }}
  components={{
    bold: <strong className="font-semibold" />,
    italic: <em />
  }}
/>

i18next automatically picks the right plural form based on count and the current locale's plural rules. The _one, _other, _zero suffixes follow the Unicode CLDR standard.

TypeScript Support#

Typed Trans Component#

If you use TypeScript with react-i18next, you can get type-safe translation keys:

tsx
// src/i18n.d.ts
import 'react-i18next';
import type en from '../public/locales/en.json';

declare module 'react-i18next' {
  interface CustomTypeOptions {
    defaultNS: 'translation';
    resources: {
      translation: typeof en;
    };
  }
}

Now i18nKey in <Trans> and t() will autocomplete and type-check:

tsx
// Type-checked — autocomplete works
<Trans i18nKey="welcome" />

// TypeScript error if "nonExistentKey" doesn't exist in en.json
<Trans i18nKey="nonExistentKey" />

Typing Component Maps#

tsx
import { Trans } from 'react-i18next';
import { ReactElement } from 'react';

// Components are typed as Record<string, ReactElement>
const components: Record<string, ReactElement> = {
  bold: <strong />,
  link: <a href="/about" />
};

<Trans i18nKey="message" components={components} />

Type-Safe Values#

tsx
// The values prop accepts Record<string, unknown>
<Trans
  i18nKey="greeting"
  values={{ name: user.name, count: items.length }}  // fully typed
  components={{ bold: <strong /> }}
/>

Numbered Tags (Legacy Syntax)#

Older versions of react-i18next used numbered tags and arrays:

json
{
  "message": "Click <0>here</0> to <1>continue</1>"
}
tsx
<Trans
  i18nKey="message"
  components={[
    <a href="/next" />,        // index 0
    <span className="highlight" />  // index 1
  ]}
/>

This still works but is discouraged. Named tags (<link>, <bold>) are:

  • More readable in translation files
  • Easier for translators to understand
  • Less prone to errors when reordering elements

If you have legacy translations with numbered tags, consider migrating to named tags gradually.

The defaults Prop — Inline Fallback#

When you're prototyping or want a fallback if a key is missing:

tsx
<Trans
  i18nKey="mayNotExistYet"
  defaults="Welcome to <bold>our platform</bold>, {{name}}!"
  values={{ name: 'Developer' }}
  components={{ bold: <strong /> }}
/>

If mayNotExistYet doesn't exist in your translation file, the defaults string is used instead. This is useful during development before translations are ready.

Performance Optimization#

Memoize Component Maps#

Creating new component objects on every render triggers unnecessary reconciliation:

tsx
// Bad — creates new objects every render
function Greeting() {
  return (
    <Trans
      i18nKey="greeting"
      components={{ bold: <strong />, link: <a href="/about" /> }}
    />
  );
}

// Good — stable reference
const GREETING_COMPONENTS = { bold: <strong />, link: <a href="/about" /> };

function Greeting() {
  return (
    <Trans i18nKey="greeting" components={GREETING_COMPONENTS} />
  );
}

When Components Have Dynamic Props#

If components depend on props or state, use useMemo:

tsx
function UserMessage({ userId }) {
  const components = useMemo(() => ({
    profileLink: <Link href={`/users/${userId}`} />,
    bold: <strong />
  }), [userId]);

  return <Trans i18nKey="userMessage" components={components} />;
}

Trans vs t() Performance#

Trans parses the translation string to build a React element tree — this is slightly more expensive than t() which returns a plain string. For lists with 100+ items where each item uses Trans, consider whether t() with separate elements would be more efficient:

tsx
// If you have 500 list items, this is faster:
<span>{t('item.label')}: <strong>{t('item.value', { val })}</strong></span>

// Than this:
<Trans i18nKey="item.full" components={{ bold: <strong /> }} values={{ val }} />

In practice, the difference is negligible for most UIs. Only optimize if you measure a bottleneck.

Debugging Trans Component Issues#

When Trans doesn't render as expected, here's a systematic debugging approach.

Debug Step 1: Check the Translation Key#

tsx
import { useTranslation } from 'react-i18next';

function Debug() {
  const { t, i18n } = useTranslation();

  // Check if the key exists and what it resolves to
  console.log('Key exists:', i18n.exists('your.key'));
  console.log('Raw value:', t('your.key'));
  console.log('Current language:', i18n.language);
  console.log('Loaded namespaces:', i18n.reportNamespaces?.getUsedNamespaces());

  return <Trans i18nKey="your.key" components={{ bold: <strong /> }} />;
}

Debug Step 2: Validate Tag Syntax in JSON#

Common JSON issues that break Trans:

json
// Unclosed tag
{ "text": "<bold>hello" }

// Mismatched tag names
{ "text": "<bold>hello</Bold>" }

// Missing slash in self-closing tag
{ "text": "check <icon> this" }

// Correct
{ "text": "<bold>hello</bold>" }
{ "text": "check <icon/> this" }

Debug Step 3: Component Name Mismatch#

Tag names in JSON must exactly match the keys in components:

json
{ "text": "<Bold>important</Bold>" }
tsx
// Won't work — lowercase "bold" doesn't match "Bold"
<Trans i18nKey="text" components={{ bold: <strong /> }} />

// Exact case match
<Trans i18nKey="text" components={{ Bold: <strong /> }} />

Debug Step 4: Check Namespace#

If you use multiple namespaces, Trans defaults to the defaultNS. Override with ns:

tsx
<Trans i18nKey="greeting" ns="marketing" components={{ bold: <strong /> }} />

Debug Step 5: React DevTools#

Install React DevTools and inspect the rendered output of Trans. You should see:

  • The wrapper element (default: fragment in v12+)
  • Child elements matching your component map
  • Text nodes between components

If you see raw HTML strings instead of elements, the components prop isn't being matched correctly.

Common Mistakes and How to Fix Them#

Mistake 1: Using Raw HTML Injection Instead of Trans#

tsx
// Security risk — never inject raw HTML from translation strings
// Always use Trans to create real React elements instead

// Safe — Trans creates real React elements, not raw HTML
<Trans i18nKey="richText" components={{ bold: <strong />, link: <a href="/about" /> }} />

Trans produces real React elements, not raw HTML strings. This is inherently safe against XSS because user input in values is escaped by default.

Mistake 2: Translating Component Props#

json
// Don't do this — attributes are ignored by Trans parser
{ "link": "<a href='/about'>About</a>" }
tsx
// Props go on the React component, not in the translation
// Translation should be: "<a>About</a>"
<Trans
  i18nKey="link"
  components={{ a: <a href="/about" /> }}
/>

Trans replaces the element but keeps the component's original props. Only the children (text between tags) come from the translation string.

Mistake 3: Whitespace Disappearing#

json
{ "label": "Status:<badge>Active</badge>" }
// Renders as "Status:Active" with no space before badge

Fix: add an explicit space in the translation:

json
{ "label": "Status: <badge>Active</badge>" }

Mistake 4: Using Trans for Simple Variables#

tsx
// Overkill — no HTML/JSX needed
<Trans i18nKey="hello" values={{ name }} />

// t() is simpler and faster for plain text
<p>{t('hello', { name })}</p>

Only use Trans when you actually need to embed React components or HTML elements.

Mistake 5: Forgetting the parent Prop#

By default, Trans renders its content wrapped in a fragment. If you need a specific wrapper element:

tsx
// Renders as a <p> tag
<Trans i18nKey="description" parent="p" components={{ bold: <strong /> }} />

// Renders without wrapper (React Fragment) — default in v12+
<Trans i18nKey="description" components={{ bold: <strong /> }} />

In older versions (v11 and below), the default parent was <div>. This caused unexpected block-level elements inside inline context. If you're upgrading, check for layout shifts.

Mistake 6: Forgetting to Pass t in Class Components#

In function components, Trans uses the i18n context automatically. In class components or outside React tree, you need to pass t explicitly:

tsx
import { withTranslation, Trans } from 'react-i18next';

class LegacyComponent extends React.Component {
  render() {
    const { t } = this.props;
    return <Trans t={t} i18nKey="message" components={{ bold: <strong /> }} />;
  }
}

export default withTranslation()(LegacyComponent);

Mistake 7: HTML Entities in Translations#

json
// HTML entities might not work in all parsers
{ "price": "Price:&nbsp;<amount>€99</amount>" }

// Use Unicode directly or handle spacing in CSS
{ "price": "Price:\u00A0<amount>€99</amount>" }

Server-Side Rendering (Next.js, Remix)#

Trans works with SSR — the component tree is rendered on the server just like any React component. Key considerations:

Next.js App Router#

tsx
// app/[locale]/page.tsx — Server Component
// Trans is a client component, can't use directly in Server Components
// Use t() in Server Components, Trans in Client Components

// components/RichMessage.tsx
'use client';
import { Trans } from 'react-i18next';

export function RichMessage() {
  return <Trans i18nKey="welcome" components={{ bold: <strong /> }} />;
}

Next.js Pages Router / Remix#

tsx
// Works directly — these are client-rendered by default
import { Trans } from 'react-i18next';

export default function Page() {
  return <Trans i18nKey="hero" components={{ highlight: <mark /> }} />;
}

Hydration Mismatch Warning#

If you see "Text content does not match server-rendered HTML", ensure:

  1. The same locale is loaded on both server and client
  2. The components map is identical on both sides
  3. No browser-only values (like window.location) in values

Testing Trans Components#

React Testing Library#

tsx
import { render, screen } from '@testing-library/react';
import { Trans } from 'react-i18next';

// Mock react-i18next
jest.mock('react-i18next', () => ({
  Trans: ({ i18nKey, children }) => <span data-testid={`trans-${i18nKey}`}>{children}</span>,
  useTranslation: () => ({ t: (key) => key }),
}));

// Or test with real translations loaded
import '../i18n'; // your i18n init

test('renders warning with bold text', () => {
  render(
    <Trans
      i18nKey="warning"
      components={{ bold: <strong data-testid="bold" /> }}
    />
  );
  expect(screen.getByTestId('bold')).toHaveTextContent('permanent');
});

Snapshot Testing#

tsx
test('Trans output matches snapshot', () => {
  const { container } = render(
    <Trans
      i18nKey="terms"
      components={{
        link: <a href="/terms" />,
        bold: <strong />
      }}
    />
  );
  expect(container).toMatchSnapshot();
});

Quick Reference#

PropTypePurpose
i18nKeystringTranslation key to look up
componentsRecord<string, ReactElement>Maps tag names to React elements
valuesRecord<string, unknown>Variables for {{interpolation}}
countnumberTriggers plural form selection
defaultsstringFallback if key is missing
nsstringNamespace override
parentstring | ComponentWrapper element (default: fragment)
tTFunctionExplicit t function (class components)
i18ni18nExplicit i18n instance

Translating JSON Files with Trans Markup#

When you translate your i18n JSON files to other languages, the tags inside translations must be preserved exactly. A translator needs to know that <bold> and </bold> are markup, not text to translate.

Example — English to German:

json
// en.json
{ "cta": "Click <link>here</link> to <bold>get started</bold>." }

// de.json — tags preserved, text translated
{ "cta": "Klicken Sie <link>hier</link>, um <bold>loszulegen</bold>." }

If your translation tool strips or corrupts these tags, your app will break at runtime. shipglobal.dev automatically detects and preserves Trans component tags, {{variables}}, and all other i18next syntax during translation.