Paraglidei18nInternationalizationReactLocalizationSaaS

Paraglide.js in Practice: Modern React i18n Workflows for SaaS

A practical guide to Paraglide.js — the modern, compile-time internationalization library for React — covering setup, message extraction, runtime switching, pluralization, date formatting, and integration with TanStack Start.

Sam Rivera
Sam Rivera
July 8, 202612 min read

TL;DR: Paraglide.js is a compile-time internationalization library that extracts messages at build time, generating optimized per-locale bundles with zero runtime overhead. Unlike traditional i18n libraries (react-intl, i18next) that ship all translations to the client, Paraglide only ships the locale the user needs. This guide covers production patterns for multilingual SaaS serving content in English, German, and Chinese.


Why Paraglide?

FeatureParaglide.jsreact-intli18nextreact-i18next
Bundle size~0 KB (compiled)~30 KB~35 KB~40 KB
Runtime overheadNone (compile-time)Message parsingRuntime lookupsRuntime lookups
Type safety✅ Full (generated TS)PartialPartialPartial
Tree-shakeable✅ (dead code elimination)
Lazy loadingBuilt-in (per-locale split)ManualManualManual
ICU messagesPartialPartial
SSR compatible

Setup

bash
npm install @inlang/paraglide-js
npx paraglide-js init
typescript
// project.inlang/settings.json
{
  "sourceLanguageTag": "en",
  "languageTags": ["en", "zh", "de"],
  "modules": ["https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@latest/dist/index.js"],
  "plugin.inlang.messageFormat": {
    "pathPattern": "./messages/{languageTag}.json"
  }
}

Message Files

json
// messages/en.json
{
  "$schema": "https://inlang.com/schema/inlang-message-format",
  "app_title": "TanStack Ship",
  "nav_home": "Home",
  "nav_pricing": "Pricing",
  "nav_docs": "Docs",
  "hero_heading": "Build Your SaaS Faster",
  "hero_subheading": "A production-ready SaaS starter with auth, billing, and multi-language support.",
  "cta_start": "Get Started",
  "cta_learn": "Learn More",
  "footer_copyright": "© 2026 TanStack Ship. All rights reserved.",
  "pricing_monthly": "Monthly",
  "pricing_yearly": "Yearly",
  "pricing_save": "Save {discount}%",
  "welcome_back": "Welcome back, {name}!",
  "items_count": "{count} {count, plural, one {item} other {items}}"
}
json
// messages/zh.json
{
  "$schema": "https://inlang.com/schema/inlang-message-format",
  "app_title": "TanStack Ship",
  "nav_home": "首页",
  "nav_pricing": "定价",
  "nav_docs": "文档",
  "hero_heading": "更快地构建您的 SaaS",
  "hero_subheading": "一个生产就绪的 SaaS 启动套件,支持认证、计费和多语言。",
  "cta_start": "开始使用",
  "cta_learn": "了解更多",
  "footer_copyright": "© 2026 TanStack Ship。保留所有权利。",
  "pricing_monthly": "月付",
  "pricing_yearly": "年付",
  "pricing_save": "节省 {discount}%",
  "welcome_back": "欢迎回来,{name}!",
  "items_count": "{count} 个{count, plural, other {项目}}"
}

Usage in React Components

Basic Usage

typescript
// Generated by Paraglide at build time
import * as m from "../paraglide/messages"

function HeroSection() {
  return (
    <section className="hero">
      <h1>{m.hero_heading()}</h1>
      <p>{m.hero_subheading()}</p>
      <div className="ctas">
        <Button>{m.cta_start()}</Button>
        <Button variant="secondary">{m.cta_learn()}</Button>
      </div>
    </section>
  )
}

Parameters and Pluralization

typescript
function WelcomeBanner({ userName, itemCount }: { userName: string; itemCount: number }) {
  return (
    <div className="welcome">
      <h2>{m.welcome_back({ name: userName })}</h2>
      <p>{m.items_count({ count: itemCount })}</p>
      {/* English: "Welcome back, Alice!" / "5 items" */}
      {/* Chinese: "欢迎回来,Alice!" / "5 个项目" */}
    </div>
  )
}

Locale Switching

Using TanStack Router Search Params

typescript
// src/routes/__root.tsx
import { useRouter } from "@tanstack/react-router"

function LocaleSwitcher() {
  const router = useRouter()

  const switchLocale = async (locale: "en" | "zh" | "de") => {
    // Set the locale for Paraglide
    await setLocale(locale)

    // Update the URL without reloading
    router.navigate({
      to: router.state.location.pathname,
      search: { locale },
      replace: true,
    })
  }

  return (
    <select onChange={(e) => switchLocale(e.target.value as any)}>
      <option value="en">English</option>
      <option value="zh">中文</option>
      <option value="de">Deutsch</option>
    </select>
  )
}

Server-Side Locale Detection

