diff --git a/CLAUDE.md b/CLAUDE.md index 296e9ab627..4fb8a3fe1d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -159,6 +159,12 @@ SQLite via `better-sqlite3`. SQL abstraction in `packages/trilium-core/src/servi - Third-party components (e.g., mind-map context menu) should use i18next `t()` for their labels, with the English strings added to `en/translation.json` under a dedicated namespace (e.g., `"mind-map"`) - When a translated string contains **interpolated components** (e.g. links, note references) whose order may vary across languages, use `` from `react-i18next` instead of `t()`. This lets translators reorder components freely (e.g. `" in "` vs `"in , "`) - When adding a new locale, follow the step-by-step guide in `docs/Developer Guide/Developer Guide/Concepts/Internationalisation Translations/Adding a new locale.md` +- **Server-side translations** (e.g. hidden subtree titles) go in `apps/server/src/assets/translations/en/server.json`, not in the client `translation.json` + +### Electron Desktop App +- Desktop entry point: `apps/desktop/src/main.ts`, window management: `apps/server/src/services/window.ts` +- IPC communication: use `electron.ipcMain.on(channel, handler)` on server side, `electron.ipcRenderer.send(channel, data)` on client side +- Electron-only features should check `isElectron()` from `apps/client/src/services/utils.ts` (client) or `utils.isElectron` (server) Three inheritance mechanisms: 1. **Standard**: `note.getInheritableAttributes()` walks parent tree @@ -215,6 +221,20 @@ Use `note.getOwnedAttribute()` for direct, `note.getAttribute()` for inherited. - `apps/server/src/routes/routes.ts` — API route registration - `packages/trilium-core/src/services/sql/sql.ts` — Database abstraction +### Adding Hidden System Notes +The hidden subtree (`_hidden`) contains system notes with predictable IDs (prefixed with `_`). Defined in `apps/server/src/services/hidden_subtree.ts` via the `HiddenSubtreeItem` interface from `@triliumnext/commons`. + +1. Add the note definition to `buildHiddenSubtreeDefinition()` in `apps/server/src/services/hidden_subtree.ts` +2. Add a translation key for the title in `apps/server/src/assets/translations/en/server.json` under `"hidden-subtree"` +3. The note is auto-created on startup by `checkHiddenSubtree()` — uses deterministic IDs so all sync cluster instances generate the same structure +4. Key properties: `id` (must start with `_`), `title`, `type`, `icon` (format: `bx-icon-name` without `bx ` prefix), `attributes`, `children`, `content` +5. Use `enforceAttributes: true` to keep attributes in sync, `enforceBranches: true` for correct placement, `enforceDeleted: true` to remove deprecated notes +6. For launcher bar entries, see `hidden_subtree_launcherbar.ts`; for templates, see `hidden_subtree_templates.ts` + +### Writing to Notes from Server Services +- `note.setContent()` requires a CLS (Continuation Local Storage) context — wrap calls in `cls.init(() => { ... })` (from `apps/server/src/services/cls.ts`) +- Operations called from Express routes already have CLS context; standalone services (schedulers, Electron IPC handlers) do not + ### Adding New LLM Tools Tools are defined using `defineTools()` in `apps/server/src/services/llm/tools/` and automatically registered for both the LLM chat and MCP server. diff --git a/apps/client/src/menus/electron_context_menu.ts b/apps/client/src/menus/electron_context_menu.ts index 547a26d3d8..a894cebfc3 100644 --- a/apps/client/src/menus/electron_context_menu.ts +++ b/apps/client/src/menus/electron_context_menu.ts @@ -38,7 +38,7 @@ function setupContextMenu() { items.push({ title: t("electron_context_menu.add-term-to-dictionary", { term: params.misspelledWord }), uiIcon: "bx bx-plus", - handler: () => webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord) + handler: () => electron.ipcRenderer.send("add-word-to-dictionary", params.misspelledWord) }); items.push({ kind: "separator" }); diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index 8b710a1435..50090d1ca4 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -1498,12 +1498,15 @@ "spellcheck": { "title": "Spell Check", "description": "These options apply only for desktop builds, browsers will use their own native spell check.", - "enable": "Enable spellcheck", - "language_code_label": "Language code(s)", - "language_code_placeholder": "for example \"en-US\", \"de-AT\"", - "multiple_languages_info": "Multiple languages can be separated by comma, e.g. \"en-US, de-DE, cs\". ", - "available_language_codes_label": "Available language codes:", - "restart-required": "Changes to the spell check options will take effect after application restart." + "enable": "Check spelling", + "language_code_label": "Spell Check Languages", + "restart-required": "Changes to the spell check options will take effect after application restart.", + "custom_dictionary_title": "Custom Dictionary", + "custom_dictionary_description": "Words added to the dictionary are synced across all your devices.", + "custom_dictionary_edit": "Custom words", + "custom_dictionary_edit_description": "Edit the list of words that should not be flagged by the spell checker. Changes will be visible after a restart.", + "custom_dictionary_open": "Edit dictionary", + "related_description": "Configure spell check languages and custom dictionary." }, "sync_2": { "config_title": "Sync Configuration", diff --git a/apps/client/src/widgets/type_widgets/options/components/OptionsRow.css b/apps/client/src/widgets/type_widgets/options/components/OptionsRow.css index a811aaa720..c565d54f47 100644 --- a/apps/client/src/widgets/type_widgets/options/components/OptionsRow.css +++ b/apps/client/src/widgets/type_widgets/options/components/OptionsRow.css @@ -45,3 +45,15 @@ .option-row.centered { justify-content: center; } + +.option-row-link.use-tn-links { + text-decoration: none; + color: inherit; + margin-inline: calc(-1 * var(--options-card-padding, 15px)); + padding-inline: var(--options-card-padding, 15px); + transition: background-color 250ms ease-in-out; +} + +.option-row-link:hover { + background: var(--hover-item-background-color); +} diff --git a/apps/client/src/widgets/type_widgets/options/components/OptionsRow.tsx b/apps/client/src/widgets/type_widgets/options/components/OptionsRow.tsx index 92f0c04315..d72ba77d04 100644 --- a/apps/client/src/widgets/type_widgets/options/components/OptionsRow.tsx +++ b/apps/client/src/widgets/type_widgets/options/components/OptionsRow.tsx @@ -1,5 +1,7 @@ -import { cloneElement, VNode } from "preact"; import "./OptionsRow.css"; + +import { cloneElement, VNode } from "preact"; + import { useUniqueName } from "../../../react/hooks"; interface OptionsRowProps { @@ -25,4 +27,24 @@ export default function OptionsRow({ name, label, description, children, centere ); -} \ No newline at end of file +} + +interface OptionsRowLinkProps { + label: string; + description?: string; + href: string; +} + +export function OptionsRowLink({ label, description, href }: OptionsRowLinkProps) { + return ( + +
+ + {description && {description}} +
+
+ +
+
+ ); +} diff --git a/apps/client/src/widgets/type_widgets/options/components/RelatedSettings.tsx b/apps/client/src/widgets/type_widgets/options/components/RelatedSettings.tsx index fea1e9add9..660a0df973 100644 --- a/apps/client/src/widgets/type_widgets/options/components/RelatedSettings.tsx +++ b/apps/client/src/widgets/type_widgets/options/components/RelatedSettings.tsx @@ -1,24 +1,36 @@ -import OptionsSection from "./OptionsSection"; -import type { OptionPages } from "../../ContentWidget"; import { t } from "../../../../services/i18n"; +import type { OptionPages } from "../../ContentWidget"; +import { OptionsRowLink } from "./OptionsRow"; +import OptionsSection from "./OptionsSection"; + +interface RelatedSettingsItem { + title: string; + description?: string; + targetPage: OptionPages; + enabled?: boolean; +} interface RelatedSettingsProps { - items: { - title: string; - targetPage: OptionPages; - }[]; + items: RelatedSettingsItem[]; } export default function RelatedSettings({ items }: RelatedSettingsProps) { + const filteredItems = items.filter(item => item.enabled !== false); + + if (filteredItems.length === 0) { + return null; + } + return ( - + {filteredItems.map((item) => ( + + ))} ); } diff --git a/apps/client/src/widgets/type_widgets/options/i18n.tsx b/apps/client/src/widgets/type_widgets/options/i18n.tsx index 2804f25e5f..5f65993021 100644 --- a/apps/client/src/widgets/type_widgets/options/i18n.tsx +++ b/apps/client/src/widgets/type_widgets/options/i18n.tsx @@ -5,13 +5,14 @@ import OptionsRow from "./components/OptionsRow"; import OptionsSection from "./components/OptionsSection"; import { useTriliumOption, useTriliumOptionJson } from "../../react/hooks"; import type { Locale } from "@triliumnext/commons"; -import { restartDesktopApp } from "../../../services/utils"; +import { isElectron, restartDesktopApp } from "../../../services/utils"; import FormRadioGroup from "../../react/FormRadioGroup"; import FormText from "../../react/FormText"; import RawHtml from "../../react/RawHtml"; import Admonition from "../../react/Admonition"; import Button from "../../react/Button"; import CheckboxList from "./components/CheckboxList"; +import RelatedSettings from "./components/RelatedSettings"; import { LocaleSelector } from "./components/LocaleSelector"; export default function InternationalizationOptions() { @@ -19,8 +20,17 @@ export default function InternationalizationOptions() { <> + {isElectron() && ( + + )} - ) + ); } function LocalizationOptions() { diff --git a/apps/client/src/widgets/type_widgets/options/media.tsx b/apps/client/src/widgets/type_widgets/options/media.tsx index 6e37b60985..ba90385016 100644 --- a/apps/client/src/widgets/type_widgets/options/media.tsx +++ b/apps/client/src/widgets/type_widgets/options/media.tsx @@ -3,6 +3,7 @@ import { useCallback, useEffect, useRef, useState } from "preact/hooks"; import { t } from "../../../services/i18n"; import server from "../../../services/server"; import toast from "../../../services/toast"; +import { isElectron } from "../../../services/utils"; import { FormTextBoxWithUnit } from "../../react/FormTextBox"; import FormToggle from "../../react/FormToggle"; import { useTriliumOption, useTriliumOptionBool } from "../../react/hooks"; @@ -93,7 +94,8 @@ function OcrSettings() { diff --git a/apps/client/src/widgets/type_widgets/options/spellcheck.tsx b/apps/client/src/widgets/type_widgets/options/spellcheck.tsx index 68a63d6ada..e91804e3f3 100644 --- a/apps/client/src/widgets/type_widgets/options/spellcheck.tsx +++ b/apps/client/src/widgets/type_widgets/options/spellcheck.tsx @@ -1,63 +1,132 @@ -import { useMemo } from "preact/hooks"; +import { useCallback, useMemo } from "preact/hooks"; + +import appContext from "../../../components/app_context"; import { t } from "../../../services/i18n"; -import FormCheckbox from "../../react/FormCheckbox"; -import FormGroup from "../../react/FormGroup"; +import { dynamicRequire, isElectron, restartDesktopApp } from "../../../services/utils"; +import Button from "../../react/Button"; import FormText from "../../react/FormText"; -import FormTextBox from "../../react/FormTextBox"; +import FormToggle from "../../react/FormToggle"; import { useTriliumOption, useTriliumOptionBool } from "../../react/hooks"; +import NoItems from "../../react/NoItems"; +import CheckboxList from "./components/CheckboxList"; +import OptionsRow from "./components/OptionsRow"; import OptionsSection from "./components/OptionsSection"; -import { dynamicRequire, isElectron } from "../../../services/utils"; export default function SpellcheckSettings() { if (isElectron()) { - return - } else { - return + return ; } + return ; +} + +interface SpellcheckLanguage { + code: string; + name: string; } function ElectronSpellcheckSettings() { const [ spellCheckEnabled, setSpellCheckEnabled ] = useTriliumOptionBool("spellCheckEnabled"); + + return ( + <> + + {t("spellcheck.restart-required")} + + + + + + +