Compose: spelling and grammar check with LanguageTool
A self-hosted LanguageTool (dev: docker/docker-compose.yml, started by `make up`) checks what the user writes in the compose window, through POST /api/spellcheck — the browser never talks to the engine directly. - Marks are CSS highlights over the text, never written into the message: red wavy for spelling, blue for grammar/style. Clicking one shows the message, up to five suggestions (applied as typed, so Ctrl+Z undoes them) and Ignore. The popup, like the link popup, opens above its anchor when there is no room below. - Checked after a pause in typing; the signature and quoted original are skipped; marks on words just edited drop at once; a check overtaken by a newer one, by turning checking off, or by closing the window is discarded. - With "auto", text is checked in chunks of lines, each in its own detected language — LanguageTool settles on one language per request, so a mixed Polish/English message had every English word flagged as a Polish typo. - Settings > Appearance: on/off and language (Auto-detect or any of the engine's languages); the compose window's More menu overrides it for one message. The browser's own spellcheck steps aside while ours runs, and comes back when it is off or the server has no engine (503 from the endpoint). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
@@ -24,6 +25,10 @@ _ALLOWED_INVITE_POLICY = {"add_update", "update_only", "none"}
|
||||
_ALLOWED_INVITE_FROM = {"contacts", "anyone"}
|
||||
|
||||
|
||||
# "auto", or a LanguageTool language code such as "pl-PL", "en-GB", "de".
|
||||
_LANG_CODE = re.compile(r"auto|[a-z]{2,3}(-[A-Za-z0-9]{2,8})*")
|
||||
|
||||
|
||||
class ReminderPref(BaseModel):
|
||||
method: str = "alert" # alert = in-app notification, email = email me
|
||||
minutes: int = 0 # minutes before the event start (0 = at time of event)
|
||||
@@ -42,6 +47,10 @@ class SettingsOut(BaseModel):
|
||||
invitation_calendar: str = "" # calendar id emailed events land in (blank => default)
|
||||
invitation_file: bool = False # also mark the invite message read + filed
|
||||
allowed_image_senders: list[str] = []
|
||||
# Spelling and grammar check in the compose window, and its language
|
||||
# ("auto" detects it per message).
|
||||
spellcheck_enabled: bool = True
|
||||
spellcheck_language: str = "auto"
|
||||
|
||||
|
||||
class SettingsPatch(BaseModel):
|
||||
@@ -56,6 +65,8 @@ class SettingsPatch(BaseModel):
|
||||
invitation_from: str | None = None
|
||||
invitation_calendar: str | None = None
|
||||
invitation_file: bool | None = None
|
||||
spellcheck_enabled: bool | None = None
|
||||
spellcheck_language: str | None = None
|
||||
|
||||
|
||||
class QuotaOut(BaseModel):
|
||||
@@ -113,6 +124,8 @@ def _settings_out(user: User) -> SettingsOut:
|
||||
invitation_calendar=str(s.get("invitation_calendar", "")),
|
||||
invitation_file=bool(s.get("invitation_file", False)),
|
||||
allowed_image_senders=sorted(allowed_image_senders(user)),
|
||||
spellcheck_enabled=bool(s.get("spellcheck_enabled", True)),
|
||||
spellcheck_language=str(s.get("spellcheck_language", "auto")),
|
||||
)
|
||||
|
||||
|
||||
@@ -144,9 +157,7 @@ async def patch_settings(body: SettingsPatch, user: CurrentUser, db: DbSession)
|
||||
if body.default_calendar is not None:
|
||||
updates["default_calendar"] = body.default_calendar
|
||||
if body.default_reminders_timed is not None:
|
||||
updates["default_reminders_timed"] = [
|
||||
r.model_dump() for r in body.default_reminders_timed
|
||||
]
|
||||
updates["default_reminders_timed"] = [r.model_dump() for r in body.default_reminders_timed]
|
||||
if body.default_reminders_allday is not None:
|
||||
updates["default_reminders_allday"] = [
|
||||
r.model_dump() for r in body.default_reminders_allday
|
||||
@@ -159,6 +170,10 @@ async def patch_settings(body: SettingsPatch, user: CurrentUser, db: DbSession)
|
||||
updates["invitation_calendar"] = body.invitation_calendar
|
||||
if body.invitation_file is not None:
|
||||
updates["invitation_file"] = body.invitation_file
|
||||
if body.spellcheck_enabled is not None:
|
||||
updates["spellcheck_enabled"] = body.spellcheck_enabled
|
||||
if body.spellcheck_language is not None and _LANG_CODE.fullmatch(body.spellcheck_language):
|
||||
updates["spellcheck_language"] = body.spellcheck_language
|
||||
user.settings = updates
|
||||
await db.commit()
|
||||
return _settings_out(user)
|
||||
|
||||
Reference in New Issue
Block a user