typescript
// server/middleware/locale.ts
export const resolveLocale = createServerFn({ method: "GET" }).handler(
  async ({ request, context }) => {
    // Priority: URL param > cookie > Accept-Language header > default
    const urlParam = new URL(request.url).searchParams.get("locale")
    if (urlParam && ["en", "zh", "de"].includes(urlParam)) {
      return urlParam
    }

    const cookie = request.headers.get("Cookie")
    const cookieLocale = cookie?.match(/locale=(\w+)/)?.[1]
    if (cookieLocale && ["en", "zh", "de"].includes(cookieLocale)) {
      return cookieLocale
    }

    const acceptLanguage = request.headers.get("Accept-Language")
    if (acceptLanguage?.startsWith("zh")) return "zh"
    if (acceptLanguage?.startsWith("de")) return "de"

    return "en" // Default
  }
)

SEO Integration

hreflang Tags

typescript
// Generated from Paraglide's language tags
function HreflangTags() {
  const locales = ["en", "zh", "de"]

  return (
    <>
      {locales.map((locale) => (
        <link
          key={locale}
          rel="alternate"
          hrefLang={locale}
          href={`https://tanstackship.com/${locale}`}
        />
      ))}
      <link rel="alternate" hrefLang="x-default" href="https://tanstackship.com" />
    </>
  )
}

Locale-Specific Canonical URLs

typescript
// TanStack Router route configuration
export const Route = createFileRoute("/pricing")({
  loader: async ({ context }) => {
    const locale = context.locale ?? "en"
    const messages = await loadMessages(locale)

    return {
      meta: {
        title: messages.page_title_pricing,
        description: messages.page_desc_pricing,
        canonical: `https://tanstackship.com/${locale}/pricing`,
        hreflang: ["en", "zh", "de"].map((l) => ({
          lang: l,
          url: `https://tanstackship.com/${l}/pricing`,
        })),
      },
    }
  },
})

Optimized Bundle Splitting

Paraglide's compile-time approach generates separate bundles per locale:

bash
# Build output — each locale is independently cacheable
dist/
  paraglide/
    en.js       # 3.2 KB — English messages
    zh.js       # 3.5 KB — Chinese messages
    de.js       # 3.4 KB — German messages

Only the user's selected locale is loaded:

typescript
// Dynamic import based on locale
// This is handled automatically by Paraglide at build time
import(`../paraglide/${locale}.js`)

Number and Date Formatting

typescript
function PriceDisplay({ amount, locale }: { amount: number; locale: string }) {
  const formatter = new Intl.NumberFormat(locale, {
    style: "currency",
    currency: "USD",
  })

  return <span>{formatter.format(amount)}</span>
  // en: "$29.00" | zh: "US$29.00" | de: "29,00 $"
}

function DateDisplay({ date, locale }: { date: Date; locale: string }) {
  return (
    <time dateTime={date.toISOString()}>
      {new Intl.DateTimeFormat(locale, {
        dateStyle: "long",
      }).format(date)}
    </time>
    // en: "June 16, 2026" | zh: "2026年6月16日" | de: "16. Juni 2026"
  )
}

Working with Translation Management

Inlang IDE Extension

Use the Inlang VS Code extension for inline message editing:

typescript
// In VS Code, hover over m.hero_heading() to see translations
// Click to edit directly in the IDE

function HeroSection() {
  return (
    <h1>{m.hero_heading()}</h1>
    // Press Cmd+. to open translation editor
  )
}

Export/Import for Translators

bash
# Export all messages to CSV for translators
npx paraglide-js export --format csv

# Import translated messages
npx paraglide-js import translated_messages.csv

Production Checklist

  • All user-facing strings use Paraglide message functions
  • Locale switching does not trigger a full page reload
  • Locale preference persisted in cookie and database
  • hreflang tags set for all supported locales
  • Canonical URLs include locale prefix
  • Server-side locale detection handles Accept-Language header
  • Number and date formatting use Intl APIs (not hardcoded)
  • Translation files have a review workflow
  • Missing translations fall back to source language (English)
  • RTL support checked if applicable (Arabic, Hebrew)

Conclusion

Paraglide.js represents the next generation of i18n for React applications. Its compile-time approach eliminates runtime overhead while providing full type safety and optimized bundle splitting. Combined with TanStack Start, locale detection and switching integrate naturally into the server function and routing architecture. For a broader perspective on entering global markets, see SaaS globalization strategy.

The key advantages for SaaS:

  1. Zero runtime cost — translations are compiled into the bundle
  2. Type safety — missing messages are caught at build time, not runtime
  3. Optimized delivery — users only download their locale's messages
  4. SEO ready — hreflang, canonical, and sitemap integration

For a complete multilingual SEO strategy covering hreflang, canonical URLs, and content localization across markets, see Multilingual SEO complete strategy and the hreflang tags ultimate guide.