Skip to content
Merged
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<img src="https://img.shields.io/badge/Version-v1.24.3-6366F1" alt="v1.24.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-2855_keys-0EA5E9" alt="i18n 19 locales — 2855 keys">
<img src="https://img.shields.io/badge/i18n-19_locales-2856_keys-0EA5E9" alt="i18n 19 locales — 2856 keys">
<img src="https://img.shields.io/badge/Tests-6477_%2F_529_files-22C55E" alt="6477 tests / 529 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">
Expand Down Expand Up @@ -383,7 +383,7 @@ Infrastructure-level features that keep the app fast and extensible as projects

### 🌐 Full Multi-Language Support

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

- 🇩🇪 **German** (Deutsch)
- 🇬🇧 **English**
Expand Down Expand Up @@ -492,7 +492,7 @@ 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`) | 2855 keys × 19 locales (de/en/es/fr/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu Beta); EN fallback; `localStorage` persistence |
| **i18n** | Custom React Context (`I18nContext.tsx`) | 2856 keys × 19 locales (de/en/es/fr/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu Beta); EN fallback; `localStorage` persistence |
| **Testing** | Vitest 4.x (6477 tests / 529 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 |
Expand Down Expand Up @@ -694,7 +694,7 @@ The main pipeline is [`.github/workflows/ci.yml`](.github/workflows/ci.yml). Opt
**Current test metrics (2026-07-30, CI-reported):**
- **6477 unit tests** across **529 test files** — all passing
- Coverage thresholds: lines ≥ 74 · branches ≥ 60 · functions ≥ 67 · statements ≥ 72 — enforced in CI (see Codecov badge for live metrics)
- i18n: **2855 keys × 19 locales** (en/de/fr/es/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu Beta)
- i18n: **2856 keys × 19 locales** (en/de/fr/es/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu 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
69 changes: 68 additions & 1 deletion components/settings/AiProviderCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ export const AiProviderCard: FC<AiProviderCardProps> = ({
// QNBS-v3: Grok's own key input state, mirroring OpenAI's pattern above.
const [grokKey, setGrokKey] = useState('');
const [isSavingGrokKey, setIsSavingGrokKey] = useState(false);
const [anthropicKey, setAnthropicKey] = useState('');
const [isSavingAnthropicKey, setIsSavingAnthropicKey] = useState(false);
const [ollamaModels, setOllamaModels] = useState<string[]>([]);
const [testStatus, setTestStatus] = useState<'idle' | 'loading' | 'ok' | 'error'>('idle');
const [testError, setTestError] = useState('');
Expand All @@ -73,6 +75,10 @@ export const AiProviderCard: FC<AiProviderCardProps> = ({
.getApiKey('grok')
.then((k) => setGrokKey(k ?? ''))
.catch(() => {});
storageService
.getApiKey('anthropic')
.then((k) => setAnthropicKey(k ?? ''))
.catch(() => {});
}, []);

// QNBS-v3: save/clear via storageService, matching every other provider's key persistence.
Expand All @@ -89,6 +95,19 @@ export const AiProviderCard: FC<AiProviderCardProps> = ({
}
}, [grokKey]);

const handleSaveAnthropicKey = useCallback(async () => {
setIsSavingAnthropicKey(true);
try {
if (anthropicKey.trim()) {
await storageService.saveApiKey('anthropic', anthropicKey.trim());
} else {
await storageService.clearApiKey('anthropic');
}
} finally {
setIsSavingAnthropicKey(false);
}
}, [anthropicKey]);

const handleSaveOpenAiKey = useCallback(async () => {
setIsSavingKey(true);
try {
Expand Down Expand Up @@ -646,7 +665,55 @@ export const AiProviderCard: FC<AiProviderCardProps> = ({
</div>
)}

{provider === 'anthropic' && (
{/* QNBS-v3 (ADR-0016 Track A): desktop bypasses Anthropic's CORS restriction via the
native-HTTP path (localServerFetch) — real key input instead of the warning-only
block web/PWA still needs until Track B's proxy ships. */}
{provider === 'anthropic' && isDesktop && (
<div className="space-y-3">
<label
htmlFor="anthropic-api-key"
className="text-sm font-medium text-[var(--sc-text-secondary)] block"
>
{t('settings.ai.anthropicKey')}
</label>
<div className="flex gap-2">
<Input
id="anthropic-api-key"
type="password"
placeholder="sk-ant-..."
value={anthropicKey}
onChange={(e) => setAnthropicKey(e.target.value)}
className="flex-1 font-mono text-sm"
/>
<Button
onClick={handleSaveAnthropicKey}
disabled={isSavingAnthropicKey}
variant="secondary"
>
{isSavingAnthropicKey ? <Spinner className="w-4 h-4" /> : t('settings.ai.save')}
</Button>
</div>
<p className="text-xs text-[var(--sc-text-muted)]">{t('settings.ai.keysEncrypted')}</p>
<label
htmlFor="anthropic-model"
className="text-sm font-medium text-[var(--sc-text-secondary)] block"
>
{t('settings.advancedAi.model')}
</label>
<Select
id="anthropic-model"
value={advancedAi.model}
onChange={(v) => onModelSelect?.(v)}
options={[
{ value: 'claude-opus-4-7', label: 'Claude Opus 4.7' },
{ value: 'claude-sonnet-4-6', label: 'Claude Sonnet 4.6' },
{ value: 'claude-haiku-4-5', label: 'Claude Haiku 4.5' },
]}
/>
</div>
)}

{provider === 'anthropic' && !isDesktop && (
<div className="p-3 rounded-lg bg-[var(--sc-warning-bg)] border border-[var(--sc-warning-border)] text-sm text-[var(--sc-warning-fg)]">
<p className="font-semibold mb-1 flex items-center gap-1">
<Icon name="warning" size="sm" aria-hidden="true" />
Expand Down
2 changes: 1 addition & 1 deletion docs/i18n/AUDIT_AND_IMPROVEMENT_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

## 1. Current state (as audited)

- **19 locales × 21 modules**, ~2855 keys, full **key parity** enforced by
- **19 locales × 21 modules**, ~2856 keys, full **key parity** enforced by
`scripts/check-i18n-keys.mjs` (CI `quality` job) + bundled to `public/locales/<lang>/bundle.json`.
- Runtime: custom `contexts/I18nContext.tsx` — lazy `bundle.json` fetch, `localStorage` persistence
(`worldscript-language`), English fallback chain, cached `Intl.*` helpers, `Intl.Segmenter` word
Expand Down
1 change: 1 addition & 0 deletions locales/ar/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
"settings.ai.adaptive.warmedModelsHint": "نماذج جاهزة في الذاكرة لاستدلال سريع",
"settings.ai.anthropicCorsNote": "لا يمكن استدعاء Anthropic Claude مباشرة من المتصفّح. أعدّ خادم وكيل أو استخدم تطبيق Tauri لسطح المكتب لاستدعاءات Anthropic.",
"settings.ai.anthropicHint": "يتطلب Anthropic خادم وكيل أو تطبيق Tauri لسطح المكتب لاستدعاءات API.",
"settings.ai.anthropicKey": "Claude API Key",
"settings.ai.connectionFailed": "فشل الاتصال",
"settings.ai.connectionSuccess": "نجح الاتصال",
"settings.ai.corsRestriction": "قيد CORS",
Expand Down
5 changes: 3 additions & 2 deletions locales/de/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,9 @@
"settings.ai.adaptive.vramTier": "GPU-VRAM-Stufe",
"settings.ai.adaptive.warmedModels": "Geladene Modelle",
"settings.ai.adaptive.warmedModelsHint": "Modelle im Speicher für schnelle Inferenz bereit",
"settings.ai.anthropicCorsNote": "Anthropic Claude kann nicht direkt aus dem Browser aufgerufen werden. Richte einen Proxy-Server ein oder nutze die Tauri-Desktop-App für Anthropic-Aufrufe.",
"settings.ai.anthropicHint": "Anthropic erfordert einen Proxy-Server oder die Tauri-Desktop-App für API-Aufrufe.",
"settings.ai.anthropicCorsNote": "Anthropic Claude kann nicht direkt aus dem Browser aufgerufen werden. Nutze die Tauri-Desktop-App für Anthropic-Aufrufe oder warte auf die geplante Proxy-Server-Unterstützung.",
"settings.ai.anthropicHint": "Anthropic erfordert die Tauri-Desktop-App oder einen Proxy-Server (in Vorbereitung) für Aufrufe im Browser.",
"settings.ai.anthropicKey": "Claude API-Schlüssel",
"settings.ai.connectionFailed": "Verbindung fehlgeschlagen",
"settings.ai.connectionSuccess": "Verbindung erfolgreich",
"settings.ai.corsRestriction": "CORS-Einschränkung",
Expand Down
1 change: 1 addition & 0 deletions locales/el/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
"settings.ai.adaptive.warmedModelsHint": "Μοντέλα έτοιμα στη μνήμη για γρήγορη εξαγωγή συμπερασμάτων",
"settings.ai.anthropicCorsNote": "Το Anthropic Claude δεν μπορεί να κληθεί απευθείας από το πρόγραμμα περιήγησης. Ρυθμίστε έναν διακομιστή μεσολάβησης ή χρησιμοποιήστε την εφαρμογή επιφάνειας εργασίας Tauri για κλήσεις Anthropic.",
"settings.ai.anthropicHint": "Το Anthropic απαιτεί διακομιστή μεσολάβησης ή την εφαρμογή επιφάνειας εργασίας Tauri για κλήσεις API.",
"settings.ai.anthropicKey": "Claude API Key",
"settings.ai.connectionFailed": "Η σύνδεση απέτυχε",
"settings.ai.connectionSuccess": "Επιτυχής σύνδεση",
"settings.ai.corsRestriction": "Περιορισμός CORS",
Expand Down
5 changes: 3 additions & 2 deletions locales/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,9 @@
"settings.ai.adaptive.vramTier": "GPU VRAM tier",
"settings.ai.adaptive.warmedModels": "Loaded models",
"settings.ai.adaptive.warmedModelsHint": "Models ready in memory for fast inference",
"settings.ai.anthropicCorsNote": "Anthropic Claude cannot be called directly from the browser. Set up a proxy server or use the Tauri desktop app for Anthropic calls.",
"settings.ai.anthropicHint": "Anthropic requires a proxy server or the Tauri desktop app for API calls.",
"settings.ai.anthropicCorsNote": "Anthropic Claude cannot be called directly from the browser. Use the Tauri desktop app for Anthropic calls, or wait for the upcoming proxy server support.",
"settings.ai.anthropicHint": "Anthropic requires the Tauri desktop app, or a proxy server (coming soon) for browser API calls.",
"settings.ai.anthropicKey": "Claude API Key",
"settings.ai.connectionFailed": "Connection failed",
"settings.ai.connectionSuccess": "Connection successful",
"settings.ai.corsRestriction": "CORS Restriction",
Expand Down
5 changes: 3 additions & 2 deletions locales/es/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,9 @@
"settings.ai.adaptive.vramTier": "Nivel de VRAM GPU",
"settings.ai.adaptive.warmedModels": "Modelos cargados",
"settings.ai.adaptive.warmedModelsHint": "Modelos en memoria listos para inferencia rápida",
"settings.ai.anthropicCorsNote": "Anthropic Claude no puede llamarse directamente desde el navegador. Configura un servidor proxy o usa la aplicación de escritorio Tauri para llamadas de Anthropic.",
"settings.ai.anthropicHint": "Anthropic requiere un servidor proxy o la aplicación de escritorio Tauri para llamadas API.",
"settings.ai.anthropicCorsNote": "Anthropic Claude no puede llamarse directamente desde el navegador. Usa la aplicación de escritorio Tauri para llamadas de Anthropic, o espera el próximo soporte de servidor proxy.",
"settings.ai.anthropicHint": "Anthropic requiere la aplicación de escritorio Tauri, o un servidor proxy (próximamente) para llamadas desde el navegador.",
"settings.ai.anthropicKey": "Clave API de Claude",
"settings.ai.connectionFailed": "Conexión fallida",
"settings.ai.connectionSuccess": "Conexión exitosa",
"settings.ai.corsRestriction": "Restricción CORS",
Expand Down
1 change: 1 addition & 0 deletions locales/eu/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
"settings.ai.adaptive.warmedModelsHint": "Memorian prest dauden ereduak inferentzia azkarrak egiteko",
"settings.ai.anthropicCorsNote": "Anthropic Claude ezin da zuzenean arakatzailetik deitu. Konfiguratu proxy zerbitzari bat edo erabili Tauri mahaigaineko aplikazioa Anthropic deietarako.",
"settings.ai.anthropicHint": "Anthropic-ek proxy zerbitzari bat edo Tauri mahaigaineko aplikazioa behar du API deiak egiteko.",
"settings.ai.anthropicKey": "Claude API Key",
"settings.ai.connectionFailed": "Konexioak huts egin du",
"settings.ai.connectionSuccess": "Konexioa arrakastatsua da",
"settings.ai.corsRestriction": "CORS Murrizketa",
Expand Down
1 change: 1 addition & 0 deletions locales/fa/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
"settings.ai.adaptive.warmedModelsHint": "مدل های آماده در حافظه برای استنتاج سریع",
"settings.ai.anthropicCorsNote": "Anthropic Claude را نمی توان مستقیماً از مرورگر فراخوانی کرد. یک سرور پراکسی راه اندازی کنید یا از برنامه دسکتاپ Tauri برای تماس های Anthropic استفاده کنید.",
"settings.ai.anthropicHint": "Anthropic به یک سرور پراکسی یا برنامه دسکتاپ Tauri برای تماس‌های API نیاز دارد.",
"settings.ai.anthropicKey": "Claude API Key",
"settings.ai.connectionFailed": "اتصال ناموفق بود",
"settings.ai.connectionSuccess": "اتصال با موفقیت انجام شد",
"settings.ai.corsRestriction": "محدودیت CORS",
Expand Down
1 change: 1 addition & 0 deletions locales/fi/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
"settings.ai.adaptive.warmedModelsHint": "Mallit valmiina muistiin nopeaa päättelyä varten",
"settings.ai.anthropicCorsNote": "Anthropic Claudea ei voi kutsua suoraan selaimesta. Määritä välityspalvelin tai käytä Tauri-työpöytäsovellusta antrooppisiin puheluihin.",
"settings.ai.anthropicHint": "Anthropic vaatii välityspalvelimen tai Tauri-työpöytäsovelluksen API-kutsuja varten.",
"settings.ai.anthropicKey": "Claude API Key",
"settings.ai.connectionFailed": "Yhteys epäonnistui",
"settings.ai.connectionSuccess": "Yhteys onnistui",
"settings.ai.corsRestriction": "CORS-rajoitus",
Expand Down
5 changes: 3 additions & 2 deletions locales/fr/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,9 @@
"settings.ai.adaptive.vramTier": "Niveau VRAM GPU",
"settings.ai.adaptive.warmedModels": "Modèles chargés",
"settings.ai.adaptive.warmedModelsHint": "Modèles en mémoire prêts pour une inférence rapide",
"settings.ai.anthropicCorsNote": "Anthropic Claude ne peut pas être appelé directement depuis le navigateur. Configurez un serveur proxy ou utilisez l'application de bureau Tauri pour les appels Anthropic.",
"settings.ai.anthropicHint": "Anthropic nécessite un serveur proxy ou l'application de bureau Tauri pour les appels API.",
"settings.ai.anthropicCorsNote": "Anthropic Claude ne peut pas être appelé directement depuis le navigateur. Utilisez l'application de bureau Tauri pour les appels Anthropic, ou attendez le futur support de serveur proxy.",
"settings.ai.anthropicHint": "Anthropic nécessite l'application de bureau Tauri, ou un serveur proxy (à venir) pour les appels depuis le navigateur.",
"settings.ai.anthropicKey": "Clé API Claude",
"settings.ai.connectionFailed": "Connexion échouée",
"settings.ai.connectionSuccess": "Connexion réussie",
"settings.ai.corsRestriction": "Restriction CORS",
Expand Down
1 change: 1 addition & 0 deletions locales/he/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
"settings.ai.adaptive.warmedModelsHint": "מודלים מוכנים בזיכרון לאינפרנס מהיר",
"settings.ai.anthropicCorsNote": "לא ניתן לקרוא ל‑Anthropic Claude ישירות מהדפדפן. הקימו שרת פרוקסי או השתמשו באפליקציית Tauri לשולחן העבודה לקריאות Anthropic.",
"settings.ai.anthropicHint": "‏Anthropic דורש שרת פרוקסי או את אפליקציית Tauri לשולחן העבודה לקריאות API.",
"settings.ai.anthropicKey": "Claude API Key",
"settings.ai.connectionFailed": "החיבור נכשל",
"settings.ai.connectionSuccess": "החיבור הצליח",
"settings.ai.corsRestriction": "הגבלת CORS",
Expand Down
1 change: 1 addition & 0 deletions locales/hu/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
"settings.ai.adaptive.warmedModelsHint": "A modellek készen állnak a memóriában a gyors következtetés érdekében",
"settings.ai.anthropicCorsNote": "Az Anthropic Claude nem hívható közvetlenül a böngészőből. Hozzon létre egy proxyszervert, vagy használja a Tauri asztali alkalmazást antropikus hívásokhoz.",
"settings.ai.anthropicHint": "Az Anthropic proxykiszolgálót vagy a Tauri asztali alkalmazást igényel API-hívásokhoz.",
"settings.ai.anthropicKey": "Claude API Key",
"settings.ai.connectionFailed": "A csatlakozás sikertelen",
"settings.ai.connectionSuccess": "Sikeres csatlakozás",
"settings.ai.corsRestriction": "CORS korlátozás",
Expand Down
1 change: 1 addition & 0 deletions locales/is/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
"settings.ai.adaptive.warmedModelsHint": "Líkön tilbúin í minni fyrir hraða ályktanir",
"settings.ai.anthropicCorsNote": "Ekki er hægt að hringja í Anthropic Claude beint úr vafranum. Settu upp proxy-miðlara eða notaðu Tauri skrifborðsforritið fyrir antropísk símtöl.",
"settings.ai.anthropicHint": "Anthropic krefst proxy-þjóns eða Tauri skrifborðsforritsins fyrir API símtöl.",
"settings.ai.anthropicKey": "Claude API Key",
"settings.ai.connectionFailed": "Tenging mistókst",
"settings.ai.connectionSuccess": "Tenging tókst",
"settings.ai.corsRestriction": "CORS takmörkun",
Expand Down
5 changes: 3 additions & 2 deletions locales/it/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,9 @@
"settings.ai.adaptive.vramTier": "Livello VRAM GPU",
"settings.ai.adaptive.warmedModels": "Modelli caricati",
"settings.ai.adaptive.warmedModelsHint": "Modelli in memoria pronti per inferenza rapida",
"settings.ai.anthropicCorsNote": "Anthropic Claude non può essere chiamato direttamente dal browser. Configura un server proxy o usa l'app desktop Tauri per le chiamate Anthropic.",
"settings.ai.anthropicHint": "Anthropic richiede un server proxy o l'app desktop Tauri per le chiamate API.",
"settings.ai.anthropicCorsNote": "Anthropic Claude non può essere chiamato direttamente dal browser. Usa l'app desktop Tauri per le chiamate Anthropic, oppure attendi il supporto proxy in arrivo.",
"settings.ai.anthropicHint": "Anthropic richiede l'app desktop Tauri, oppure un server proxy (in arrivo) per le chiamate dal browser.",
"settings.ai.anthropicKey": "Chiave API Claude",
"settings.ai.connectionFailed": "Connessione fallita",
"settings.ai.connectionSuccess": "Connessione riuscita",
"settings.ai.corsRestriction": "Restrizione CORS",
Expand Down
Loading
Loading