Message formatting
18ways uses waysParser by default. That parser is what lets you keep the source copy readable
while still handling variables and locale-aware formatting.
Variables
Use braces for variables and pass values through vars.
'use client';
import { T } from '@18ways/react';
export function WelcomeMessage() {
return (
<T vars={{ name: 'Ada' }}>
Hello {name}
</T>
);
}Pluralization
Use ICU-style plural blocks when the sentence genuinely changes shape.
import { T } from '@18ways/react';
export function InboxCount({
count,
}: {
count: number;
}) {
return (
<T vars={{ count }}>
{
'{count, plural, =0{No messages} =1{One message} other{{count} messages}}'
}
</T>
);
}Dates and money
If you pass a real Date or a money-like object with amount and currency, 18ways will format
it automatically even with a bare placeholder like {publishedAt} or {renewalTotal}.
Use the explicit formatter only when you want to control the output, such as dateStyle:long or a
custom money divisor.
'use client';
import { T } from '@18ways/react';
export function BillingSummary() {
const publishedAt = new Date(
'2026-03-14T09:00:00Z'
);
const renewalTotal = {
amount: 1900,
currency: 'USD',
};
return (
<>
<p>
<T vars={{ publishedAt }}>
Last updated {publishedAt}
</T>
</p>
<p>
<T vars={{ renewalTotal }}>
Renewal total {renewalTotal}
</T>
</p>
<p>
<T vars={{ publishedAt }}>
{
'Detailed date: {publishedAt, date, dateStyle:long}'
}
</T>
</p>
</>
);
}By default, money amounts are treated as minor units. If you already have major units, use the
explicit money formatter with divisor: 1.
Select blocks
Use select when the copy depends on a small set of discrete values.
import { T } from '@18ways/react';
export function SignInState({
isSignedIn,
}: {
isSignedIn: boolean;
}) {
return (
<T vars={{ isSignedIn, name: 'Ada' }}>
{
'{isSignedIn, select, true{Welcome back, {name}} false{Sign in to continue} other{Sign in to continue}}'
}
</T>
);
}Component composition
Keep the sentence together in JSX and 18ways will preserve the real UI structure for common cases
like <T><a href="#">Click here</a> to see more</T> without you stitching fragments together by
hand.
'use client';
import Link from 'next/link';
import { T } from '@18ways/react';
export function HeroCta() {
return (
<T>
<Link href="/pricing">Click here</Link> to
see more
</T>
);
}Turning the parser off
@18ways/react lets you set messageFormatter="none" at the root or provide a custom formatter
function. That is an advanced escape hatch, not the default path.
Practical rule
If the sentence can be written as a complete phrase with a few variables, keep it in one message. That makes translation quality dramatically better than stitching fragments together in code.