Better Translation

Quick start

Install Better Translation, configure local runtime loading, provide messages, and mark your first Message.

This gets Better Translation running in local dev mode. Local mode is the default: the Vite plugin scans your app, writes generated Runtime bundles, and generates the virtual better-translation/messages module your app uses at runtime.

1. Install

Add the package to a Vite app running Vite 8 or later.

terminal
bun add better-translation

2. Configure the Vite plugin

Add the plugin to your Vite config, list your Locales, and choose the Default locale.

vite.config.ts
import { defineConfig } from "vite"
import react from "@vitejs/plugin-react"
import { tanstackStart } from "@tanstack/react-start/plugin/vite"
import { betterTranslation } from "better-translation/vite"

export default defineConfig({
  plugins: [
    betterTranslation({
      locales: ["en", "es", "fr"],
      defaultLocale: "en",
    }),
    tanstackStart(),
    react(),
  ],
})

Plugin order

Put betterTranslation before the framework plugin. It rewrites Translation markers before React or Svelte transforms see your code.

3. Load messages and provide them

The plugin config above generates better-translation/messages. Load the active Locale from that virtual module, then wrap your app with TranslateProvider.

Load messages before rendering your translated app, then pass them to the framework provider.

src/routes/__root.tsx
import { createRootRoute, Outlet } from "@tanstack/react-router"

import { loadMessages } from "better-translation/messages"
import { TranslateProvider } from "better-translation/react"

import { getLocale } from "./-locale"

export const Route = createRootRoute({
  beforeLoad: async () => {
    const locale = await getLocale()
    const messages = await loadMessages(locale)

    return { locale, messages }
  },
  component: RootComponent,
})

function RootComponent() {
  const { locale, messages } = Route.useRouteContext()

  return (
    <TranslateProvider locale={locale} messages={messages}>
      <Outlet />
    </TranslateProvider>
  )
}

The runtime target only changes how the generated loadMessages implementation gets the Runtime bundle.

4. Mark copy with T

Once the provider is in place, wrap UI copy where you author it. The plugin extracts this as a Message and gives it a stable lookup id.

CheckoutButton.tsx
import { T } from "better-translation/react"

export function CheckoutButton() {
  return (
    <button>
      <T>Pay now</T>
    </button>
  )
}

5. Start dev

Run your Vite dev server. The plugin scans your source and writes local Runtime bundles.

terminal
bun run dev

The default module target writes files under src/lib/bt. The public target writes files under Vite's public directory, at /bt by default.

Bonus: auto-fill Locale values

Local mode can fill missing non-default Locale values during dev with a translate function. Use the built-in AI helper, or call your own translation API.

vite.config.ts
import { defineConfig } from "vite"
import { betterTranslation } from "better-translation/vite"
import { createAiTranslate } from "better-translation/ai"

export default defineConfig({
  plugins: [
    betterTranslation({
      locales: ["en", "es", "fr"],
      defaultLocale: "en",
      runtime: {
        type: "local",
        translate: createAiTranslate({
          prompt: "Friendly, concise product UI copy.",
        }),
      },
    }),
  ],
})

The plugin writes generated values into your local Runtime bundles and cache. Production local builds never call translate; they only check that committed Runtime bundles are complete.

On this page