diff --git a/apps/desktop/src/renderer/components/RendererRouter/RendererRouter.test.ts b/apps/desktop/src/renderer/components/RendererRouter/RendererRouter.test.ts
new file mode 100644
index 00000000000..3d0fdab53c7
--- /dev/null
+++ b/apps/desktop/src/renderer/components/RendererRouter/RendererRouter.test.ts
@@ -0,0 +1,10 @@
+import { expect, test } from "bun:test";
+
+test("root failures retain real translation context and report the original error", () => {
+ const result = Bun.spawnSync({
+ cmd: [process.execPath, `${import.meta.dir}/fixtures/route-errors.tsx`],
+ env: { ...process.env, NODE_ENV: "test" },
+ });
+ expect(result.exitCode, result.stderr.toString()).toBe(0);
+ expect(result.stdout.toString().trim()).toBe("passed");
+});
diff --git a/apps/desktop/src/renderer/components/RendererRouter/RendererRouter.tsx b/apps/desktop/src/renderer/components/RendererRouter/RendererRouter.tsx
new file mode 100644
index 00000000000..ff61d76f95c
--- /dev/null
+++ b/apps/desktop/src/renderer/components/RendererRouter/RendererRouter.tsx
@@ -0,0 +1,13 @@
+import { I18nProvider } from "@lingui/react";
+import { i18n, initI18n } from "@superset/i18n";
+import { type AnyRouter, RouterProvider } from "@tanstack/react-router";
+
+if (!i18n.locale) initI18n();
+
+export function RendererRouter({ router }: { router: AnyRouter }) {
+ return (
+
+
+
+ );
+}
diff --git a/apps/desktop/src/renderer/components/RendererRouter/fixtures/route-errors.tsx b/apps/desktop/src/renderer/components/RendererRouter/fixtures/route-errors.tsx
new file mode 100644
index 00000000000..1f09953c270
--- /dev/null
+++ b/apps/desktop/src/renderer/components/RendererRouter/fixtures/route-errors.tsx
@@ -0,0 +1,156 @@
+import { mock } from "bun:test";
+import { transformAsync } from "@babel/core";
+import { GlobalRegistrator } from "@happy-dom/global-registrator";
+import linguiMacro from "@lingui/babel-plugin-lingui-macro";
+import { plugin } from "bun";
+import type { ReactNode } from "react";
+
+plugin({
+ name: "real-lingui-react-macros",
+ setup(build) {
+ build.onLoad({ filter: /\/(error|not-found)\.tsx$/ }, async ({ path }) => {
+ if (path.includes("/node_modules/")) return;
+ const code = await Bun.file(path).text();
+ if (!/@lingui\/(react|core)\/macro/.test(code)) return;
+ const result = await transformAsync(code, {
+ filename: path,
+ babelrc: false,
+ configFile: false,
+ parserOpts: { plugins: ["typescript", "jsx"] },
+ plugins: [linguiMacro],
+ });
+ if (!result?.code) throw new Error(`Could not compile ${path}`);
+ return { contents: result.code, loader: "tsx" };
+ });
+ },
+});
+
+GlobalRegistrator.register({ url: "http://localhost" });
+(
+ globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }
+).IS_REACT_ACT_ENVIRONMENT = true;
+
+const captured: unknown[] = [];
+mock.module("@sentry/electron/renderer", () => ({
+ captureException: (error: unknown) => captured.push(error),
+}));
+const originalError = new Error("Original route failure");
+let failLayout = false;
+mock.module("../../../routes/-layout", () => ({
+ RootLayout: ({ children }: { children: ReactNode }) => {
+ if (failLayout) throw originalError;
+ return children;
+ },
+}));
+
+const { act, cleanup, fireEvent, render, waitFor } = await import(
+ "@testing-library/react"
+);
+const { createMemoryHistory, createRoute, createRouter } = await import(
+ "@tanstack/react-router"
+);
+const { QueryClient } = await import("@tanstack/react-query");
+const { i18n, initI18nAsync } = await import("@superset/i18n");
+const { RendererRouter } = await import("../RendererRouter");
+const { Route } = await import("../../../routes/__root");
+
+const home = createRoute({
+ getParentRoute: () => Route,
+ path: "/",
+ component: () =>
,
+});
+const broken = createRoute({
+ getParentRoute: () => Route,
+ path: "/broken",
+ loader: () => {
+ throw originalError;
+ },
+});
+const routeTree = Route.addChildren([home, broken]);
+const consoleErrors: unknown[][] = [];
+console.error = (...args: unknown[]) => consoleErrors.push(args);
+console.warn = () => {};
+
+function check(condition: unknown, message: string): asserts condition {
+ if (!condition) throw new Error(message);
+}
+
+try {
+ for (const failure of ["layout", "loader", "not-found"] as const) {
+ captured.length = 0;
+ consoleErrors.length = 0;
+ failLayout = failure === "layout";
+ await initI18nAsync("fr");
+ const router = createRouter({
+ routeTree,
+ history: createMemoryHistory({
+ initialEntries: [
+ failure === "loader"
+ ? "/broken"
+ : failure === "not-found"
+ ? "/missing"
+ : "/",
+ ],
+ }),
+ context: { queryClient: new QueryClient() },
+ });
+ await act(async () => {
+ render();
+ await router.load();
+ });
+ const heading =
+ failure === "not-found" ? "Page introuvable" : "Une erreur est survenue";
+ await waitFor(() =>
+ check(
+ document.body.textContent?.includes(heading),
+ `${failure}: missing translated fallback`,
+ ),
+ );
+ check(i18n.locale === "fr", `${failure}: fallback reset the active locale`);
+ if (failure !== "not-found") {
+ await waitFor(() =>
+ check(
+ captured.includes(originalError),
+ `${failure}: original error not reported`,
+ ),
+ );
+ await act(async () => {
+ fireEvent.click(
+ document.querySelector(
+ "button[aria-controls='error-details']",
+ ) as Element,
+ );
+ });
+ check(
+ document
+ .querySelector("pre")
+ ?.textContent?.includes(originalError.stack ?? originalError.message),
+ `${failure}: original error details missing`,
+ );
+ } else {
+ check(captured.length === 0, "404 was reported as a crash");
+ }
+ check(
+ !consoleErrors.some((args) =>
+ args.some((arg) => /useLingui|I18nProvider/.test(String(arg))),
+ ),
+ `${failure}: translation context failed`,
+ );
+ if (failure !== "layout") {
+ await act(async () => {
+ fireEvent.click(document.querySelector("a") as Element);
+ });
+ await waitFor(() =>
+ check(
+ document.querySelector('[data-testid="recovered-home"]'),
+ "Go home did not recover",
+ ),
+ );
+ }
+ cleanup();
+ }
+ console.log("passed");
+} finally {
+ cleanup();
+ await GlobalRegistrator.unregister();
+}
diff --git a/apps/desktop/src/renderer/components/RendererRouter/index.ts b/apps/desktop/src/renderer/components/RendererRouter/index.ts
new file mode 100644
index 00000000000..9581a37736f
--- /dev/null
+++ b/apps/desktop/src/renderer/components/RendererRouter/index.ts
@@ -0,0 +1 @@
+export { RendererRouter } from "./RendererRouter";
diff --git a/apps/desktop/src/renderer/index.tsx b/apps/desktop/src/renderer/index.tsx
index db09122b855..7fafa4a5124 100644
--- a/apps/desktop/src/renderer/index.tsx
+++ b/apps/desktop/src/renderer/index.tsx
@@ -2,9 +2,10 @@ import { initSentry } from "./lib/sentry";
initSentry();
-import { createRouter, RouterProvider } from "@tanstack/react-router";
+import { createRouter } from "@tanstack/react-router";
import ReactDom from "react-dom/client";
import { BootErrorBoundary } from "./components/BootErrorBoundary";
+import { RendererRouter } from "./components/RendererRouter";
import {
cleanupBootErrorHandling,
initBootErrorHandling,
@@ -85,7 +86,7 @@ if (!rootElement) {
reportBootError("Render failed", error)}
>
-
+
,
);
markBootMounted();