Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
<img src="https://img.shields.io/badge/Release-v1.28.3-6366F1" alt="Release v1.28.3">
<img src="https://img.shields.io/badge/Storage-IndexedDB_v8-F59E0B" alt="IndexedDB v8">
<img src="https://img.shields.io/badge/PWA-v3.0-5BB974?logo=pwa" alt="PWA v3.0">
<img src="https://img.shields.io/badge/i18n-19_locales-2937_keys-0EA5E9" alt="i18n 19 locales — 2937 keys">
<img src="https://img.shields.io/badge/Tests-7317%2B_%2F_594_files-22C55E" alt="7317+ tests / 594 files">
<img src="https://img.shields.io/badge/i18n-19_locales-2939_keys-0EA5E9" alt="i18n 19 locales — 2939 keys">
<img src="https://img.shields.io/badge/Tests-7320%2B_%2F_594_files-22C55E" alt="7320+ tests / 594 files">
<img src="https://img.shields.io/codecov/c/github/qnbs/WorldScript-Studio?logo=codecov&label=Coverage" alt="Codecov Coverage">
<img src="https://img.shields.io/badge/License-MIT-22C55E" alt="License MIT">
<img src="https://img.shields.io/github/actions/workflow/status/qnbs/WorldScript-Studio/.github/workflows/ci.yml?branch=main&logo=github" alt="CI Status">
Expand Down Expand Up @@ -401,7 +401,7 @@ Infrastructure-level features that keep the app fast and extensible as projects

### 🌐 Full Multi-Language Support

Shipped UI locales with **2937 i18n keys** across all 19 languages — zero hardcoded user-facing strings:
Shipped UI locales with **2939 i18n keys** across all 19 languages — zero hardcoded user-facing strings:

- 🇩🇪 **German** (Deutsch)
- 🇬🇧 **English**
Expand Down Expand Up @@ -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`) | 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) |
| **i18n** | Custom React Context (`I18nContext.tsx`) | 2939 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 (7320+ 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` |
Expand Down Expand Up @@ -549,7 +549,7 @@ WorldScript-Studio/
│ ├── sw.js # PWA Service Worker
│ └── manifest.json # PWA Web App Manifest v3
├── tests/
│ ├── unit/ # Vitest unit tests (7317+ tests, 594 files) — count spans tests/, components/, packages/*/tests/, not just this folder
│ ├── unit/ # Vitest unit tests (7320+ tests, 594 files) — count spans tests/, components/, packages/*/tests/, not just this folder
│ │ ├── ai/ # aiSmallModules, aiCoreFallbackPaths
│ │ └── settings/ # WebLlmPanel, AiSections
│ └── e2e/ # Playwright specs + helpers.ts
Expand Down Expand Up @@ -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):**
- **7317+ unit tests** across **594 test files** — CI is authoritative for pass/fail
- **7320+ 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: **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)
- i18n: **2939 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.

Expand Down
56 changes: 46 additions & 10 deletions components/StorageErrorScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ import React from 'react';
import { ProjectQuarantineError } from '../services/fs/projectFsStore';
import { getStaticTranslation } from '../services/i18n/staticTranslate';
import { logger } from '../services/logger';
import type { StartupRecoveryFailureKind } from '../services/startupRecoveryPolicy';

