Skip to content

Latest commit

 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

React Router v7 framework-mode + Locize example

A minimal React Router v7 (framework mode) sample showing how to wire remix-i18next (v7.x, the RR-v7-targeting major) with Locize for server-side language detection plus client-side translation loading.

Companion blog post: How to internationalize a React Router v7 app with remix-i18next.

Stack: React Router 7.15 (framework mode, middleware enabled) · remix-i18next 7.5 · i18next 26 · react-i18next 17 · i18next-locize-backend 10 · locize 4.0.24 (with the SSR-safe isInIframe fix and the hydration-mismatch popup fix) · React 19 · Vite 7.

Migrating from Remix v2? See the companion locize-remix-i18next-example (blog walkthrough: Part 1 / Part 2) which stays on Remix v2 + remix-i18next 6.x for projects that haven't moved to React Router v7 framework mode yet.

Getting started

  1. Create a free account and a project at https://www.locize.com/?from=locize-react-router-example, then grab your project id (and an API key if you want saveMissing writes during local dev).
  2. Either edit app/locize.config.ts and package.json's downloadLocales/syncLocales scripts, or keep the shipped demo-project credentials to try it out first.
  3. Pre-bundle the translations: npm run downloadLocales — pulls the latest published JSON from Locize into app/locales/ (the JSON files there are committed as sane defaults so the example builds without a Locize account).
  4. npm install && npm run dev, then open http://localhost:3000.

How the Locize integration works

React Router v7's framework mode runs the same React tree on the server and on the client. We use different translation sources on each side to get the best of both:

Server side — app/middleware/i18next.ts

createI18nextMiddleware from remix-i18next/middleware (the new RR-v7 middleware pattern, opt-in via future.v8_middleware: true in react-router.config.ts) detects the locale from the request (cookie → URL param → Accept-Language) and seeds a per-request i18next instance.

Resources come from the statically-imported JSON in app/locales/index.ts — i.e. the JSON files in app/locales/ that locize download placed there at build time. No runtime calls to the Locize CDN from the server. This is important on serverless platforms (Vercel/Netlify/Cloudflare Workers) where each request would otherwise be a fresh cold-start re-download.

getLocale(context) and getInstance(context) (returned by createI18nextMiddleware) are how loaders and entry.server.tsx access what the middleware decided.

Client side — app/entry.client.tsx

After the SSR-rendered HTML reaches the browser, i18next initialises with i18next-locize-backend directly against the Locize CDN. This means content updates published in Locize show up on the next page view without a redeploy. The locize in-context editor plugin (append ?incontext=true to the URL) and dev-only locize-lastused for unused-key tracking round out the integration.

hydrateRoot is wrapped in startTransition per React 19 + RR v7 guidance.

Pushing missing keys back — saveMissing

Set in app/entry.client.tsx via saveMissing: !isProduction. When i18next requests a key that the loaded JSON doesn't yet have, it pushes the key to Locize so translators can fill it in — analogous to a build-time extraction step, but live during dev.

locize@4.0.24 SSR + hydration safety

Two recent fixes in the locize package matter specifically for React Router v7 framework mode (full details in the changelog):

  • 4.0.23 fixed isInIframe detection in Node 20+ SSR builds. Prior versions evaluated self !== top (where self is now globalThis in modern Node), making locize wrongly believe it was inside the Locize editor iframe during the SSR pass.
  • 4.0.24 fixed the InContext editor popup disappearing when ?incontext=true is opened in apps that hydrate the whole document via hydrateRoot(document, ...) — which RR v7 framework mode does by default. React's hydration-mismatch recovery (triggered on the client by i18next-subliminal's invisible-marker post-processor) was sweeping the popup out of <body> along with everything else it didn't own. The popup now lives outside <body>, and a short-lived MutationObserver re-attaches it (against the same iframe element, preserving message ports) if a recovery still removes it. Same release also re-resolves the iframe's contentWindow on every outgoing postMessage so the editor handshake survives the iframe re-navigation that the resurrection triggers.

Production safety

The write-enabled apiKey is hardcoded in app/locize.config.ts only because the demo project is intentionally public. For your own project, source the dev apiKey from a git-ignored env file (e.g. read from import.meta.env.VITE_LOCIZE_APIKEY in dev, omit in prod) and never ship a write-enabled key in production builds.

Scripts

Command What it does
npm run dev React Router dev server on http://localhost:3000
npm run build Production build into build/
npm start Run the production build via @react-router/serve
npm run typecheck react-router typegen && tsc
npm run downloadLocales locize download — pull translations into app/locales/
npm run syncLocales locize sync — two-way sync (dry-run by default)

Locize CDN endpoint

The example talks to the Pro CDN (api.locize.app) by default because the shipped demo project lives there. If your project is on the Standard CDN (api.lite.locize.app, the default for new Locize projects), set cdnType: 'standard' in app/locize.config.ts. See CDN types: Standard vs. Pro for the full comparison.

Related

About

React Router v7 framework-mode + remix-i18next + Locize example. Companion to locize-remix-i18next-example which stays on Remix v2.

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages