Modern i18n DX in TanStack Start with Paraglide JS

Updated on

This guide walks through implementing internationalization in TanStack Start using Paraglide JS, focusing on cookie-based language detection and switching.

Official Examples For more advanced patterns including URL-based routing (e.g., /en/about, /de/about), check out TanStack’s maintained reference implementations:

Why Paraglide?

While there are many i18n solutions available like react-i18next, FormatJS, and others, Paraglide JS stands out by offering a more modern, type-safe approach to internationalization. Here are the key benefits:

  • Type Safety: Full TypeScript support with compile-time checks for missing translations and parameters
  • Performance: Zero runtime overhead through compile-time optimizations and tree-shaking
  • Developer Experience: Excellent IDE support with inline previews and real-time validation
  • Modern Tooling: Part of the inlang ecosystem with AI-powered suggestions and automated workflows

Paraglide JS documentation with advanced features, API reference, and guides.

Getting Started

Before diving into Paraglide, let’s set up a fresh TanStack Start project. If you already have an existing project, just skip this.

First, create a new TanStack Start project using the official CLI:

npm create @tanstack/start@latest

For this guide, I’ve chosen a clean, minimal setup with Tailwind CSS and Biome as the toolchain, without any add-ons or examples. Feel free to adjust these options based on your preferences.

Once created, navigate to your project and start the dev server:

cd start-basic
npm run dev

TanStack Start quick start guide with setup options and configuration.

Initialize Paraglide

With our project ready, we can now add Paraglide. We’ll set it up with English (en) and German (de) as our languages.

Run the initialization command:

npx @inlang/paraglide-js@latest init

The CLI guides you through setup with interactive prompts:

  • Compiled files location (default: ./src/paraglide)
  • VS Code/Cursor extension recommendation (creates .vscode/extensions.json if selected)
  • Machine translations (optional - uses Google Cloud Translation API for initial drafts)

inlang CLI documentation for machine translations and automation. Requires Google Cloud Translation API key.

Configure Vite Plugin

Update vite.config.ts with cookie and detection options:

import { defineConfig } from 'vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import viteReact from '@vitejs/plugin-react'
import { paraglideVitePlugin } from '@inlang/paraglide-js'

export default defineConfig({
  plugins: [
    paraglideVitePlugin({
      project: './project.inlang',
      outdir: './src/paraglide',
      outputStructure: 'message-modules',
      cookieName: 'PARAGLIDE_LOCALE',
      strategy: ['cookie', 'preferredLanguage', 'baseLocale'],
    }),
    tanstackStart(),
    viteReact(),
  ],
})

Configuration:

  • outputStructure: 'message-modules' - Clean import syntax
  • cookieName - Persist language across sessions
  • strategy - Detection order: cookie → browser → default

Official TanStack Start example with URL-based routing patterns like /en/about.

Creating Your First Translations

Update messages/en.json:

{
  "$schema": "https://inlang.com/schema/inlang-message-format",
  "hello": "Hello, world!",
  "greeting": "Hey {name}, how are you?"
}

Update messages/de.json:

{
  "$schema": "https://inlang.com/schema/inlang-message-format",
  "hello": "Hallo, Welt!",
  "greeting": "Hey {name}, wie geht's dir?"
}

Parameters in curly braces (like {name}) become required function parameters in your TypeScript code, providing compile-time safety. The Vite plugin watches these files and automatically recompiles when you make changes.

Message parameters, pluralization, and advanced formatting guide.

Using Translations in Your Application

Start the dev server and create src/routes/hello.tsx:

npm run dev
import { createFileRoute } from '@tanstack/react-router'
import { m } from '@/paraglide/messages'

export const Route = createFileRoute('/hello')({
  component: HelloRoute,
})

function HelloRoute() {
  return (
    <div className="p-4">
      {/* Simple message without parameters */}
      <h1 className="text-2xl font-bold mb-4">{m.hello()}</h1>

      {/* Message with required parameter */}
      <p>{m.greeting({ name: 'Eugene' })}</p>
    </div>
  )
}

Thanks to Paraglide’s type safety, your IDE will show errors if you:

  • Try to use a message that doesn’t exist
  • Forget to provide required parameters
  • Pass parameters with the wrong type

