Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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-2925_keys-0EA5E9" alt="i18n 19 locales — 2925 keys">
<img src="https://img.shields.io/badge/Tests-7266%2B_%2F_591_files-22C55E" alt="7266+ tests / 591 files">
<img src="https://img.shields.io/badge/i18n-19_locales-2936_keys-0EA5E9" alt="i18n 19 locales — 2936 keys">
<img src="https://img.shields.io/badge/Tests-7282%2B_%2F_594_files-22C55E" alt="7282+ 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 **2925 i18n keys** across all 19 languages — zero hardcoded user-facing strings:
Shipped UI locales with **2936 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`) | 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 (7266+ tests / 591 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) |
| **i18n** | Custom React Context (`I18nContext.tsx`) | 2936 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 (7282+ 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 (7266+ tests, 591 files) — count spans tests/, components/, packages/*/tests/, not just this folder
│ ├── unit/ # Vitest unit tests (7282+ 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-27, source-synchronized; CI remains authoritative for pass/fail):**
- **7266+ unit tests** across **591 test files** — CI is authoritative for pass/fail
- **7282+ 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: **2936 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
226 changes: 226 additions & 0 deletions components/StorageErrorScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
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;
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.',
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<string> {
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<StorageErrorCopy> {
const [
description,
storageUnavailable,
projectUnavailable,
reload,
recover,
recovering,
reset,
quarantineNotice,
recoveryFailed,
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.recoveryAlreadyPreserved',
STARTUP_COPY_FALLBACKS.recoveryAlreadyPreserved,
),
startupTranslation('error.startup.resetWarning', STARTUP_COPY_FALLBACKS.resetWarning),
]);
return {
description,
storageUnavailable,
projectUnavailable,
reload,
recover,
recovering,
reset,
quarantineNotice,
recoveryFailed,
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<void>;
}) {
const [recoveryStatus, setRecoveryStatus] = React.useState<'failed' | 'already-preserved' | 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'
: 'failed',
);
setIsRecovering(false);
}
};

return (
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
minHeight: '100vh',
padding: '2rem',
fontFamily: 'Inter, system-ui, sans-serif',
background: '#0f172a',
color: '#f1f5f9',
textAlign: 'center',
gap: '1rem',
}}
>
<h1 style={{ fontSize: '1.5rem', fontWeight: 700 }}>WorldScript Studio</h1>
<p style={{ color: '#94a3b8', maxWidth: '32rem' }}>{copy.description}</p>
<p
style={{
background: '#1e293b',
borderRadius: '0.5rem',
padding: '0.75rem 1rem',
fontSize: '0.875rem',
color: '#fca5a5',
maxWidth: '32rem',
wordBreak: 'break-word',
}}
>
{onRecover ? copy.projectUnavailable : copy.storageUnavailable}
</p>
<div style={{ display: 'flex', gap: '0.75rem', flexWrap: 'wrap', justifyContent: 'center' }}>
<button
type="button"
onClick={() => window.location.reload()}
style={{
padding: '0.5rem 1.25rem',
borderRadius: '0.5rem',
border: '1px solid #334155',
background: '#1e293b',
color: '#f1f5f9',
cursor: 'pointer',
fontFamily: 'inherit',
}}
>
{copy.reload}
</button>
{onRecover && (
<button
type="button"
onClick={() => void handleRecover()}
disabled={isRecovering}
style={{
padding: '0.5rem 1.25rem',
borderRadius: '0.5rem',
border: 'none',
background: '#2563eb',
color: '#fff',
cursor: isRecovering ? 'wait' : 'pointer',
fontFamily: 'inherit',
}}
>
{isRecovering ? copy.recovering : copy.recover}
</button>
)}
{onReset && (
<button
type="button"
onClick={onReset}
style={{
padding: '0.5rem 1.25rem',
borderRadius: '0.5rem',
border: 'none',
background: '#dc2626',
color: '#fff',
cursor: 'pointer',
fontFamily: 'inherit',
}}
>
{copy.reset}
</button>
)}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</div>
{onRecover && (
<p style={{ fontSize: '0.75rem', color: '#94a3b8', maxWidth: '32rem' }}>
{copy.quarantineNotice}
</p>
)}
{recoveryStatus && (
<p style={{ fontSize: '0.875rem', color: '#fca5a5', maxWidth: '32rem' }}>
Comment thread
qnbs marked this conversation as resolved.
Outdated
{recoveryStatus === 'already-preserved'
? copy.recoveryAlreadyPreserved
: copy.recoveryFailed}
</p>
)}
Comment thread
qnbs marked this conversation as resolved.
Outdated
{onReset && <p style={{ fontSize: '0.75rem', color: '#475569' }}>{copy.resetWarning}</p>}
</div>
);
}
Loading
Loading