export interface StorageErrorCopy {
description: string;
storageUnavailable: string;
projectUnavailable: string;
projectIoUnavailable: string;
reload: string;
retry: string;
recover: string;
recovering: string;
reset: string;
Expand All @@ -22,7 +25,10 @@ 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.',
projectIoUnavailable:
'The project could not currently be read because of a storage or file-access problem. It has not been classified as corrupt or changed. Check the storage device and file permissions, then retry.',
reload: 'Reload',
retry: 'Retry',
recover: 'Quarantine project and reload',
recovering: 'Preserving project…',
reset: 'Reset database and reload',
Expand All @@ -47,7 +53,9 @@ export async function loadStorageErrorCopy(): Promise<StorageErrorCopy> {
description,
storageUnavailable,
projectUnavailable,
projectIoUnavailable,
reload,
retry,
recover,
recovering,
reset,
Expand All @@ -66,7 +74,12 @@ export async function loadStorageErrorCopy(): Promise<StorageErrorCopy> {
'error.startup.projectUnavailable',
STARTUP_COPY_FALLBACKS.projectUnavailable,
),
startupTranslation(
'error.startup.projectIoUnavailable',
STARTUP_COPY_FALLBACKS.projectIoUnavailable,
),
startupTranslation('error.startup.reload', STARTUP_COPY_FALLBACKS.reload),
startupTranslation('error.startup.retry', STARTUP_COPY_FALLBACKS.retry),
startupTranslation('error.startup.recover', STARTUP_COPY_FALLBACKS.recover),
startupTranslation('error.startup.recovering', STARTUP_COPY_FALLBACKS.recovering),
startupTranslation('error.startup.reset', STARTUP_COPY_FALLBACKS.reset),
Expand All @@ -83,7 +96,9 @@ export async function loadStorageErrorCopy(): Promise<StorageErrorCopy> {
description,
storageUnavailable,
projectUnavailable,
projectIoUnavailable,
reload,
retry,
recover,
recovering,
reset,
Expand All @@ -95,23 +110,34 @@ export async function loadStorageErrorCopy(): Promise<StorageErrorCopy> {
};
}

// QNBS-v3: show stable localized recovery actions while keeping technical filesystem diagnostics in the logger.
// QNBS-v3: use explicit failure kinds so I/O retry, quarantine, and database reset cannot be inferred from callbacks.
export function StorageErrorScreen({
copy,
onReset,
onRecover,
onRetry,
failureKind,
}: {
copy: StorageErrorCopy;
failureKind: StartupRecoveryFailureKind;
onReset?: () => void;
onRecover?: () => Promise<void>;
onRetry?: () => void;
}) {
const [recoveryStatus, setRecoveryStatus] = React.useState<
'failed' | 'already-preserved' | 'source-missing' | null
>(null);
const [isRecovering, setIsRecovering] = React.useState(false);
const isProjectCorrupt = failureKind === 'project-corrupt';
const isProjectIo = failureKind === 'project-io';
const failureMessage = isProjectCorrupt
? copy.projectUnavailable
: isProjectIo
? copy.projectIoUnavailable
: copy.storageUnavailable;

const handleRecover = async () => {
if (!onRecover) return;
if (!onRecover || !isProjectCorrupt) return;
setRecoveryStatus(null);
setIsRecovering(true);
try {
Expand All @@ -132,6 +158,14 @@ export function StorageErrorScreen({
}
};

const handleRetry = () => {
if (onRetry) {
onRetry();
return;
}
window.location.reload();
};

return (
<div
style={{
Expand Down Expand Up @@ -161,12 +195,12 @@ export function StorageErrorScreen({
wordBreak: 'break-word',
}}
>
{onRecover ? copy.projectUnavailable : copy.storageUnavailable}
{failureMessage}
</p>
<div style={{ display: 'flex', gap: '0.75rem', flexWrap: 'wrap', justifyContent: 'center' }}>
<button
type="button"
onClick={() => window.location.reload()}
onClick={handleRetry}
style={{
padding: '0.5rem 1.25rem',
borderRadius: '0.5rem',
Expand All @@ -177,9 +211,9 @@ export function StorageErrorScreen({
fontFamily: 'inherit',
}}
>
{copy.reload}
{isProjectIo ? copy.retry : copy.reload}
</button>
{onRecover && (
{isProjectCorrupt && onRecover && (
<button
type="button"
onClick={() => void handleRecover()}
Expand All @@ -197,7 +231,7 @@ export function StorageErrorScreen({
{isRecovering ? copy.recovering : copy.recover}
</button>
)}
{onReset && (
{failureKind === 'storage' && onReset && (
<button
type="button"
onClick={() => {
Expand All @@ -218,12 +252,12 @@ export function StorageErrorScreen({
</button>
)}
</div>
{onRecover && (
{isProjectCorrupt && onRecover && (
<p style={{ fontSize: '0.75rem', color: '#94a3b8', maxWidth: '32rem' }}>
{copy.quarantineNotice}
</p>
)}
{onRecover && (
{isProjectCorrupt && onRecover && (
<p
role="status"
aria-live="polite"
Expand All @@ -245,7 +279,9 @@ export function StorageErrorScreen({
: ''}
</p>
)}
{onReset && <p style={{ fontSize: '0.75rem', color: '#475569' }}>{copy.resetWarning}</p>}
{failureKind === 'storage' && onReset && (
<p style={{ fontSize: '0.75rem', color: '#475569' }}>{copy.resetWarning}</p>
)}
</div>
);
}
2 changes: 2 additions & 0 deletions locales/ar/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -699,7 +699,9 @@
"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.projectIoUnavailable": "The project could not currently be read because of a storage or file-access problem. It has not been classified as corrupt or changed. Check the storage device and file permissions, then retry.",
"error.startup.reload": "Reload",
"error.startup.retry": "Retry",
"error.startup.recover": "Quarantine project and reload",
"error.startup.recovering": "Preserving project…",
"error.startup.reset": "Reset database and reload",
Expand Down
2 changes: 2 additions & 0 deletions locales/de/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -699,7 +699,9 @@
"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.projectIoUnavailable": "Das Projekt konnte derzeit aufgrund eines Speicher- oder Dateizugriffsproblems nicht gelesen werden. Es wurde nicht als beschädigt eingestuft und nicht verändert. Prüfen Sie die Verfügbarkeit des Speichers und die Dateiberechtigungen und versuchen Sie es erneut.",
"error.startup.reload": "Neu laden",
"error.startup.retry": "Erneut versuchen",
"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",
Expand Down
2 changes: 2 additions & 0 deletions locales/el/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -699,7 +699,9 @@
"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.projectIoUnavailable": "The project could not currently be read because of a storage or file-access problem. It has not been classified as corrupt or changed. Check the storage device and file permissions, then retry.",
"error.startup.reload": "Reload",
"error.startup.retry": "Retry",
"error.startup.recover": "Quarantine project and reload",
"error.startup.recovering": "Preserving project…",
"error.startup.reset": "Reset database and reload",
Expand Down
2 changes: 2 additions & 0 deletions locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -699,7 +699,9 @@
"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.projectIoUnavailable": "The project could not currently be read because of a storage or file-access problem. It has not been classified as corrupt or changed. Check the storage device and file permissions, then retry.",
"error.startup.reload": "Reload",
"error.startup.retry": "Retry",
"error.startup.recover": "Quarantine project and reload",
"error.startup.recovering": "Preserving project…",
"error.startup.reset": "Reset database and reload",
Expand Down
2 changes: 2 additions & 0 deletions locales/es/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -699,7 +699,9 @@
"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.projectIoUnavailable": "No se pudo leer el proyecto en este momento debido a un problema de almacenamiento o acceso a los archivos. No se ha clasificado como dañado ni se ha modificado. Comprueba la disponibilidad del dispositivo de almacenamiento y los permisos de los archivos, y vuelve a intentarlo.",
"error.startup.reload": "Recargar",
"error.startup.retry": "Reintentar",
"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",
Expand Down
2 changes: 2 additions & 0 deletions locales/eu/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -699,7 +699,9 @@
"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.projectIoUnavailable": "The project could not currently be read because of a storage or file-access problem. It has not been classified as corrupt or changed. Check the storage device and file permissions, then retry.",
"error.startup.reload": "Reload",
"error.startup.retry": "Retry",
"error.startup.recover": "Quarantine project and reload",
"error.startup.recovering": "Preserving project…",
"error.startup.reset": "Reset database and reload",
Expand Down
2 changes: 2 additions & 0 deletions locales/fa/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -699,7 +699,9 @@
"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.projectIoUnavailable": "The project could not currently be read because of a storage or file-access problem. It has not been classified as corrupt or changed. Check the storage device and file permissions, then retry.",
"error.startup.reload": "Reload",
"error.startup.retry": "Retry",
"error.startup.recover": "Quarantine project and reload",
"error.startup.recovering": "Preserving project…",
"error.startup.reset": "Reset database and reload",
Expand Down
2 changes: 2 additions & 0 deletions locales/fi/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -699,7 +699,9 @@
"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.projectIoUnavailable": "The project could not currently be read because of a storage or file-access problem. It has not been classified as corrupt or changed. Check the storage device and file permissions, then retry.",
"error.startup.reload": "Reload",
"error.startup.retry": "Retry",
"error.startup.recover": "Quarantine project and reload",
"error.startup.recovering": "Preserving project…",
"error.startup.reset": "Reset database and reload",
Expand Down
2 changes: 2 additions & 0 deletions locales/fr/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -699,7 +699,9 @@
"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.projectIoUnavailable": "Le projet n’a pas pu être lu pour le moment en raison d’un problème de stockage ou d’accès aux fichiers. Il n’a pas été considéré comme corrompu et n’a pas été modifié. Vérifiez la disponibilité du support de stockage et les autorisations des fichiers, puis réessayez.",
"error.startup.reload": "Recharger",
"error.startup.retry": "Réessayer",
"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",
Expand Down
2 changes: 2 additions & 0 deletions locales/he/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -699,7 +699,9 @@
"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.projectIoUnavailable": "The project could not currently be read because of a storage or file-access problem. It has not been classified as corrupt or changed. Check the storage device and file permissions, then retry.",
"error.startup.reload": "Reload",
"error.startup.retry": "Retry",
"error.startup.recover": "Quarantine project and reload",
"error.startup.recovering": "Preserving project…",
"error.startup.reset": "Reset database and reload",
Expand Down
Loading
Loading