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 @better-i18n/vite use-intl
yarn add @better-i18n/use-intl @better-i18n/vite use-intl
pnpm add @better-i18n/use-intl @better-i18n/vite use-intl
bun add @better-i18n/use-intl @better-i18n/vite use-intl

Configuration

Step 1: Add the Vite Plugin

Configure @better-i18n/vite in your Vite config. This is the single source of truth for your project — no need to repeat it in the provider.

vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { betterI18n } from '@better-i18n/vite'

export default defineConfig({
  plugins: [
    betterI18n({ project: 'your-org/your-project' }), 
    react(),
  ],
})

The plugin fetches translations server-side (in the Vite dev server or at build time) and injects them into your HTML as a <script> tag. No client-side CDN requests, no FOUC.

Step 2: Setup Provider

Wrap your app with BetterI18nProvider — no props needed:

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

function App() {
  return (
    <BetterI18nProvider> // [!code ++]
      <Header />
      <Router />
    </BetterI18nProvider> 
  )
}

function Header() {
  return (
    <header>
      <LocaleDropdown /> {/* Works out of the box */}
    </header>
  )
}

export default App

The provider automatically reads project, locale, and messages from the plugin-injected <script> tag. You can still pass props explicitly — they take precedence over injected data.

Without the Vite plugin, project and locale props are required. The provider will fetch translations from CDN on mount, which causes a brief flash of untranslated content on the first render.

BetterI18nProvider Props

PropTypeRequiredDescription
projectstringWith plugin: NoProject identifier (org/project). Auto-read from plugin.
localestringWith plugin: NoCurrent locale. Auto-detected by plugin.
messagesMessagesNoPre-loaded translations (auto-injected by plugin)
onLocaleChange(locale: string) => voidNoCalled when user switches language via setLocale()
timeZonestringNoIANA timezone
nowDateNoPin current time (for testing or SSR consistency)
storageTranslationStorageNoPersistent translation cache adapter
fetchTimeoutnumberNoCDN fetch timeout in ms (default: 10000)
retryCountnumberNoRetry attempts on CDN failure (default: 1)

Vite Plugin Options

OptionTypeRequiredDescription
projectstringYesProject identifier (org/project)
defaultLocalestringNoFallback locale (default: "en")
localeCookiestringNoCookie name for locale persistence (default: "locale")
cdnBaseUrlstringNoCDN base URL override

Step 3: Add Loading State

With the Vite plugin, the provider reads all config from the injected <script> tag — no props needed:

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

function App() {
  return (
    <Suspense fallback={<LoadingScreen />}> // [!code ++]
      <BetterI18nProvider> // [!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>
  )
}

Performance Tips

Use the Vite Plugin

The @better-i18n/vite plugin embeds translations in the HTML at build time — zero client-side CDN requests and no FOUC.

Preload Translations

If not using the plugin, preload messages in your HTML:

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

Cache in Service Worker

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

Next Steps

On this page