Better Translation is local-first internationalization for Vite apps.
Mark copy where you author it in React or Svelte. The Vite plugin discovers Messages, generates flat Locale value JSON, and creates the runtime loader your app uses to render them. Add your own AI translator or edit Locale values locally; no account or hosted runtime is required.
Website · Documentation · npm
- A Vite plugin that scans source files for Translation markers.
- Stable lookup ids generated from Message text, Variable placeholders, and context.
- Local Runtime bundles as flat
id -> translated stringJSON objects. - React and Svelte helpers for marking UI copy and reading Locale values at runtime.
- Optional dev-time translation through your own async
translate()function. - Optional dev-only local editor for local Locale values.
- Node 24 or later
- Vite 8 or later
- React 19 or later when using
better-translation/react - Svelte 5 and
@sveltejs/vite-plugin-sveltewhen usingbetter-translation/svelte
This repo uses Bun:
bun installInstall the package in a TanStack Start app:
bun add better-translationAdd the Vite plugin before the TanStack Start and React plugins:
// vite.config.ts
import { tanstackStart } from "@tanstack/react-start/plugin/vite"
import { defineConfig } from "vite"
import react from "@vitejs/plugin-react"
import { betterTranslation } from "better-translation/vite"
export default defineConfig({
plugins: [
betterTranslation({
locales: ["en", "es", "fr"],
defaultLocale: "en",
}),
tanstackStart(),
react(),
],
})If your Start app also uses plugins such as Nitro, Tailwind, or tsconfig-path resolution, keep betterTranslation() before the plugins that transform React route code.
For Svelte apps, keep betterTranslation() before the Svelte plugin:
// vite.config.ts
import { svelte } from "@sveltejs/vite-plugin-svelte"
import { defineConfig } from "vite"
import { betterTranslation } from "better-translation/vite"
export default defineConfig({
plugins: [
betterTranslation({
locales: ["en", "es", "fr"],
defaultLocale: "en",
}),
svelte(),
],
})The default local runtime works well for TanStack Start. It writes generated Locale values under src/lib/bt and exposes them through the virtual better-translation/messages module.
Load messages in your root route's beforeLoad, then wrap the route outlet:
// 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>
)
}getLocale() is whatever your app uses to resolve the active Locale, such as a cookie, route param, or request header.
Passing it to TranslateProvider lets completed local development translations replace the authored fallback without
reloading the page or remounting application state.
Mark UI copy where you author it:
import { T, useT } from "better-translation/react"
export function CheckoutButton() {
const t = useT()
return (
<button aria-label={t("Complete checkout")}>
<T>Pay now</T>
</button>
)
}Static inline elements inside React and Svelte <T> markers are preserved automatically while their text is translated:
<T>
Always make sure <strong>you are safe first</strong> before approaching the casualty.
</T>This works with supported inline elements such as <strong>, <b>, <i>, <em>, <span>, <a>, and <br />, including
nested elements. Source-owned React components such as <B> or <Text.Italic> and normal Svelte components work too, so a
design-system component can render <b> or another element under the hood. Better Translation represents each authored
element or component as a numbered rich-text slot in the Message, then restores that same source-owned renderer and its props
at runtime. Consumer apps do not import a parser, register tag callbacks, or validate translations themselves. Translated
strings never become arbitrary HTML, and dynamic values should still use <Var>.
Run your dev server. The plugin scans your source and writes Runtime bundles for each configured Locale.
bun run devLocal mode is the account-free workflow. The plugin owns local Locale value files and your app loads those values at runtime.
betterTranslation({
locales: ["en", "es", "fr"],
defaultLocale: "en",
runtime: {
type: "local",
target: "module",
},
})Use target: "module" when Vite can import generated files from source. Use target: "public" when the runtime should fetch JSON from public assets instead:
betterTranslation({
locales: ["en", "es", "fr"],
defaultLocale: "en",
runtime: {
type: "local",
target: "public",
},
})Local mode can call a translate() function during dev for missing non-default Locale values:
import { createAiTranslate } from "better-translation/ai"
import { betterTranslation } from "better-translation/vite"
betterTranslation({
locales: ["en", "es", "fr"],
defaultLocale: "en",
runtime: {
type: "local",
translationBatchSize: 25,
translate: createAiTranslate({
prompt: "Use concise product UI copy.",
}),
},
})You can also pass your own translate(messages, locale) implementation. Return a record keyed by lookup id.
translationBatchSize controls how many missing Messages are sent to that function before the cache and local Runtime bundles are
persisted, so completed batches survive an interruption.
Enable the dev-only editor when you want to search Messages and edit local Locale values in the browser:
betterTranslation({
locales: ["en", "es", "fr"],
defaultLocale: "en",
runtime: {
type: "local",
editor: true,
},
})By default it is served during vite dev at /__better-translation.
Better Translation is moving toward a hosted workflow where the Vite plugin syncs a Manifest to a Project, Branches isolate Locale value changes, and Consumer apps load flat Runtime bundles by Project, Branch, and Locale.
The current package keeps the local bundle-first workflow working while the hosted service evolves.
packages/better-translation: published Vite plugin, runtime helpers, local editor middleware, and AI translation helper. Public build entry modules stay at the top ofsrc; private Message grammar lives insrc/message, Vite-plugin behavior insrc/vite-plugin, and Svelte implementation files insrc/svelte.apps/web: hosted app and service scaffold.apps/docs: documentation site.examples/tanstack-start: local-first TanStack Start example.examples/svelte-kit: local-first SvelteKit example.CONTEXT.md: canonical product vocabulary.docs/platform.md: current platform behavior and intended hosted-service direction.
Run routine verification from the repo root:
bun run format
bun run lint
bun run check
bun run testBuild the published package:
bun --filter better-translation buildMeasure Vite-plugin and runtime performance with statistically sampled benchmarks:
bun run bench:performancebun run test also runs coarse scaling guards. They reject gross superlinear growth across TypeScript and Svelte source
analysis, Message-template parsing, Manifest aggregation, and cached source edits without relying on hardware-specific
millisecond limits. Use
bun run bench:performance:json when benchmark results need to be captured for a same-machine comparison.
The package regression suite is split by the interface it protects:
packages/better-translation/test/source-analysis.test.ts: extraction, source edits, and diagnostics.packages/better-translation/test/runtime.test.tsx: React and Svelte rendering, interpolation, and rich text.packages/better-translation/test/vite-plugin.test.ts: Manifest, cache, local artifacts, and plugin lifecycle.
The docs app has the fuller guide and API reference:
apps/docs/content/docs/index.mdx: quick startapps/docs/content/docs/how-it-works.mdx: extraction and runtime modelapps/docs/content/docs/local-mode.mdx: local Runtime bundles and editorapps/docs/content/docs/reference.mdx: API reference