The outputStructure: "message-modules" option in our Vite config enables this clean import syntax. All your translations are available through the m object with full TypeScript support.

Configure Language Detection

Paraglide’s Vite plugin generates server middleware that automatically handles language detection. The middleware checks for a saved language preference in cookies, falls back to the browser’s language settings, and finally defaults to your base locale.

Create src/server.ts to set up the middleware:

import { paraglideMiddleware } from './paraglide/server.js'
import handler from '@tanstack/react-start/server-entry'

export default {
  fetch(req: Request): Promise<Response> {
    return paraglideMiddleware(req, ({ request }) => handler.fetch(request))
  },
}

The middleware intercepts all requests and:

  1. Reads the PARAGLIDE_LOCALE cookie if it exists
  2. Falls back to the Accept-Language header from the browser
  3. Defaults to your base locale (English in our case)

This happens automatically on every request, ensuring the correct language is always detected.

Create Language Switcher

Now let’s build a component that allows users to switch between languages. Create src/components/LanguageSwitcher.tsx:

import { getLocale, locales, setLocale } from '@/paraglide/runtime'

export function LanguageSwitcher() {
  return (
    <div className="flex gap-2">
      {locales.map((locale) => (
        <button
          key={locale}
          onClick={() => setLocale(locale)}
          className={`px-3 py-1 rounded border ${
            locale === getLocale()
              ? 'bg-blue-500 text-white border-blue-600'
              : 'bg-gray-100 text-gray-700 border-gray-300 hover:bg-gray-200'
          }`}
        >
          {locale.toUpperCase()}
        </button>
      ))}
    </div>
  )
}

Wire It All Together

Update src/routes/__root.tsx:

import { HeadContent, Scripts, createRootRoute } from '@tanstack/react-router'
import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'
import { TanStackDevtools } from '@tanstack/react-devtools'
import { getLocale } from '@/paraglide/runtime'

import { LanguageSwitcher } from '../components/LanguageSwitcher'

export const Route = createRootRoute({
  head: () => ({
    meta: [
      {
        charSet: 'utf-8',
      },
      {
        name: 'viewport',
        content: 'width=device-width, initial-scale=1',
      },
      {
        title: 'TanStack Start Starter',
      },
    ],
  }),
  shellComponent: RootDocument,
})

function RootDocument({ children }: { children: React.ReactNode }) {
  return (
    <html lang={getLocale()}>
      <head>
        <HeadContent />
      </head>
      <body>
        <div className="p-4 border-b">
          <LanguageSwitcher />
        </div>

        {children}

        <TanStackDevtools position="bottom-right" />
        <Scripts />
      </body>
    </html>
  )
}

VS Code / Cursor Integration

If you selected the extension recommendation during CLI setup, a .vscode/extensions.json file was created recommending the Sherlock extension.

Sherlock extension for VS Code and Cursor with inline translation previews.

Once installed, the extension provides:

  • Inline translation previews - See translations directly in your code
  • One-click message extraction - Add new translations without switching files
  • Quick editing - Update translations inline without opening JSON files

Sherlock extension showing inline translation previews

This makes working with translations seamless - you can see and edit everything without leaving your code. Combined with TypeScript’s type safety catching missing translations at compile time, you get a robust development experience.

Limitations and Considerations

While Paraglide JS offers many advantages, it has some limitations to consider:

  • Runtime Flexibility: Translations are compiled at build time, so dynamic loading isn’t possible. This mainly affects content-heavy sites needing translations from a database or CMS.
  • Ecosystem Maturity: Being newer than alternatives, some advanced features like pluralization and gender support are still in active development.
  • Build Time Overhead: The compilation step adds build overhead, though it’s usually negligible.

Wrapping Up

This guide covered integrating Paraglide JS with TanStack Start for type-safe internationalization:

  • Setting up Paraglide JS with cookie-based locale detection
  • Creating and managing translations with full TypeScript support
  • Implementing language switching with built-in runtime functions
  • VS Code integration for inline translation previews

Paraglide JS stands out for its developer experience, type safety, and performance through compile-time optimizations.

Resources

Official Examples

Documentation

Tools & Ecosystem

© 2025 Eugen Eistrach.