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:
{
"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():
| Scenario | Use |
|---|---|
| Plain text, variables only | t() |
| Bold, italic, or styled text inside a sentence | Trans |
Links (React Router or <a>) embedded in text | Trans |
| Icons or images inline with text | Trans |
| Complex nested JSX structures | Trans |
If your translation is pure text with {{variables}}, stick with t(). The moment HTML or JSX enters the picture, switch to Trans.
Setup & Import#
npm install react-i18next i18next
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#
// en.json
{
"warning": "This action is <bold>permanent</bold> and <italic>cannot be undone</italic>."
}
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)#
{
"verified": "<icon/> Verified account",
"address": "Line 1<br/>Line 2"
}
<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.
Links in Translations#
One of the most common use cases — links that need real click handling.
External Links#
{
"terms": "By continuing, you agree to our <termsLink>Terms</termsLink> and <privacyLink>Privacy Policy</privacyLink>."
}
<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" />
}}
/>
React Router / Next.js Links#
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.
Links with Icons#
{
"docs": "Read the <link>documentation <icon/></link>"
}
<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:
{
"greeting": "Hello <bold>{{name}}</bold>, you have <badge>{{count}}</badge> new messages."
}
<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#
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.
Nested Components#
Tags can be nested inside each other:
{
"help": "For assistance, <link>contact <bold>our support team</bold></link> or check the <faqLink>FAQ</faqLink>."
}
<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:
{
"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>."
}
<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:
// 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:
// Type-checked — autocomplete works
<Trans i18nKey="welcome" />
// TypeScript error if "nonExistentKey" doesn't exist in en.json
<Trans i18nKey="nonExistentKey" />
Typing Component Maps#
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#
// 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:
{
"message": "Click <0>here</0> to <1>continue</1>"
}
<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:
<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:
// 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:
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:
// 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#
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:
// 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:
{ "text": "<Bold>important</Bold>" }
// 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:
<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#
// 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#
// Don't do this — attributes are ignored by Trans parser
{ "link": "<a href='/about'>About</a>" }
// 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#
{ "label": "Status:<badge>Active</badge>" }
// Renders as "Status:Active" with no space before badge
Fix: add an explicit space in the translation:
{ "label": "Status: <badge>Active</badge>" }
Mistake 4: Using Trans for Simple Variables#
// 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:
// 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:
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#
// HTML entities might not work in all parsers
{ "price": "Price: <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#
// 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#
// 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:
- The same locale is loaded on both server and client
- The
componentsmap is identical on both sides - No browser-only values (like
window.location) invalues
Testing Trans Components#
React Testing Library#
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#
test('Trans output matches snapshot', () => {
const { container } = render(
<Trans
i18nKey="terms"
components={{
link: <a href="/terms" />,
bold: <strong />
}}
/>
);
expect(container).toMatchSnapshot();
});
Quick Reference#
| Prop | Type | Purpose |
|---|---|---|
i18nKey | string | Translation key to look up |
components | Record<string, ReactElement> | Maps tag names to React elements |
values | Record<string, unknown> | Variables for {{interpolation}} |
count | number | Triggers plural form selection |
defaults | string | Fallback if key is missing |
ns | string | Namespace override |
parent | string | Component | Wrapper element (default: fragment) |
t | TFunction | Explicit t function (class components) |
i18n | i18n | Explicit 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:
// 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.
Related Resources#
- i18next Interpolation & Placeholders Guide — Variables, formatting, and the
{{var}}syntax in depth - How to Localize a React App — Full setup guide from zero to translated React app
- React i18n — Translate Your React App — Translate react-intl, react-i18next, next-intl JSON files to 45+ languages
- Vue i18n Guide — Vue.js equivalent of this guide