docs

Add Linguana to TanStack Start

For the public 0.1.0 SDK, configure Vite scripts with --configLoader runner as shown in Install Linguana. This lets Node load the packages’ TypeScript source.

This guide shows how to keep the selected language consistent between the server and the browser.

Install the React packages and configure the config loader using Installation. Keep your existing TanStack Start and deployment plugins; the Nitro example below applies to apps that already use Nitro.

1. Configure Vite

vite.config.ts
import { linguana } from "@linguanahq/vite";
import { react as linguanaReact } from "@linguanahq/vite/react";
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import viteReact from "@vitejs/plugin-react";
import { nitro } from "nitro/vite";
import { defineConfig } from "vite";

const token = process.env.LINGUANA_PROJECT_TOKEN;

export default defineConfig({
  ssr: { noExternal: [/^@linguanahq\//] },
  plugins: [
    linguana({
      adapter: linguanaReact(),
      projectId: "your-project-id",
      environment: "development",
      sourceLocale: "en",
      apiUrl: process.env.LINGUANA_API_URL,
      token,
      upload: process.env.LINGUANA_UPLOAD === "true",
      translation: token && process.env.LINGUANA_TRANSLATE !== "false"
        ? {
            enabled: true,
            maxChargeMicros: 100_000,
            waitTimeoutMs: 300_000,
            publish: "development",
          }
        : undefined,
    }),
    tanstackStart(),
    nitro(),
    viteReact(),
  ],
});

The ssr.noExternal setting lets Vite transform the published SDK TypeScript and TSX for server rendering. Keep the project token in the build environment. For values stored in .env.local, use loadEnv as shown in Connect hosted translations.

2. Resolve the language for each request

src/locale-bootstrap.ts
import type { Catalog } from "@linguanahq/catalog";
import { createHydrationState, loadCatalog } from "@linguanahq/react";
import { resolveRequestLocale } from "@linguanahq/react/ssr";
import { createServerFn } from "@tanstack/react-start";
import { getRequest } from "@tanstack/react-start/server";
import { embeddedEnglish } from "./catalogs/en";

const projectId = "your-project-id";
const environment = "development";
const supportedLocales = ["en", "fr"] as const;

export const getLocaleBootstrap = createServerFn({ method: "GET" }).handler(
  async () => {
    const locale = resolveRequestLocale({
      request: getRequest(),
      supportedLocales,
      defaultLocale: "en",
    });

    let remote: Catalog | undefined;
    if (locale !== "en") {
      try {
        remote = await loadCatalog({
          endpoint: process.env.LINGUANA_API_URL!,
          projectId,
          environment,
          locale,
          publishableKey: process.env.LINGUANA_PUBLISHABLE_KEY!,
        });
      } catch {
        // Return deterministic source state when the target is unavailable.
      }
    }

    return createHydrationState(
      remote ? locale : "en",
      remote ? [embeddedEnglish, remote] : [embeddedEnglish],
    );
  },
);

The server uses LINGUANA_API_URL. The browser may use VITE_LINGUANA_API_URL for later locale changes when container and public origins differ.

3. Pass the same state to the browser

Call getLocaleBootstrap() in the route that owns your application shell, then pass its return value unchanged:

src/routes/app.tsx
import { createFileRoute, Outlet } from "@tanstack/react-router";
import { LocaleProvider } from "@linguanahq/react";
import { getLocaleBootstrap } from "../locale-bootstrap";

export const Route = createFileRoute("/app")({
  loader: () => getLocaleBootstrap(),
  component: ApplicationRoute,
});

const availableLocales = [
  { code: "en", displayName: "English", nativeName: "English" },
  { code: "fr", displayName: "French", nativeName: "Français" },
];

function ApplicationRoute() {
  const hydrationState = Route.useLoaderData();

  return (
    <LocaleProvider
      projectId="your-project-id"
      environment="development"
      initialLocale="en"
      fallbackLocale="en"
      hydrationState={hydrationState}
      catalogEndpoint={import.meta.env.VITE_LINGUANA_API_URL}
      publishableKey={import.meta.env.VITE_LINGUANA_PUBLISHABLE_KEY}
      availableLocales={availableLocales}
    >
      <Outlet />
    </LocaleProvider>
  );
}

Adapt /app and the relative import to your existing route layout. Use the same environment in the build, embedded catalog, server loader, and provider; switch them together when targeting production.

Do not independently re-resolve the locale in the browser. hydrationState is authoritative for the initial render.

4. Test with Accept-Language

terminal
curl -H 'Accept-Language: fr' http://localhost:3000

When a French development catalog exists, both server HTML and hydration use French. When it does not, both use the embedded English state.

Next

Read the server rendering guide, then add client-side language switching with Show translated content.

Did this page get you to a working result?

On this page

No Headings