Skip to content

Lesson 2: Client with URL-Prefix Routing (SPA) ​

In this lesson, you will learn how to configure lingui-rr for a Client-Only / SPA application (server: false) that still represents locales in the URL paths.


Step 1: Configure the Router i18n Object ​

When configuring the router for client-only execution, specify server: false. You must use client-specific detectors and persistence adapters.

ts
// app/lib/i18n.ts
import {
  createLinguiRouter,
  clientDetectors,
  clientPersistence,
} from 'lingui-rr'

export const localeMetadata = {
  ar: { label: 'العربية', dir: 'rtl' },
  en: { label: 'English', dir: 'ltr' },
} as const

export const defaultLocale = 'ar'

export const i18n = createLinguiRouter({
  server: false, // SPA Mode
  mode: 'url-prefix',
  locales: localeMetadata,
  defaultLocale,
  prefixDefaultLocale: false,
  detection: [clientDetectors.cookie('locale'), clientDetectors.navigator()],
  persistence: [clientPersistence.cookie('locale')],
  ignorePaths: [
    /^\/assets\//,
    /^\/build\//,
    /^\/favicon\.ico$/,
    /^\/robots\.txt$/,
    /^\/sitemap\.xml$/,
    /^\/manifest\.webmanifest$/,
    /^\/api\//,
    /^\/change-locale$/,
  ],
  catalogs: {
    ar: () => import('../locales/ar.po'),
    en: () => import('../locales/en.po'),
  },
})

Validation Error

If you accidentally supply a server detector (like serverDetectors.acceptLanguage()) to a router configured with server: false, lingui-rr will throw a config validation error on start:

txt
[lingui-rr] config.detection: server: false configs can only use client detectors, got a "server" detector.

Step 2: Wire the Root Route (root.tsx) ​

For client-only applications, React Router exposes client-side hooks for middleware and loading. In app/root.tsx, you must export clientMiddleware and clientLoader instead of their server-side counterparts.

Additionally, you must instantiate the client middleware using createLinguiClientMiddleware().

tsx
// app/root.tsx
import {
  createLinguiClientMiddleware, 
  createLinguiRootLoader, 
  LinguiRouterProvider, 
} from 'lingui-rr'
import {
  Links,
  Meta,
  Outlet,
  Scripts,
  ScrollRestoration,
  useLoaderData,
  useRouteLoaderData,
} from 'react-router'
import { defaultLocale, i18n } from './lib/i18n'

// Export client-side middleware & loader
export const clientMiddleware = [createLinguiClientMiddleware(i18n)] 
export const clientLoader = createLinguiRootLoader(i18n) 

export function Layout({ children }: { readonly children: React.ReactNode }) {
  const lingui = useRouteLoaderData<typeof clientLoader>('root') 
  const defaultLocaleMeta =
    i18n.locales.find((item) => item.code === defaultLocale) ?? i18n.locales[0]

  return (
    <html
      {...(lingui?.htmlAttrs ?? {
        lang: defaultLocaleMeta.code, 
        dir: defaultLocaleMeta.dir, 
      })} 
      suppressHydrationWarning
    >
      <head>
        <meta charSet="utf-8" />
        <meta content="width=device-width, initial-scale=1" name="viewport" />
        <Meta />
        <Links />
      </head>
      <body>
        {children}
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  )
}

export default function App() {
  const lingui = useLoaderData<typeof clientLoader>() 

  return (
    <LinguiRouterProvider state={lingui} >
      <Outlet />
    </LinguiRouterProvider> 
  )
}

Why are there two separate middleware factories? ​

You might wonder why we call createLinguiClientMiddleware() for client builds and createLinguiMiddleware() for server builds.

React Router defines distinct result types for these two hooks:

  • Server Middleware: Expects to return or receive a standard web Response object.
  • Client Middleware: Receives and returns a record of route strategical results (Record<string, DataStrategyResult>).

Because these types are strictly invariant, lingui-rr provides separate factories. Under the hood, they share the same router logic, but createLinguiClientMiddleware handles redirects by throwing a React Router client navigation error, enabling smooth client-side redirects during path resolution.

Released under the MIT License.