diff --git a/README.md b/README.md index 9eff55e76..51988a5e0 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@ Release v1.28.3 IndexedDB v8 PWA v3.0 - i18n 19 locales — 2925 keys - 7304+ tests / 591 files + i18n 19 locales — 2937 keys + 7317+ tests / 594 files Codecov Coverage License MIT CI Status @@ -401,7 +401,7 @@ Infrastructure-level features that keep the app fast and extensible as projects ### 🌐 Full Multi-Language Support -Shipped UI locales with **2925 i18n keys** across all 19 languages — zero hardcoded user-facing strings: +Shipped UI locales with **2937 i18n keys** across all 19 languages — zero hardcoded user-facing strings: - 🇩🇪 **German** (Deutsch) - 🇬🇧 **English** @@ -510,8 +510,8 @@ The Settings → AI panel shows a live GPU status badge with adapter details and | **PDF Export** | jsPDF | Client-side, configurable PDF document generation | | **Document Export** | docx + jszip | Word-compatible `.docx` generation (lazy-loaded) | | **PWA** | Service Worker + Web App Manifest v3 | Offline support, installability, Workbox chunking | -| **i18n** | Custom React Context (`I18nContext.tsx`) | 2925 keys × 19 locales (de/en/es/fr/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta); EN fallback; `localStorage` persistence | -| **Testing** | Vitest 4.x (7304+ tests / 591 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) | +| **i18n** | Custom React Context (`I18nContext.tsx`) | 2937 keys × 19 locales (de/en/es/fr/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta); EN fallback; `localStorage` persistence | +| **Testing** | Vitest 4.x (7317+ tests / 594 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) | | **Code Quality** | Biome (lint + format) + TypeScript 7 (tsgo) strict | `--error-on-warnings` in CI; zero `any` policy | | **Visualization** | Force-directed graph | Interactive character relationship network | | **Desktop** | Tauri v2 | Cross-platform installer; auto-updater via `latest.json` | @@ -549,7 +549,7 @@ WorldScript-Studio/ │ ├── sw.js # PWA Service Worker │ └── manifest.json # PWA Web App Manifest v3 ├── tests/ -│ ├── unit/ # Vitest unit tests (7304+ tests, 591 files) — count spans tests/, components/, packages/*/tests/, not just this folder +│ ├── unit/ # Vitest unit tests (7317+ tests, 594 files) — count spans tests/, components/, packages/*/tests/, not just this folder │ │ ├── ai/ # aiSmallModules, aiCoreFallbackPaths │ │ └── settings/ # WebLlmPanel, AiSections │ └── e2e/ # Playwright specs + helpers.ts @@ -711,9 +711,9 @@ The main pipeline is [`.github/workflows/ci.yml`](.github/workflows/ci.yml). Opt | `scorecard` | weekly + `main` push | OpenSSF Scorecard — SARIF uploaded to GitHub Code Scanning | **Current test metrics (2026-08-30, source-synchronized; CI remains authoritative for pass/fail):** -- **7304+ unit tests** across **591 test files** — CI is authoritative for pass/fail +- **7317+ unit tests** across **594 test files** — CI is authoritative for pass/fail - Coverage thresholds: lines ≥ 80 · branches ≥ 66 · functions ≥ 72 · statements ≥ 78 — enforced in CI (see Codecov badge for live metrics) -- i18n: **2925 keys × 19 locales** (en/de/fr/es/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta) +- i18n: **2937 keys × 19 locales** (en/de/fr/es/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta) **CI-cloud-first workflow (recommended):** On constrained hardware run **`pnpm run lint && pnpm run i18n:check && pnpm run typecheck`** locally, then push and let CI handle coverage, E2E, Lighthouse, and Stryker. Authoritative numbers come from CI artifacts (Codecov, JUnit). After CI goes green, update the README badges and `AUDIT.md` quality-gate line from the reported metrics. See **[`docs/CI.md`](docs/CI.md) § Cloud CI-first vs local development** for the full post-merge doc-update checklist. diff --git a/components/StorageErrorScreen.tsx b/components/StorageErrorScreen.tsx new file mode 100644 index 000000000..c398c9b83 --- /dev/null +++ b/components/StorageErrorScreen.tsx @@ -0,0 +1,251 @@ +import React from 'react'; +import { ProjectQuarantineError } from '../services/fs/projectFsStore'; +import { getStaticTranslation } from '../services/i18n/staticTranslate'; +import { logger } from '../services/logger'; + +export interface StorageErrorCopy { + description: string; + storageUnavailable: string; + projectUnavailable: string; + reload: string; + recover: string; + recovering: string; + reset: string; + quarantineNotice: string; + recoveryFailed: string; + recoveryUnknown: string; + recoveryAlreadyPreserved: string; + resetWarning: string; +} + +export const STARTUP_COPY_FALLBACKS: StorageErrorCopy = { + description: 'The local project or database could not be opened. Reload and try again.', + storageUnavailable: 'Local storage could not be opened. Reload and try again.', + projectUnavailable: 'A local project could not be opened. Reload and try again.', + reload: 'Reload', + recover: 'Quarantine project and reload', + recovering: 'Preserving project…', + reset: 'Reset database and reload', + quarantineNotice: + 'The complete project folder will be moved to quarantine. No project data will be deleted.', + recoveryFailed: + 'Project preservation failed. The original project was not deleted. Reload and try again.', + recoveryUnknown: 'Project preservation could not be confirmed. Reload and try again.', + recoveryAlreadyPreserved: + 'The project appears to have been preserved by another recovery attempt. Reload to continue.', + resetWarning: 'Resetting the database will delete all local projects and settings.', +}; + +async function startupTranslation(key: string, fallback: string): Promise { + const translated = await getStaticTranslation(key); + return translated === key ? fallback : translated; +} + +// QNBS-v3: load safe localized recovery copy before rendering so raw storage errors never reach users. +export async function loadStorageErrorCopy(): Promise { + const [ + description, + storageUnavailable, + projectUnavailable, + reload, + recover, + recovering, + reset, + quarantineNotice, + recoveryFailed, + recoveryUnknown, + recoveryAlreadyPreserved, + resetWarning, + ] = await Promise.all([ + startupTranslation('error.startup.description', STARTUP_COPY_FALLBACKS.description), + startupTranslation( + 'error.startup.storageUnavailable', + STARTUP_COPY_FALLBACKS.storageUnavailable, + ), + startupTranslation( + 'error.startup.projectUnavailable', + STARTUP_COPY_FALLBACKS.projectUnavailable, + ), + startupTranslation('error.startup.reload', STARTUP_COPY_FALLBACKS.reload), + startupTranslation('error.startup.recover', STARTUP_COPY_FALLBACKS.recover), + startupTranslation('error.startup.recovering', STARTUP_COPY_FALLBACKS.recovering), + startupTranslation('error.startup.reset', STARTUP_COPY_FALLBACKS.reset), + startupTranslation('error.startup.quarantineNotice', STARTUP_COPY_FALLBACKS.quarantineNotice), + startupTranslation('error.startup.recoveryFailed', STARTUP_COPY_FALLBACKS.recoveryFailed), + startupTranslation('error.startup.recoveryUnknown', STARTUP_COPY_FALLBACKS.recoveryUnknown), + startupTranslation( + 'error.startup.recoveryAlreadyPreserved', + STARTUP_COPY_FALLBACKS.recoveryAlreadyPreserved, + ), + startupTranslation('error.startup.resetWarning', STARTUP_COPY_FALLBACKS.resetWarning), + ]); + return { + description, + storageUnavailable, + projectUnavailable, + reload, + recover, + recovering, + reset, + quarantineNotice, + recoveryFailed, + recoveryUnknown, + recoveryAlreadyPreserved, + resetWarning, + }; +} + +// QNBS-v3: show stable localized recovery actions while keeping technical filesystem diagnostics in the logger. +export function StorageErrorScreen({ + copy, + onReset, + onRecover, +}: { + copy: StorageErrorCopy; + onReset?: () => void; + onRecover?: () => Promise; +}) { + const [recoveryStatus, setRecoveryStatus] = React.useState< + 'failed' | 'already-preserved' | 'source-missing' | null + >(null); + const [isRecovering, setIsRecovering] = React.useState(false); + + const handleRecover = async () => { + if (!onRecover) return; + setRecoveryStatus(null); + setIsRecovering(true); + try { + await onRecover(); + setIsRecovering(false); + } catch (error) { + logger.error('Project quarantine failed', { + error: error instanceof Error ? error.message : String(error), + }); + setRecoveryStatus( + error instanceof ProjectQuarantineError && error.reason === 'already-preserved' + ? 'already-preserved' + : error instanceof ProjectQuarantineError && error.reason === 'source-missing' + ? 'source-missing' + : 'failed', + ); + setIsRecovering(false); + } + }; + + return ( +
+

WorldScript Studio

+

{copy.description}

+

+ {onRecover ? copy.projectUnavailable : copy.storageUnavailable} +

