feat(desktop): spellcheck toggle and system-language dictionaries (#59129)

Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com>
This commit is contained in:
opencode-agent[bot]
2026-10-30 04:21:10 +00:00
committed by GitHub
Unverified
parent a88d3ca0bc
commit bf94de965d
20 changed files with 218 additions and 29 deletions
+18 -23
View File
@@ -1,3 +1,5 @@
import { flow, Option, Schema } from "effect"
export const DESKTOP_NATIVE_LOCALES = [
"en",
"zh",
@@ -302,6 +304,9 @@ export const DESKTOP_NATIVE_ENGLISH = {
"desktop.picker.error.notSelected": "File was not selected by the picker",
"desktop.picker.error.sizeLimit": "Selected attachments exceed the {{limit}} MB limit",
"desktop.contextMenu.spellcheck": "Spellcheck",
"desktop.contextMenu.spellcheckLanguages": "Spellcheck Languages",
} as const
export type DesktopNativeKey = keyof typeof DESKTOP_NATIVE_ENGLISH
@@ -310,6 +315,7 @@ export type DesktopNativeMessages = Record<DesktopNativeKey, string>
export type DesktopNativeBundle = { locale: DesktopNativeLocale; messages: DesktopNativeMessages }
// SAFETY: DESKTOP_NATIVE_ENGLISH is a const literal, so its own keys are exactly DesktopNativeKey.
export const DESKTOP_NATIVE_KEYS = Object.keys(DESKTOP_NATIVE_ENGLISH) as DesktopNativeKey[]
export const DESKTOP_NATIVE_MAX_PAYLOAD_BYTES = 64 * 1024
@@ -320,35 +326,24 @@ export function createDesktopNativeBundle(
): DesktopNativeBundle {
return {
locale,
// SAFETY: the entries map every DesktopNativeKey to a string, which is exactly DesktopNativeMessages.
messages: Object.fromEntries(DESKTOP_NATIVE_KEYS.map((key) => [key, translate(key)])) as DesktopNativeMessages,
}
}
export function parseDesktopNativeBundle(value: unknown): DesktopNativeBundle | undefined {
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined
const DesktopNativeBundleSchema = Schema.Struct({
locale: Schema.Literals(DESKTOP_NATIVE_LOCALES),
messages: Schema.Record(Schema.Literals(DESKTOP_NATIVE_KEYS), Schema.String),
}).check(
Schema.makeFilter(
(bundle) => new TextEncoder().encode(JSON.stringify(bundle)).byteLength <= DESKTOP_NATIVE_MAX_PAYLOAD_BYTES,
),
)
try {
if (new TextEncoder().encode(JSON.stringify(value)).byteLength > DESKTOP_NATIVE_MAX_PAYLOAD_BYTES) return undefined
} catch {
return undefined
}
const decodeDesktopNativeBundle = Schema.decodeUnknownOption(DesktopNativeBundleSchema, { onExcessProperty: "error" })
const bundle = value as { locale?: unknown; messages?: unknown }
if (!DESKTOP_NATIVE_LOCALES.some((locale) => locale === bundle.locale)) return undefined
if (!bundle.messages || typeof bundle.messages !== "object" || Array.isArray(bundle.messages)) return undefined
const messages = bundle.messages as Record<string, unknown>
const keys = Object.keys(messages)
if (keys.length !== DESKTOP_NATIVE_KEYS.length) return undefined
if (!DESKTOP_NATIVE_KEYS.every((key) => typeof messages[key] === "string")) return undefined
if (!keys.every((key) => key in DESKTOP_NATIVE_ENGLISH)) return undefined
return bundle as DesktopNativeBundle
}
/** Validates a bundle the renderer sent over IPC: a supported locale, exactly the native keys, and a bounded size. */
export const parseDesktopNativeBundle = flow(decodeDesktopNativeBundle, Option.getOrUndefined)
export function formatDesktopNativeMessage(message: string, params?: Record<string, string | number>) {
if (!params) return message
+2
View File
@@ -1005,6 +1005,8 @@ export const dict = {
"settings.general.row.followUpBehavior.steer": "Steer",
"settings.general.row.pinchZoom.title": "Pinch to zoom",
"settings.general.row.pinchZoom.description": "Allow trackpad pinch and Ctrl-scroll gestures to zoom",
"settings.general.row.spellcheck.title": "Spellcheck",
"settings.general.row.spellcheck.description": "Underline misspelled words while typing",
"sound.option.none": "None",
"sound.option.alert01": "Alert 01",
+1 -1
View File
@@ -72,7 +72,7 @@ const desktop = [
]
export const dict = {
...Object.fromEntries(DESKTOP_NATIVE_KEYS.map((key, index) => [key, desktop[index]])),
...Object.fromEntries(desktop.map((value, index) => [DESKTOP_NATIVE_KEYS[index], value])),
"command.category.suggested": "Predloženo",
"command.category.view": "Prikaz",
"command.category.project": "Projekt",
+1 -1
View File
@@ -72,7 +72,7 @@ const desktop = [
]
export const dict = {
...Object.fromEntries(DESKTOP_NATIVE_KEYS.map((key, index) => [key, desktop[index]])),
...Object.fromEntries(desktop.map((value, index) => [DESKTOP_NATIVE_KEYS[index], value])),
"command.category.suggested": "Javasolt",
"command.category.view": "Nézet",
"command.category.project": "Projekt",
+1 -1
View File
@@ -72,7 +72,7 @@ const desktop = [
]
export const dict = {
...Object.fromEntries(DESKTOP_NATIVE_KEYS.map((key, index) => [key, desktop[index]])),
...Object.fromEntries(desktop.map((value, index) => [DESKTOP_NATIVE_KEYS[index], value])),
"command.category.suggested": "Tillögur",
"command.category.view": "Skoða",
"command.category.project": "Verkefni",
+1 -1
View File
@@ -72,7 +72,7 @@ const desktop = [
]
export const dict = {
...Object.fromEntries(DESKTOP_NATIVE_KEYS.map((key, index) => [key, desktop[index]])),
...Object.fromEntries(desktop.map((value, index) => [DESKTOP_NATIVE_KEYS[index], value])),
"command.category.suggested": "Siūloma",
"command.category.view": "Rodinys",
"command.category.project": "Projektas",
@@ -108,6 +108,12 @@ type PlatformBase = {
/** Allow native pinch/Ctrl-scroll zoom gestures (desktop only) */
setPinchZoomEnabled?(enabled: boolean): Promise<void> | void
/** Get whether the native spellchecker underlines misspelled words (desktop only) */
getSpellcheckEnabled?(): Promise<boolean>
/** Turn the native spellchecker on or off (desktop only) */
setSpellcheckEnabled?(enabled: boolean): Promise<void>
/** Run a desktop-only menu action from the app chrome */
runDesktopMenuAction?(action: DesktopMenuAction): Promise<void> | void
@@ -249,6 +249,17 @@ export const SettingsGeneral: Component = () => {
void update.catch(() => setPinchZoom(!checked))
}
const [spellcheck, { mutate: setSpellcheck }] = createResource(
() => desktop() && "getSpellcheckEnabled" in platform,
() => platform.getSpellcheckEnabled?.().catch(() => true) ?? true,
{ initialValue: true },
)
const onSpellcheckChange = (checked: boolean) => {
setSpellcheck(checked)
void platform.setSpellcheckEnabled?.(checked).catch(() => setSpellcheck(!checked))
}
const GeneralSection = () => (
<div class="settings-section">
<h3 class="settings-section-title">{language.t("settings.general.section.general")}</h3>
@@ -282,6 +293,14 @@ export const SettingsGeneral: Component = () => {
<Switch checked={pinchZoom.latest} onChange={onPinchZoomChange} />
</div>
</SettingsRow>
<SettingsRow
title={language.t("settings.general.row.spellcheck.title")}
description={language.t("settings.general.row.spellcheck.description")}
>
<div data-action="settings-spellcheck">
<Switch checked={spellcheck.latest} onChange={onSpellcheckChange} />
</div>
</SettingsRow>
</Show>
<PluginSettingsSections page="general" section="general" />
@@ -55,6 +55,13 @@ export const clientSettings: Entry<SettingsRootTab>[] = [
target: "settings-pinch-zoom",
available: "desktop",
},
{
tab: "general",
label: "settings.general.row.spellcheck.title",
target: "settings-spellcheck",
keywords: "spelling spell check dictionary",
available: "desktop",
},
{
tab: "general",
label: "settings.general.row.mobileTitlebarBottom.title",