- Status: Accepted (PR-C1 on-demand panel + PR-C2 live inline overlay shipped; new-locale rollout staged)
- Date: 2026-06-25
- Deciders: Maintainer + Claude Code
- Context tags: editor, i18n, privacy, grammar, languagetool
The manuscript editor advertised "spell-check with suggestions" and "grammar & style hints", but the
implementation was a hardcoded list of ~16 English + ~16 German typos (TYPOS_EN/TYPOS_DE in
ManuscriptEditor.tsx) β not a real checker, no other languages, no grammar. Separately,
services/languageToolClient.ts existed only as a connectivity ping (a gate +
POST /v2/check with language=en-US), never parsing results; and the Writer "grammar check" tool is
an AI/Gemini prompt, unrelated and token-costly.
The app is offline-first and privacy-first: API keys live only in the browser, analytics are gated, and manuscript text must not leave the device without explicit consent. Any real grammar feature has to respect that. We also ship 19 locales of widely varying maturity, and the proofreading engine we pick does not support all of them.
Adopt LanguageTool as the real grammar/spell engine, self-hosted only, integrated through the editor's existing overlay + popover rather than a new rendering layer.
- Self-hosted, privacy-gated. Keep
assertLanguageToolAllowedas the gate: underprivacy.localStorageOnly, onlylocalhost/127.0.0.1base URLs are allowed β a cloud LanguageTool can never silently receive prose. Off by default; the user runs the server (docker run -p 8010:8010 erikvl87/languagetool). The service never logs text and degrades silently on any failure. - One service, one apply path.
services/languageToolService.tsdoes the real work (checkTextβ parsematches[], text-hash cache, abortable, offset-safeapplyMatchReplacementwith an anchor check).hooks/useLanguageToolCheck.tsorchestrates both surfaces so the on-demand panel and the live overlay share gating, apply, ignore, and dictionary logic. - Locale support in the SSOT.
i18n/locales.tscarrieslanguageToolSupport+languageToolCode; the feature is hidden for unsupported locales (none) β verified data, no fake promise (notably Turkish, Hebrew, Finnish, Hungarian, Icelandic, Basque, Korean are unsupported). - Reuse the editor overlay. The editor already renders an aligned, font-matched overlay with clickable spell-error spans + a suggestion popover. Live underlines map LanguageTool single-word matches to their exact offset in that overlay; multi-word grammar findings are surfaced by the on-demand panel instead of inventing a measured-span overlay.
- Cloud LanguageTool / a paid proofreading API β rejected: sends manuscript text off-device, violates the privacy model, adds cost and a network dependency.
- Bundle a WASM/JS grammar engine β rejected for now: large download, narrower language coverage than LanguageTool's server, and duplicates the heavy-model concerns we already manage for voice/RAG.
- Keep using the AI
grammarCheckprompt for everything β rejected as the primary path: non-deterministic, token-costly, and not a real per-token diagnostics layer (it stays available as a separate creative tool). - A new offset-driven underline overlay (full phrase underlines) β deferred: a larger, riskier rewrite of a core editor file; the single-word overlay + panel covers the common case now.
- Positive: real multilingual spell/grammar checking; deterministic and free (no tokens); privacy-preserving by construction; minimal new UI surface (reuses overlay + popover); offset-safe, undoable corrections shared across both entry points.
- Negative / trade-offs: requires the user to run a local server (no bundled engine); inline underlines are single-word only (phrase grammar lives in the panel); coverage is uneven across locales and absent for several.
- Follow-ups: roll out new high-coverage UI locales (pl/nl/tr/uk/ro) β tracked separately; a future ADR could revisit a full offset-driven inline layer if phrase-level underlines are wanted.
The default self-hosted port (http://localhost:8010 / http://127.0.0.1:8010) was missing from
connect-src on all 5 CSP surfaces (index.html, vercel.json, public/_headers,
nginx.conf Γ3, src-tauri/tauri.conf.json) from this feature's initial ship through 2026-07-29 β
not just Tauri, as the originating audit (F-08) assumed. The audit's reasoning ("the web https:
scheme-source masks the gap") does not hold here: LanguageTool's default URL is http://, not
https://, and CSP source-matching is scheme-sensitive β a bare https: token never matches an
http:// origin. LanguageTool has never worked against its own default local server, on any
platform, under a CSP-enforcing browser, since it shipped (#235/#236/#238). Fixed on all 5
surfaces by adding the explicit port, alongside the existing 11434/1234/8000 local-service
entries. Regression test: tests/unit/cspCorrectness.test.ts
(describe('CSP correctness β connect-src completeness (F-08)'), asserted across all 5 surfaces).
See docs/LANGUAGETOOL.md for the feature guide and setup.