+
+ + {onRecover && ( + + )} + {onReset && ( + + )} +
+ {onRecover && ( +

+ {copy.quarantineNotice} +

+ )} + {onRecover && ( +

+ {isRecovering + ? copy.recovering + : recoveryStatus === 'already-preserved' + ? copy.recoveryAlreadyPreserved + : recoveryStatus === 'source-missing' + ? copy.recoveryUnknown + : recoveryStatus === 'failed' + ? copy.recoveryFailed + : ''} +

+ )} + {onReset &&

{copy.resetWarning}

} +
+ ); +} diff --git a/index.tsx b/index.tsx index abdd5d72d..b2e0d634b 100644 --- a/index.tsx +++ b/index.tsx @@ -8,8 +8,12 @@ import { IdbUnlockModal } from './components/settings/IdbUnlockModal'; import { I18nProvider } from './contexts/I18nContext'; import { versionControlActions } from './features/versionControl/versionControlSlice'; import { loadPersistedRootState } from './services/appBootstrap'; -import { initializeStorage, resetAllDatabases } from './services/dbInitialization'; +import { initializeStorage } from './services/dbInitialization'; import { logger } from './services/logger'; +import { + renderProjectInitializationFailure, + renderStorageInitializationFailure, +} from './services/startupRecovery'; import { IdbStorageLockedError } from './services/storage/storageEncryptionService'; /* ── Self-hosted fonts (@fontsource) ── */ import '@fontsource/inter/300.css'; @@ -103,98 +107,13 @@ window.addEventListener('unhandledrejection', (event) => { renderStartupError(message); }); -/** Recovery UI shown when IndexedDB initialisation fails. Intentionally no i18n dependency. */ -function StorageErrorScreen({ message, onReset }: { message: string; onReset: () => void }) { - return ( -
-

WorldScript Studio

-

- The local database could not be opened. This can happen after a browser update or when - storage is full. -

-

- {message} -

-
- - -
-

- Warning: resetting the database will delete all local projects and settings. -

-
- ); -} - // QNBS-v3: named (not an anonymous IIFE) so a locked-storage catch below can retry the full boot // sequence after the user unlocks, without a page reload — a reload would lose the in-memory key. async function bootApp(): Promise { const initResult = await initializeStorage(); if (!initResult.success) { logger.error('StorageBackend: initializeStorage failed:', initResult.error); - root.render( - - { - await resetAllDatabases(); - window.location.reload(); - }} - /> - , - ); + await renderStorageInitializationFailure(root); return; } @@ -285,19 +204,7 @@ async function bootApp(): Promise { ); return; } - logger.error('Failed to initialize the application:', error); - const msg = error instanceof Error ? error.message : 'Could not load project data.'; - root.render( - - { - await resetAllDatabases(); - window.location.reload(); - }} - /> - , - ); + await renderProjectInitializationFailure(root, error); } } diff --git a/locales/ar/common.json b/locales/ar/common.json index 1e510e033..3658decde 100644 --- a/locales/ar/common.json +++ b/locales/ar/common.json @@ -695,5 +695,17 @@ "voice.stopDictation": "إيقاف الإملاء", "voice.stopListening": "إيقاف الاستماع", "worlds.emptyState.description": "ابنِ الأماكن والقواعد والتواريخ التي تعيش فيها قصتك. ابدأ بموقع.", - "worlds.emptyState.title": "العالم بانتظارك" + "worlds.emptyState.title": "العالم بانتظارك", + "error.startup.description": "The local project or database could not be opened. Reload and try again.", + "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", + "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.reload": "Reload", + "error.startup.recover": "Quarantine project and reload", + "error.startup.recovering": "Preserving project…", + "error.startup.reset": "Reset database and reload", + "error.startup.quarantineNotice": "The complete project folder will be moved to quarantine. No project data will be deleted.", + "error.startup.recoveryFailed": "Project preservation failed. The original project was not deleted. Reload and try again.", + "error.startup.recoveryUnknown": "Project preservation could not be confirmed. Reload and try again.", + "error.startup.recoveryAlreadyPreserved": "The project appears to have been preserved by another recovery attempt. Reload to continue.", + "error.startup.resetWarning": "Resetting the database will delete all local projects and settings." } diff --git a/locales/de/common.json b/locales/de/common.json index c30ccfb00..3331dbbfa 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -695,5 +695,17 @@ "voice.stopDictation": "Diktat stoppen", "voice.stopListening": "Zuhören stoppen", "worlds.emptyState.description": "Baue die Orte, Regeln und Geschichten auf, in denen deine Geschichte lebt. Beginne mit einem Ort.", - "worlds.emptyState.title": "Die Welt wartet" + "worlds.emptyState.title": "Die Welt wartet", + "error.startup.description": "Das lokale Projekt oder die lokale Datenbank konnte nicht geöffnet werden. Laden Sie die Anwendung neu und versuchen Sie es erneut.", + "error.startup.storageUnavailable": "Der lokale Speicher konnte nicht geöffnet werden. Laden Sie die Anwendung neu und versuchen Sie es erneut.", + "error.startup.projectUnavailable": "Ein lokales Projekt konnte nicht geöffnet werden. Laden Sie die Anwendung neu und versuchen Sie es erneut.", + "error.startup.reload": "Neu laden", + "error.startup.recover": "Projekt unter Quarantäne stellen und neu laden", + "error.startup.recovering": "Projekt wird gesichert …", + "error.startup.reset": "Datenbank zurücksetzen und neu laden", + "error.startup.quarantineNotice": "Der vollständige Projektordner wird in die Quarantäne verschoben. Es werden keine Projektdaten gelöscht.", + "error.startup.recoveryFailed": "Die Projektsicherung ist fehlgeschlagen. Das ursprüngliche Projekt wurde nicht gelöscht. Laden Sie die Anwendung neu und versuchen Sie es erneut.", + "error.startup.recoveryUnknown": "Die Aufbewahrung des Projekts konnte nicht bestätigt werden. Laden Sie die Anwendung neu und versuchen Sie es erneut.", + "error.startup.recoveryAlreadyPreserved": "Das Projekt wurde offenbar bereits durch einen anderen Wiederherstellungsversuch gesichert. Laden Sie die Anwendung neu, um fortzufahren.", + "error.startup.resetWarning": "Das Zurücksetzen der Datenbank löscht alle lokalen Projekte und Einstellungen." } diff --git a/locales/el/common.json b/locales/el/common.json index e05d539b2..e5dbcb634 100644 --- a/locales/el/common.json +++ b/locales/el/common.json @@ -695,5 +695,17 @@ "voice.stopDictation": "Σταματήστε την υπαγόρευση", "voice.stopListening": "Σταμάτα να ακούς", "worlds.emptyState.description": "Δημιουργήστε τα μέρη, τους κανόνες και τις ιστορίες στα οποία ζει η ιστορία σας. Ξεκινήστε με μια τοποθεσία.", - "worlds.emptyState.title": "Ο κόσμος περιμένει" + "worlds.emptyState.title": "Ο κόσμος περιμένει", + "error.startup.description": "The local project or database could not be opened. Reload and try again.", + "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", + "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.reload": "Reload", + "error.startup.recover": "Quarantine project and reload", + "error.startup.recovering": "Preserving project…", + "error.startup.reset": "Reset database and reload", + "error.startup.quarantineNotice": "The complete project folder will be moved to quarantine. No project data will be deleted.", + "error.startup.recoveryFailed": "Project preservation failed. The original project was not deleted. Reload and try again.", + "error.startup.recoveryUnknown": "Project preservation could not be confirmed. Reload and try again.", + "error.startup.recoveryAlreadyPreserved": "The project appears to have been preserved by another recovery attempt. Reload to continue.", + "error.startup.resetWarning": "Resetting the database will delete all local projects and settings." } diff --git a/locales/en/common.json b/locales/en/common.json index 0e0bf3776..9494cd404 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -695,5 +695,17 @@ "plotBoard.heuristic.complicate.rationale": "A fresh complication tests the characters and opens new threads.", "plotBoard.heuristic.reverse.title": "Add a reversal", "plotBoard.heuristic.reverse.description": "Flip an expectation — an ally falters, a win turns sour, or a hidden truth surfaces.", - "plotBoard.heuristic.reverse.rationale": "Reversals refresh tension and deepen the arc." + "plotBoard.heuristic.reverse.rationale": "Reversals refresh tension and deepen the arc.", + "error.startup.description": "The local project or database could not be opened. Reload and try again.", + "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", + "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.reload": "Reload", + "error.startup.recover": "Quarantine project and reload", + "error.startup.recovering": "Preserving project…", + "error.startup.reset": "Reset database and reload", + "error.startup.quarantineNotice": "The complete project folder will be moved to quarantine. No project data will be deleted.", + "error.startup.recoveryFailed": "Project preservation failed. The original project was not deleted. Reload and try again.", + "error.startup.recoveryUnknown": "Project preservation could not be confirmed. Reload and try again.", + "error.startup.recoveryAlreadyPreserved": "The project appears to have been preserved by another recovery attempt. Reload to continue.", + "error.startup.resetWarning": "Resetting the database will delete all local projects and settings." } diff --git a/locales/es/common.json b/locales/es/common.json index 71511fc46..41117663e 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -695,5 +695,17 @@ "voice.stopDictation": "Detener dictado", "voice.stopListening": "Detener escucha", "worlds.emptyState.description": "Construye los lugares, reglas e historias en los que vive tu historia. Comienza con una ubicación.", - "worlds.emptyState.title": "El mundo te espera" + "worlds.emptyState.title": "El mundo te espera", + "error.startup.description": "No se pudo abrir el proyecto local o la base de datos. Recarga la aplicación e inténtalo de nuevo.", + "error.startup.storageUnavailable": "No se pudo abrir el almacenamiento local. Recarga la aplicación e inténtalo de nuevo.", + "error.startup.projectUnavailable": "No se pudo abrir un proyecto local. Recarga la aplicación e inténtalo de nuevo.", + "error.startup.reload": "Recargar", + "error.startup.recover": "Poner el proyecto en cuarentena y recargar", + "error.startup.recovering": "Preservando el proyecto…", + "error.startup.reset": "Restablecer la base de datos y recargar", + "error.startup.quarantineNotice": "La carpeta completa del proyecto se moverá a la cuarentena. No se eliminarán datos del proyecto.", + "error.startup.recoveryFailed": "La preservación del proyecto falló. El proyecto original no se eliminó. Recarga la aplicación e inténtalo de nuevo.", + "error.startup.recoveryUnknown": "No se pudo confirmar la preservación del proyecto. Recarga la aplicación e inténtalo de nuevo.", + "error.startup.recoveryAlreadyPreserved": "Parece que otro intento de recuperación ya ha preservado el proyecto. Recarga la aplicación para continuar.", + "error.startup.resetWarning": "Restablecer la base de datos eliminará todos los proyectos y la configuración locales." } diff --git a/locales/eu/common.json b/locales/eu/common.json index 11683111b..921244f15 100644 --- a/locales/eu/common.json +++ b/locales/eu/common.json @@ -695,5 +695,17 @@ "voice.stopDictation": "Utzi diktaketari", "voice.stopListening": "Utzi entzuteari", "worlds.emptyState.description": "Eraiki zure istorioa bizi den lekuak, arauak eta historiak. Hasi kokapen batekin.", - "worlds.emptyState.title": "Mundua zain dago" + "worlds.emptyState.title": "Mundua zain dago", + "error.startup.description": "The local project or database could not be opened. Reload and try again.", + "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", + "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.reload": "Reload", + "error.startup.recover": "Quarantine project and reload", + "error.startup.recovering": "Preserving project…", + "error.startup.reset": "Reset database and reload", + "error.startup.quarantineNotice": "The complete project folder will be moved to quarantine. No project data will be deleted.", + "error.startup.recoveryFailed": "Project preservation failed. The original project was not deleted. Reload and try again.", + "error.startup.recoveryUnknown": "Project preservation could not be confirmed. Reload and try again.", + "error.startup.recoveryAlreadyPreserved": "The project appears to have been preserved by another recovery attempt. Reload to continue.", + "error.startup.resetWarning": "Resetting the database will delete all local projects and settings." } diff --git a/locales/fa/common.json b/locales/fa/common.json index 28dd9bf19..775617ef7 100644 --- a/locales/fa/common.json +++ b/locales/fa/common.json @@ -695,5 +695,17 @@ "voice.stopDictation": "دیکته را متوقف کنید", "voice.stopListening": "گوش دادن را متوقف کنید", "worlds.emptyState.description": "مکان‌ها، قوانین و تاریخ‌هایی را بسازید که داستانتان در آن زندگی می‌کند. با یک مکان شروع کنید.", - "worlds.emptyState.title": "دنیا منتظر است" + "worlds.emptyState.title": "دنیا منتظر است", + "error.startup.description": "The local project or database could not be opened. Reload and try again.", + "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", + "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.reload": "Reload", + "error.startup.recover": "Quarantine project and reload", + "error.startup.recovering": "Preserving project…", + "error.startup.reset": "Reset database and reload", + "error.startup.quarantineNotice": "The complete project folder will be moved to quarantine. No project data will be deleted.", + "error.startup.recoveryFailed": "Project preservation failed. The original project was not deleted. Reload and try again.", + "error.startup.recoveryUnknown": "Project preservation could not be confirmed. Reload and try again.", + "error.startup.recoveryAlreadyPreserved": "The project appears to have been preserved by another recovery attempt. Reload to continue.", + "error.startup.resetWarning": "Resetting the database will delete all local projects and settings." } diff --git a/locales/fi/common.json b/locales/fi/common.json index 2f9e73e25..bdd59edca 100644 --- a/locales/fi/common.json +++ b/locales/fi/common.json @@ -695,5 +695,17 @@ "voice.stopDictation": "Lopeta sanelu", "voice.stopListening": "Lopeta kuunteleminen", "worlds.emptyState.description": "Rakenna paikat, säännöt ja historiat, joissa tarinasi elää. Aloita sijainnista.", - "worlds.emptyState.title": "Maailma odottaa" + "worlds.emptyState.title": "Maailma odottaa", + "error.startup.description": "The local project or database could not be opened. Reload and try again.", + "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", + "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.reload": "Reload", + "error.startup.recover": "Quarantine project and reload", + "error.startup.recovering": "Preserving project…", + "error.startup.reset": "Reset database and reload", + "error.startup.quarantineNotice": "The complete project folder will be moved to quarantine. No project data will be deleted.", + "error.startup.recoveryFailed": "Project preservation failed. The original project was not deleted. Reload and try again.", + "error.startup.recoveryUnknown": "Project preservation could not be confirmed. Reload and try again.", + "error.startup.recoveryAlreadyPreserved": "The project appears to have been preserved by another recovery attempt. Reload to continue.", + "error.startup.resetWarning": "Resetting the database will delete all local projects and settings." } diff --git a/locales/fr/common.json b/locales/fr/common.json index 5710d2efc..1c0fc7cd9 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -695,5 +695,17 @@ "voice.stopDictation": "Arrêter la dictée", "voice.stopListening": "Arrêter l’écoute", "worlds.emptyState.description": "Construisez les lieux, les règles et les histoires dans lesquels votre récit prend vie. Commencez par un lieu.", - "worlds.emptyState.title": "Le monde vous attend" + "worlds.emptyState.title": "Le monde vous attend", + "error.startup.description": "Le projet local ou la base de données n’a pas pu être ouvert. Rechargez l’application et réessayez.", + "error.startup.storageUnavailable": "Le stockage local n’a pas pu être ouvert. Rechargez l’application et réessayez.", + "error.startup.projectUnavailable": "Un projet local n’a pas pu être ouvert. Rechargez l’application et réessayez.", + "error.startup.reload": "Recharger", + "error.startup.recover": "Mettre le projet en quarantaine et recharger", + "error.startup.recovering": "Préservation du projet…", + "error.startup.reset": "Réinitialiser la base de données et recharger", + "error.startup.quarantineNotice": "Le dossier complet du projet sera déplacé en quarantaine. Aucune donnée du projet ne sera supprimée.", + "error.startup.recoveryFailed": "La préservation du projet a échoué. Le projet d’origine n’a pas été supprimé. Rechargez l’application et réessayez.", + "error.startup.recoveryUnknown": "La préservation du projet n’a pas pu être confirmée. Rechargez l’application et réessayez.", + "error.startup.recoveryAlreadyPreserved": "Le projet semble avoir été préservé par une autre tentative de récupération. Rechargez l’application pour continuer.", + "error.startup.resetWarning": "La réinitialisation de la base de données supprimera tous les projets et réglages locaux." } diff --git a/locales/he/common.json b/locales/he/common.json index ad004c4b2..c0fce188c 100644 --- a/locales/he/common.json +++ b/locales/he/common.json @@ -695,5 +695,17 @@ "voice.stopDictation": "עצירת הכתבה", "voice.stopListening": "עצירת האזנה", "worlds.emptyState.description": "בנו את המקומות, החוקים וההיסטוריות שבהם הסיפור שלכם חי. התחילו במיקום.", - "worlds.emptyState.title": "העולם ממתין" + "worlds.emptyState.title": "העולם ממתין", + "error.startup.description": "The local project or database could not be opened. Reload and try again.", + "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", + "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.reload": "Reload", + "error.startup.recover": "Quarantine project and reload", + "error.startup.recovering": "Preserving project…", + "error.startup.reset": "Reset database and reload", + "error.startup.quarantineNotice": "The complete project folder will be moved to quarantine. No project data will be deleted.", + "error.startup.recoveryFailed": "Project preservation failed. The original project was not deleted. Reload and try again.", + "error.startup.recoveryUnknown": "Project preservation could not be confirmed. Reload and try again.", + "error.startup.recoveryAlreadyPreserved": "The project appears to have been preserved by another recovery attempt. Reload to continue.", + "error.startup.resetWarning": "Resetting the database will delete all local projects and settings." } diff --git a/locales/hu/common.json b/locales/hu/common.json index 72c3b23d6..3519d768f 100644 --- a/locales/hu/common.json +++ b/locales/hu/common.json @@ -695,5 +695,17 @@ "voice.stopDictation": "Hagyd abba a diktálást", "voice.stopListening": "Ne hallgasson", "worlds.emptyState.description": "Építsd fel azokat a helyeket, szabályokat és történeteket, amelyekben a történeted él. Kezdd egy hellyel.", - "worlds.emptyState.title": "A világ vár" + "worlds.emptyState.title": "A világ vár", + "error.startup.description": "The local project or database could not be opened. Reload and try again.", + "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", + "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.reload": "Reload", + "error.startup.recover": "Quarantine project and reload", + "error.startup.recovering": "Preserving project…", + "error.startup.reset": "Reset database and reload", + "error.startup.quarantineNotice": "The complete project folder will be moved to quarantine. No project data will be deleted.", + "error.startup.recoveryFailed": "Project preservation failed. The original project was not deleted. Reload and try again.", + "error.startup.recoveryUnknown": "Project preservation could not be confirmed. Reload and try again.", + "error.startup.recoveryAlreadyPreserved": "The project appears to have been preserved by another recovery attempt. Reload to continue.", + "error.startup.resetWarning": "Resetting the database will delete all local projects and settings." } diff --git a/locales/is/common.json b/locales/is/common.json index 95b86efd3..c8925c403 100644 --- a/locales/is/common.json +++ b/locales/is/common.json @@ -695,5 +695,17 @@ "voice.stopDictation": "Hættu einræði", "voice.stopListening": "Hættu að hlusta", "worlds.emptyState.description": "Búðu til staðina, reglurnar og söguna sem sagan þín býr í. Byrjaðu á staðsetningu.", - "worlds.emptyState.title": "Heimurinn bíður" + "worlds.emptyState.title": "Heimurinn bíður", + "error.startup.description": "The local project or database could not be opened. Reload and try again.", + "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", + "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.reload": "Reload", + "error.startup.recover": "Quarantine project and reload", + "error.startup.recovering": "Preserving project…", + "error.startup.reset": "Reset database and reload", + "error.startup.quarantineNotice": "The complete project folder will be moved to quarantine. No project data will be deleted.", + "error.startup.recoveryFailed": "Project preservation failed. The original project was not deleted. Reload and try again.", + "error.startup.recoveryUnknown": "Project preservation could not be confirmed. Reload and try again.", + "error.startup.recoveryAlreadyPreserved": "The project appears to have been preserved by another recovery attempt. Reload to continue.", + "error.startup.resetWarning": "Resetting the database will delete all local projects and settings." } diff --git a/locales/it/common.json b/locales/it/common.json index cfa715895..a4197f98b 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -695,5 +695,17 @@ "voice.stopDictation": "Ferma dettatura", "voice.stopListening": "Ferma ascolto", "worlds.emptyState.description": "Costruisci i luoghi, le regole e le storie in cui vive la tua narrazione. Inizia con una location.", - "worlds.emptyState.title": "Il mondo ti aspetta" + "worlds.emptyState.title": "Il mondo ti aspetta", + "error.startup.description": "Non è stato possibile aprire il progetto locale o il database. Ricarica l’applicazione e riprova.", + "error.startup.storageUnavailable": "Non è stato possibile aprire l’archiviazione locale. Ricarica l’applicazione e riprova.", + "error.startup.projectUnavailable": "Non è stato possibile aprire un progetto locale. Ricarica l’applicazione e riprova.", + "error.startup.reload": "Ricarica", + "error.startup.recover": "Metti il progetto in quarantena e ricarica", + "error.startup.recovering": "Conservazione del progetto…", + "error.startup.reset": "Reimposta il database e ricarica", + "error.startup.quarantineNotice": "La cartella completa del progetto verrà spostata in quarantena. Nessun dato del progetto verrà eliminato.", + "error.startup.recoveryFailed": "La conservazione del progetto non è riuscita. Il progetto originale non è stato eliminato. Ricarica l’applicazione e riprova.", + "error.startup.recoveryUnknown": "Non è stato possibile confermare la conservazione del progetto. Ricarica l’applicazione e riprova.", + "error.startup.recoveryAlreadyPreserved": "Sembra che un altro tentativo di recupero abbia già conservato il progetto. Ricarica per continuare.", + "error.startup.resetWarning": "La reimpostazione del database eliminerà tutti i progetti e le impostazioni locali." } diff --git a/locales/ja/common.json b/locales/ja/common.json index 65af5ab70..3a39a6f95 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -695,5 +695,17 @@ "voice.stopDictation": "ディクテーションを停止する", "voice.stopListening": "聞くのをやめる", "worlds.emptyState.description": "あなたの物語が生きる場所、ルール、歴史を構築します。場所から始めます。", - "worlds.emptyState.title": "世界が待っています" + "worlds.emptyState.title": "世界が待っています", + "error.startup.description": "The local project or database could not be opened. Reload and try again.", + "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", + "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.reload": "Reload", + "error.startup.recover": "Quarantine project and reload", + "error.startup.recovering": "Preserving project…", + "error.startup.reset": "Reset database and reload", + "error.startup.quarantineNotice": "The complete project folder will be moved to quarantine. No project data will be deleted.", + "error.startup.recoveryFailed": "Project preservation failed. The original project was not deleted. Reload and try again.", + "error.startup.recoveryUnknown": "Project preservation could not be confirmed. Reload and try again.", + "error.startup.recoveryAlreadyPreserved": "The project appears to have been preserved by another recovery attempt. Reload to continue.", + "error.startup.resetWarning": "Resetting the database will delete all local projects and settings." } diff --git a/locales/ko/common.json b/locales/ko/common.json index c35d294bb..3ae366b81 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -695,5 +695,17 @@ "voice.stopDictation": "받아쓰기 중지", "voice.stopListening": "듣기 중지", "worlds.emptyState.description": "당신의 이야기가 담긴 장소, 규칙, 역사를 만들어 보세요. 위치부터 시작하세요.", - "worlds.emptyState.title": "세계가 기다리고 있다" + "worlds.emptyState.title": "세계가 기다리고 있다", + "error.startup.description": "The local project or database could not be opened. Reload and try again.", + "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", + "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.reload": "Reload", + "error.startup.recover": "Quarantine project and reload", + "error.startup.recovering": "Preserving project…", + "error.startup.reset": "Reset database and reload", + "error.startup.quarantineNotice": "The complete project folder will be moved to quarantine. No project data will be deleted.", + "error.startup.recoveryFailed": "Project preservation failed. The original project was not deleted. Reload and try again.", + "error.startup.recoveryUnknown": "Project preservation could not be confirmed. Reload and try again.", + "error.startup.recoveryAlreadyPreserved": "The project appears to have been preserved by another recovery attempt. Reload to continue.", + "error.startup.resetWarning": "Resetting the database will delete all local projects and settings." } diff --git a/locales/pt/common.json b/locales/pt/common.json index 8e9994a3c..9e5a0d511 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -695,5 +695,17 @@ "voice.stopDictation": "Pare o ditado", "voice.stopListening": "Pare de ouvir", "worlds.emptyState.description": "Construa os lugares, regras e histórias em que sua história vive. Comece com um local.", - "worlds.emptyState.title": "O mundo espera" + "worlds.emptyState.title": "O mundo espera", + "error.startup.description": "The local project or database could not be opened. Reload and try again.", + "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", + "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.reload": "Reload", + "error.startup.recover": "Quarantine project and reload", + "error.startup.recovering": "Preserving project…", + "error.startup.reset": "Reset database and reload", + "error.startup.quarantineNotice": "The complete project folder will be moved to quarantine. No project data will be deleted.", + "error.startup.recoveryFailed": "Project preservation failed. The original project was not deleted. Reload and try again.", + "error.startup.recoveryUnknown": "Project preservation could not be confirmed. Reload and try again.", + "error.startup.recoveryAlreadyPreserved": "The project appears to have been preserved by another recovery attempt. Reload to continue.", + "error.startup.resetWarning": "Resetting the database will delete all local projects and settings." } diff --git a/locales/ru/common.json b/locales/ru/common.json index adbed0250..25ea3864c 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -695,5 +695,17 @@ "voice.stopDictation": "Остановить диктовку", "voice.stopListening": "Хватит слушать", "worlds.emptyState.description": "Создайте места, правила и историю, в которых живет ваша история. Начните с локации.", - "worlds.emptyState.title": "Мир ждет" + "worlds.emptyState.title": "Мир ждет", + "error.startup.description": "The local project or database could not be opened. Reload and try again.", + "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", + "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.reload": "Reload", + "error.startup.recover": "Quarantine project and reload", + "error.startup.recovering": "Preserving project…", + "error.startup.reset": "Reset database and reload", + "error.startup.quarantineNotice": "The complete project folder will be moved to quarantine. No project data will be deleted.", + "error.startup.recoveryFailed": "Project preservation failed. The original project was not deleted. Reload and try again.", + "error.startup.recoveryUnknown": "Project preservation could not be confirmed. Reload and try again.", + "error.startup.recoveryAlreadyPreserved": "The project appears to have been preserved by another recovery attempt. Reload to continue.", + "error.startup.resetWarning": "Resetting the database will delete all local projects and settings." } diff --git a/locales/sv/common.json b/locales/sv/common.json index 7e82b08fa..fa5a3ddf4 100644 --- a/locales/sv/common.json +++ b/locales/sv/common.json @@ -695,5 +695,17 @@ "voice.stopDictation": "Sluta diktera", "voice.stopListening": "Sluta lyssna", "worlds.emptyState.description": "Bygg upp platserna, reglerna och historien som din berättelse lever i. Börja med en plats.", - "worlds.emptyState.title": "Världen väntar" + "worlds.emptyState.title": "Världen väntar", + "error.startup.description": "The local project or database could not be opened. Reload and try again.", + "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", + "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.reload": "Reload", + "error.startup.recover": "Quarantine project and reload", + "error.startup.recovering": "Preserving project…", + "error.startup.reset": "Reset database and reload", + "error.startup.quarantineNotice": "The complete project folder will be moved to quarantine. No project data will be deleted.", + "error.startup.recoveryFailed": "Project preservation failed. The original project was not deleted. Reload and try again.", + "error.startup.recoveryUnknown": "Project preservation could not be confirmed. Reload and try again.", + "error.startup.recoveryAlreadyPreserved": "The project appears to have been preserved by another recovery attempt. Reload to continue.", + "error.startup.resetWarning": "Resetting the database will delete all local projects and settings." } diff --git a/locales/zh/common.json b/locales/zh/common.json index 972777528..cca1db166 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -695,5 +695,17 @@ "voice.stopDictation": "停止听写", "voice.stopListening": "停止聆听", "worlds.emptyState.description": "构建你的故事所存在的地点、规则和历史。从一个地点开始。", - "worlds.emptyState.title": "世界等待着" + "worlds.emptyState.title": "世界等待着", + "error.startup.description": "The local project or database could not be opened. Reload and try again.", + "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", + "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.reload": "Reload", + "error.startup.recover": "Quarantine project and reload", + "error.startup.recovering": "Preserving project…", + "error.startup.reset": "Reset database and reload", + "error.startup.quarantineNotice": "The complete project folder will be moved to quarantine. No project data will be deleted.", + "error.startup.recoveryFailed": "Project preservation failed. The original project was not deleted. Reload and try again.", + "error.startup.recoveryUnknown": "Project preservation could not be confirmed. Reload and try again.", + "error.startup.recoveryAlreadyPreserved": "The project appears to have been preserved by another recovery attempt. Reload to continue.", + "error.startup.resetWarning": "Resetting the database will delete all local projects and settings." } diff --git a/public/locales/ar/bundle.json b/public/locales/ar/bundle.json index 286c1452e..faf17d4b5 100644 --- a/public/locales/ar/bundle.json +++ b/public/locales/ar/bundle.json @@ -812,6 +812,18 @@ "voice.stopListening": "إيقاف الاستماع", "worlds.emptyState.description": "ابنِ الأماكن والقواعد والتواريخ التي تعيش فيها قصتك. ابدأ بموقع.", "worlds.emptyState.title": "العالم بانتظارك", + "error.startup.description": "The local project or database could not be opened. Reload and try again.", + "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", + "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.reload": "Reload", + "error.startup.recover": "Quarantine project and reload", + "error.startup.recovering": "Preserving project…", + "error.startup.reset": "Reset database and reload", + "error.startup.quarantineNotice": "The complete project folder will be moved to quarantine. No project data will be deleted.", + "error.startup.recoveryFailed": "Project preservation failed. The original project was not deleted. Reload and try again.", + "error.startup.recoveryUnknown": "Project preservation could not be confirmed. Reload and try again.", + "error.startup.recoveryAlreadyPreserved": "The project appears to have been preserved by another recovery attempt. Reload to continue.", + "error.startup.resetWarning": "Resetting the database will delete all local projects and settings.", "copilot.annotationCount": "{{count}} insight for this chapter", "copilot.announceClosed": "AI Copilot closed", "copilot.announceOpened": "AI Copilot opened", diff --git a/public/locales/de/bundle.json b/public/locales/de/bundle.json index c4e75899d..f8c2e723c 100644 --- a/public/locales/de/bundle.json +++ b/public/locales/de/bundle.json @@ -812,6 +812,18 @@ "voice.stopListening": "Zuhören stoppen", "worlds.emptyState.description": "Baue die Orte, Regeln und Geschichten auf, in denen deine Geschichte lebt. Beginne mit einem Ort.", "worlds.emptyState.title": "Die Welt wartet", + "error.startup.description": "Das lokale Projekt oder die lokale Datenbank konnte nicht geöffnet werden. Laden Sie die Anwendung neu und versuchen Sie es erneut.", + "error.startup.storageUnavailable": "Der lokale Speicher konnte nicht geöffnet werden. Laden Sie die Anwendung neu und versuchen Sie es erneut.", + "error.startup.projectUnavailable": "Ein lokales Projekt konnte nicht geöffnet werden. Laden Sie die Anwendung neu und versuchen Sie es erneut.", + "error.startup.reload": "Neu laden", + "error.startup.recover": "Projekt unter Quarantäne stellen und neu laden", + "error.startup.recovering": "Projekt wird gesichert …", + "error.startup.reset": "Datenbank zurücksetzen und neu laden", + "error.startup.quarantineNotice": "Der vollständige Projektordner wird in die Quarantäne verschoben. Es werden keine Projektdaten gelöscht.", + "error.startup.recoveryFailed": "Die Projektsicherung ist fehlgeschlagen. Das ursprüngliche Projekt wurde nicht gelöscht. Laden Sie die Anwendung neu und versuchen Sie es erneut.", + "error.startup.recoveryUnknown": "Die Aufbewahrung des Projekts konnte nicht bestätigt werden. Laden Sie die Anwendung neu und versuchen Sie es erneut.", + "error.startup.recoveryAlreadyPreserved": "Das Projekt wurde offenbar bereits durch einen anderen Wiederherstellungsversuch gesichert. Laden Sie die Anwendung neu, um fortzufahren.", + "error.startup.resetWarning": "Das Zurücksetzen der Datenbank löscht alle lokalen Projekte und Einstellungen.", "copilot.annotationCount": "{{count}} insight for this chapter", "copilot.announceClosed": "KI-Copilot geschlossen", "copilot.announceOpened": "KI-Copilot geöffnet", diff --git a/public/locales/el/bundle.json b/public/locales/el/bundle.json index 27ef72a23..9fbad20e1 100644 --- a/public/locales/el/bundle.json +++ b/public/locales/el/bundle.json @@ -812,6 +812,18 @@ "voice.stopListening": "Σταμάτα να ακούς", "worlds.emptyState.description": "Δημιουργήστε τα μέρη, τους κανόνες και τις ιστορίες στα οποία ζει η ιστορία σας. Ξεκινήστε με μια τοποθεσία.", "worlds.emptyState.title": "Ο κόσμος περιμένει", + "error.startup.description": "The local project or database could not be opened. Reload and try again.", + "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", + "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.reload": "Reload", + "error.startup.recover": "Quarantine project and reload", + "error.startup.recovering": "Preserving project…", + "error.startup.reset": "Reset database and reload", + "error.startup.quarantineNotice": "The complete project folder will be moved to quarantine. No project data will be deleted.", + "error.startup.recoveryFailed": "Project preservation failed. The original project was not deleted. Reload and try again.", + "error.startup.recoveryUnknown": "Project preservation could not be confirmed. Reload and try again.", + "error.startup.recoveryAlreadyPreserved": "The project appears to have been preserved by another recovery attempt. Reload to continue.", + "error.startup.resetWarning": "Resetting the database will delete all local projects and settings.", "copilot.annotationCount": "{{count}} πληροφορίες για αυτό το κεφάλαιο", "copilot.announceClosed": "Το AI Copilot έκλεισε", "copilot.announceOpened": "Άνοιξε το AI Copilot", diff --git a/public/locales/en/bundle.json b/public/locales/en/bundle.json index dfea22b2a..abb0be733 100644 --- a/public/locales/en/bundle.json +++ b/public/locales/en/bundle.json @@ -812,6 +812,18 @@ "plotBoard.heuristic.reverse.title": "Add a reversal", "plotBoard.heuristic.reverse.description": "Flip an expectation — an ally falters, a win turns sour, or a hidden truth surfaces.", "plotBoard.heuristic.reverse.rationale": "Reversals refresh tension and deepen the arc.", + "error.startup.description": "The local project or database could not be opened. Reload and try again.", + "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", + "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.reload": "Reload", + "error.startup.recover": "Quarantine project and reload", + "error.startup.recovering": "Preserving project…", + "error.startup.reset": "Reset database and reload", + "error.startup.quarantineNotice": "The complete project folder will be moved to quarantine. No project data will be deleted.", + "error.startup.recoveryFailed": "Project preservation failed. The original project was not deleted. Reload and try again.", + "error.startup.recoveryUnknown": "Project preservation could not be confirmed. Reload and try again.", + "error.startup.recoveryAlreadyPreserved": "The project appears to have been preserved by another recovery attempt. Reload to continue.", + "error.startup.resetWarning": "Resetting the database will delete all local projects and settings.", "copilot.title": "AI Copilot", "copilot.subtitle": "Your writing assistant", "copilot.launcherLabel": "Open AI Copilot", diff --git a/public/locales/es/bundle.json b/public/locales/es/bundle.json index a9eaebea9..d7f3bba7e 100644 --- a/public/locales/es/bundle.json +++ b/public/locales/es/bundle.json @@ -812,6 +812,18 @@ "voice.stopListening": "Detener escucha", "worlds.emptyState.description": "Construye los lugares, reglas e historias en los que vive tu historia. Comienza con una ubicación.", "worlds.emptyState.title": "El mundo te espera", + "error.startup.description": "No se pudo abrir el proyecto local o la base de datos. Recarga la aplicación e inténtalo de nuevo.", + "error.startup.storageUnavailable": "No se pudo abrir el almacenamiento local. Recarga la aplicación e inténtalo de nuevo.", + "error.startup.projectUnavailable": "No se pudo abrir un proyecto local. Recarga la aplicación e inténtalo de nuevo.", + "error.startup.reload": "Recargar", + "error.startup.recover": "Poner el proyecto en cuarentena y recargar", + "error.startup.recovering": "Preservando el proyecto…", + "error.startup.reset": "Restablecer la base de datos y recargar", + "error.startup.quarantineNotice": "La carpeta completa del proyecto se moverá a la cuarentena. No se eliminarán datos del proyecto.", + "error.startup.recoveryFailed": "La preservación del proyecto falló. El proyecto original no se eliminó. Recarga la aplicación e inténtalo de nuevo.", + "error.startup.recoveryUnknown": "No se pudo confirmar la preservación del proyecto. Recarga la aplicación e inténtalo de nuevo.", + "error.startup.recoveryAlreadyPreserved": "Parece que otro intento de recuperación ya ha preservado el proyecto. Recarga la aplicación para continuar.", + "error.startup.resetWarning": "Restablecer la base de datos eliminará todos los proyectos y la configuración locales.", "copilot.annotationCount": "{{count}} insight for this chapter", "copilot.announceClosed": "Copiloto IA cerrado", "copilot.announceOpened": "Copiloto IA abierto", diff --git a/public/locales/eu/bundle.json b/public/locales/eu/bundle.json index 7b6c5a74c..87d4676e6 100644 --- a/public/locales/eu/bundle.json +++ b/public/locales/eu/bundle.json @@ -812,6 +812,18 @@ "voice.stopListening": "Utzi entzuteari", "worlds.emptyState.description": "Eraiki zure istorioa bizi den lekuak, arauak eta historiak. Hasi kokapen batekin.", "worlds.emptyState.title": "Mundua zain dago", + "error.startup.description": "The local project or database could not be opened. Reload and try again.", + "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", + "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.reload": "Reload", + "error.startup.recover": "Quarantine project and reload", + "error.startup.recovering": "Preserving project…", + "error.startup.reset": "Reset database and reload", + "error.startup.quarantineNotice": "The complete project folder will be moved to quarantine. No project data will be deleted.", + "error.startup.recoveryFailed": "Project preservation failed. The original project was not deleted. Reload and try again.", + "error.startup.recoveryUnknown": "Project preservation could not be confirmed. Reload and try again.", + "error.startup.recoveryAlreadyPreserved": "The project appears to have been preserved by another recovery attempt. Reload to continue.", + "error.startup.resetWarning": "Resetting the database will delete all local projects and settings.", "copilot.annotationCount": "{{count}} kapitulu honetarako ikuspegia", "copilot.announceClosed": "AI Copilot itxita", "copilot.announceOpened": "AI Copilot ireki da", diff --git a/public/locales/fa/bundle.json b/public/locales/fa/bundle.json index ec94a5385..b5f8d0a11 100644 --- a/public/locales/fa/bundle.json +++ b/public/locales/fa/bundle.json @@ -812,6 +812,18 @@ "voice.stopListening": "گوش دادن را متوقف کنید", "worlds.emptyState.description": "مکان‌ها، قوانین و تاریخ‌هایی را بسازید که داستانتان در آن زندگی می‌کند. با یک مکان شروع کنید.", "worlds.emptyState.title": "دنیا منتظر است", + "error.startup.description": "The local project or database could not be opened. Reload and try again.", + "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", + "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.reload": "Reload", + "error.startup.recover": "Quarantine project and reload", + "error.startup.recovering": "Preserving project…", + "error.startup.reset": "Reset database and reload", + "error.startup.quarantineNotice": "The complete project folder will be moved to quarantine. No project data will be deleted.", + "error.startup.recoveryFailed": "Project preservation failed. The original project was not deleted. Reload and try again.", + "error.startup.recoveryUnknown": "Project preservation could not be confirmed. Reload and try again.", + "error.startup.recoveryAlreadyPreserved": "The project appears to have been preserved by another recovery attempt. Reload to continue.", + "error.startup.resetWarning": "Resetting the database will delete all local projects and settings.", "copilot.annotationCount": "{{count}} بینش برای این فصل", "copilot.announceClosed": "AI Copilot بسته شد", "copilot.announceOpened": "AI Copilot باز شد", diff --git a/public/locales/fi/bundle.json b/public/locales/fi/bundle.json index e57df0d80..de65cd7af 100644 --- a/public/locales/fi/bundle.json +++ b/public/locales/fi/bundle.json @@ -812,6 +812,18 @@ "voice.stopListening": "Lopeta kuunteleminen", "worlds.emptyState.description": "Rakenna paikat, säännöt ja historiat, joissa tarinasi elää. Aloita sijainnista.", "worlds.emptyState.title": "Maailma odottaa", + "error.startup.description": "The local project or database could not be opened. Reload and try again.", + "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", + "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.reload": "Reload", + "error.startup.recover": "Quarantine project and reload", + "error.startup.recovering": "Preserving project…", + "error.startup.reset": "Reset database and reload", + "error.startup.quarantineNotice": "The complete project folder will be moved to quarantine. No project data will be deleted.", + "error.startup.recoveryFailed": "Project preservation failed. The original project was not deleted. Reload and try again.", + "error.startup.recoveryUnknown": "Project preservation could not be confirmed. Reload and try again.", + "error.startup.recoveryAlreadyPreserved": "The project appears to have been preserved by another recovery attempt. Reload to continue.", + "error.startup.resetWarning": "Resetting the database will delete all local projects and settings.", "copilot.annotationCount": "{{count}} tietoa tästä luvusta", "copilot.announceClosed": "AI Copilot suljettu", "copilot.announceOpened": "AI Copilot avattiin", diff --git a/public/locales/fr/bundle.json b/public/locales/fr/bundle.json index 22d930b1d..d6f017439 100644 --- a/public/locales/fr/bundle.json +++ b/public/locales/fr/bundle.json @@ -812,6 +812,18 @@ "voice.stopListening": "Arrêter l’écoute", "worlds.emptyState.description": "Construisez les lieux, les règles et les histoires dans lesquels votre récit prend vie. Commencez par un lieu.", "worlds.emptyState.title": "Le monde vous attend", + "error.startup.description": "Le projet local ou la base de données n’a pas pu être ouvert. Rechargez l’application et réessayez.", + "error.startup.storageUnavailable": "Le stockage local n’a pas pu être ouvert. Rechargez l’application et réessayez.", + "error.startup.projectUnavailable": "Un projet local n’a pas pu être ouvert. Rechargez l’application et réessayez.", + "error.startup.reload": "Recharger", + "error.startup.recover": "Mettre le projet en quarantaine et recharger", + "error.startup.recovering": "Préservation du projet…", + "error.startup.reset": "Réinitialiser la base de données et recharger", + "error.startup.quarantineNotice": "Le dossier complet du projet sera déplacé en quarantaine. Aucune donnée du projet ne sera supprimée.", + "error.startup.recoveryFailed": "La préservation du projet a échoué. Le projet d’origine n’a pas été supprimé. Rechargez l’application et réessayez.", + "error.startup.recoveryUnknown": "La préservation du projet n’a pas pu être confirmée. Rechargez l’application et réessayez.", + "error.startup.recoveryAlreadyPreserved": "Le projet semble avoir été préservé par une autre tentative de récupération. Rechargez l’application pour continuer.", + "error.startup.resetWarning": "La réinitialisation de la base de données supprimera tous les projets et réglages locaux.", "copilot.annotationCount": "{{count}} insight for this chapter", "copilot.announceClosed": "Copilot IA fermé", "copilot.announceOpened": "Copilot IA ouvert", diff --git a/public/locales/he/bundle.json b/public/locales/he/bundle.json index 5c6d014a1..7c484db70 100644 --- a/public/locales/he/bundle.json +++ b/public/locales/he/bundle.json @@ -812,6 +812,18 @@ "voice.stopListening": "עצירת האזנה", "worlds.emptyState.description": "בנו את המקומות, החוקים וההיסטוריות שבהם הסיפור שלכם חי. התחילו במיקום.", "worlds.emptyState.title": "העולם ממתין", + "error.startup.description": "The local project or database could not be opened. Reload and try again.", + "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", + "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.reload": "Reload", + "error.startup.recover": "Quarantine project and reload", + "error.startup.recovering": "Preserving project…", + "error.startup.reset": "Reset database and reload", + "error.startup.quarantineNotice": "The complete project folder will be moved to quarantine. No project data will be deleted.", + "error.startup.recoveryFailed": "Project preservation failed. The original project was not deleted. Reload and try again.", + "error.startup.recoveryUnknown": "Project preservation could not be confirmed. Reload and try again.", + "error.startup.recoveryAlreadyPreserved": "The project appears to have been preserved by another recovery attempt. Reload to continue.", + "error.startup.resetWarning": "Resetting the database will delete all local projects and settings.", "copilot.annotationCount": "{{count}} insight for this chapter", "copilot.announceClosed": "AI Copilot closed", "copilot.announceOpened": "AI Copilot opened", diff --git a/public/locales/hu/bundle.json b/public/locales/hu/bundle.json index f779a2cea..7b6e27317 100644 --- a/public/locales/hu/bundle.json +++ b/public/locales/hu/bundle.json @@ -812,6 +812,18 @@ "voice.stopListening": "Ne hallgasson", "worlds.emptyState.description": "Építsd fel azokat a helyeket, szabályokat és történeteket, amelyekben a történeted él. Kezdd egy hellyel.", "worlds.emptyState.title": "A világ vár", + "error.startup.description": "The local project or database could not be opened. Reload and try again.", + "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", + "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.reload": "Reload", + "error.startup.recover": "Quarantine project and reload", + "error.startup.recovering": "Preserving project…", + "error.startup.reset": "Reset database and reload", + "error.startup.quarantineNotice": "The complete project folder will be moved to quarantine. No project data will be deleted.", + "error.startup.recoveryFailed": "Project preservation failed. The original project was not deleted. Reload and try again.", + "error.startup.recoveryUnknown": "Project preservation could not be confirmed. Reload and try again.", + "error.startup.recoveryAlreadyPreserved": "The project appears to have been preserved by another recovery attempt. Reload to continue.", + "error.startup.resetWarning": "Resetting the database will delete all local projects and settings.", "copilot.annotationCount": "{{count}} betekintést nyújt ehhez a fejezethez", "copilot.announceClosed": "Az AI másodpilóta zárva", "copilot.announceOpened": "Az AI másodpilóta megnyílt", diff --git a/public/locales/is/bundle.json b/public/locales/is/bundle.json index 9ba1aad4f..d42fc65ae 100644 --- a/public/locales/is/bundle.json +++ b/public/locales/is/bundle.json @@ -812,6 +812,18 @@ "voice.stopListening": "Hættu að hlusta", "worlds.emptyState.description": "Búðu til staðina, reglurnar og söguna sem sagan þín býr í. Byrjaðu á staðsetningu.", "worlds.emptyState.title": "Heimurinn bíður", + "error.startup.description": "The local project or database could not be opened. Reload and try again.", + "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", + "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.reload": "Reload", + "error.startup.recover": "Quarantine project and reload", + "error.startup.recovering": "Preserving project…", + "error.startup.reset": "Reset database and reload", + "error.startup.quarantineNotice": "The complete project folder will be moved to quarantine. No project data will be deleted.", + "error.startup.recoveryFailed": "Project preservation failed. The original project was not deleted. Reload and try again.", + "error.startup.recoveryUnknown": "Project preservation could not be confirmed. Reload and try again.", + "error.startup.recoveryAlreadyPreserved": "The project appears to have been preserved by another recovery attempt. Reload to continue.", + "error.startup.resetWarning": "Resetting the database will delete all local projects and settings.", "copilot.annotationCount": "{{count}} innsýn fyrir þennan kafla", "copilot.announceClosed": "AI Copilot lokað", "copilot.announceOpened": "AI Copilot opnaður", diff --git a/public/locales/it/bundle.json b/public/locales/it/bundle.json index 6e6a5c7f7..ec6d6fb4f 100644 --- a/public/locales/it/bundle.json +++ b/public/locales/it/bundle.json @@ -812,6 +812,18 @@ "voice.stopListening": "Ferma ascolto", "worlds.emptyState.description": "Costruisci i luoghi, le regole e le storie in cui vive la tua narrazione. Inizia con una location.", "worlds.emptyState.title": "Il mondo ti aspetta", + "error.startup.description": "Non è stato possibile aprire il progetto locale o il database. Ricarica l’applicazione e riprova.", + "error.startup.storageUnavailable": "Non è stato possibile aprire l’archiviazione locale. Ricarica l’applicazione e riprova.", + "error.startup.projectUnavailable": "Non è stato possibile aprire un progetto locale. Ricarica l’applicazione e riprova.", + "error.startup.reload": "Ricarica", + "error.startup.recover": "Metti il progetto in quarantena e ricarica", + "error.startup.recovering": "Conservazione del progetto…", + "error.startup.reset": "Reimposta il database e ricarica", + "error.startup.quarantineNotice": "La cartella completa del progetto verrà spostata in quarantena. Nessun dato del progetto verrà eliminato.", + "error.startup.recoveryFailed": "La conservazione del progetto non è riuscita. Il progetto originale non è stato eliminato. Ricarica l’applicazione e riprova.", + "error.startup.recoveryUnknown": "Non è stato possibile confermare la conservazione del progetto. Ricarica l’applicazione e riprova.", + "error.startup.recoveryAlreadyPreserved": "Sembra che un altro tentativo di recupero abbia già conservato il progetto. Ricarica per continuare.", + "error.startup.resetWarning": "La reimpostazione del database eliminerà tutti i progetti e le impostazioni locali.", "copilot.annotationCount": "{{count}} insight for this chapter", "copilot.announceClosed": "Copilota IA chiuso", "copilot.announceOpened": "Copilota IA aperto", diff --git a/public/locales/ja/bundle.json b/public/locales/ja/bundle.json index 58e4b9ac1..8c78269ab 100644 --- a/public/locales/ja/bundle.json +++ b/public/locales/ja/bundle.json @@ -812,6 +812,18 @@ "voice.stopListening": "聞くのをやめる", "worlds.emptyState.description": "あなたの物語が生きる場所、ルール、歴史を構築します。場所から始めます。", "worlds.emptyState.title": "世界が待っています", + "error.startup.description": "The local project or database could not be opened. Reload and try again.", + "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", + "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.reload": "Reload", + "error.startup.recover": "Quarantine project and reload", + "error.startup.recovering": "Preserving project…", + "error.startup.reset": "Reset database and reload", + "error.startup.quarantineNotice": "The complete project folder will be moved to quarantine. No project data will be deleted.", + "error.startup.recoveryFailed": "Project preservation failed. The original project was not deleted. Reload and try again.", + "error.startup.recoveryUnknown": "Project preservation could not be confirmed. Reload and try again.", + "error.startup.recoveryAlreadyPreserved": "The project appears to have been preserved by another recovery attempt. Reload to continue.", + "error.startup.resetWarning": "Resetting the database will delete all local projects and settings.", "copilot.annotationCount": "{{count}} この章の洞察", "copilot.announceClosed": "AI コパイロットは終了しました", "copilot.announceOpened": "AI Copilot がオープンしました", diff --git a/public/locales/ko/bundle.json b/public/locales/ko/bundle.json index 0f329e17f..4e4ae470d 100644 --- a/public/locales/ko/bundle.json +++ b/public/locales/ko/bundle.json @@ -812,6 +812,18 @@ "voice.stopListening": "듣기 중지", "worlds.emptyState.description": "당신의 이야기가 담긴 장소, 규칙, 역사를 만들어 보세요. 위치부터 시작하세요.", "worlds.emptyState.title": "세계가 기다리고 있다", + "error.startup.description": "The local project or database could not be opened. Reload and try again.", + "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", + "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.reload": "Reload", + "error.startup.recover": "Quarantine project and reload", + "error.startup.recovering": "Preserving project…", + "error.startup.reset": "Reset database and reload", + "error.startup.quarantineNotice": "The complete project folder will be moved to quarantine. No project data will be deleted.", + "error.startup.recoveryFailed": "Project preservation failed. The original project was not deleted. Reload and try again.", + "error.startup.recoveryUnknown": "Project preservation could not be confirmed. Reload and try again.", + "error.startup.recoveryAlreadyPreserved": "The project appears to have been preserved by another recovery attempt. Reload to continue.", + "error.startup.resetWarning": "Resetting the database will delete all local projects and settings.", "copilot.annotationCount": "{{count}} 이 장에 대한 통찰력", "copilot.announceClosed": "AI 부조종사 폐쇄", "copilot.announceOpened": "AI 코파일럿 오픈", diff --git a/public/locales/pt/bundle.json b/public/locales/pt/bundle.json index f451d8c25..6e84c85bd 100644 --- a/public/locales/pt/bundle.json +++ b/public/locales/pt/bundle.json @@ -812,6 +812,18 @@ "voice.stopListening": "Pare de ouvir", "worlds.emptyState.description": "Construa os lugares, regras e histórias em que sua história vive. Comece com um local.", "worlds.emptyState.title": "O mundo espera", + "error.startup.description": "The local project or database could not be opened. Reload and try again.", + "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", + "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.reload": "Reload", + "error.startup.recover": "Quarantine project and reload", + "error.startup.recovering": "Preserving project…", + "error.startup.reset": "Reset database and reload", + "error.startup.quarantineNotice": "The complete project folder will be moved to quarantine. No project data will be deleted.", + "error.startup.recoveryFailed": "Project preservation failed. The original project was not deleted. Reload and try again.", + "error.startup.recoveryUnknown": "Project preservation could not be confirmed. Reload and try again.", + "error.startup.recoveryAlreadyPreserved": "The project appears to have been preserved by another recovery attempt. Reload to continue.", + "error.startup.resetWarning": "Resetting the database will delete all local projects and settings.", "copilot.annotationCount": "{{count}} visão para este capítulo", "copilot.announceClosed": "Copiloto AI fechado", "copilot.announceOpened": "Copiloto AI aberto", diff --git a/public/locales/ru/bundle.json b/public/locales/ru/bundle.json index 9bfa0ea85..9b9d0ad76 100644 --- a/public/locales/ru/bundle.json +++ b/public/locales/ru/bundle.json @@ -812,6 +812,18 @@ "voice.stopListening": "Хватит слушать", "worlds.emptyState.description": "Создайте места, правила и историю, в которых живет ваша история. Начните с локации.", "worlds.emptyState.title": "Мир ждет", + "error.startup.description": "The local project or database could not be opened. Reload and try again.", + "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", + "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.reload": "Reload", + "error.startup.recover": "Quarantine project and reload", + "error.startup.recovering": "Preserving project…", + "error.startup.reset": "Reset database and reload", + "error.startup.quarantineNotice": "The complete project folder will be moved to quarantine. No project data will be deleted.", + "error.startup.recoveryFailed": "Project preservation failed. The original project was not deleted. Reload and try again.", + "error.startup.recoveryUnknown": "Project preservation could not be confirmed. Reload and try again.", + "error.startup.recoveryAlreadyPreserved": "The project appears to have been preserved by another recovery attempt. Reload to continue.", + "error.startup.resetWarning": "Resetting the database will delete all local projects and settings.", "copilot.annotationCount": "{{count}} информация по этой главе", "copilot.announceClosed": "AI второй пилот закрыт", "copilot.announceOpened": "AI второй пилот открыт", diff --git a/public/locales/sv/bundle.json b/public/locales/sv/bundle.json index edab8844a..aed1cfcf4 100644 --- a/public/locales/sv/bundle.json +++ b/public/locales/sv/bundle.json @@ -812,6 +812,18 @@ "voice.stopListening": "Sluta lyssna", "worlds.emptyState.description": "Bygg upp platserna, reglerna och historien som din berättelse lever i. Börja med en plats.", "worlds.emptyState.title": "Världen väntar", + "error.startup.description": "The local project or database could not be opened. Reload and try again.", + "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", + "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.reload": "Reload", + "error.startup.recover": "Quarantine project and reload", + "error.startup.recovering": "Preserving project…", + "error.startup.reset": "Reset database and reload", + "error.startup.quarantineNotice": "The complete project folder will be moved to quarantine. No project data will be deleted.", + "error.startup.recoveryFailed": "Project preservation failed. The original project was not deleted. Reload and try again.", + "error.startup.recoveryUnknown": "Project preservation could not be confirmed. Reload and try again.", + "error.startup.recoveryAlreadyPreserved": "The project appears to have been preserved by another recovery attempt. Reload to continue.", + "error.startup.resetWarning": "Resetting the database will delete all local projects and settings.", "copilot.annotationCount": "{{count}} insikt för detta kapitel", "copilot.announceClosed": "AI Copilot stängd", "copilot.announceOpened": "AI Copilot öppnade", diff --git a/public/locales/zh/bundle.json b/public/locales/zh/bundle.json index a8c487bc5..43abb3b5a 100644 --- a/public/locales/zh/bundle.json +++ b/public/locales/zh/bundle.json @@ -812,6 +812,18 @@ "voice.stopListening": "停止聆听", "worlds.emptyState.description": "构建你的故事所存在的地点、规则和历史。从一个地点开始。", "worlds.emptyState.title": "世界等待着", + "error.startup.description": "The local project or database could not be opened. Reload and try again.", + "error.startup.storageUnavailable": "Local storage could not be opened. Reload and try again.", + "error.startup.projectUnavailable": "A local project could not be opened. Reload and try again.", + "error.startup.reload": "Reload", + "error.startup.recover": "Quarantine project and reload", + "error.startup.recovering": "Preserving project…", + "error.startup.reset": "Reset database and reload", + "error.startup.quarantineNotice": "The complete project folder will be moved to quarantine. No project data will be deleted.", + "error.startup.recoveryFailed": "Project preservation failed. The original project was not deleted. Reload and try again.", + "error.startup.recoveryUnknown": "Project preservation could not be confirmed. Reload and try again.", + "error.startup.recoveryAlreadyPreserved": "The project appears to have been preserved by another recovery attempt. Reload to continue.", + "error.startup.resetWarning": "Resetting the database will delete all local projects and settings.", "copilot.annotationCount": "{{count}} 本章见解", "copilot.announceClosed": "AI副驾驶关闭", "copilot.announceOpened": "AI副驾驶开启", diff --git a/services/startupRecovery.tsx b/services/startupRecovery.tsx new file mode 100644 index 000000000..d0283a3d8 --- /dev/null +++ b/services/startupRecovery.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import type { Root } from 'react-dom/client'; +import { loadStorageErrorCopy, StorageErrorScreen } from '../components/StorageErrorScreen'; +import { resetAllDatabases } from './dbInitialization'; +import { ProjectLoadError, ProjectQuarantineError } from './fs/projectFsStore'; +import { logger } from './logger'; +import { getStartupRecoveryActions } from './startupRecoveryPolicy'; +import { storageService } from './storageService'; + +// QNBS-v3: keep startup recovery render decisions testable so destructive reset stays bound to backend provenance. +export async function renderStorageInitializationFailure(root: Root): Promise { + const copy = await loadStorageErrorCopy(); + root.render( + + { + await resetAllDatabases(); + window.location.reload(); + }} + /> + , + ); +} + +// QNBS-v3: filesystem failures get preserve-first actions while only IndexedDB failures retain destructive reset authority. +export async function renderProjectInitializationFailure( + root: Root, + error: unknown, +): Promise { + logger.error('Failed to initialize the application:', error); + const projectLoadError = error instanceof ProjectLoadError ? error : null; + const backendKind = await storageService.getStorageBackendKind(); + const copy = await loadStorageErrorCopy(); + const { canQuarantine, canReset } = getStartupRecoveryActions(error, backendKind); + const corruptProjectId = + projectLoadError?.reason === 'corrupt' ? projectLoadError.projectId : null; + root.render( + + { + if (!corruptProjectId) throw new ProjectQuarantineError('io-error'); + const result = await storageService.quarantineProject(corruptProjectId); + if (!result) throw new ProjectQuarantineError('io-error'); + window.location.reload(); + }, + } + : {})} + {...(canReset + ? { + onReset: async () => { + await resetAllDatabases(); + window.location.reload(); + }, + } + : {})} + /> + , + ); +} diff --git a/services/startupRecoveryPolicy.ts b/services/startupRecoveryPolicy.ts new file mode 100644 index 000000000..4fede0c95 --- /dev/null +++ b/services/startupRecoveryPolicy.ts @@ -0,0 +1,15 @@ +import { ProjectLoadError } from './fs/projectFsStore'; + +export type StartupStorageBackend = 'indexeddb' | 'filesystem'; + +// QNBS-v3: bind destructive reset to IndexedDB provenance so desktop filesystem failures stay preserve-first. +export function getStartupRecoveryActions( + error: unknown, + backend: StartupStorageBackend, +): { canQuarantine: boolean; canReset: boolean } { + const projectLoadError = error instanceof ProjectLoadError ? error : null; + return { + canQuarantine: projectLoadError?.reason === 'corrupt' && backend === 'filesystem', + canReset: !projectLoadError && backend === 'indexeddb', + }; +} diff --git a/tests/unit/StorageErrorScreen.test.tsx b/tests/unit/StorageErrorScreen.test.tsx new file mode 100644 index 000000000..b32627f1c --- /dev/null +++ b/tests/unit/StorageErrorScreen.test.tsx @@ -0,0 +1,142 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { + loadStorageErrorCopy, + STARTUP_COPY_FALLBACKS, + StorageErrorScreen, +} from '../../components/StorageErrorScreen'; +import { ProjectQuarantineError } from '../../services/fs/projectFsStore'; + +const { getStaticTranslation, loggerError } = vi.hoisted(() => ({ + getStaticTranslation: vi.fn(), + loggerError: vi.fn(), +})); + +vi.mock('../../services/i18n/staticTranslate', () => ({ getStaticTranslation })); +vi.mock('../../services/logger', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + logger: { debug: vi.fn(), warn: vi.fn(), info: vi.fn(), error: loggerError }, + }; +}); + +// QNBS-v3: preserve truthful, accessible recovery outcomes while preventing destructive action races. +describe('StorageErrorScreen', () => { + beforeEach(() => { + vi.clearAllMocks(); + getStaticTranslation.mockImplementation((key: string) => Promise.resolve(`translated:${key}`)); + }); + + it('loads translated recovery copy and falls back safely when a key is unavailable', async () => { + await expect(loadStorageErrorCopy()).resolves.toMatchObject({ + description: 'translated:error.startup.description', + recoveryAlreadyPreserved: 'translated:error.startup.recoveryAlreadyPreserved', + }); + + getStaticTranslation.mockImplementation((key: string) => Promise.resolve(key)); + await expect(loadStorageErrorCopy()).resolves.toEqual(STARTUP_COPY_FALLBACKS); + }); + + it('renders localized actions and never renders a raw recovery exception', async () => { + const user = userEvent.setup(); + const onRecover = vi.fn().mockRejectedValue(new Error('EACCES /private/project.json')); + const copy = { + ...STARTUP_COPY_FALLBACKS, + description: 'Localized description', + projectUnavailable: 'Localized project failure', + recover: 'Localized preserve', + recoveryFailed: 'Localized safe failure', + }; + + render(); + expect(screen.getByText('Localized description')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Localized preserve' })).toBeInTheDocument(); + + await user.click(screen.getByRole('button', { name: 'Localized preserve' })); + + expect(await screen.findByText('Localized safe failure')).toBeInTheDocument(); + expect(screen.queryByText('EACCES /private/project.json')).not.toBeInTheDocument(); + expect(loggerError).toHaveBeenCalledWith( + 'Project quarantine failed', + expect.objectContaining({ error: 'EACCES /private/project.json' }), + ); + }); + + it('announces recovery and blocks destructive reset while preservation is pending', async () => { + const user = userEvent.setup(); + let resolveRecovery: () => void = () => undefined; + const onRecover = vi.fn( + () => + new Promise((resolve) => { + resolveRecovery = resolve; + }), + ); + const onReset = vi.fn(); + + render( + , + ); + + await user.click(screen.getByRole('button', { name: STARTUP_COPY_FALLBACKS.recover })); + + expect(screen.getByRole('status')).toHaveTextContent(STARTUP_COPY_FALLBACKS.recovering); + expect(screen.getByRole('button', { name: STARTUP_COPY_FALLBACKS.reset })).toBeDisabled(); + await user.click(screen.getByRole('button', { name: STARTUP_COPY_FALLBACKS.reset })); + expect(onReset).not.toHaveBeenCalled(); + + resolveRecovery(); + await waitFor(() => expect(screen.getByRole('status')).toHaveTextContent('')); + }); + + it('keeps source-missing wording truthful when preservation cannot be confirmed', async () => { + const user = userEvent.setup(); + const onRecover = vi.fn().mockRejectedValue(new ProjectQuarantineError('source-missing')); + + render(); + + await user.click(screen.getByRole('button', { name: STARTUP_COPY_FALLBACKS.recover })); + + expect(await screen.findByText(STARTUP_COPY_FALLBACKS.recoveryUnknown)).toBeInTheDocument(); + expect(screen.queryByText(STARTUP_COPY_FALLBACKS.recoveryFailed)).not.toBeInTheDocument(); + }); + + it('reports the typed already-preserved outcome and supports successful recovery and reset actions', async () => { + const user = userEvent.setup(); + const onRecover = vi.fn().mockRejectedValue(new ProjectQuarantineError('already-preserved')); + const onReset = vi.fn(); + const firstRender = render( + , + ); + + await user.click(screen.getByRole('button', { name: STARTUP_COPY_FALLBACKS.recover })); + expect( + await screen.findByText(STARTUP_COPY_FALLBACKS.recoveryAlreadyPreserved), + ).toBeInTheDocument(); + + await user.click(screen.getByRole('button', { name: STARTUP_COPY_FALLBACKS.reset })); + expect(onReset).toHaveBeenCalledOnce(); + + firstRender.unmount(); + const successfulRecover = vi.fn().mockResolvedValue(undefined); + render(); + await user.click(screen.getByRole('button', { name: STARTUP_COPY_FALLBACKS.recover })); + await waitFor(() => expect(successfulRecover).toHaveBeenCalledOnce()); + expect( + screen.queryByRole('button', { name: STARTUP_COPY_FALLBACKS.reset }), + ).not.toBeInTheDocument(); + }); + + it('omits unsupported recovery actions when no handlers are provided', () => { + render(); + + expect( + screen.queryByRole('button', { name: STARTUP_COPY_FALLBACKS.recover }), + ).not.toBeInTheDocument(); + expect( + screen.queryByRole('button', { name: STARTUP_COPY_FALLBACKS.reset }), + ).not.toBeInTheDocument(); + expect(screen.getByText(STARTUP_COPY_FALLBACKS.storageUnavailable)).toBeInTheDocument(); + }); +}); diff --git a/tests/unit/startupRecovery.test.tsx b/tests/unit/startupRecovery.test.tsx new file mode 100644 index 000000000..19f657920 --- /dev/null +++ b/tests/unit/startupRecovery.test.tsx @@ -0,0 +1,134 @@ +import React from 'react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const { mockRoot, mockReset, mockBackendKind, mockQuarantine, mockCopy, loggerError } = vi.hoisted( + () => ({ + mockRoot: { render: vi.fn() }, + mockReset: vi.fn().mockResolvedValue(undefined), + mockBackendKind: vi.fn(), + mockQuarantine: vi.fn(), + mockCopy: { + description: 'description', + storageUnavailable: 'storage unavailable', + projectUnavailable: 'project unavailable', + reload: 'reload', + recover: 'recover', + recovering: 'recovering', + reset: 'reset', + quarantineNotice: 'quarantine notice', + recoveryFailed: 'recovery failed', + recoveryUnknown: 'recovery unknown', + recoveryAlreadyPreserved: 'already preserved', + resetWarning: 'reset warning', + }, + loggerError: vi.fn(), + }), +); + +vi.mock('../../services/dbInitialization', () => ({ resetAllDatabases: mockReset })); +vi.mock('../../services/storageService', () => ({ + storageService: { + getStorageBackendKind: mockBackendKind, + quarantineProject: mockQuarantine, + }, +})); +vi.mock('../../services/i18n/staticTranslate', () => ({ + getStaticTranslation: (key: string) => Promise.resolve(key), +})); +vi.mock('../../services/logger', () => ({ + logger: { debug: vi.fn(), warn: vi.fn(), info: vi.fn(), error: loggerError }, +})); +vi.mock('../../components/StorageErrorScreen', () => ({ + loadStorageErrorCopy: () => Promise.resolve(mockCopy), + StorageErrorScreen: () => React.createElement('div'), +})); +vi.mock('../../services/fs/projectFsStore', () => { + class ProjectLoadError extends Error { + constructor( + public readonly reason: 'corrupt' | 'io-error', + message: string, + public readonly projectId: string, + ) { + super(message); + this.name = 'ProjectLoadError'; + } + } + class ProjectQuarantineError extends Error { + constructor(public readonly reason: string) { + super('safe quarantine failure'); + this.name = 'ProjectQuarantineError'; + } + } + return { ProjectLoadError, ProjectQuarantineError }; +}); + +import { ProjectLoadError } from '../../services/fs/projectFsStore'; +import { + renderProjectInitializationFailure, + renderStorageInitializationFailure, +} from '../../services/startupRecovery'; + +type RecoveryScreenProps = { + copy: typeof mockCopy; + onReset?: () => Promise; + onRecover?: () => Promise; +}; + +function renderedScreenProps(): RecoveryScreenProps { + const strictMode = mockRoot.render.mock.calls.at(-1)?.[0] as React.ReactElement<{ + children: React.ReactElement; + }>; + return strictMode.props.children.props; +} + +// QNBS-v3: keep startup recovery actions constrained by the originating storage backend. +describe('startup recovery rendering', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('renders the database reset screen when IndexedDB initialization fails', async () => { + await renderStorageInitializationFailure(mockRoot as never); + + expect(mockRoot.render).toHaveBeenCalledOnce(); + expect(renderedScreenProps().copy).toBe(mockCopy); + expect(renderedScreenProps().onReset).toEqual(expect.any(Function)); + }); + + it('renders quarantine for corrupt filesystem projects and preserves the exact project ID', async () => { + mockBackendKind.mockResolvedValue('filesystem'); + mockQuarantine.mockResolvedValue({ projectId: 'p1', path: '/quarantine/p1' }); + await renderProjectInitializationFailure( + mockRoot as never, + new ProjectLoadError('corrupt', 'corrupt', 'p1'), + ); + + const props = renderedScreenProps(); + expect(props.onRecover).toEqual(expect.any(Function)); + expect(props.onReset).toBeUndefined(); + await (props.onRecover as () => Promise)(); + expect(mockQuarantine).toHaveBeenCalledWith('p1'); + }); + + it('does not expose reset for raw or project filesystem failures', async () => { + mockBackendKind.mockResolvedValue('filesystem'); + + await renderProjectInitializationFailure(mockRoot as never, new Error('EACCES /projects/p1')); + expect(renderedScreenProps().onReset).toBeUndefined(); + + await renderProjectInitializationFailure( + mockRoot as never, + new ProjectLoadError('io-error', 'io', 'p1'), + ); + expect(renderedScreenProps().onReset).toBeUndefined(); + expect(loggerError).toHaveBeenCalled(); + }); + + it('retains reset only for a non-project IndexedDB failure', async () => { + mockBackendKind.mockResolvedValue('indexeddb'); + await renderProjectInitializationFailure(mockRoot as never, new Error('QuotaExceededError')); + + expect(renderedScreenProps().onRecover).toBeUndefined(); + expect(renderedScreenProps().onReset).toEqual(expect.any(Function)); + }); +}); diff --git a/tests/unit/startupRecoveryPolicy.test.ts b/tests/unit/startupRecoveryPolicy.test.ts new file mode 100644 index 000000000..a264d83c1 --- /dev/null +++ b/tests/unit/startupRecoveryPolicy.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from 'vitest'; +import { ProjectLoadError } from '../../services/fs/projectFsStore'; +import { getStartupRecoveryActions } from '../../services/startupRecoveryPolicy'; + +// QNBS-v3: prevent filesystem corruption from acquiring destructive database-reset authority. +describe('startup recovery action policy', () => { + it('offers quarantine only for corrupt projects on the filesystem backend', () => { + const error = new ProjectLoadError('corrupt', 'corrupt', 'project-1'); + + expect(getStartupRecoveryActions(error, 'filesystem')).toEqual({ + canQuarantine: true, + canReset: false, + }); + expect(getStartupRecoveryActions(error, 'indexeddb')).toEqual({ + canQuarantine: false, + canReset: false, + }); + }); + + it('never offers destructive reset for filesystem-origin failures, including raw errors', () => { + expect(getStartupRecoveryActions(new Error('EACCES /projects/p1'), 'filesystem')).toEqual({ + canQuarantine: false, + canReset: false, + }); + expect( + getStartupRecoveryActions(new ProjectLoadError('io-error', 'io', 'project-1'), 'filesystem'), + ).toEqual({ + canQuarantine: false, + canReset: false, + }); + }); + + it('retains database reset for non-project failures from IndexedDB', () => { + expect(getStartupRecoveryActions(new Error('QuotaExceededError'), 'indexeddb')).toEqual({ + canQuarantine: false, + canReset: true, + }); + }); +});