diff --git a/messages/de.json b/messages/de.json index fad8a7342..5fea285f0 100644 --- a/messages/de.json +++ b/messages/de.json @@ -7490,7 +7490,11 @@ "countOther": "{count} Fehlschläge", "lastFailed": "Letzter Fehlschlag {when}" }, - "deleteAllDetail": "Nutzerkonten bleiben bestehen." + "deleteAllDetail": "Nutzerkonten bleiben bestehen.", + "cardError": { + "title": "Dieser Teil der Seite konnte nicht angezeigt werden", + "description": "Der Rest der Seite funktioniert weiter; versuche es erneut oder lade neu, wenn es bleibt." + } }, "achievements": { "title": "Erfolge", diff --git a/messages/en.json b/messages/en.json index f03291ef3..ff67a71e9 100644 --- a/messages/en.json +++ b/messages/en.json @@ -7490,7 +7490,11 @@ "countOther": "{count} failures", "lastFailed": "Last failure {when}" }, - "deleteAllDetail": "User accounts will be preserved." + "deleteAllDetail": "User accounts will be preserved.", + "cardError": { + "title": "This part of the page could not be shown", + "description": "The rest of the page still works; try again or reload if it keeps failing." + } }, "achievements": { "title": "Achievements", diff --git a/messages/es.json b/messages/es.json index 2cf130ebd..241ebd154 100644 --- a/messages/es.json +++ b/messages/es.json @@ -7490,7 +7490,11 @@ "countOther": "{count} fallos", "lastFailed": "Último fallo {when}" }, - "deleteAllDetail": "Las cuentas de usuario se conservarán." + "deleteAllDetail": "Las cuentas de usuario se conservarán.", + "cardError": { + "title": "Esta parte de la página no se pudo mostrar", + "description": "El resto de la página sigue funcionando; inténtalo de nuevo o recarga si el fallo continúa." + } }, "achievements": { "title": "Logros", diff --git a/messages/fr.json b/messages/fr.json index dafcf4538..64cbe69f1 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -7490,7 +7490,11 @@ "countOther": "{count} échecs", "lastFailed": "Dernier échec {when}" }, - "deleteAllDetail": "Les comptes utilisateurs sont conservés." + "deleteAllDetail": "Les comptes utilisateurs sont conservés.", + "cardError": { + "title": "Cette partie de la page n’a pas pu s’afficher", + "description": "Le reste de la page fonctionne ; réessayez ou rechargez si le problème persiste." + } }, "achievements": { "title": "Réussites", diff --git a/messages/it.json b/messages/it.json index 8dbe44376..404c0ba2d 100644 --- a/messages/it.json +++ b/messages/it.json @@ -7490,7 +7490,11 @@ "countOther": "{count} errori", "lastFailed": "Ultimo errore {when}" }, - "deleteAllDetail": "Gli account utente vengono conservati." + "deleteAllDetail": "Gli account utente vengono conservati.", + "cardError": { + "title": "Questa parte della pagina non è stata mostrata", + "description": "Il resto della pagina funziona; riprova o ricarica se il problema continua." + } }, "achievements": { "title": "Obiettivi", diff --git a/messages/ko.json b/messages/ko.json index a0f83be9a..8ce36f817 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -7490,6 +7490,10 @@ "typeLocal": "로컬 모델 (개인)", "typeGateway": "OpenAI 호환 게이트웨이 (개인)", "spendToday": "오늘({date}): 토큰 {total}개, 그중 {operator}개는 운영자 키에서 사용되었습니다." + }, + "cardError": { + "title": "페이지의 이 부분을 표시할 수 없습니다", + "description": "페이지의 나머지 부분은 계속 작동합니다. 다시 시도하거나 계속 실패하면 새로고침하세요." } }, "achievements": { diff --git a/messages/pl.json b/messages/pl.json index dedf56606..fccc01aca 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -7490,7 +7490,11 @@ "countOther": "{count} niepowodzeń", "lastFailed": "Ostatnie niepowodzenie {when}" }, - "deleteAllDetail": "Konta użytkowników zostaną zachowane." + "deleteAllDetail": "Konta użytkowników zostaną zachowane.", + "cardError": { + "title": "Nie udało się wyświetlić tej części strony", + "description": "Reszta strony działa; spróbuj ponownie lub odśwież, jeśli błąd się powtarza." + } }, "achievements": { "title": "Osiągnięcia", diff --git a/src/app/admin/[section]/renderer.tsx b/src/app/admin/[section]/renderer.tsx index 24ac6042a..cfb9fdc8e 100644 --- a/src/app/admin/[section]/renderer.tsx +++ b/src/app/admin/[section]/renderer.tsx @@ -12,10 +12,11 @@ * read `useAuth()` to gate rendering). */ -import type { JSX } from "react"; +import { Children, type JSX } from "react"; import { useAuth } from "@/hooks/use-auth"; import { AboutSection } from "@/components/settings/about-section"; +import { AdminCardBoundary } from "@/components/admin/admin-card-boundary"; import { AiQualitySection } from "@/components/admin/ai-quality-section"; import { AiServerKeySection } from "@/components/admin/ai-server-key-section"; import { ProviderHealthSection } from "@/components/admin/provider-health-section"; @@ -189,5 +190,13 @@ export function AdminSectionRenderer({ * believes, so it is gone. */ function SectionFrame({ children }: { children: React.ReactNode }) { - return
card body
, + }); + // Outside a mounted tree `setState` does nothing, so apply it in place. + boundary.setState = ((update: unknown) => { + const next = + typeof update === "function" + ? (update as (s: typeof boundary.state) => object)(boundary.state) + : (update as object); + boundary.state = { ...boundary.state, ...next }; + }) as typeof boundary.setState; + return boundary; +} + +describe("AdminCardBoundary", () => { + it("renders the card untouched while nothing has thrown", () => { + const html = markup( +card body
+fallback
); + const boundary = boundaryWith(fallback); + boundary.setState(Boundary.getDerivedStateFromError()); + + expect(markup(boundary.render())).toContain('data-slot="fallback"'); + expect(fallback).toHaveBeenCalledWith(boundary.retry); + }); + + it("paints the translated error card with a keyboard-reachable retry", () => { + const html = markup(