Better I18NBetter I18N
Vite

Setup

Installation and provider setup for Vite + React applications

This guide walks you through installing Better i18n and setting up the provider in your Vite + React application.

Installation

npm install @better-i18n/use-intl use-intl
yarn add @better-i18n/use-intl use-intl
pnpm add @better-i18n/use-intl use-intl
bun add @better-i18n/use-intl use-intl

Configuration

Step 1: Create i18n Configuration

Create a configuration file to centralize your i18n settings:

src/i18n.config.ts
export const i18nConfig = { 
  project: 'your-org/your-project', 
  defaultLocale: 'en', 
} as const

Step 2: Setup Provider

Wrap your app with BetterI18nProvider:

src/App.tsx
import { BetterI18nProvider } from '@better-i18n/use-intl'
import { useState } from 'react'
import { i18nConfig } from './i18n.config'
import { Router } from './Router'

function App() {
  // Get initial locale from localStorage or browser
  const [locale, setLocale] = useState(() => {
    return localStorage.getItem('locale') || navigator.language.split('-')[0] || 'en'
  })

  const handleLocaleChange = (newLocale: string) => {
    localStorage.setItem('locale', newLocale)
    setLocale(newLocale)
  }

  return (
    <BetterI18nProvider
      project={i18nConfig.project} 
      locale={locale} 
      onLocaleChange={handleLocaleChange} 
    > // [!code ++]
      <Router />
    </BetterI18nProvider> 
  )
}

export default App

BetterI18nProvider renders null until translations are loaded. If no messages prop is provided, the component tree is invisible while the CDN fetch is in progress. Wrap with a Suspense boundary or show a loading indicator via the onLocaleChange state.

BetterI18nProvider Props

PropTypeRequiredDescription
projectstringYesProject identifier (org/project)
localestringYesCurrent locale (from state/localStorage)
onLocaleChange(locale: string) => voidYesCalled when user switches language
messagesMessagesNoPre-loaded translations (SSR or static)
timeZonestringNoIANA timezone
nowDateNoPin current time (for testing or SSR consistency)
storageTranslationStorageNoPersistent translation cache adapter
staticDataRecord<string, Messages>NoOffline fallback translations
fetchTimeoutnumberNoCDN fetch timeout in ms (default: 10000)
retryCountnumberNoRetry attempts on CDN failure (default: 1)

Step 3: Add Loading State

For a better UX, handle the loading state with Suspense:

src/App.tsx
import { BetterI18nProvider } from '@better-i18n/use-intl'
import { Suspense } from 'react'

function App() {
  return (
    <Suspense fallback={<LoadingScreen />}> // [!code ++]
      <BetterI18nProvider
        project="your-org/your-project"
        locale="en"
      > // [!code ++]
        <Router />
      </BetterI18nProvider> // [!code ++]
    </Suspense> 
  )
}

function LoadingScreen() {
  return (
    <div className="flex items-center justify-center h-screen">
      <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900" />
    </div>
  )
}

Using Translations

Basic Usage

src/pages/HomePage.tsx
import { useTranslations } from '@better-i18n/use-intl'

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

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

With Variables

function WelcomeMessage({ user }) {
  const t = useTranslations('greeting')

  return (
    <p>{t('hello', { name: user.name, count: user.notifications })}</p>
  )
}

Formatting

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

function OrderSummary({ order }) {
  const format = useFormatter()

  return (
    <div>
      <p>Total: {format.number(order.total, { style: 'currency', currency: 'USD' })}</p>
      <p>Date: {format.dateTime(order.date, { dateStyle: 'long' })}</p>
      <p>Items: {format.list(order.items, { type: 'conjunction' })}</p>
    </div>
  )
}

Language Switcher

Simple Select

src/components/LanguageSwitcher.tsx
import { useLocale, useLanguages } from '@better-i18n/use-intl'

export function LanguageSwitcher() {
  const { locale, setLocale } = useLocale()
  const { languages, isLoading } = useLanguages()

  if (isLoading) {
    return <select disabled><option>Loading...</option></select>
  }

  return (
    <select
      value={locale}
      onChange={(e) => setLocale(e.target.value)}
      className="px-3 py-2 border rounded"
    >
      {languages.map((lang) => (
        <option key={lang.code} value={lang.code}>
          {lang.nativeName}
        </option>
      ))}
    </select>
  )
}

Built-in Component

Use the included LanguageSwitcher component:

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

function Header() {
  return (
    <header className="flex justify-between items-center p-4">
      <Logo />
      <LanguageSwitcher className="border rounded px-3 py-2" />
    </header>
  )
}

Performance Tips

Preload Default Locale

For faster initial load, preload messages in your HTML:

<link rel="preload" href="https://cdn.better-i18n.com/org/project/en.json" as="fetch" crossorigin>

Use Suspense

Wrap your app in Suspense to show a loading state while translations load.

Cache in Service Worker

For offline support, cache translation files in a service worker.

Next Steps

On this page