diff --git a/e2e/react-router/i18n-paraglide/.gitignore b/e2e/react-router/i18n-paraglide/.gitignore
new file mode 100644
index 00000000000..633517e72cc
--- /dev/null
+++ b/e2e/react-router/i18n-paraglide/.gitignore
@@ -0,0 +1,7 @@
+node_modules
+dist
+src/routeTree.gen.ts
+src/paraglide
+*.local
+port*.txt
+test-results
diff --git a/e2e/react-router/i18n-paraglide/index.html b/e2e/react-router/i18n-paraglide/index.html
new file mode 100644
index 00000000000..3292330d0bf
--- /dev/null
+++ b/e2e/react-router/i18n-paraglide/index.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+ TanStack Router i18n-paraglide e2e
+
+
+
+
+
+
diff --git a/e2e/react-router/i18n-paraglide/messages/de.json b/e2e/react-router/i18n-paraglide/messages/de.json
new file mode 100644
index 00000000000..11498bd8b60
--- /dev/null
+++ b/e2e/react-router/i18n-paraglide/messages/de.json
@@ -0,0 +1,7 @@
+{
+ "$schema": "https://inlang.com/schema/inlang-message-format",
+ "example_message": "Guten Tag {username}",
+ "hello_about": "Hallo /ueber!",
+ "home_page": "Startseite",
+ "about_page": "Über uns"
+}
diff --git a/e2e/react-router/i18n-paraglide/messages/en.json b/e2e/react-router/i18n-paraglide/messages/en.json
new file mode 100644
index 00000000000..8a85ed03fc9
--- /dev/null
+++ b/e2e/react-router/i18n-paraglide/messages/en.json
@@ -0,0 +1,7 @@
+{
+ "$schema": "https://inlang.com/schema/inlang-message-format",
+ "example_message": "Hello world {username}",
+ "hello_about": "Hello /about!",
+ "home_page": "Home page",
+ "about_page": "About page"
+}
diff --git a/e2e/react-router/i18n-paraglide/package.json b/e2e/react-router/i18n-paraglide/package.json
new file mode 100644
index 00000000000..58dc5a1689f
--- /dev/null
+++ b/e2e/react-router/i18n-paraglide/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "tanstack-router-e2e-react-i18n-paraglide",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite --port 3000",
+ "dev:e2e": "vite",
+ "build": "vite build && tsc --noEmit",
+ "preview": "vite preview",
+ "start": "vite",
+ "test:e2e": "rm -rf port*.txt; playwright test --project=chromium"
+ },
+ "dependencies": {
+ "@tailwindcss/vite": "^4.1.18",
+ "@tanstack/react-router": "workspace:^",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ "tailwindcss": "^4.1.18"
+ },
+ "devDependencies": {
+ "@inlang/paraglide-js": "^2.4.0",
+ "@playwright/test": "^1.50.1",
+ "@tanstack/router-e2e-utils": "workspace:^",
+ "@tanstack/router-plugin": "workspace:^",
+ "@types/node": "^22.10.2",
+ "@types/react": "^19.0.8",
+ "@types/react-dom": "^19.0.3",
+ "@vitejs/plugin-react": "^4.3.4",
+ "typescript": "^5.7.2",
+ "vite": "^7.1.7"
+ }
+}
diff --git a/e2e/react-router/i18n-paraglide/playwright.config.ts b/e2e/react-router/i18n-paraglide/playwright.config.ts
new file mode 100644
index 00000000000..91ef8fbd9ff
--- /dev/null
+++ b/e2e/react-router/i18n-paraglide/playwright.config.ts
@@ -0,0 +1,35 @@
+import { defineConfig, devices } from '@playwright/test'
+import { getTestServerPort } from '@tanstack/router-e2e-utils'
+import packageJson from './package.json' with { type: 'json' }
+
+const PORT = await getTestServerPort(packageJson.name)
+const baseURL = `http://localhost:${PORT}`
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+ testDir: './tests',
+ workers: 1,
+
+ reporter: [['line']],
+
+ use: {
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ baseURL,
+ },
+
+ webServer: {
+ command: `VITE_SERVER_PORT=${PORT} pnpm build && pnpm preview --port ${PORT}`,
+ url: baseURL,
+ reuseExistingServer: !process.env.CI,
+ stdout: 'pipe',
+ },
+
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+ ],
+})
diff --git a/e2e/react-router/i18n-paraglide/project.inlang/.gitignore b/e2e/react-router/i18n-paraglide/project.inlang/.gitignore
new file mode 100644
index 00000000000..06cf65390f5
--- /dev/null
+++ b/e2e/react-router/i18n-paraglide/project.inlang/.gitignore
@@ -0,0 +1 @@
+cache
diff --git a/e2e/react-router/i18n-paraglide/project.inlang/project_id b/e2e/react-router/i18n-paraglide/project.inlang/project_id
new file mode 100644
index 00000000000..203a95649e0
--- /dev/null
+++ b/e2e/react-router/i18n-paraglide/project.inlang/project_id
@@ -0,0 +1 @@
+apLjNFHNH2yAHYTN5d
\ No newline at end of file
diff --git a/e2e/react-router/i18n-paraglide/project.inlang/settings.json b/e2e/react-router/i18n-paraglide/project.inlang/settings.json
new file mode 100644
index 00000000000..9bdce4c8cc9
--- /dev/null
+++ b/e2e/react-router/i18n-paraglide/project.inlang/settings.json
@@ -0,0 +1,12 @@
+{
+ "$schema": "https://inlang.com/schema/project-settings",
+ "baseLocale": "en",
+ "locales": ["en", "de"],
+ "modules": [
+ "https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js",
+ "https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@2/dist/index.js"
+ ],
+ "plugin.inlang.messageFormat": {
+ "pathPattern": "./messages/{locale}.json"
+ }
+}
diff --git a/e2e/react-router/i18n-paraglide/src/main.tsx b/e2e/react-router/i18n-paraglide/src/main.tsx
new file mode 100644
index 00000000000..b30558a9472
--- /dev/null
+++ b/e2e/react-router/i18n-paraglide/src/main.tsx
@@ -0,0 +1,40 @@
+import { StrictMode } from 'react'
+import ReactDOM from 'react-dom/client'
+import { RouterProvider, createRouter } from '@tanstack/react-router'
+import './styles.css'
+// Import the generated route tree
+import { routeTree } from './routeTree.gen'
+import { deLocalizeUrl, localizeUrl } from './paraglide/runtime.js'
+
+// Create a new router instance
+const router = createRouter({
+ routeTree,
+ context: {},
+ defaultPreload: 'intent',
+ scrollRestoration: true,
+ defaultStructuralSharing: true,
+ defaultPreloadStaleTime: 0,
+
+ rewrite: {
+ input: ({ url }) => deLocalizeUrl(url),
+ output: ({ url }) => localizeUrl(url),
+ },
+})
+
+// Register the router instance for type safety
+declare module '@tanstack/react-router' {
+ interface Register {
+ router: typeof router
+ }
+}
+
+// Render the app
+const rootElement = document.getElementById('app')
+if (rootElement && !rootElement.innerHTML) {
+ const root = ReactDOM.createRoot(rootElement)
+ root.render(
+
+
+ ,
+ )
+}
diff --git a/e2e/react-router/i18n-paraglide/src/routes/__root.tsx b/e2e/react-router/i18n-paraglide/src/routes/__root.tsx
new file mode 100644
index 00000000000..6f50edef003
--- /dev/null
+++ b/e2e/react-router/i18n-paraglide/src/routes/__root.tsx
@@ -0,0 +1,68 @@
+import { Link, Outlet, createRootRoute, redirect } from '@tanstack/react-router'
+import {
+ getLocale,
+ locales,
+ setLocale,
+ shouldRedirect,
+} from '@/paraglide/runtime'
+import { m } from '@/paraglide/messages'
+
+export const Route = createRootRoute({
+ beforeLoad: async () => {
+ document.documentElement.setAttribute('lang', getLocale())
+
+ const decision = await shouldRedirect({ url: window.location.href })
+
+ if (decision.redirectUrl) {
+ throw redirect({ href: decision.redirectUrl.href })
+ }
+ },
+ component: () => (
+ <>
+
+
+
+ {m.home_page()}
+
+
+
+ {m.about_page()}
+
+
+
+
+ {locales.map((locale) => (
+
+ ))}
+
+
+
+
+
+
+
+
+ >
+ ),
+})
diff --git a/e2e/react-router/i18n-paraglide/src/routes/about.tsx b/e2e/react-router/i18n-paraglide/src/routes/about.tsx
new file mode 100644
index 00000000000..93cfdb3bd70
--- /dev/null
+++ b/e2e/react-router/i18n-paraglide/src/routes/about.tsx
@@ -0,0 +1,10 @@
+import { m } from '@/paraglide/messages'
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/about')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return {m.hello_about()}
+}
diff --git a/e2e/react-router/i18n-paraglide/src/routes/index.tsx b/e2e/react-router/i18n-paraglide/src/routes/index.tsx
new file mode 100644
index 00000000000..accd04a1a19
--- /dev/null
+++ b/e2e/react-router/i18n-paraglide/src/routes/index.tsx
@@ -0,0 +1,18 @@
+import { createFileRoute } from '@tanstack/react-router'
+import { m } from '@/paraglide/messages'
+
+export const Route = createFileRoute('/')({
+ component: App,
+})
+
+function App() {
+ return (
+
+
+ {m.example_message({
+ username: 'TanStack Router!',
+ })}
+
+
+ )
+}
diff --git a/e2e/react-router/i18n-paraglide/src/styles.css b/e2e/react-router/i18n-paraglide/src/styles.css
new file mode 100644
index 00000000000..d4b5078586e
--- /dev/null
+++ b/e2e/react-router/i18n-paraglide/src/styles.css
@@ -0,0 +1 @@
+@import 'tailwindcss';
diff --git a/e2e/react-router/i18n-paraglide/tests/navigation.spec.ts b/e2e/react-router/i18n-paraglide/tests/navigation.spec.ts
new file mode 100644
index 00000000000..8ec0d822925
--- /dev/null
+++ b/e2e/react-router/i18n-paraglide/tests/navigation.spec.ts
@@ -0,0 +1,246 @@
+import { expect, test } from '@playwright/test'
+
+// These tests verify that client-side i18n rewrites work correctly in a pure SPA
+// (no server-side rendering). The router uses the rewrite API to:
+// - input: de-localize URLs for route matching (e.g., /de/ueber -> /about)
+// - output: localize URLs for display (e.g., /about -> /de/ueber)
+
+test.describe('Client-side i18n navigation', () => {
+ test('should load the home page without redirect loops', async ({ page }) => {
+ // Navigate to root - should work without any redirect loop issues
+ await page.goto('/')
+ await page.waitForLoadState('networkidle')
+
+ // Verify the page loaded successfully
+ await expect(page.getByTestId('home-content')).toBeVisible()
+ await expect(page.getByTestId('home-content')).toContainText('Hello world')
+ })
+
+ test('should load the German home page (/de)', async ({ page }) => {
+ await page.goto('/de')
+ await page.waitForLoadState('networkidle')
+
+ // Verify we're on the German page
+ await expect(page.getByTestId('home-content')).toBeVisible()
+ await expect(page.getByTestId('home-content')).toContainText('Guten Tag')
+
+ // URL should remain /de
+ expect(page.url()).toContain('/de')
+ })
+
+ test('should navigate to about page and update URL correctly', async ({
+ page,
+ }) => {
+ await page.goto('/')
+ await page.waitForLoadState('networkidle')
+
+ // Click on about link
+ await page.getByTestId('about-link').click()
+ await page.waitForLoadState('networkidle')
+
+ // Verify we're on the about page
+ await expect(page.getByTestId('about-content')).toBeVisible()
+ await expect(page.getByTestId('about-content')).toContainText(
+ 'Hello /about!',
+ )
+
+ // URL should be /about (English)
+ expect(page.url()).toContain('/about')
+ })
+
+ test('should navigate to German about page with translated URL', async ({
+ page,
+ }) => {
+ await page.goto('/de')
+ await page.waitForLoadState('networkidle')
+
+ // Click on about link (should navigate to /de/ueber)
+ await page.getByTestId('about-link').click()
+ await page.waitForLoadState('networkidle')
+
+ // Verify we're on the about page with German content
+ await expect(page.getByTestId('about-content')).toBeVisible()
+ await expect(page.getByTestId('about-content')).toContainText(
+ 'Hallo /ueber!',
+ )
+
+ // URL should be /de/ueber (German translated path)
+ expect(page.url()).toContain('/de/ueber')
+ })
+
+ test('should directly access German about page (/de/ueber)', async ({
+ page,
+ }) => {
+ // Direct navigation to translated URL
+ await page.goto('/de/ueber')
+ await page.waitForLoadState('networkidle')
+
+ // Verify the page loaded correctly
+ await expect(page.getByTestId('about-content')).toBeVisible()
+ await expect(page.getByTestId('about-content')).toContainText(
+ 'Hallo /ueber!',
+ )
+
+ // URL should stay /de/ueber
+ expect(page.url()).toContain('/de/ueber')
+ })
+
+ test('should switch locale and update URLs accordingly', async ({ page }) => {
+ // Start at English home page
+ await page.goto('/')
+ await page.waitForLoadState('networkidle')
+
+ // Verify English content
+ await expect(page.getByTestId('home-content')).toContainText('Hello world')
+
+ // Switch to German
+ await page.getByTestId('locale-de').click()
+ await page.waitForLoadState('networkidle')
+
+ // Verify German content
+ await expect(page.getByTestId('home-content')).toContainText('Guten Tag')
+
+ // URL should now include /de
+ expect(page.url()).toContain('/de')
+ })
+
+ test('should switch locale on about page and update translated URL', async ({
+ page,
+ }) => {
+ // Start at English about page
+ await page.goto('/about')
+ await page.waitForLoadState('networkidle')
+
+ // Verify English content
+ await expect(page.getByTestId('about-content')).toContainText(
+ 'Hello /about!',
+ )
+
+ // Switch to German
+ await page.getByTestId('locale-de').click()
+ await page.waitForLoadState('networkidle')
+
+ // Verify German content
+ await expect(page.getByTestId('about-content')).toContainText(
+ 'Hallo /ueber!',
+ )
+
+ // URL should now be /de/ueber (translated path)
+ expect(page.url()).toContain('/de/ueber')
+ })
+
+ test('should maintain correct links after locale switch', async ({
+ page,
+ }) => {
+ // Start at German home page
+ await page.goto('/de')
+ await page.waitForLoadState('networkidle')
+
+ // Verify about link has German translated href
+ const aboutLink = page.getByTestId('about-link')
+ await expect(aboutLink).toHaveAttribute('href', '/de/ueber')
+
+ // Switch to English
+ await page.getByTestId('locale-en').click()
+ await page.waitForLoadState('networkidle')
+
+ // Verify about link now has English href
+ await expect(aboutLink).toHaveAttribute('href', '/about')
+ })
+})
+
+test.describe('Client-side navigation without redirect loops', () => {
+ test('navigating back and forth should not cause issues', async ({
+ page,
+ }) => {
+ await page.goto('/')
+ await page.waitForLoadState('networkidle')
+
+ // Navigate to about
+ await page.getByTestId('about-link').click()
+ await page.waitForLoadState('networkidle')
+ await expect(page.getByTestId('about-content')).toBeVisible()
+
+ // Navigate back to home
+ await page.getByTestId('home-link').click()
+ await page.waitForLoadState('networkidle')
+ await expect(page.getByTestId('home-content')).toBeVisible()
+
+ // Navigate to about again
+ await page.getByTestId('about-link').click()
+ await page.waitForLoadState('networkidle')
+ await expect(page.getByTestId('about-content')).toBeVisible()
+ })
+
+ test('switching locales multiple times should work correctly', async ({
+ page,
+ }) => {
+ await page.goto('/')
+ await page.waitForLoadState('networkidle')
+
+ // Switch to German
+ await page.getByTestId('locale-de').click()
+ await page.waitForLoadState('networkidle')
+ await expect(page.getByTestId('home-content')).toContainText('Guten Tag')
+
+ // Switch back to English
+ await page.getByTestId('locale-en').click()
+ await page.waitForLoadState('networkidle')
+ await expect(page.getByTestId('home-content')).toContainText('Hello world')
+
+ // Switch to German again
+ await page.getByTestId('locale-de').click()
+ await page.waitForLoadState('networkidle')
+ await expect(page.getByTestId('home-content')).toContainText('Guten Tag')
+ })
+
+ test('browser back/forward should work with locale changes', async ({
+ page,
+ }) => {
+ await page.goto('/')
+ await page.waitForLoadState('networkidle')
+
+ // Navigate to about
+ await page.getByTestId('about-link').click()
+ await page.waitForLoadState('networkidle')
+
+ // Switch to German
+ await page.getByTestId('locale-de').click()
+ await page.waitForLoadState('networkidle')
+ expect(page.url()).toContain('/de/ueber')
+
+ // Go back
+ await page.goBack()
+ await page.waitForLoadState('networkidle')
+
+ // Should be on about page (English or previous state)
+ await expect(page.getByTestId('about-content')).toBeVisible()
+ })
+})
+
+test.describe('URL rewrite consistency', () => {
+ test('internal navigation should use rewritten URLs in links', async ({
+ page,
+ }) => {
+ await page.goto('/de')
+ await page.waitForLoadState('networkidle')
+
+ // Check that links are properly localized
+ const homeLink = page.getByTestId('home-link')
+ const aboutLink = page.getByTestId('about-link')
+
+ await expect(homeLink).toHaveAttribute('href', '/de')
+ await expect(aboutLink).toHaveAttribute('href', '/de/ueber')
+ })
+
+ test('English links should not have locale prefix', async ({ page }) => {
+ await page.goto('/')
+ await page.waitForLoadState('networkidle')
+
+ const homeLink = page.getByTestId('home-link')
+ const aboutLink = page.getByTestId('about-link')
+
+ await expect(homeLink).toHaveAttribute('href', '/')
+ await expect(aboutLink).toHaveAttribute('href', '/about')
+ })
+})
diff --git a/e2e/react-router/i18n-paraglide/tsconfig.json b/e2e/react-router/i18n-paraglide/tsconfig.json
new file mode 100644
index 00000000000..c478245689c
--- /dev/null
+++ b/e2e/react-router/i18n-paraglide/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "compilerOptions": {
+ "strict": true,
+ "noImplicitAny": false,
+ "esModuleInterop": true,
+ "jsx": "react-jsx",
+ "target": "ESNext",
+ "moduleResolution": "Bundler",
+ "module": "ESNext",
+ "resolveJsonModule": true,
+ "allowSyntheticDefaultImports": true,
+ "skipLibCheck": true,
+ "allowJs": true,
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": ["src", "tests"]
+}
diff --git a/e2e/react-router/i18n-paraglide/vite.config.ts b/e2e/react-router/i18n-paraglide/vite.config.ts
new file mode 100644
index 00000000000..97e9dda7bc0
--- /dev/null
+++ b/e2e/react-router/i18n-paraglide/vite.config.ts
@@ -0,0 +1,49 @@
+import { defineConfig } from 'vite'
+import viteReact from '@vitejs/plugin-react'
+import { tanstackRouter } from '@tanstack/router-plugin/vite'
+import { resolve } from 'node:path'
+import { paraglideVitePlugin } from '@inlang/paraglide-js'
+import tailwindcss from '@tailwindcss/vite'
+
+export default defineConfig({
+ plugins: [
+ tailwindcss(),
+ paraglideVitePlugin({
+ project: './project.inlang',
+ outdir: './src/paraglide',
+ outputStructure: 'message-modules',
+ cookieName: 'PARAGLIDE_LOCALE',
+ strategy: ['url', 'cookie', 'preferredLanguage', 'baseLocale'],
+ urlPatterns: [
+ {
+ pattern: '/',
+ localized: [
+ ['en', '/'],
+ ['de', '/de'],
+ ],
+ },
+ {
+ pattern: '/about',
+ localized: [
+ ['en', '/about'],
+ ['de', '/de/ueber'],
+ ],
+ },
+ {
+ pattern: '/:path(.*)?',
+ localized: [
+ ['en', '/:path(.*)?'],
+ ['de', '/de/:path(.*)?'],
+ ],
+ },
+ ],
+ }),
+ tanstackRouter({ autoCodeSplitting: true }),
+ viteReact(),
+ ],
+ resolve: {
+ alias: {
+ '@': resolve(__dirname, './src'),
+ },
+ },
+})
diff --git a/e2e/react-start/custom-basepath/tests/navigation.spec.ts b/e2e/react-start/custom-basepath/tests/navigation.spec.ts
index e66f14be9e2..e238134d2ed 100644
--- a/e2e/react-start/custom-basepath/tests/navigation.spec.ts
+++ b/e2e/react-start/custom-basepath/tests/navigation.spec.ts
@@ -61,14 +61,15 @@ test('server-side redirect', async ({ page, baseURL }) => {
expect(page.url()).toBe(`${baseURL}/posts/1`)
// do not follow redirects since we want to test the Location header
- // first go to the route WITHOUT the base path, this will just add the base path
+ // Both requests (with or without basepath) should redirect directly to the final destination.
+ // The router is smart enough to skip the intermediate "add basepath" redirect and go
+ // straight to where the route's beforeLoad redirect points to.
await page.request
.get('/redirect/throw-it', { maxRedirects: 0 })
.then((res) => {
const headers = new Headers(res.headers())
- expect(headers.get('location')).toBe('/custom/basepath/redirect/throw-it')
+ expect(headers.get('location')).toBe('/custom/basepath/posts/1')
})
- // now go to the route WITH the base path, this will redirect to the final destination
await page.request
.get('/custom/basepath/redirect/throw-it', { maxRedirects: 0 })
.then((res) => {
diff --git a/e2e/react-start/i18n-paraglide/.gitignore b/e2e/react-start/i18n-paraglide/.gitignore
new file mode 100644
index 00000000000..eb0412b5f5b
--- /dev/null
+++ b/e2e/react-start/i18n-paraglide/.gitignore
@@ -0,0 +1,10 @@
+node_modules
+.DS_Store
+dist
+dist-ssr
+*.local
+count.txt
+.env
+.nitro
+.tanstack
+.output
diff --git a/e2e/react-start/i18n-paraglide/.prettierignore b/e2e/react-start/i18n-paraglide/.prettierignore
new file mode 100644
index 00000000000..2be5eaa6ece
--- /dev/null
+++ b/e2e/react-start/i18n-paraglide/.prettierignore
@@ -0,0 +1,4 @@
+**/build
+**/public
+pnpm-lock.yaml
+routeTree.gen.ts
\ No newline at end of file
diff --git a/e2e/react-start/i18n-paraglide/.vscode/extensions.json b/e2e/react-start/i18n-paraglide/.vscode/extensions.json
new file mode 100644
index 00000000000..8cf06c2f61a
--- /dev/null
+++ b/e2e/react-start/i18n-paraglide/.vscode/extensions.json
@@ -0,0 +1,3 @@
+{
+ "recommendations": ["inlang.vs-code-extension"]
+}
diff --git a/e2e/react-start/i18n-paraglide/.vscode/settings.json b/e2e/react-start/i18n-paraglide/.vscode/settings.json
new file mode 100644
index 00000000000..00b5278e580
--- /dev/null
+++ b/e2e/react-start/i18n-paraglide/.vscode/settings.json
@@ -0,0 +1,11 @@
+{
+ "files.watcherExclude": {
+ "**/routeTree.gen.ts": true
+ },
+ "search.exclude": {
+ "**/routeTree.gen.ts": true
+ },
+ "files.readonlyInclude": {
+ "**/routeTree.gen.ts": true
+ }
+}
diff --git a/e2e/react-start/i18n-paraglide/messages/de.json b/e2e/react-start/i18n-paraglide/messages/de.json
new file mode 100644
index 00000000000..2c2167c3ca0
--- /dev/null
+++ b/e2e/react-start/i18n-paraglide/messages/de.json
@@ -0,0 +1,8 @@
+{
+ "$schema": "https://inlang.com/schema/inlang-message-format",
+ "example_message": "Guten Tag {username}",
+ "server_message": "Server Nachricht {emoji}",
+ "about_message": "Über uns",
+ "home_page": "Startseite",
+ "about_page": "Über uns"
+}
diff --git a/e2e/react-start/i18n-paraglide/messages/en.json b/e2e/react-start/i18n-paraglide/messages/en.json
new file mode 100644
index 00000000000..204789de75c
--- /dev/null
+++ b/e2e/react-start/i18n-paraglide/messages/en.json
@@ -0,0 +1,8 @@
+{
+ "$schema": "https://inlang.com/schema/inlang-message-format",
+ "example_message": "Hello world {username}",
+ "server_message": "Server message {emoji}",
+ "about_message": "About message",
+ "home_page": "Home page",
+ "about_page": "About page"
+}
diff --git a/e2e/react-start/i18n-paraglide/package.json b/e2e/react-start/i18n-paraglide/package.json
new file mode 100644
index 00000000000..8124319abc2
--- /dev/null
+++ b/e2e/react-start/i18n-paraglide/package.json
@@ -0,0 +1,34 @@
+{
+ "name": "tanstack-react-start-e2e-i18n-paraglide",
+ "private": true,
+ "sideEffects": false,
+ "type": "module",
+ "scripts": {
+ "dev": "vite dev --port 3000",
+ "start": "pnpx srvx --prod -s ../client dist/server/server.js",
+ "build": "vite build && tsc --noEmit",
+ "preview": "vite preview",
+ "test:e2e": "rm -rf port*.txt; playwright test --project=chromium"
+ },
+ "dependencies": {
+ "@tanstack/react-router": "workspace:^",
+ "@tanstack/react-router-devtools": "workspace:^",
+ "@tanstack/react-start": "workspace:^",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0"
+ },
+ "devDependencies": {
+ "@inlang/paraglide-js": "^2.4.0",
+ "@playwright/test": "^1.50.1",
+ "@tailwindcss/vite": "^4.1.18",
+ "@tanstack/router-e2e-utils": "workspace:^",
+ "@types/node": "^22.10.2",
+ "@types/react": "^19.0.8",
+ "@types/react-dom": "^19.0.3",
+ "@vitejs/plugin-react": "^4.3.4",
+ "tailwindcss": "^4.1.18",
+ "typescript": "^5.7.2",
+ "vite": "^7.1.7",
+ "vite-tsconfig-paths": "^5.1.4"
+ }
+}
diff --git a/e2e/react-start/i18n-paraglide/playwright.config.ts b/e2e/react-start/i18n-paraglide/playwright.config.ts
new file mode 100644
index 00000000000..38690f09d8b
--- /dev/null
+++ b/e2e/react-start/i18n-paraglide/playwright.config.ts
@@ -0,0 +1,35 @@
+import { defineConfig, devices } from '@playwright/test'
+import { getTestServerPort } from '@tanstack/router-e2e-utils'
+import packageJson from './package.json' with { type: 'json' }
+
+const PORT = await getTestServerPort(packageJson.name)
+const baseURL = `http://localhost:${PORT}`
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+ testDir: './tests',
+ workers: 1,
+
+ reporter: [['line']],
+
+ use: {
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ baseURL,
+ },
+
+ webServer: {
+ command: `VITE_SERVER_PORT=${PORT} PORT=${PORT} pnpm build && VITE_SERVER_PORT=${PORT} PORT=${PORT} pnpm start`,
+ url: baseURL,
+ reuseExistingServer: !process.env.CI,
+ stdout: 'pipe',
+ },
+
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+ ],
+})
diff --git a/e2e/react-start/i18n-paraglide/project.inlang/.gitignore b/e2e/react-start/i18n-paraglide/project.inlang/.gitignore
new file mode 100644
index 00000000000..5e465967597
--- /dev/null
+++ b/e2e/react-start/i18n-paraglide/project.inlang/.gitignore
@@ -0,0 +1 @@
+cache
\ No newline at end of file
diff --git a/e2e/react-start/i18n-paraglide/project.inlang/project_id b/e2e/react-start/i18n-paraglide/project.inlang/project_id
new file mode 100644
index 00000000000..a956b223fac
--- /dev/null
+++ b/e2e/react-start/i18n-paraglide/project.inlang/project_id
@@ -0,0 +1 @@
+UoZ15Q8qSGIbImRS3Y
\ No newline at end of file
diff --git a/e2e/react-start/i18n-paraglide/project.inlang/settings.json b/e2e/react-start/i18n-paraglide/project.inlang/settings.json
new file mode 100644
index 00000000000..9bdce4c8cc9
--- /dev/null
+++ b/e2e/react-start/i18n-paraglide/project.inlang/settings.json
@@ -0,0 +1,12 @@
+{
+ "$schema": "https://inlang.com/schema/project-settings",
+ "baseLocale": "en",
+ "locales": ["en", "de"],
+ "modules": [
+ "https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js",
+ "https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@2/dist/index.js"
+ ],
+ "plugin.inlang.messageFormat": {
+ "pathPattern": "./messages/{locale}.json"
+ }
+}
diff --git a/e2e/react-start/i18n-paraglide/public/favicon.ico b/e2e/react-start/i18n-paraglide/public/favicon.ico
new file mode 100644
index 00000000000..a11777cc471
Binary files /dev/null and b/e2e/react-start/i18n-paraglide/public/favicon.ico differ
diff --git a/e2e/react-start/i18n-paraglide/public/logo192.png b/e2e/react-start/i18n-paraglide/public/logo192.png
new file mode 100644
index 00000000000..fc44b0a3796
Binary files /dev/null and b/e2e/react-start/i18n-paraglide/public/logo192.png differ
diff --git a/e2e/react-start/i18n-paraglide/public/logo512.png b/e2e/react-start/i18n-paraglide/public/logo512.png
new file mode 100644
index 00000000000..a4e47a6545b
Binary files /dev/null and b/e2e/react-start/i18n-paraglide/public/logo512.png differ
diff --git a/e2e/react-start/i18n-paraglide/public/manifest.json b/e2e/react-start/i18n-paraglide/public/manifest.json
new file mode 100644
index 00000000000..078ef501162
--- /dev/null
+++ b/e2e/react-start/i18n-paraglide/public/manifest.json
@@ -0,0 +1,25 @@
+{
+ "short_name": "TanStack App",
+ "name": "Create TanStack App Sample",
+ "icons": [
+ {
+ "src": "favicon.ico",
+ "sizes": "64x64 32x32 24x24 16x16",
+ "type": "image/x-icon"
+ },
+ {
+ "src": "logo192.png",
+ "type": "image/png",
+ "sizes": "192x192"
+ },
+ {
+ "src": "logo512.png",
+ "type": "image/png",
+ "sizes": "512x512"
+ }
+ ],
+ "start_url": ".",
+ "display": "standalone",
+ "theme_color": "#000000",
+ "background_color": "#ffffff"
+}
diff --git a/e2e/react-start/i18n-paraglide/public/robots.txt b/e2e/react-start/i18n-paraglide/public/robots.txt
new file mode 100644
index 00000000000..e9e57dc4d41
--- /dev/null
+++ b/e2e/react-start/i18n-paraglide/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/e2e/react-start/i18n-paraglide/src/logo.svg b/e2e/react-start/i18n-paraglide/src/logo.svg
new file mode 100644
index 00000000000..fe53fe8d0d2
--- /dev/null
+++ b/e2e/react-start/i18n-paraglide/src/logo.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/e2e/react-start/i18n-paraglide/src/routeTree.gen.ts b/e2e/react-start/i18n-paraglide/src/routeTree.gen.ts
new file mode 100644
index 00000000000..421daf2790a
--- /dev/null
+++ b/e2e/react-start/i18n-paraglide/src/routeTree.gen.ts
@@ -0,0 +1,86 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+import { Route as rootRouteImport } from './routes/__root'
+import { Route as AboutRouteImport } from './routes/about'
+import { Route as IndexRouteImport } from './routes/index'
+
+const AboutRoute = AboutRouteImport.update({
+ id: '/about',
+ path: '/about',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '/about': typeof AboutRoute
+}
+export interface FileRoutesByTo {
+ '/': typeof IndexRoute
+ '/about': typeof AboutRoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/': typeof IndexRoute
+ '/about': typeof AboutRoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths: '/' | '/about'
+ fileRoutesByTo: FileRoutesByTo
+ to: '/' | '/about'
+ id: '__root__' | '/' | '/about'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
+ AboutRoute: typeof AboutRoute
+}
+
+declare module '@tanstack/react-router' {
+ interface FileRoutesByPath {
+ '/about': {
+ id: '/about'
+ path: '/about'
+ fullPath: '/about'
+ preLoaderRoute: typeof AboutRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ }
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
+ AboutRoute: AboutRoute,
+}
+export const routeTree = rootRouteImport
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
+
+import type { getRouter } from './router.tsx'
+import type { createStart } from '@tanstack/react-start'
+declare module '@tanstack/react-start' {
+ interface Register {
+ ssr: true
+ router: Awaited>
+ }
+}
diff --git a/e2e/react-start/i18n-paraglide/src/router.tsx b/e2e/react-start/i18n-paraglide/src/router.tsx
new file mode 100644
index 00000000000..3a8413a9645
--- /dev/null
+++ b/e2e/react-start/i18n-paraglide/src/router.tsx
@@ -0,0 +1,18 @@
+import { createRouter } from '@tanstack/react-router'
+
+// Import the generated route tree
+import { routeTree } from './routeTree.gen'
+import { deLocalizeUrl, localizeUrl } from './paraglide/runtime'
+
+// Create a new router instance
+export const getRouter = () => {
+ return createRouter({
+ routeTree,
+ scrollRestoration: true,
+ defaultPreloadStaleTime: 0,
+ rewrite: {
+ input: ({ url }) => deLocalizeUrl(url),
+ output: ({ url }) => localizeUrl(url),
+ },
+ })
+}
diff --git a/e2e/react-start/i18n-paraglide/src/routes/__root.tsx b/e2e/react-start/i18n-paraglide/src/routes/__root.tsx
new file mode 100644
index 00000000000..c5c84d708a2
--- /dev/null
+++ b/e2e/react-start/i18n-paraglide/src/routes/__root.tsx
@@ -0,0 +1,85 @@
+import {
+ HeadContent,
+ Link,
+ Outlet,
+ Scripts,
+ createRootRoute,
+} from '@tanstack/react-router'
+import styles from '../styles.css?url'
+import { getLocale, locales, setLocale } from '@/paraglide/runtime'
+import { m } from '@/paraglide/messages'
+
+export const Route = createRootRoute({
+ head: () => ({
+ meta: [
+ {
+ charSet: 'utf-8',
+ },
+ {
+ name: 'viewport',
+ content: 'width=device-width, initial-scale=1',
+ },
+ {
+ title: 'TanStack Start Starter',
+ },
+ ],
+ links: [{ rel: 'stylesheet', href: styles }],
+ }),
+
+ component: RootComponent,
+})
+
+function RootComponent() {
+ return (
+
+
+
+
+
+
+
+
+ {m.home_page()}
+
+
+
+ {m.about_page()}
+
+
+
+
+ {locales.map((locale) => (
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/e2e/react-start/i18n-paraglide/src/routes/about.tsx b/e2e/react-start/i18n-paraglide/src/routes/about.tsx
new file mode 100644
index 00000000000..0ee95a577a2
--- /dev/null
+++ b/e2e/react-start/i18n-paraglide/src/routes/about.tsx
@@ -0,0 +1,10 @@
+import { createFileRoute } from '@tanstack/react-router'
+import { m } from '@/paraglide/messages'
+
+export const Route = createFileRoute('/about')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return {m.about_message()}
+}
diff --git a/e2e/react-start/i18n-paraglide/src/routes/index.tsx b/e2e/react-start/i18n-paraglide/src/routes/index.tsx
new file mode 100644
index 00000000000..d86af585cb4
--- /dev/null
+++ b/e2e/react-start/i18n-paraglide/src/routes/index.tsx
@@ -0,0 +1,30 @@
+import { createFileRoute } from '@tanstack/react-router'
+import { m } from '@/paraglide/messages.js'
+import { createServerFn } from '@tanstack/react-start'
+
+const getServerMessage = createServerFn()
+ .inputValidator((emoji: string) => emoji)
+ .handler((ctx) => {
+ return m.server_message({ emoji: ctx.data })
+ })
+
+export const Route = createFileRoute('/')({
+ component: Home,
+ loader: async () => {
+ return {
+ messageFromLoader: m.example_message({ username: 'John Doe' }),
+ serverFunctionMessage: await getServerMessage({ data: '📩' }),
+ }
+ },
+})
+
+function Home() {
+ const { serverFunctionMessage, messageFromLoader } = Route.useLoaderData()
+ return (
+
+
Message from loader: {messageFromLoader}
+ Server function message: {serverFunctionMessage}:
+ {m.example_message({ username: 'John Doe' })}
+
+ )
+}
diff --git a/e2e/react-start/i18n-paraglide/src/server.ts b/e2e/react-start/i18n-paraglide/src/server.ts
new file mode 100644
index 00000000000..9542b01d4ac
--- /dev/null
+++ b/e2e/react-start/i18n-paraglide/src/server.ts
@@ -0,0 +1,8 @@
+import { paraglideMiddleware } from './paraglide/server.js'
+import handler from '@tanstack/react-start/server-entry'
+
+export default {
+ fetch(req: Request): Promise {
+ return paraglideMiddleware(req, ({ request }) => handler.fetch(request))
+ },
+}
diff --git a/e2e/react-start/i18n-paraglide/src/styles.css b/e2e/react-start/i18n-paraglide/src/styles.css
new file mode 100644
index 00000000000..d4b5078586e
--- /dev/null
+++ b/e2e/react-start/i18n-paraglide/src/styles.css
@@ -0,0 +1 @@
+@import 'tailwindcss';
diff --git a/e2e/react-start/i18n-paraglide/src/utils/prerender.ts b/e2e/react-start/i18n-paraglide/src/utils/prerender.ts
new file mode 100644
index 00000000000..0cb1630595f
--- /dev/null
+++ b/e2e/react-start/i18n-paraglide/src/utils/prerender.ts
@@ -0,0 +1,8 @@
+import { localizeHref } from '../paraglide/runtime'
+
+export const prerenderRoutes = ['/', '/about'].map((path) => ({
+ path: localizeHref(path),
+ prerender: {
+ enabled: true,
+ },
+}))
diff --git a/e2e/react-start/i18n-paraglide/src/utils/seo.ts b/e2e/react-start/i18n-paraglide/src/utils/seo.ts
new file mode 100644
index 00000000000..d18ad84b74e
--- /dev/null
+++ b/e2e/react-start/i18n-paraglide/src/utils/seo.ts
@@ -0,0 +1,33 @@
+export const seo = ({
+ title,
+ description,
+ keywords,
+ image,
+}: {
+ title: string
+ description?: string
+ image?: string
+ keywords?: string
+}) => {
+ const tags = [
+ { title },
+ { name: 'description', content: description },
+ { name: 'keywords', content: keywords },
+ { name: 'twitter:title', content: title },
+ { name: 'twitter:description', content: description },
+ { name: 'twitter:creator', content: '@tannerlinsley' },
+ { name: 'twitter:site', content: '@tannerlinsley' },
+ { name: 'og:type', content: 'website' },
+ { name: 'og:title', content: title },
+ { name: 'og:description', content: description },
+ ...(image
+ ? [
+ { name: 'twitter:image', content: image },
+ { name: 'twitter:card', content: 'summary_large_image' },
+ { name: 'og:image', content: image },
+ ]
+ : []),
+ ]
+
+ return tags
+}
diff --git a/e2e/react-start/i18n-paraglide/src/utils/translated-pathnames.ts b/e2e/react-start/i18n-paraglide/src/utils/translated-pathnames.ts
new file mode 100644
index 00000000000..bb8649ff4e0
--- /dev/null
+++ b/e2e/react-start/i18n-paraglide/src/utils/translated-pathnames.ts
@@ -0,0 +1,56 @@
+import { Locale } from '@/paraglide/runtime'
+import { FileRoutesByTo } from '../routeTree.gen'
+
+type RoutePath = keyof FileRoutesByTo
+
+const excludedPaths = ['admin', 'docs', 'api'] as const
+
+type PublicRoutePath = Exclude<
+ RoutePath,
+ `${string}${(typeof excludedPaths)[number]}${string}`
+>
+
+type TranslatedPathname = {
+ pattern: string
+ localized: Array<[Locale, string]>
+}
+
+function toUrlPattern(path: string) {
+ return (
+ path
+ // catch-all
+ .replace(/\/\$$/, '/:path(.*)?')
+ // optional parameters: {-$param}
+ .replace(/\{-\$([a-zA-Z0-9_]+)\}/g, ':$1?')
+ // named parameters: $param
+ .replace(/\$([a-zA-Z0-9_]+)/g, ':$1')
+ // remove trailing slash
+ .replace(/\/+$/, '')
+ )
+}
+
+function createTranslatedPathnames(
+ input: Record>,
+): TranslatedPathname[] {
+ return Object.entries(input).map(([pattern, locales]) => ({
+ pattern: toUrlPattern(pattern),
+ localized: Object.entries(locales).map(
+ ([locale, path]) =>
+ [locale as Locale, `/${locale}${toUrlPattern(path)}`] satisfies [
+ Locale,
+ string,
+ ],
+ ),
+ }))
+}
+
+export const translatedPathnames = createTranslatedPathnames({
+ '/': {
+ en: '/',
+ de: '/',
+ },
+ '/about': {
+ en: '/about',
+ de: '/ueber',
+ },
+})
diff --git a/e2e/react-start/i18n-paraglide/tests/navigation.spec.ts b/e2e/react-start/i18n-paraglide/tests/navigation.spec.ts
new file mode 100644
index 00000000000..ebb18900499
--- /dev/null
+++ b/e2e/react-start/i18n-paraglide/tests/navigation.spec.ts
@@ -0,0 +1,101 @@
+import { expect, test } from '@playwright/test'
+
+test('should not cause redirect loops when accessing the home page', async ({
+ page,
+}) => {
+ // This test verifies that accessing the root URL does not cause a redirect loop
+ // The issue is that with i18n rewrites, the server may keep redirecting between
+ // / and /en (or similar locale prefixed paths) causing "too many redirects"
+
+ // Navigate to the root - should eventually land on a locale-prefixed page
+ await page.goto('/')
+ await page.waitForLoadState('networkidle')
+
+ // Verify the page loaded successfully and shows content
+ expect(await page.getByText('Hello world').first().isVisible()).toBe(true)
+})
+
+test('should not cause redirect loops when accessing locale-prefixed home page', async ({
+ page,
+}) => {
+ // Navigate to the English home page
+ await page.goto('/en')
+ await page.waitForLoadState('networkidle')
+
+ // Verify the page loaded successfully
+ expect(await page.getByText('Hello world').first().isVisible()).toBe(true)
+})
+
+test('should not cause redirect loops when accessing German home page', async ({
+ page,
+}) => {
+ // Navigate to the German home page
+ await page.goto('/de')
+ await page.waitForLoadState('networkidle')
+
+ // Verify the page loaded successfully - German message
+ expect(await page.getByText('Guten Tag').first().isVisible()).toBe(true)
+})
+
+test('should navigate between locales without redirect loops', async ({
+ page,
+}) => {
+ // Start at English page
+ await page.goto('/en')
+ await page.waitForLoadState('networkidle')
+
+ // Click the German locale button
+ await page.getByRole('button', { name: 'de' }).click()
+ await page.waitForLoadState('networkidle')
+
+ // Verify we're now on the German page
+ expect(page.url()).toContain('/de')
+ expect(await page.getByText('Guten Tag').first().isVisible()).toBe(true)
+})
+
+test('server-side navigation to about page should not cause redirect loops', async ({
+ page,
+}) => {
+ // Navigate directly to English about page
+ await page.goto('/en/about')
+ await page.waitForLoadState('networkidle')
+
+ // Verify the page loaded successfully
+ expect(await page.getByText('About message').isVisible()).toBe(true)
+})
+
+test('server-side navigation to German about page should not cause redirect loops', async ({
+ page,
+}) => {
+ // Navigate directly to German about page (translated path)
+ await page.goto('/de/ueber')
+ await page.waitForLoadState('networkidle')
+
+ // Verify the page loaded successfully by checking the URL
+ expect(page.url()).toContain('/de/ueber')
+
+ // Verify the page content loaded - check for the German nav link which is always present
+ // The about page also contains "Über uns" as the content
+ await expect(page.locator('a[href="/de/ueber"]')).toBeVisible()
+})
+
+test('check redirect behavior does not loop', async ({ page }) => {
+ // Test that requesting the root without locale prefix works with a single redirect
+ const response = await page.request.get('/', { maxRedirects: 0 })
+
+ // We expect either a 200 (if no redirect needed) or a redirect to a locale-prefixed path
+ const status = response.status()
+ if (status >= 300 && status < 400) {
+ const location = response.headers()['location']
+ // The redirect should be to a locale-prefixed path
+ expect(location).toMatch(/^\/(en|de)/)
+
+ // Following the redirect should not cause another redirect loop
+ const followUp = await page.request.get(location!, { maxRedirects: 0 })
+ // The follow-up should be a 200 or another valid response, not another redirect to the same location
+ expect(followUp.status()).toBeLessThan(400)
+ } else {
+ // If no redirect, the page should load successfully
+ expect(status).toBe(200)
+ }
+})
diff --git a/e2e/react-start/i18n-paraglide/tsconfig.json b/e2e/react-start/i18n-paraglide/tsconfig.json
new file mode 100644
index 00000000000..3e42c72626a
--- /dev/null
+++ b/e2e/react-start/i18n-paraglide/tsconfig.json
@@ -0,0 +1,29 @@
+{
+ "include": ["**/*.ts", "**/*.tsx"],
+ "compilerOptions": {
+ "target": "ES2022",
+ "jsx": "react-jsx",
+ "module": "ESNext",
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "types": ["vite/client"],
+ "allowJs": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": false,
+ "noEmit": true,
+
+ /* Linting */
+ "skipLibCheck": true,
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true,
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ }
+}
diff --git a/e2e/react-start/i18n-paraglide/vite.config.ts b/e2e/react-start/i18n-paraglide/vite.config.ts
new file mode 100644
index 00000000000..a0d8a6f77e1
--- /dev/null
+++ b/e2e/react-start/i18n-paraglide/vite.config.ts
@@ -0,0 +1,47 @@
+import { paraglideVitePlugin } from '@inlang/paraglide-js'
+import { defineConfig } from 'vite'
+import { tanstackStart } from '@tanstack/react-start/plugin/vite'
+import viteReact from '@vitejs/plugin-react'
+import viteTsConfigPaths from 'vite-tsconfig-paths'
+import tailwindcss from '@tailwindcss/vite'
+
+const config = defineConfig({
+ plugins: [
+ paraglideVitePlugin({
+ project: './project.inlang',
+ outdir: './src/paraglide',
+ outputStructure: 'message-modules',
+ cookieName: 'PARAGLIDE_LOCALE',
+ strategy: ['url', 'cookie', 'preferredLanguage', 'baseLocale'],
+ urlPatterns: [
+ {
+ pattern: '/',
+ localized: [
+ ['en', '/en'],
+ ['de', '/de'],
+ ],
+ },
+ {
+ pattern: '/about',
+ localized: [
+ ['en', '/en/about'],
+ ['de', '/de/ueber'],
+ ],
+ },
+ {
+ pattern: '/:path(.*)?',
+ localized: [
+ ['en', '/en/:path(.*)?'],
+ ['de', '/de/:path(.*)?'],
+ ],
+ },
+ ],
+ }),
+ viteTsConfigPaths(),
+ tanstackStart(),
+ viteReact(),
+ tailwindcss(),
+ ],
+})
+
+export default config
diff --git a/e2e/solid-start/custom-basepath/tests/navigation.spec.ts b/e2e/solid-start/custom-basepath/tests/navigation.spec.ts
index e66f14be9e2..e238134d2ed 100644
--- a/e2e/solid-start/custom-basepath/tests/navigation.spec.ts
+++ b/e2e/solid-start/custom-basepath/tests/navigation.spec.ts
@@ -61,14 +61,15 @@ test('server-side redirect', async ({ page, baseURL }) => {
expect(page.url()).toBe(`${baseURL}/posts/1`)
// do not follow redirects since we want to test the Location header
- // first go to the route WITHOUT the base path, this will just add the base path
+ // Both requests (with or without basepath) should redirect directly to the final destination.
+ // The router is smart enough to skip the intermediate "add basepath" redirect and go
+ // straight to where the route's beforeLoad redirect points to.
await page.request
.get('/redirect/throw-it', { maxRedirects: 0 })
.then((res) => {
const headers = new Headers(res.headers())
- expect(headers.get('location')).toBe('/custom/basepath/redirect/throw-it')
+ expect(headers.get('location')).toBe('/custom/basepath/posts/1')
})
- // now go to the route WITH the base path, this will redirect to the final destination
await page.request
.get('/custom/basepath/redirect/throw-it', { maxRedirects: 0 })
.then((res) => {
diff --git a/e2e/vue-start/custom-basepath/tests/navigation.spec.ts b/e2e/vue-start/custom-basepath/tests/navigation.spec.ts
index e66f14be9e2..e238134d2ed 100644
--- a/e2e/vue-start/custom-basepath/tests/navigation.spec.ts
+++ b/e2e/vue-start/custom-basepath/tests/navigation.spec.ts
@@ -61,14 +61,15 @@ test('server-side redirect', async ({ page, baseURL }) => {
expect(page.url()).toBe(`${baseURL}/posts/1`)
// do not follow redirects since we want to test the Location header
- // first go to the route WITHOUT the base path, this will just add the base path
+ // Both requests (with or without basepath) should redirect directly to the final destination.
+ // The router is smart enough to skip the intermediate "add basepath" redirect and go
+ // straight to where the route's beforeLoad redirect points to.
await page.request
.get('/redirect/throw-it', { maxRedirects: 0 })
.then((res) => {
const headers = new Headers(res.headers())
- expect(headers.get('location')).toBe('/custom/basepath/redirect/throw-it')
+ expect(headers.get('location')).toBe('/custom/basepath/posts/1')
})
- // now go to the route WITH the base path, this will redirect to the final destination
await page.request
.get('/custom/basepath/redirect/throw-it', { maxRedirects: 0 })
.then((res) => {
diff --git a/packages/react-router/src/Transitioner.tsx b/packages/react-router/src/Transitioner.tsx
index f547cdd5f58..83fb46cfe0a 100644
--- a/packages/react-router/src/Transitioner.tsx
+++ b/packages/react-router/src/Transitioner.tsx
@@ -52,9 +52,12 @@ export function Transitioner() {
_includeValidateSearch: true,
})
+ // Check if the current URL matches the canonical form.
+ // Compare publicHref (browser-facing URL) for consistency with
+ // the server-side redirect check in router.beforeLoad.
if (
- trimPathRight(router.latestLocation.href) !==
- trimPathRight(nextLocation.href)
+ trimPathRight(router.latestLocation.publicHref) !==
+ trimPathRight(nextLocation.publicHref)
) {
router.commitLocation({ ...nextLocation, replace: true })
}
diff --git a/packages/react-router/tests/router.test.tsx b/packages/react-router/tests/router.test.tsx
index 1228f837118..4708552d869 100644
--- a/packages/react-router/tests/router.test.tsx
+++ b/packages/react-router/tests/router.test.tsx
@@ -3124,6 +3124,247 @@ describe('Router rewrite functionality', () => {
expect(history.location.pathname).toBe('/user')
})
+
+ it('should not cause redirect loops with i18n locale prefix rewriting', async () => {
+ // This test simulates an i18n middleware that:
+ // - Input: strips locale prefix (e.g., /en/home -> /home)
+ // - Output: adds locale prefix back (e.g., /home -> /en/home)
+
+ const rootRoute = createRootRoute({
+ component: () => ,
+ })
+
+ const homeRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/home',
+ component: () => Home
,
+ })
+
+ const routeTree = rootRoute.addChildren([homeRoute])
+
+ // The history starts at the public-facing locale-prefixed URL.
+ // The input rewrite strips the locale prefix for internal routing.
+ const history = createMemoryHistory({ initialEntries: ['/en/home'] })
+
+ const router = createRouter({
+ routeTree,
+ history,
+ rewrite: {
+ input: ({ url }) => {
+ // Strip locale prefix: /en/home -> /home
+ if (url.pathname.startsWith('/en')) {
+ url.pathname = url.pathname.replace(/^\/en/, '')
+ }
+ return url
+ },
+ },
+ })
+
+ render()
+
+ await waitFor(() => {
+ expect(screen.getByTestId('home')).toBeInTheDocument()
+ })
+
+ // The internal pathname should be /home (after input rewrite strips /en)
+ expect(router.state.location.pathname).toBe('/home')
+
+ // The publicHref should include the locale prefix (via output rewrite)
+ // Since we only have input rewrite here, publicHref equals the internal href
+ expect(router.state.location.publicHref).toBe('/home')
+ })
+
+ it('should handle i18n rewriting with navigation between localized routes', async () => {
+ // Tests navigation between routes with i18n locale prefix rewriting
+
+ const rootRoute = createRootRoute({
+ component: () => ,
+ })
+
+ const homeRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: () => (
+
+ Home
+
+ About
+
+
+ ),
+ })
+
+ const aboutRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/about',
+ component: () => About
,
+ })
+
+ const routeTree = rootRoute.addChildren([homeRoute, aboutRoute])
+
+ // Start at the public-facing locale-prefixed URL
+ const history = createMemoryHistory({ initialEntries: ['/en'] })
+
+ const router = createRouter({
+ routeTree,
+ history,
+ rewrite: {
+ input: ({ url }) => {
+ // Strip locale prefix
+ if (url.pathname.startsWith('/en')) {
+ url.pathname = url.pathname.replace(/^\/en/, '') || '/'
+ return url
+ }
+ return url
+ },
+ output: ({ url }) => {
+ // Add locale prefix
+ if (!url.pathname.startsWith('/en')) {
+ url.pathname = `/en${url.pathname === '/' ? '' : url.pathname}`
+ return url
+ }
+ return url
+ },
+ },
+ })
+
+ render()
+
+ await waitFor(() => {
+ expect(screen.getByTestId('home')).toBeInTheDocument()
+ })
+
+ // Click the about link
+ const aboutLink = screen.getByTestId('about-link')
+ fireEvent.click(aboutLink)
+
+ await waitFor(() => {
+ expect(screen.getByTestId('about')).toBeInTheDocument()
+ })
+
+ // Internal pathname should be /about
+ expect(router.state.location.pathname).toBe('/about')
+
+ // Public href should be /en/about
+ expect(router.state.location.publicHref).toBe('/en/about')
+
+ // History should show the public-facing path
+ expect(history.location.pathname).toBe('/en/about')
+ })
+
+ it('should handle i18n rewriting with direct navigation to localized URL', async () => {
+ // Tests that navigating directly to a locale-prefixed URL works correctly
+
+ const rootRoute = createRootRoute({
+ component: () => ,
+ })
+
+ const aboutRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/about',
+ component: () => About
,
+ })
+
+ const routeTree = rootRoute.addChildren([aboutRoute])
+
+ // Start at German locale-prefixed URL
+ const history = createMemoryHistory({ initialEntries: ['/de/about'] })
+
+ const router = createRouter({
+ routeTree,
+ history,
+ rewrite: {
+ input: ({ url }) => {
+ // Strip any locale prefix
+ const match = url.pathname.match(/^\/(en|de)(.*)$/)
+ if (match) {
+ url.pathname = match[2] || '/'
+ return url
+ }
+ return url
+ },
+ output: ({ url }) => {
+ // Default to German locale
+ if (!url.pathname.match(/^\/(en|de)/)) {
+ url.pathname = `/de${url.pathname === '/' ? '' : url.pathname}`
+ return url
+ }
+ return url
+ },
+ },
+ })
+
+ render()
+
+ await waitFor(() => {
+ expect(screen.getByTestId('about')).toBeInTheDocument()
+ })
+
+ // Internal pathname should be /about (de-localized)
+ expect(router.state.location.pathname).toBe('/about')
+
+ // Public href should include locale
+ expect(router.state.location.publicHref).toBe('/de/about')
+ })
+
+ it('should maintain consistent publicHref between parseLocation and buildLocation', async () => {
+ // This test specifically verifies the fix for the redirect loop bug:
+ // parseLocation and buildLocation must compute the same publicHref
+ // for the same logical location.
+
+ const rootRoute = createRootRoute({
+ component: () => ,
+ })
+
+ const homeRoute = createRoute({
+ getParentRoute: () => rootRoute,
+ path: '/',
+ component: () => Home
,
+ })
+
+ const routeTree = rootRoute.addChildren([homeRoute])
+
+ // Start at the locale-prefixed URL
+ const history = createMemoryHistory({ initialEntries: ['/fr'] })
+
+ const router = createRouter({
+ routeTree,
+ history,
+ rewrite: {
+ input: ({ url }) => {
+ // De-localize: /fr -> /
+ if (url.pathname.startsWith('/fr')) {
+ url.pathname = url.pathname.replace(/^\/fr/, '') || '/'
+ }
+ return url
+ },
+ output: ({ url }) => {
+ // Re-localize: / -> /fr
+ if (!url.pathname.startsWith('/fr')) {
+ url.pathname = `/fr${url.pathname === '/' ? '' : url.pathname}`
+ }
+ return url
+ },
+ },
+ })
+
+ render()
+
+ await waitFor(() => {
+ expect(screen.getByTestId('home')).toBeInTheDocument()
+ })
+
+ // Get the current location's publicHref (computed by parseLocation)
+ const parsedPublicHref = router.state.location.publicHref
+
+ // Build a location to the same path and check its publicHref
+ const builtLocation = router.buildLocation({ to: '/' })
+
+ // These must match - if they don't, the router will think it needs
+ // to redirect, causing an infinite loop
+ expect(parsedPublicHref).toBe(builtLocation.publicHref)
+ expect(parsedPublicHref).toBe('/fr')
+ })
})
describe('basepath', () => {
diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts
index b0748bc2c0c..eb846b0afa1 100644
--- a/packages/router-core/src/router.ts
+++ b/packages/router-core/src/router.ts
@@ -1177,6 +1177,7 @@ export class RouterCore<
// Before we do any processing, we need to allow rewrites to modify the URL
// build up the full URL by combining the href from history with the router's origin
const fullUrl = new URL(href, this.origin)
+
const url = executeRewriteInput(this.rewrite, fullUrl)
const parsedSearch = this.options.parseSearch(url.search)
@@ -1187,11 +1188,33 @@ export class RouterCore<
const fullPath = url.href.replace(url.origin, '')
+ // Save the internal pathname for route matching (before output rewrite)
+ const internalPathname = url.pathname
+
+ // Compute publicHref by applying the output rewrite.
+ //
+ // The publicHref represents the URL as it should appear in the browser.
+ // This must match what buildLocation computes for the same logical route,
+ // otherwise the server-side redirect check will see a mismatch and trigger
+ // an infinite redirect loop.
+ //
+ // We always apply the output rewrite (not conditionally) because the
+ // incoming URL may have already been transformed by external middleware
+ // before reaching the router. In that case, the input rewrite has nothing
+ // to do, but we still need the output rewrite to reconstruct the correct
+ // public-facing URL.
+ //
+ // Clone the URL to avoid mutating the one used for route matching.
+ const urlForOutput = new URL(url.href)
+ const rewrittenUrl = executeRewriteOutput(this.rewrite, urlForOutput)
+ const publicHref =
+ rewrittenUrl.pathname + rewrittenUrl.search + rewrittenUrl.hash
+
return {
href: fullPath,
- publicHref: href,
+ publicHref,
url: url,
- pathname: decodePath(url.pathname),
+ pathname: decodePath(internalPathname),
searchStr,
search: replaceEqualDeep(previousLocation?.search, parsedSearch) as any,
hash: url.hash.split('#').reverse()[0] ?? '',
diff --git a/packages/solid-router/src/Transitioner.tsx b/packages/solid-router/src/Transitioner.tsx
index 1708f1bcb8c..c9df0b20537 100644
--- a/packages/solid-router/src/Transitioner.tsx
+++ b/packages/solid-router/src/Transitioner.tsx
@@ -55,9 +55,12 @@ export function Transitioner() {
_includeValidateSearch: true,
})
+ // Check if the current URL matches the canonical form.
+ // Compare publicHref (browser-facing URL) for consistency with
+ // the server-side redirect check in router.beforeLoad.
if (
- trimPathRight(router.latestLocation.href) !==
- trimPathRight(nextLocation.href)
+ trimPathRight(router.latestLocation.publicHref) !==
+ trimPathRight(nextLocation.publicHref)
) {
router.commitLocation({ ...nextLocation, replace: true })
}
diff --git a/packages/vue-router/src/Transitioner.tsx b/packages/vue-router/src/Transitioner.tsx
index 743ac6f7b87..4b4c2316772 100644
--- a/packages/vue-router/src/Transitioner.tsx
+++ b/packages/vue-router/src/Transitioner.tsx
@@ -123,9 +123,12 @@ export function useTransitionerSetup() {
_includeValidateSearch: true,
})
+ // Check if the current URL matches the canonical form.
+ // Compare publicHref (browser-facing URL) for consistency with
+ // the server-side redirect check in router.beforeLoad.
if (
- trimPathRight(router.latestLocation.href) !==
- trimPathRight(nextLocation.href)
+ trimPathRight(router.latestLocation.publicHref) !==
+ trimPathRight(nextLocation.publicHref)
) {
router.commitLocation({ ...nextLocation, replace: true })
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ec29f2343b2..7f824556f1d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -726,6 +726,55 @@ importers:
specifier: ^7.1.7
version: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
+ e2e/react-router/i18n-paraglide:
+ dependencies:
+ '@tailwindcss/vite':
+ specifier: ^4.1.18
+ version: 4.1.18(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+ '@tanstack/react-router':
+ specifier: workspace:*
+ version: link:../../../packages/react-router
+ react:
+ specifier: ^19.2.0
+ version: 19.2.0
+ react-dom:
+ specifier: ^19.2.0
+ version: 19.2.0(react@19.2.0)
+ tailwindcss:
+ specifier: ^4.1.18
+ version: 4.1.18
+ devDependencies:
+ '@inlang/paraglide-js':
+ specifier: ^2.4.0
+ version: 2.4.0(babel-plugin-macros@3.1.0)
+ '@playwright/test':
+ specifier: ^1.56.1
+ version: 1.56.1
+ '@tanstack/router-e2e-utils':
+ specifier: workspace:^
+ version: link:../../e2e-utils
+ '@tanstack/router-plugin':
+ specifier: workspace:*
+ version: link:../../../packages/router-plugin
+ '@types/node':
+ specifier: 22.10.2
+ version: 22.10.2
+ '@types/react':
+ specifier: ^19.2.2
+ version: 19.2.2
+ '@types/react-dom':
+ specifier: ^19.2.2
+ version: 19.2.2(@types/react@19.2.2)
+ '@vitejs/plugin-react':
+ specifier: ^4.3.4
+ version: 4.7.0(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+ typescript:
+ specifier: ^5.7.2
+ version: 5.9.2
+ vite:
+ specifier: ^7.1.7
+ version: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
+
e2e/react-router/js-only-file-based:
dependencies:
'@tailwindcss/vite':
@@ -1538,6 +1587,61 @@ importers:
specifier: ^5.1.4
version: 5.1.4(typescript@5.8.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+ e2e/react-start/i18n-paraglide:
+ dependencies:
+ '@tanstack/react-router':
+ specifier: workspace:*
+ version: link:../../../packages/react-router
+ '@tanstack/react-router-devtools':
+ specifier: workspace:^
+ version: link:../../../packages/react-router-devtools
+ '@tanstack/react-start':
+ specifier: workspace:*
+ version: link:../../../packages/react-start
+ react:
+ specifier: ^19.2.0
+ version: 19.2.0
+ react-dom:
+ specifier: ^19.2.0
+ version: 19.2.0(react@19.2.0)
+ devDependencies:
+ '@inlang/paraglide-js':
+ specifier: ^2.4.0
+ version: 2.4.0(babel-plugin-macros@3.1.0)
+ '@playwright/test':
+ specifier: ^1.56.1
+ version: 1.56.1
+ '@tailwindcss/vite':
+ specifier: ^4.1.18
+ version: 4.1.18(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+ '@tanstack/router-e2e-utils':
+ specifier: workspace:^
+ version: link:../../e2e-utils
+ '@types/node':
+ specifier: 22.10.2
+ version: 22.10.2
+ '@types/react':
+ specifier: ^19.2.2
+ version: 19.2.2
+ '@types/react-dom':
+ specifier: ^19.2.2
+ version: 19.2.2(@types/react@19.2.2)
+ '@vitejs/plugin-react':
+ specifier: ^4.3.4
+ version: 4.7.0(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+ tailwindcss:
+ specifier: ^4.1.18
+ version: 4.1.18
+ typescript:
+ specifier: ^5.7.2
+ version: 5.9.2
+ vite:
+ specifier: ^7.1.7
+ version: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
+ vite-tsconfig-paths:
+ specifier: ^5.1.4
+ version: 5.1.4(typescript@5.9.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+
e2e/react-start/query-integration:
dependencies:
'@tanstack/react-query':
@@ -10167,7 +10271,7 @@ importers:
devDependencies:
'@netlify/vite-plugin-tanstack-start':
specifier: ^1.1.4
- version: 1.1.4(@tanstack/solid-start@packages+solid-start)(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+ version: 1.1.4(@tanstack/solid-start@packages+solid-start)(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
'@tailwindcss/vite':
specifier: ^4.1.18
version: 4.1.18(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
@@ -26395,13 +26499,13 @@ snapshots:
uuid: 11.1.0
write-file-atomic: 5.0.1
- '@netlify/dev@4.6.3(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)(rollup@4.52.5)':
+ '@netlify/dev@4.6.3(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5)':
dependencies:
'@netlify/blobs': 10.1.0
'@netlify/config': 23.2.0
'@netlify/dev-utils': 4.3.0
'@netlify/edge-functions-dev': 1.0.0
- '@netlify/functions-dev': 1.0.0(rollup@4.52.5)
+ '@netlify/functions-dev': 1.0.0(encoding@0.1.13)(rollup@4.52.5)
'@netlify/headers': 2.1.0
'@netlify/images': 1.3.0(@netlify/blobs@10.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)
'@netlify/redirects': 3.1.0
@@ -26469,12 +26573,12 @@ snapshots:
dependencies:
'@netlify/types': 2.1.0
- '@netlify/functions-dev@1.0.0(rollup@4.52.5)':
+ '@netlify/functions-dev@1.0.0(encoding@0.1.13)(rollup@4.52.5)':
dependencies:
'@netlify/blobs': 10.1.0
'@netlify/dev-utils': 4.3.0
'@netlify/functions': 5.0.0
- '@netlify/zip-it-and-ship-it': 14.1.11(rollup@4.52.5)
+ '@netlify/zip-it-and-ship-it': 14.1.11(encoding@0.1.13)(rollup@4.52.5)
cron-parser: 4.9.0
decache: 4.6.2
extract-zip: 2.0.1
@@ -26564,9 +26668,9 @@ snapshots:
'@netlify/types@2.1.0': {}
- '@netlify/vite-plugin-tanstack-start@1.1.4(@tanstack/solid-start@packages+solid-start)(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))':
+ '@netlify/vite-plugin-tanstack-start@1.1.4(@tanstack/solid-start@packages+solid-start)(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))':
dependencies:
- '@netlify/vite-plugin': 2.7.4(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
+ '@netlify/vite-plugin': 2.7.4(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))
vite: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
optionalDependencies:
'@tanstack/solid-start': link:packages/solid-start
@@ -26594,9 +26698,9 @@ snapshots:
- supports-color
- uploadthing
- '@netlify/vite-plugin@2.7.4(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))':
+ '@netlify/vite-plugin@2.7.4(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))':
dependencies:
- '@netlify/dev': 4.6.3(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)(rollup@4.52.5)
+ '@netlify/dev': 4.6.3(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5)
'@netlify/dev-utils': 4.3.0
dedent: 1.7.0(babel-plugin-macros@3.1.0)
vite: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)
@@ -26624,13 +26728,13 @@ snapshots:
- supports-color
- uploadthing
- '@netlify/zip-it-and-ship-it@14.1.11(rollup@4.52.5)':
+ '@netlify/zip-it-and-ship-it@14.1.11(encoding@0.1.13)(rollup@4.52.5)':
dependencies:
'@babel/parser': 7.28.5
'@babel/types': 7.28.4
'@netlify/binary-info': 1.0.0
'@netlify/serverless-functions-api': 2.7.1
- '@vercel/nft': 0.29.4(rollup@4.52.5)
+ '@vercel/nft': 0.29.4(encoding@0.1.13)(rollup@4.52.5)
archiver: 7.0.1
common-path-prefix: 3.0.0
copy-file: 11.1.0
@@ -29731,7 +29835,7 @@ snapshots:
'@unrs/resolver-binding-win32-x64-msvc@1.11.1':
optional: true
- '@vercel/nft@0.29.4(rollup@4.52.5)':
+ '@vercel/nft@0.29.4(encoding@0.1.13)(rollup@4.52.5)':
dependencies:
'@mapbox/node-pre-gyp': 2.0.0(encoding@0.1.13)
'@rollup/pluginutils': 5.1.4(rollup@4.52.5)