Better I18NBetter I18N

Translations

Access translations with useTranslations, interpolation, and namespaces

useTranslations Hook

Access namespaced translations in any component:

import { useTranslations } from '@better-i18n/use-intl'

function HomePage() {
  const t = useTranslations('home')

  return (
    <div>
      <h1>{t('title')}</h1>
      <p>{t('description')}</p>
    </div>
  )
}

API Reference

ParameterTypeRequiredDescription
namespacestringThe namespace to scope translations to

Return Methods

MethodDescription
t(key, values?)Get translated string with optional interpolation
t.rich(key, values)Get translated string with React components
t.raw(key)Get raw translation string
t.has(key)Check if translation exists

Namespaces

Namespaces help organize translations into logical groups, making large translation files manageable.

What Are Namespaces?

A namespace is a top-level key in your translation file:

en.json
{
  "home": {
    "title": "Welcome",
    "description": "The best app ever"
  },
  "about": {
    "title": "About Us",
    "mission": "Our mission is..."
  },
  "common": {
    "save": "Save",
    "cancel": "Cancel",
    "loading": "Loading..."
  }
}

Namespace Patterns

By Page:

// app/routes/about.tsx
const t = useTranslations('about')

By Feature:

// components/LoginForm.tsx
const t = useTranslations('auth')
t('login.title')

Shared Namespace:

const t = useTranslations('common')
<button>{t('actions.save')}</button>

Nested Keys

Access nested keys with dot notation:

{
  "settings": {
    "profile": {
      "title": "Profile Settings",
      "fields": {
        "name": "Full Name",
        "email": "Email Address"
      }
    }
  }
}
const t = useTranslations('settings')

t('profile.title')           // "Profile Settings"
t('profile.fields.name')     // "Full Name"

Multiple Namespaces

Use multiple namespaces in one component:

function CheckoutPage() {
  const tCheckout = useTranslations('checkout')
  const tCommon = useTranslations('common')

  return (
    <div>
      <h1>{tCheckout('title')}</h1>
      <button>{tCommon('actions.save')}</button>
      <button>{tCommon('actions.cancel')}</button>
    </div>
  )
}

Interpolation

Interpolation lets you insert dynamic values into translations using ICU Message Format.

Basic Interpolation

Insert variables with curly braces:

en.json
{
  "greeting": {
    "hello": "Hello, {name}!",
    "welcome": "Welcome back, {firstName} {lastName}"
  }
}
const t = useTranslations('greeting')

t('hello', { name: 'John' })
// "Hello, John!"

t('welcome', { firstName: 'John', lastName: 'Doe' })
// "Welcome back, John Doe"

Pluralization

Handle plural forms with plural:

{
  "cart": {
    "items": "{count, plural, =0 {No items} one {# item} other {# items}}"
  }
}
t('items', { count: 0 })   // "No items"
t('items', { count: 1 })   // "1 item"
t('items', { count: 5 })   // "5 items"

Plural Categories

CategoryDescriptionExample
=0, =1, =NExact match=0 {Empty}
zeroZero form (some languages)zero {No items}
oneSingularone {# item}
twoDual (some languages)two {# items}
fewFew (some languages)few {# items}
manyMany (some languages)many {# items}
otherDefault/pluralother {# items}

# is replaced with the count value. Use one and other for English; other languages may need more categories.

Select (Gender/Variants)

Choose based on a string value:

{
  "notifications": {
    "liked": "{gender, select, male {He} female {She} other {They}} liked your post"
  }
}
t('liked', { gender: 'male' })    // "He liked your post"
t('liked', { gender: 'female' })  // "She liked your post"
t('liked', { gender: 'other' })   // "They liked your post"

SelectOrdinal

Handle ordinal numbers (1st, 2nd, 3rd):

{
  "ranking": {
    "place": "You finished in {position, selectordinal, one {#st} two {#nd} few {#rd} other {#th}} place"
  }
}
t('place', { position: 1 })   // "You finished in 1st place"
t('place', { position: 2 })   // "You finished in 2nd place"
t('place', { position: 3 })   // "You finished in 3rd place"

Rich Text

Insert React components with .rich():

{
  "legal": {
    "terms": "By signing up, you agree to our <link>Terms of Service</link> and <bold>Privacy Policy</bold>."
  }
}
const t = useTranslations('legal')

t.rich('terms', {
  link: (chunks) => <a href="/terms">{chunks}</a>,
  bold: (chunks) => <strong>{chunks}</strong>,
})

Self-Closing Tags

For elements without children:

{
  "message": "Loading<spinner/> Please wait..."
}
t.rich('message', {
  spinner: () => <Spinner />,
})

Number & Date Formatting

Format numbers and dates inline:

{
  "stats": {
    "users": "{count, number} active users",
    "created": "Created on {date, date, medium}"
  }
}
t('users', { count: 1234567 })
// "1,234,567 active users"

t('created', { date: new Date() })
// "Created on Jan 18, 2026"

Escaping

Use single quotes to escape:

{
  "code": "Use '{name}' to insert the user''s name"
}
t('code')
// "Use {name} to insert the user's name"

TypeScript Support

Enable type-safe translation keys:

src/types/i18n.d.ts
import en from '../locales/en.json'

type Messages = typeof en

declare global {
  interface IntlMessages extends Messages {}
}

Now you get autocomplete for translation keys:

const t = useTranslations('home')
t('title')       // ✅ Valid
t('typoTitle')   // ❌ TypeScript error

Best Practices

Keep Namespaces Focused

// ✅ Good - focused namespaces (~20-30 keys each)
{
  "home": { ... },
  "dashboard": { ... },
  "settings": { ... }
}

// ❌ Bad - giant namespace
{
  "app": { /* 500+ nested keys */ }
}

Use Consistent Naming

// ✅ Good - consistent camelCase
{
  "home": {
    "meta": { "title": "..." },
    "hero": { "title": "...", "cta": "..." }
  }
}

// ❌ Bad - inconsistent
{
  "home": {
    "pageTitle": "...",
    "HeroTitle": "...",
    "button_text": "..."
  }
}

Avoid Deep Nesting

// ✅ Good - reasonable depth (2-3 levels)
{
  "settings": {
    "profile": {
      "name": "Name",
      "email": "Email"
    }
  }
}

// ❌ Bad - too deep
{
  "settings": {
    "user": {
      "profile": {
        "personal": {
          "details": {
            "name": "Name"
          }
        }
      }
    }
  }
}

On this page