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.

tsx
'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.

tsx
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.

tsx
'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.

tsx
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.

tsx
'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.