Compare commits

..

51 Commits

Author SHA1 Message Date
Jin
a820d1d6ef refactor: clean up heavy test case 2026-03-27 22:24:36 +00:00
Jin
69af1f6e82 refactor: use normal Preact lifecycle 2026-03-27 17:45:56 +00:00
Jin
a8408fbc68 Merge branch 'main' into autocomplete 2026-03-27 17:20:23 +00:00
Jin
8828df733a refactor: fix input focus issue 2026-03-27 17:17:31 +00:00
Jin
d1cb56de71 refactor: use Preact for rendering 2026-03-22 13:46:02 +00:00
Jin
e6c5df30d7 refactor: remove redundant code 2026-03-22 13:38:31 +00:00
Jin
4f7cf741ab Merge branch 'main' into autocomplete 2026-03-22 13:36:07 +00:00
JYC333
f65ddb08a0 fix: pnpm lock conflict 2026-03-19 10:53:55 +00:00
JYC333
22507f75bd chore: fix pnpm lock file 2026-03-19 10:34:21 +00:00
Jin
915c49b472 fix: address white space issue 2026-03-19 10:34:21 +00:00
Jin
aa58ad2812 fix: test file error 2026-03-19 10:34:21 +00:00
Jin
17609799da refactor: remove plan file 2026-03-19 10:34:21 +00:00
Jin
f6201d8581 fix: add link dialog enter act correctly 2026-03-19 10:34:21 +00:00
Jin
5b77152fdf refactor: address gemini review 2026-03-19 10:34:21 +00:00
Jin
b419602d74 test: add test for NoteAutocomplete 2026-03-19 10:34:21 +00:00
Jin
2c9a0ed682 test: add test for autocomplete_core 2026-03-19 10:34:21 +00:00
Jin
59ebd0f122 test: add test for attribute_autocomplete 2026-03-19 10:34:21 +00:00
Jin
334c8cbea3 refactor: cleanup unused parts 2026-03-19 10:34:21 +00:00
Jin
5adc79f867 refactor: fix related test 2026-03-19 10:34:21 +00:00
Jin
d1fc4780b7 refactor: remove old autocomplete completely 2026-03-19 10:34:18 +00:00
Jin
99937bd8f4 refactor: fix attribute detail can't save with ctrl+enter directly 2026-03-19 10:33:27 +00:00
Jin
03a9685c96 refactor: remove old autocomplete declare 2026-03-19 10:33:27 +00:00
Jin
39408f2b22 refactor: update related css 2026-03-19 10:33:27 +00:00
Jin
eeb917ea97 refactor: fix enter can't execute action in dialog 2026-03-19 10:33:27 +00:00
Jin
5ea355a587 refactor: avoid xss attack 2026-03-19 10:33:27 +00:00
Jin
e4ad356a02 refactor: fix attribute panel mouse hover behavior 2026-03-19 10:33:27 +00:00
JYC333
ff939071ac refactor: migrate cleanup function 2026-03-19 10:33:27 +00:00
JYC333
3f97516d98 refactor: extract common logic 2026-03-19 10:33:27 +00:00
JYC333
f06dd3cfea refactor: fix missing function def 2026-03-19 10:33:27 +00:00
JYC333
0dee06262b refactor: migrate react part 2026-03-19 10:33:27 +00:00
JYC333
3ac2e2785d refactor: address gemini review 2026-03-19 10:33:27 +00:00
JYC333
9869d29146 refactor: minor cleanup 2026-03-19 10:33:27 +00:00
JYC333
a1b51e1de8 refactor: fix full search 2026-03-19 10:33:27 +00:00
JYC333
cd7fb3d584 refactor: minor fix 2026-03-19 10:33:27 +00:00
JYC333
b92a5d1188 refactor: fix attribute detail autocomplete doesn't catch default value 2026-03-19 10:33:27 +00:00
JYC333
530e606ddb refactor: restore behaviors 2026-03-19 10:33:27 +00:00
JYC333
b6dea44460 refactor: fix behaviour difference 2026-03-19 10:33:26 +00:00
JYC333
a5445d35cb refactor: add back UI for note_autocomplete 2026-03-19 10:33:26 +00:00
JYC333
128fa63e7e refactor: migrate note_autocomplete core function 2026-03-19 10:33:26 +00:00
JYC333
8a4a06e656 refactor: address gemini code review 2026-03-19 10:33:26 +00:00
JYC333
0ca54396aa refactor: migrate label autocomplete 2026-03-19 10:33:26 +00:00
JYC333
3a6606b9ac refactor: limit ctrl+enter action only at when creating reation on relation map 2026-03-19 10:33:26 +00:00
JYC333
1614ccf6f6 refactor: fix cleanup to avoid DOM leaks 2026-03-19 10:33:26 +00:00
JYC333
27a7a157d5 refactor: use ctrl+enter to confirm in relation creation at relation map page 2026-03-19 10:33:26 +00:00
JYC333
d5b496e597 fix: relation definition is not included when create relation in relation map 2026-03-19 10:33:26 +00:00
JYC333
06f2aa1fd8 refactor: clean up old autocomplete implementation 2026-03-19 10:33:26 +00:00
JYC333
3328266cae refactor: migrate relation map 2026-03-19 10:33:26 +00:00
JYC333
6dd5352f40 fix: dropdown menu not follow the input when attribute detail dialog height changed 2026-03-19 10:33:26 +00:00
JYC333
eaf89c63a1 refactor: use headless autocomplete, migrate attribute deatil 2026-03-19 10:33:26 +00:00
JYC333
34ce5ebcbb refactor: add new autocomplete registry 2026-03-19 10:33:26 +00:00
JYC333
c7980f42fe refactor: add plan and package 2026-03-19 10:33:23 +00:00
761 changed files with 26748 additions and 49063 deletions

View File

@@ -1,7 +0,0 @@
{
"permissions": {
"allow": [
"Bash(gh issue *)"
]
}
}

View File

@@ -66,20 +66,12 @@ runs:
if: ${{ inputs.os == 'linux' }}
shell: ${{ inputs.shell }}
run: |
sudo apt-get update && sudo apt-get install rpm flatpak-builder elfutils libfuse2
sudo apt-get update && sudo apt-get install rpm flatpak-builder elfutils
flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
FLATPAK_ARCH=$(if [[ ${{ inputs.arch }} = 'arm64' ]]; then echo 'aarch64'; else echo 'x86_64'; fi)
FLATPAK_VERSION='24.08'
flatpak install --user --no-deps --arch $FLATPAK_ARCH --assumeyes runtime/org.freedesktop.Platform/$FLATPAK_ARCH/$FLATPAK_VERSION runtime/org.freedesktop.Sdk/$FLATPAK_ARCH/$FLATPAK_VERSION org.electronjs.Electron2.BaseApp/$FLATPAK_ARCH/$FLATPAK_VERSION
- name: Install appimagetool
if: ${{ inputs.os == 'linux' }}
shell: ${{ inputs.shell }}
run: |
APPIMAGETOOL_ARCH=$(if [[ ${{ inputs.arch }} = 'arm64' ]]; then echo 'aarch64'; else echo 'x86_64'; fi)
wget -q "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-${APPIMAGETOOL_ARCH}.AppImage" -O /usr/local/bin/appimagetool
chmod +x /usr/local/bin/appimagetool
- name: Update build info
shell: ${{ inputs.shell }}
run: pnpm run chore:update-build-info
@@ -98,14 +90,6 @@ runs:
TARGET_ARCH: ${{ inputs.arch }}
run: pnpm run --filter desktop electron-forge:make --arch=${{ inputs.arch }} --platform=${{ inputs.forge_platform }}
- name: Build AppImage
if: ${{ inputs.os == 'linux' }}
shell: ${{ inputs.shell }}
env:
TRILIUM_ARTIFACT_NAME_HINT: TriliumNotes-${{ github.ref_name }}-${{ inputs.os }}-${{ inputs.arch }}
APPIMAGE_EXTRACT_AND_RUN: "1"
run: bash apps/desktop/scripts/build-appimage.sh ${{ inputs.arch }}
# Add DMG signing step
- name: Sign DMG
if: inputs.os == 'macos'

View File

@@ -8,7 +8,7 @@ inputs:
runs:
using: composite
steps:
- uses: pnpm/action-setup@v6
- uses: pnpm/action-setup@v5
- name: Set up node & dependencies
uses: actions/setup-node@v6
with:

View File

@@ -55,7 +55,7 @@ runs:
# Post deployment URL as PR comment
- name: Comment PR with Preview URL
if: github.event_name == 'pull_request'
uses: actions/github-script@v9
uses: actions/github-script@v8
env:
COMMENT_BODY: ${{ inputs.comment_body }}
PRODUCTION_URL: ${{ inputs.production_url }}

View File

@@ -1,7 +1,5 @@
# Trilium Notes - AI Coding Agent Instructions
> **Note**: When updating this file, also update `CLAUDE.md` in the repository root to keep both AI coding assistants in sync.
## Project Overview
Trilium Notes is a hierarchical note-taking application with advanced features like synchronization, scripting, and rich text editing. Built as a TypeScript monorepo using pnpm, it implements a three-layer caching architecture (Becca/Froca/Shaca) with a widget-based UI system and supports extensive user scripting capabilities.
@@ -117,15 +115,6 @@ class MyNoteWidget extends NoteContextAwareWidget {
**Important**: Widgets use jQuery (`this.$widget`) for DOM manipulation. Don't mix React patterns here.
### Reusable Preact Components
Common UI components are available in `apps/client/src/widgets/react/` — prefer reusing these over creating custom implementations:
- `NoItems` - Empty state placeholder with icon and message (use for "no results", "too many items", error states)
- `ActionButton` - Consistent button styling with icon support
- `FormTextBox` - Text input with validation and controlled input handling
- `Slider` - Range slider with label
- `Checkbox`, `RadioButton` - Form controls
- `CollapsibleSection` - Expandable content sections
## Development Workflow
### Running & Testing
@@ -197,14 +186,6 @@ When adding query parameters to ETAPI endpoints (`apps/server/src/etapi/`), main
**Auth note**: ETAPI uses basic auth with tokens. Internal API endpoints trust the frontend.
### 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.
1. Add the tool definition in the appropriate module (`note_tools.ts`, `attribute_tools.ts`, `hierarchy_tools.ts`) or create a new module
2. Each tool needs: `description`, `inputSchema` (Zod), `execute` function, and optionally `mutates: true` for write operations or `needsContext: true` for tools that need the current note context
3. If creating a new module, wrap tools in `defineTools({...})` and add the registry to `allToolRegistries` in `tools/index.ts`
4. Add a client-side friendly name in `apps/client/src/translations/en/translation.json` under `llm.tools.<tool_name>` — use **imperative tense** (e.g. "Search notes", "Create note", "Get attributes"), not present continuous
### Database Migrations
- Add scripts in `apps/server/src/migrations/YYMMDD_HHMM__description.sql`
- Update schema in `apps/server/src/assets/db/schema.sql`
@@ -232,12 +213,6 @@ Tools are defined using `defineTools()` in `apps/server/src/services/llm/tools/`
10. **Attribute inheritance can be complex** - When checking for labels/relations, use `note.getOwnedAttribute()` for direct attributes or `note.getAttribute()` for inherited ones. Don't assume attributes are directly on the note.
## MCP Server
- Trilium exposes an MCP (Model Context Protocol) server at `http://localhost:8080/mcp`, configured in `.mcp.json`
- The MCP server is **only available when the Trilium server is running** (`pnpm run server:start`)
- It provides tools for reading, searching, and modifying notes directly from the AI assistant
- Use it to interact with actual note data when developing or debugging note-related features
## TypeScript Configuration
- **Project references**: Monorepo uses TypeScript project references (`tsconfig.json`)
@@ -300,12 +275,6 @@ View types are configured via `#viewType` label (e.g., `#viewType=table`). Each
- Register in `packages/ckeditor5/src/plugins.ts`
- See `ckeditor5-admonition`, `ckeditor5-footnotes`, `ckeditor5-math`, `ckeditor5-mermaid` for examples
### Updating PDF.js
1. Update `pdfjs-dist` version in `packages/pdfjs-viewer/package.json`
2. Run `npx tsx scripts/update-viewer.ts` from that directory
3. Run `pnpm build` to verify success
4. Commit all changes including updated viewer files
### Database Migrations
- Add migration scripts in `apps/server/src/migrations/YYMMDD_HHMM__description.sql`
- Update schema in `apps/server/src/assets/db/schema.sql`
@@ -330,29 +299,9 @@ Trilium provides powerful user scripting capabilities:
- Translation files in `apps/client/src/translations/`
- Use translation system via `t()` function
- Automatic pluralization: Add `_other` suffix to translation keys (e.g., `item` and `item_other` for singular/plural)
- When a translated string contains **interpolated components** (e.g. links, note references) whose order may vary across languages, use `<Trans>` from `react-i18next` instead of `t()`. This lets translators reorder components freely (e.g. `"<Note/> in <Parent/>"` vs `"in <Parent/>, <Note/>"`)
- 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`
#### Client vs Server Translation Usage
- **Client-side**: `import { t } from "../services/i18n"` with keys in `apps/client/src/translations/en/translation.json`
- **Server-side**: `import { t } from "i18next"` with keys in `apps/server/src/assets/translations/en/server.json`
- **Interpolation**: Use `{{variable}}` for normal interpolation; use `{{- variable}}` (with hyphen) for **unescaped** interpolation when the value contains special characters like quotes that shouldn't be HTML-escaped
### Storing User Preferences
- **Do not use `localStorage`** for user preferences — Trilium has a synced options system that persists across devices
- To add a new user preference:
1. Add the option type to `OptionDefinitions` in `packages/commons/src/lib/options_interface.ts`
2. Add a default value in `apps/server/src/services/options_init.ts` in the `defaultOptions` array
3. **Whitelist the option** in `apps/server/src/routes/api/options.ts` by adding it to `ALLOWED_OPTIONS` (required for client updates)
4. Use `useTriliumOption("optionName")` hook in React components to read/write the option
- Available hooks: `useTriliumOption` (string), `useTriliumOptionBool`, `useTriliumOptionInt`, `useTriliumOptionJson`
- See `docs/Developer Guide/Developer Guide/Concepts/Options/Creating a new option.md` for detailed documentation
## Testing Conventions
- **Write concise tests**: Group related assertions together in a single test case rather than creating many one-shot tests
- **Extract and test business logic**: When adding pure business logic (e.g., data transformations, migrations, validations), extract it as a separate function and always write unit tests for it
```typescript
// ETAPI test pattern
describe("etapi/feature", () => {

View File

@@ -1,44 +0,0 @@
name: Claude Code Review
on:
pull_request:
types: [opened, synchronize, ready_for_review, reopened]
# Optional: Only run on specific file changes
# paths:
# - "src/**/*.ts"
# - "src/**/*.tsx"
# - "src/**/*.js"
# - "src/**/*.jsx"
jobs:
claude-review:
# Optional: Filter by PR author
# if: |
# github.event.pull_request.user.login == 'external-contributor' ||
# github.event.pull_request.user.login == 'new-developer' ||
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
issues: read
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Run Claude Code Review
id: claude-review
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
plugins: 'code-review@claude-code-plugins'
prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options

View File

@@ -1,50 +0,0 @@
name: Claude Code
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]
jobs:
claude:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
id-token: write
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# This is an optional setting that allows Claude to read CI results on PRs
additional_permissions: |
actions: read
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
# prompt: 'Update the pull request description to include a summary of changes.'
# Optional: Add claude_args to customize behavior and configuration
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options
# claude_args: '--allowed-tools Bash(gh pr *)'

View File

@@ -45,7 +45,7 @@ jobs:
uses: actions/checkout@v6
- name: Setup pnpm
uses: pnpm/action-setup@v6
uses: pnpm/action-setup@v5
- name: Setup Node.js
uses: actions/setup-node@v6

View File

@@ -1,13 +1,9 @@
name: Dev
on:
push:
branches:
- main
- "release/*"
branches: [ main ]
pull_request:
branches:
- main
- "release/*"
branches: [ main ]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -30,7 +26,7 @@ jobs:
- name: Checkout the repository
uses: actions/checkout@v6
- uses: pnpm/action-setup@v6
- uses: pnpm/action-setup@v5
- name: Set up node & dependencies
uses: actions/setup-node@v6
with:
@@ -78,7 +74,7 @@ jobs:
- test_dev
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v6
- uses: pnpm/action-setup@v5
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Update build info
@@ -113,7 +109,7 @@ jobs:
- name: Checkout the repository
uses: actions/checkout@v6
- uses: pnpm/action-setup@v6
- uses: pnpm/action-setup@v5
- name: Install dependencies
run: pnpm install --frozen-lockfile

View File

@@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v6
- uses: pnpm/action-setup@v5
- name: Set up node & dependencies
uses: actions/setup-node@v6
with:

View File

@@ -42,7 +42,7 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- uses: pnpm/action-setup@v6
- uses: pnpm/action-setup@v5
- name: Set up node & dependencies
uses: actions/setup-node@v6
with:
@@ -142,7 +142,7 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v6
- uses: pnpm/action-setup@v6
- uses: pnpm/action-setup@v5
- name: Set up node & dependencies
uses: actions/setup-node@v6
with:

View File

@@ -42,7 +42,7 @@ jobs:
shell: bash
forge_platform: linux
- name: windows
image: windows-latest
image: win-signing
shell: cmd
forge_platform: win32
# Exclude ARM64 Linux from default matrix to use native runner
@@ -61,7 +61,7 @@ jobs:
runs-on: ${{ matrix.os.image }}
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v6
- uses: pnpm/action-setup@v5
- name: Set up node & dependencies
uses: actions/setup-node@v6
with:
@@ -69,8 +69,6 @@ jobs:
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
env:
npm_config_package_import_method: copy
- name: Update nightly version
run: pnpm run chore:ci-update-nightly-version
- name: Run the build
@@ -88,10 +86,12 @@ jobs:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
WINDOWS_SIGN_EXECUTABLE: ${{ vars.WINDOWS_SIGN_EXECUTABLE }}
WINDOWS_SIGN_ERROR_LOG: ${{ vars.WINDOWS_SIGN_ERROR_LOG }}
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGN_KEY }}
- name: Publish release
uses: softprops/action-gh-release@v3.0.0
uses: softprops/action-gh-release@v2.6.1
if: ${{ github.event_name != 'pull_request' }}
with:
make_latest: false
@@ -132,7 +132,7 @@ jobs:
arch: ${{ matrix.arch }}
- name: Publish release
uses: softprops/action-gh-release@v3.0.0
uses: softprops/action-gh-release@v2.6.1
if: ${{ github.event_name != 'pull_request' }}
with:
make_latest: false

View File

@@ -38,7 +38,7 @@ jobs:
filter: tree:0
fetch-depth: 0
- uses: pnpm/action-setup@v6
- uses: pnpm/action-setup@v5
- uses: actions/setup-node@v6
with:
node-version: 24

View File

@@ -17,7 +17,7 @@ jobs:
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v6
- uses: pnpm/action-setup@v5
- name: Set up node & dependencies
uses: actions/setup-node@v6
with:
@@ -66,7 +66,7 @@ jobs:
runs-on: ${{ matrix.os.image }}
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v6
- uses: pnpm/action-setup@v5
- name: Set up node & dependencies
uses: actions/setup-node@v6
with:
@@ -150,7 +150,7 @@ jobs:
path: upload
- name: Publish stable release
uses: softprops/action-gh-release@v3.0.0
uses: softprops/action-gh-release@v2.6.1
with:
draft: false
body_path: docs/Release Notes/Release Notes/${{ github.ref_name }}.md

View File

@@ -32,7 +32,7 @@ jobs:
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v6
- uses: pnpm/action-setup@v5
- name: Set up node & dependencies
uses: actions/setup-node@v6
with:
@@ -58,7 +58,7 @@ jobs:
compression-level: 0
- name: Release web clipper extension
uses: softprops/action-gh-release@v3.0.0
uses: softprops/action-gh-release@v2.6.1
if: ${{ startsWith(github.ref, 'refs/tags/web-clipper-v') }}
with:
draft: false

View File

@@ -26,7 +26,7 @@ jobs:
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v6
- uses: pnpm/action-setup@v5
- name: Set up node & dependencies
uses: actions/setup-node@v6
with:

118
.mailmap
View File

@@ -1,116 +1,2 @@
# Format: Canonical Name <canonical-email> <commit-email>
# Merges aliases so `git shortlog`, `git log --use-mailmap`, etc. group commits per person.
# Core maintainers
zadam <zadam.apps@gmail.com>
zadam <zadam.apps@gmail.com> <adam.zivner@gmail.com>
zadam <zadam.apps@gmail.com> <adam.zivner@gemalto.com>
Elian Doran <contact@eliandoran.me>
Elian Doran <contact@eliandoran.me> <online@eliandoran.me>
Adorian Doran <adorian@esevo.ro>
Adorian Doran <adorian@esevo.ro> <adoriandoran@gmail.com>
# Contributors with multiple emails / name variants
Panagiotis Papadopoulos <pano_90@gmx.net> <102623907+pano9000@users.noreply.github.com>
Jon Fuller <jonfuller2012@gmail.com>
SiriusXT <1160925501@qq.com>
SiriusXT <1160925501@qq.com> <11609255001@qq.com>
SiriusXT <1160925501@qq.com> <37627919+SiriusXT@users.noreply.github.com>
JYC333 <22962980+JYC333@users.noreply.github.com>
JYC333 <22962980+JYC333@users.noreply.github.com> <yuchuanjin333@gmail.com>
Nriver <6752679+Nriver@users.noreply.github.com>
Francis C. <normitomf@gmail.com>
Francis C. <normitomf@gmail.com> <francistw@users.noreply.github.com>
Thomas Frei <7283497+thfrei@users.noreply.github.com>
hasecilu <hasecilu@tuta.io>
meinzzzz <lukas.geiselhart35@gmail.com>
FliegendeWurst <arne.keller@posteo.de>
FliegendeWurst <arne.keller@posteo.de> <2012gdwu@web.de>
FliegendeWurst <arne.keller@posteo.de> <2012gdwu+github@posteo.de>
MeIchthys <github.com@meichthys.com>
MeIchthys <github.com@meichthys.com> <10717998+meichthys@users.noreply.github.com>
Marcel Wiechmann <marcel.wiechmann@gmail.com>
Marcel Wiechmann <marcel.wiechmann@gmail.com> <github.y3y0w@sl.wiechmann.at>
Tomas Adamek <ad.tomik@seznam.cz>
Tomas Adamek <ad.tomik@seznam.cz> <50672285+Kureii@users.noreply.github.com>
soulsands <407221377@qq.com>
chesspro13 <chesspro13@gmail.com>
sigaloid <69441971+sigaloid@users.noreply.github.com>
Marek Lewandowski <m.lewandowski@cksource.com>
Marek Lewandowski <m.lewandowski@cksource.com> <code@mlewandowski.com>
Marek Lewandowski <m.lewandowski@cksource.com> <mlewand@users.noreply.github.com>
lzinga <lucas.elzinga@outlook.com>
lzinga <lucas.elzinga@outlook.com> <lzinga@users.noreply.github.com>
Sukant Gujar <sukantgujar@yahoo.com>
Matt Wilkie <maphew@gmail.com>
Matt Wilkie <maphew@gmail.com> <matt.wilkie@yukon.ca>
Andreas Haan <andreas.mobil1@googlemail.com>
Potjoe-97 <42873357+Potjoe-97@users.noreply.github.com>
Potjoe-97 <42873357+Potjoe-97@users.noreply.github.com> <giann@LAPTOPT490-GF>
Alex Pietsch <54153428+alexpietsch@users.noreply.github.com>
Laurent Cozic <laurent@cozic.net>
Laurent Cozic <laurent@cozic.net> <laurent22@users.noreply.github.com>
Zexin Yuan <git@yzx9.xyz>
Zexin Yuan <git@yzx9.xyz> <yuan.zx@outlook.com>
hulmgulm <hulmgulm@users.noreply.github.com>
hulmgulm <hulmgulm@users.noreply.github.com> <12165268+hulmgulm@users.noreply.github.com>
hulmgulm <hulmgulm@users.noreply.github.com> <github@hulmgulm.de>
Jules Bertholet <jules.bertholet@gmail.com>
Charles Dagenais <dagenais.charles@gmail.com>
Giulia Ye <yg97.cs@gmail.com>
baddate <37013819+baddate@users.noreply.github.com>
DerVogel101 <128903814+DerVogel101@users.noreply.github.com>
DerVogel101 <128903814+DerVogel101@users.noreply.github.com> <jan.irmer@outlook.de>
Marcello Fuschi <marcellofuschi1@gmail.com>
Jiahao Lee <lijiahao34@live.com>
Dmitry Matveyev <dev@greenfork.me>
Dmitry Matveyev <dev@greenfork.me> <info@greenfork.me>
Grant Zhu <a1065135230@gmail.com>
Sylvain Pasche <sylvain.pasche@gmail.com>
Sylvain Pasche <sylvain.pasche@gmail.com> <spasche@spasche.net>
mm21 <8033134+mm21@users.noreply.github.com>
mm21 <8033134+mm21@users.noreply.github.com> <mm21.dev@gmail.com>
BeatLink <git@beatlink.simplelogin.com>
BeatLink <git@beatlink.simplelogin.com> <github@beatlink.simplelogin.com>
Florian Meißner <161936+Mystler@users.noreply.github.com>
Florian Meißner <161936+Mystler@users.noreply.github.com> <developer@mystler.eu>
zadam <adam.zivner@gmail.com>
zadam <zadam.apps@gmail.com>

View File

@@ -1,8 +0,0 @@
{
"mcpServers": {
"trilium": {
"type": "http",
"url": "http://localhost:8080/mcp"
}
}
}

2
.nvmrc
View File

@@ -1 +1 @@
24.15.0
24.14.1

View File

@@ -2,8 +2,6 @@
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
> **Note**: When updating this file, also update `.github/copilot-instructions.md` to keep both AI coding assistants in sync.
## Overview
Trilium Notes is a hierarchical note-taking application with advanced features like synchronization, scripting, and rich text editing. It's built as a TypeScript monorepo using pnpm, with multiple applications and shared packages.
@@ -68,15 +66,6 @@ Frontend uses a widget system (`apps/client/src/widgets/`):
- `RightPanelWidget` - Widgets displayed in the right panel
- Type-specific widgets in `type_widgets/` directory
#### Reusable Preact Components
Common UI components are available in `apps/client/src/widgets/react/` — prefer reusing these over creating custom implementations:
- `NoItems` - Empty state placeholder with icon and message (use for "no results", "too many items", error states)
- `ActionButton` - Consistent button styling with icon support
- `FormTextBox` - Text input with validation and controlled input handling
- `Slider` - Range slider with label
- `Checkbox`, `RadioButton` - Form controls
- `CollapsibleSection` - Expandable content sections
#### API Architecture
- **Internal API**: REST endpoints in `apps/server/src/routes/api/`
- **ETAPI**: External API for third-party integrations (`apps/server/src/etapi/`)
@@ -119,8 +108,6 @@ Trilium supports multiple note types, each with specialized widgets:
- Client tests can run in parallel
- E2E tests use Playwright for both server and desktop apps
- Build validation tests check artifact integrity
- **Write concise tests**: Group related assertions together in a single test case rather than creating many one-shot tests
- **Extract and test business logic**: When adding pure business logic (e.g., data transformations, migrations, validations), extract it as a separate function and always write unit tests for it
### Scripting System
Trilium provides powerful user scripting capabilities:
@@ -131,21 +118,6 @@ Trilium provides powerful user scripting capabilities:
### Internationalization
- Translation files in `apps/client/src/translations/`
- Supported languages: English, German, Spanish, French, Romanian, Chinese
- **Only add new translation keys to `en/translation.json`** — translations for other languages are managed via Weblate and will be contributed by the community
- 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 `<Trans>` from `react-i18next` instead of `t()`. This lets translators reorder components freely (e.g. `"<Note/> in <Parent/>"` vs `"in <Parent/>, <Note/>"`)
- 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`
#### Client vs Server Translation Usage
- **Client-side**: `import { t } from "../services/i18n"` with keys in `apps/client/src/translations/en/translation.json`
- **Server-side**: `import { t } from "i18next"` with keys in `apps/server/src/assets/translations/en/server.json`
- **Interpolation**: Use `{{variable}}` for normal interpolation; use `{{- variable}}` (with hyphen) for **unescaped** interpolation when the value contains special characters like quotes that shouldn't be HTML-escaped
### 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)
### Security Considerations
- Per-note encryption with granular protected sessions
@@ -153,26 +125,6 @@ Trilium provides powerful user scripting capabilities:
- OpenID and TOTP authentication support
- Sanitization of user-generated content
### Client-Side API Restrictions
- **Do not use `crypto.randomUUID()`** or other Web Crypto APIs that require secure contexts - Trilium can run over HTTP, not just HTTPS
- Use `randomString()` from `apps/client/src/services/utils.ts` for generating IDs instead
### Storing User Preferences
- **Do not use `localStorage`** for user preferences — Trilium has a synced options system that persists across devices
- To add a new user preference:
1. Add the option type to `OptionDefinitions` in `packages/commons/src/lib/options_interface.ts`
2. Add a default value in `apps/server/src/services/options_init.ts` in the `defaultOptions` array
3. **Whitelist the option** in `apps/server/src/routes/api/options.ts` by adding it to the `ALLOWED_OPTIONS` array — **without this, the API will reject changes with "Option 'X' is not allowed to be changed"**
4. If the option should be user-editable in the UI, add a control in the appropriate settings component (e.g., `apps/client/src/widgets/type_widgets/options/other.tsx`) and a translation key in `apps/client/src/translations/en/translation.json`
5. Use `useTriliumOption("optionName")` hook in React components to read/write the option
- Available hooks: `useTriliumOption` (string), `useTriliumOptionBool`, `useTriliumOptionInt`, `useTriliumOptionJson`
- See `docs/Developer Guide/Developer Guide/Concepts/Options/Creating a new option.md` for detailed documentation
### Shared Types Policy
- Types shared between client and server belong in `@triliumnext/commons` (`packages/commons/src/lib/`)
- Import shared types directly from `@triliumnext/commons` - do not re-export them from app-specific modules
- Keep app-specific types (e.g., `LlmProvider` for server, `StreamCallbacks` for client) in their respective apps
## Common Development Tasks
### Adding New Note Types
@@ -188,51 +140,10 @@ Trilium provides powerful user scripting capabilities:
- Create new package in `packages/` following existing plugin structure
- Register in `packages/ckeditor5/src/plugins.ts`
### 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.
1. Add the tool definition in the appropriate module (`note_tools.ts`, `attribute_tools.ts`, `attachment_tools.ts`, `hierarchy_tools.ts`) or create a new module
2. Each tool needs: `description`, `inputSchema` (Zod), `execute` function, and optionally `mutates: true` for write operations
3. If creating a new module, wrap tools in `defineTools({...})` and add the registry to `allToolRegistries` in `tools/index.ts`
4. Add a client-side friendly name in `apps/client/src/translations/en/translation.json` under `llm.tools.<tool_name>` — use **imperative tense** (e.g. "Search notes", "Create note", "Get attributes"), not present continuous
5. Use ETAPI (`apps/server/src/etapi/`) as inspiration for what fields to expose, but **do not import ETAPI mappers** — inline the field mappings directly in the tool so the LLM layer stays decoupled from the API layer
### Updating PDF.js
1. Update `pdfjs-dist` version in `packages/pdfjs-viewer/package.json`
2. Run `npx tsx scripts/update-viewer.ts` from that directory
3. Run `pnpm build` to verify success
4. Commit all changes including updated viewer files
### Database Migrations
- Add migration scripts in `apps/server/src/migrations/`
- Update schema in `apps/server/src/assets/db/schema.sql`
### Server-Side Static Assets
- Static assets (templates, SQL, translations, etc.) go in `apps/server/src/assets/`
- Access them at runtime via `RESOURCE_DIR` from `apps/server/src/services/resource_dir.ts` (e.g. `path.join(RESOURCE_DIR, "llm", "skills", "file.md")`)
- **Do not use `import.meta.url`/`fileURLToPath`** to resolve file paths — the server is bundled into CJS for production, so `import.meta.url` will not point to the source directory
- **Do not use `__dirname` with relative paths** from source files — after bundling, `__dirname` points to the bundle output, not the original source tree
## MCP Server
- Trilium exposes an MCP (Model Context Protocol) server at `http://localhost:8080/mcp`, configured in `.mcp.json`
- The MCP server is **only available when the Trilium server is running** (`pnpm run server:start`)
- It provides tools for reading, searching, and modifying notes directly from the AI assistant
- Use it to interact with actual note data when developing or debugging note-related features
## Build System Notes
- Uses pnpm for monorepo management
- Vite for fast development builds

View File

@@ -2,87 +2,13 @@
## Supported Versions
Only the latest stable minor release receives security fixes.
In the (still active) 0.X phase of the project only the latest stable minor release is getting bugfixes (including security ones).
For example, if the latest stable version is 0.92.3 and the latest beta is 0.93.0-beta, then only the 0.92.x line will receive security patches. Older versions (like 0.91.x) will not receive fixes.
So e.g. if the latest stable version is 0.42.3 and the latest beta version is 0.43.0-beta, then 0.42 line will still get security fixes but older versions (like 0.41.X) won't get any fixes.
This policy may be altered on a case-by-case basis for critical vulnerabilities.
Description above is a general rule and may be altered on case by case basis.
## Reporting a Vulnerability
**Please report all security vulnerabilities through [GitHub Security Advisories](https://github.com/TriliumNext/Notes/security/advisories/new).**
We do not accept security reports via email, public issues, or other channels. GitHub Security Advisories allows us to:
- Discuss and triage vulnerabilities privately
- Coordinate fixes before public disclosure
- Credit reporters appropriately
- Publish advisories with CVE identifiers
### What to Include
When reporting, please provide:
- A clear description of the vulnerability
- Steps to reproduce or proof-of-concept
- Affected versions (if known)
- Potential impact assessment
- Any suggested mitigations or fixes
### Response Timeline
- **Initial response**: Within 7 days
- **Triage decision**: Within 14 days
- **Fix timeline**: Depends on severity and complexity
## Scope
### In Scope
- Remote code execution
- Authentication/authorization bypass
- Cross-site scripting (XSS) that affects other users
- SQL injection
- Path traversal
- Sensitive data exposure
- Privilege escalation
### Out of Scope (Won't Fix)
The following are considered out of scope or accepted risks:
#### Self-XSS / Self-Injection
Trilium is a personal knowledge base where users have full control over their own data. Users can intentionally create notes containing scripts, HTML, or other executable content. This is by design - Trilium's scripting system allows users to extend functionality with custom JavaScript.
Vulnerabilities that require a user to inject malicious content into their own notes and then view it themselves are not considered security issues.
#### Electron Architecture (nodeIntegration)
Trilium's desktop application runs with `nodeIntegration: true` to enable its powerful scripting features. This is an intentional design decision, similar to VS Code extensions having full system access. We mitigate risks by:
- Sanitizing content at input boundaries
- Fixing specific XSS vectors as they're discovered
- Using Electron fuses to prevent external abuse
#### Authenticated User Actions
Actions that require valid authentication and only affect the authenticated user's own data are generally not vulnerabilities.
#### Denial of Service via Resource Exhaustion
Creating extremely large notes or performing many operations is expected user behavior in a note-taking application.
#### Missing Security Headers on Non-Sensitive Endpoints
We implement security headers where they provide meaningful protection, but may omit them on endpoints where they provide no practical benefit.
## Coordinated Disclosure
We follow a coordinated disclosure process:
1. **Report received** - We acknowledge receipt and begin triage
2. **Fix developed** - We develop and test a fix privately
3. **Release prepared** - Security release is prepared with vague changelog
4. **Users notified** - Release is published, users encouraged to upgrade
5. **Advisory published** - After reasonable upgrade window (typically 2-4 weeks), full advisory is published
We appreciate reporters allowing us time to fix issues before public disclosure. We aim to credit all reporters in published advisories unless they prefer to remain anonymous.
## Security Updates
Security fixes are released as patch versions (e.g., 0.92.1 → 0.92.2) to minimize upgrade friction. We recommend all users keep their installations up to date.
Subscribe to GitHub releases or watch the repository to receive notifications of new releases.
* For low severity vulnerabilities, they can be reported as GitHub issues.
* For severe vulnerabilities, please report it using [GitHub Security Advisories](https://github.com/TriliumNext/Trilium/security/advisories).

View File

@@ -16,11 +16,13 @@
"license": "AGPL-3.0-only",
"packageManager": "pnpm@10.33.0",
"devDependencies": {
"@redocly/cli": "2.28.0",
"@redocly/cli": "2.25.1",
"archiver": "7.0.1",
"fs-extra": "11.3.4",
"js-yaml": "4.1.1",
"typedoc": "0.28.19",
"typedoc-plugin-missing-exports": "4.1.3"
"react": "19.2.4",
"react-dom": "19.2.4",
"typedoc": "0.28.18",
"typedoc-plugin-missing-exports": "4.1.2"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@triliumnext/client",
"version": "0.102.2",
"version": "0.102.1",
"description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)",
"private": true,
"license": "AGPL-3.0-only",
@@ -16,6 +16,7 @@
"circular-deps": "dpdm -T src/**/*.ts --tree=false --warning=false --skip-dynamic-imports=circular"
},
"dependencies": {
"@algolia/autocomplete-js": "1.19.6",
"@excalidraw/excalidraw": "0.18.0",
"@fullcalendar/core": "6.1.20",
"@fullcalendar/daygrid": "6.1.20",
@@ -27,49 +28,49 @@
"@maplibre/maplibre-gl-leaflet": "0.1.3",
"@mermaid-js/layout-elk": "0.2.1",
"@mind-elixir/node-menu": "5.0.1",
"@preact/signals": "2.9.0",
"@popperjs/core": "2.11.8",
"@preact/signals": "2.8.2",
"@triliumnext/ckeditor5": "workspace:*",
"@triliumnext/codemirror": "workspace:*",
"@triliumnext/commons": "workspace:*",
"@triliumnext/highlightjs": "workspace:*",
"@triliumnext/share-theme": "workspace:*",
"@triliumnext/split.js": "workspace:*",
"@univerjs/preset-sheets-conditional-formatting": "0.20.1",
"@univerjs/preset-sheets-core": "0.20.1",
"@univerjs/preset-sheets-data-validation": "0.20.1",
"@univerjs/preset-sheets-filter": "0.20.1",
"@univerjs/preset-sheets-find-replace": "0.20.1",
"@univerjs/preset-sheets-note": "0.20.1",
"@univerjs/preset-sheets-sort": "0.20.1",
"@univerjs/presets": "0.20.1",
"@zumer/snapdom": "2.8.0",
"autocomplete.js": "0.38.1",
"@univerjs/preset-sheets-conditional-formatting": "0.18.0",
"@univerjs/preset-sheets-core": "0.18.0",
"@univerjs/preset-sheets-data-validation": "0.18.0",
"@univerjs/preset-sheets-filter": "0.18.0",
"@univerjs/preset-sheets-find-replace": "0.18.0",
"@univerjs/preset-sheets-note": "0.18.0",
"@univerjs/preset-sheets-sort": "0.18.0",
"@univerjs/presets": "0.18.0",
"@zumer/snapdom": "2.5.0",
"bootstrap": "5.3.8",
"boxicons": "2.1.4",
"clsx": "2.1.1",
"color": "5.0.3",
"debounce": "3.0.0",
"dompurify": "3.4.0",
"draggabilly": "3.0.0",
"force-graph": "1.51.4",
"htmldiff-js": "1.0.5",
"i18next": "26.0.4",
"i18next-http-backend": "3.0.4",
"force-graph": "1.51.2",
"globals": "17.4.0",
"i18next": "25.10.10",
"i18next-http-backend": "3.0.2",
"jquery": "4.0.0",
"jquery.fancytree": "2.38.5",
"jsplumb": "2.15.6",
"katex": "0.16.45",
"katex": "0.16.43",
"leaflet": "1.9.4",
"leaflet-gpx": "2.2.0",
"mark.js": "8.11.1",
"marked": "18.0.1",
"mermaid": "11.14.0",
"mind-elixir": "5.10.0",
"marked": "17.0.5",
"mermaid": "11.13.0",
"mind-elixir": "5.9.3",
"normalize.css": "8.0.1",
"panzoom": "9.4.4",
"preact": "10.29.1",
"react-i18next": "17.0.3",
"preact": "10.29.0",
"react-i18next": "16.6.6",
"react-window": "2.2.7",
"reveal.js": "6.0.1",
"reveal.js": "6.0.0",
"rrule": "2.8.1",
"svg-pan-zoom": "3.6.2",
"tabulator-tables": "6.4.0",
@@ -77,7 +78,7 @@
},
"devDependencies": {
"@ckeditor/ckeditor5-inspector": "5.0.0",
"@prefresh/vite": "3.0.0",
"@prefresh/vite": "2.4.12",
"@types/bootstrap": "5.2.10",
"@types/jquery": "4.0.0",
"@types/leaflet": "1.9.21",
@@ -85,9 +86,9 @@
"@types/mark.js": "8.11.12",
"@types/tabulator-tables": "6.3.1",
"copy-webpack-plugin": "14.0.0",
"happy-dom": "20.9.0",
"happy-dom": "20.8.9",
"lightningcss": "1.32.0",
"script-loader": "0.7.2",
"vite-plugin-static-copy": "4.0.1"
"vite-plugin-static-copy": "3.4.0"
}
}

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve"><path d="M63.966,45.043c0.008-0.009,0.021-0.021,0.027-0.029c0.938-1.156-0.823-13.453-5.063-20.125 c-1.389-2.186-2.239-3.423-3.219-4.719c-3.907-5.166-6-6.125-6-6.125S35.732,24.78,36.149,44.315 c-1.754,0.065-11.218,7.528-14.826,14.388c-1.206,2.291-1.856,3.645-2.493,5.141c-2.539,5.957-2.33,8.25-2.33,8.25 s16.271,6.79,33.014-3.294c0.007,0.021,0.013,0.046,0.02,0.063c0.537,1.389,12.08,5.979,19.976,5.621 c2.587-0.116,4.084-0.238,5.696-0.444c6.424-0.818,8.298-2.157,8.298-2.157S81.144,54.396,63.966,45.043z M50.787,65.343 c1.059-1.183,4.648-5.853,0.995-11.315c-0.253-0.377-0.496-0.236-0.496-0.236s0.063,10.822-5.162,12.359 c-5.225,1.537-13.886,4.4-20.427,0.455C25,66.186,26.924,53.606,38.544,47.229c0.546,1.599,2.836,6.854,9.292,6.409 c0.453-0.031,0.453-0.313,0.453-0.313s-9.422-5.328-8.156-10.625s3.089-14.236,9.766-17.948c0.714-0.397,10.746,7.593,10.417,20.94 c-1.606-0.319-7.377-1.004-10.226,4.864c-0.198,0.409,0.046,0.549,0.046,0.549s9.31-5.521,13.275-1.789 c3.965,3.733,10.813,9.763,10.71,17.4C74.111,67.533,62.197,72.258,50.787,65.343z M35.613,35.145c0,0-0.991,3.241-0.603,7.524 l-13.393-7.524C21.618,35.145,27.838,30.931,35.613,35.145z M21.193,36.03l13.344,7.612c-3.872,1.872-6.142,4.388-6.142,4.388 C20.78,43.531,21.193,36.03,21.193,36.03z M72.287,49.064c0,0-2.321-2.471-6.23-4.263l13.187-7.881 C79.243,36.92,79.808,44.413,72.287,49.064z M78.687,36.113l-13.237,7.794c0.3-4.291-0.754-7.511-0.754-7.511 C72.383,32.025,78.687,36.113,78.687,36.113z M42.076,73.778c0,0,3.309-0.737,6.845-3.185l0.056,15.361 C48.977,85.955,42.244,82.621,42.076,73.778z M49.956,85.888L50,70.526c3.539,2.445,6.846,3.181,6.846,3.181 C56.686,82.551,49.956,85.888,49.956,85.888z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg enable-background="new 0 0 256 256" version="1.1" viewBox="0 0 256 256" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
<title>Trilium Notes</title>
<g>
<path d="m202.9 112.7c-22.5 16.1-54.5 12.8-74.9 6.3l14.8-11.8 14.1-11.3 49.1-39.3-51.2 35.9-14.3 10-14.9 10.5c0.7-21.2 7-49.9 28.6-65.4 1.8-1.3 3.9-2.6 6.1-3.8 2.7-1.5 5.7-2.9 8.8-4.1 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.9 65.9-2.4 2.8-4.9 5.4-7.4 7.8-3.4 3.5-6.8 6.4-10.1 8.8z" fill="#ab60e3"/>
<path d="m213.1 104c-22.2 12.6-51.4 9.3-70.3 3.2l14.1-11.3 49.1-39.3-51.2 35.9-14.3 10c0.5-18.1 4.9-42.1 19.7-58.6 2.7-1.5 5.7-2.9 8.8-4.1 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.9 65.9-2.3 2.8-4.8 5.4-7.2 7.8z" fill="#8038b8"/>
<path d="m220.5 96.2c-21.1 8.6-46.6 5.3-63.7-0.2l49.2-39.4-51.2 35.9c0.3-15.8 3.5-36.6 14.3-52.8 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.8 66z" fill="#560a8f"/>
<path d="m106.7 179c-5.8-21 5.2-43.8 15.5-57.2l4.8 14.2 4.5 13.4 15.9 47-12.8-47.6-3.6-13.2-3.7-13.9c15.5 6.2 35.1 18.6 40.7 38.8 0.5 1.7 0.9 3.6 1.2 5.5 0.4 2.4 0.6 5 0.7 7.7 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8-1.4-2.6-2.7-5.1-3.8-7.6-1.6-3.5-2.9-6.8-3.8-10z" fill="#bb9dd2"/>
<path d="m110.4 188.9c-3.4-19.8 6.9-40.5 16.6-52.9l4.5 13.4 15.9 47-12.8-47.6-3.6-13.2c13.3 5.2 29.9 15 38.1 30.4 0.4 2.4 0.6 5 0.7 7.7 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8-1.4-2.6-2.7-5.2-3.8-7.7z" fill="#9a6cbc"/>
<path d="m114.2 196.5c-0.7-18 8.6-35.9 17.3-47.1l15.9 47-12.8-47.6c11.6 4.4 26.1 12.4 35.2 24.8 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8z" fill="#783ba5"/>
<path d="m86.3 59.1c21.7 10.9 32.4 36.6 35.8 54.9l-15.2-6.6-14.5-6.3-50.6-22 48.8 24.9 13.6 6.9 14.3 7.3c-16.6 7.9-41.3 14.5-62.1 4.1-1.8-0.9-3.6-1.9-5.4-3.2-2.3-1.5-4.5-3.2-6.8-5.1-19.9-16.4-40.3-46.4-42.7-61.5 12.4-6.5 41.5-5.8 64.8-0.3 3.2 0.8 6.2 1.6 9.1 2.5 4 1.3 7.6 2.8 10.9 4.4z" fill="#ab60e3"/>
<path d="m75.4 54.8c18.9 12 28.4 35.6 31.6 52.6l-14.5-6.3-50.6-22 48.7 24.9 13.6 6.9c-14.1 6.8-34.5 13-53.3 8.2-2.3-1.5-4.5-3.2-6.8-5.1-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3 3.1 0.8 6.2 1.6 9.1 2.6z" fill="#8038b8"/>
<path d="m66.3 52.2c15.3 12.8 23.3 33.6 26.1 48.9l-50.6-22 48.8 24.9c-12.2 6-29.6 11.8-46.5 10-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3z" fill="#6f2796"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -1,28 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg enable-background="new 0 0 256 256" version="1.1" viewBox="0 0 256 256" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
<title>Trilium Notes</title>
<style type="text/css">
.st0{fill:#95C980;}
.st1{fill:#72B755;}
.st2{fill:#4FA52B;}
.st3{fill:#EE8C89;}
.st4{fill:#E96562;}
.st5{fill:#E33F3B;}
.st6{fill:#EFB075;}
.st7{fill:#E99547;}
.st8{fill:#E47B19;}
</style>
<g>
<path class="st0" d="m202.9 112.7c-22.5 16.1-54.5 12.8-74.9 6.3l14.8-11.8 14.1-11.3 49.1-39.3-51.2 35.9-14.3 10-14.9 10.5c0.7-21.2 7-49.9 28.6-65.4 1.8-1.3 3.9-2.6 6.1-3.8 2.7-1.5 5.7-2.9 8.8-4.1 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.9 65.9-2.4 2.8-4.9 5.4-7.4 7.8-3.4 3.5-6.8 6.4-10.1 8.8z"/>
<path class="st1" d="m213.1 104c-22.2 12.6-51.4 9.3-70.3 3.2l14.1-11.3 49.1-39.3-51.2 35.9-14.3 10c0.5-18.1 4.9-42.1 19.7-58.6 2.7-1.5 5.7-2.9 8.8-4.1 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.9 65.9-2.3 2.8-4.8 5.4-7.2 7.8z"/>
<path class="st2" d="m220.5 96.2c-21.1 8.6-46.6 5.3-63.7-0.2l49.2-39.4-51.2 35.9c0.3-15.8 3.5-36.6 14.3-52.8 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.8 66z"/>
<path class="st3" d="m106.7 179c-5.8-21 5.2-43.8 15.5-57.2l4.8 14.2 4.5 13.4 15.9 47-12.8-47.6-3.6-13.2-3.7-13.9c15.5 6.2 35.1 18.6 40.7 38.8 0.5 1.7 0.9 3.6 1.2 5.5 0.4 2.4 0.6 5 0.7 7.7 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8-1.4-2.6-2.7-5.1-3.8-7.6-1.6-3.5-2.9-6.8-3.8-10z"/>
<path class="st4" d="m110.4 188.9c-3.4-19.8 6.9-40.5 16.6-52.9l4.5 13.4 15.9 47-12.8-47.6-3.6-13.2c13.3 5.2 29.9 15 38.1 30.4 0.4 2.4 0.6 5 0.7 7.7 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8-1.4-2.6-2.7-5.2-3.8-7.7z"/>
<path class="st5" d="m114.2 196.5c-0.7-18 8.6-35.9 17.3-47.1l15.9 47-12.8-47.6c11.6 4.4 26.1 12.4 35.2 24.8 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8z"/>
<path class="st6" d="m86.3 59.1c21.7 10.9 32.4 36.6 35.8 54.9l-15.2-6.6-14.5-6.3-50.6-22 48.8 24.9 13.6 6.9 14.3 7.3c-16.6 7.9-41.3 14.5-62.1 4.1-1.8-0.9-3.6-1.9-5.4-3.2-2.3-1.5-4.5-3.2-6.8-5.1-19.9-16.4-40.3-46.4-42.7-61.5 12.4-6.5 41.5-5.8 64.8-0.3 3.2 0.8 6.2 1.6 9.1 2.5 4 1.3 7.6 2.8 10.9 4.4z"/>
<path class="st7" d="m75.4 54.8c18.9 12 28.4 35.6 31.6 52.6l-14.5-6.3-50.6-22 48.7 24.9 13.6 6.9c-14.1 6.8-34.5 13-53.3 8.2-2.3-1.5-4.5-3.2-6.8-5.1-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3 3.1 0.8 6.2 1.6 9.1 2.6z"/>
<path class="st8" d="m66.3 52.2c15.3 12.8 23.3 33.6 26.1 48.9l-50.6-22 48.8 24.9c-12.2 6-29.6 11.8-46.5 10-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -1,11 +1,10 @@
import type { CKTextEditor } from "@triliumnext/ckeditor5";
import type CodeMirror from "@triliumnext/codemirror";
import { type LOCALE_IDS, SqlExecuteResponse } from "@triliumnext/commons";
import { SqlExecuteResponse } from "@triliumnext/commons";
import type { NativeImage, TouchBar } from "electron";
import { ColumnComponent } from "tabulator-tables";
import type { Attribute } from "../services/attribute_parser.js";
import bundleService from "../services/bundle.js";
import froca from "../services/froca.js";
import { initLocale, t } from "../services/i18n.js";
import keyboardActionsService from "../services/keyboard_actions.js";
@@ -24,7 +23,6 @@ import { IncludeNoteOpts } from "../widgets/dialogs/include_note.jsx";
import type { InfoProps } from "../widgets/dialogs/info.jsx";
import type { MarkdownImportOpts } from "../widgets/dialogs/markdown_import.jsx";
import { ChooseNoteTypeCallback } from "../widgets/dialogs/note_type_chooser.jsx";
import type { PrintPreviewData } from "../widgets/dialogs/print_preview.jsx";
import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js";
import type NoteTreeWidget from "../widgets/note_tree.js";
import Component from "./component.js";
@@ -281,7 +279,6 @@ export type CommandMappings = {
backInNoteHistory: CommandData;
forwardInNoteHistory: CommandData;
forceSaveRevision: CommandData;
saveNamedRevision: CommandData;
scrollToActiveNote: CommandData;
quickSearch: CommandData;
collapseTree: CommandData;
@@ -305,7 +302,6 @@ export type CommandMappings = {
ninthTab: CommandData;
lastTab: CommandData;
showNoteSource: CommandData;
showNoteOCRText: CommandData;
showSQLConsole: CommandData;
showBackendLog: CommandData;
showCheatsheet: CommandData;
@@ -332,7 +328,6 @@ export type CommandMappings = {
toggleRightPane: CommandData;
printActiveNote: CommandData;
exportAsPdf: CommandData;
showPrintPreview: PrintPreviewData;
openNoteExternally: CommandData;
openNoteCustom: CommandData;
openNoteOnServer: CommandData;
@@ -513,7 +508,7 @@ type EventMappings = {
contentSafeMarginChanged: {
top: number;
noteContext: NoteContext;
};
}
};
export type EventListener<T extends EventNames> = {
@@ -567,7 +562,7 @@ export class AppContext extends Component {
*/
async earlyInit() {
await options.initializedPromise;
await initLocale((options.get("locale") || "en") as LOCALE_IDS);
await initLocale();
}
setLayout(layout: Layout) {
@@ -582,6 +577,7 @@ export class AppContext extends Component {
this.tabManager.loadTabs();
const bundleService = (await import("../services/bundle.js")).default;
setTimeout(() => bundleService.executeStartupBundles(), 2000);
}

View File

@@ -1,7 +1,7 @@
import { CreateChildrenResponse, SqlExecuteResponse } from "@triliumnext/commons";
import { closeAllHeadlessAutocompletes } from "../services/autocomplete_core.js";
import bundleService from "../services/bundle.js";
import dialog from "../services/dialog.js";
import dateNoteService from "../services/date_notes.js";
import froca from "../services/froca.js";
import { t } from "../services/i18n.js";
@@ -198,7 +198,7 @@ export default class Entrypoints extends Component {
hideAllPopups() {
if (utils.isDesktop()) {
$(".aa-input").autocomplete("close");
closeAllHeadlessAutocompletes();
}
}
@@ -217,21 +217,4 @@ export default class Entrypoints extends Component {
toastService.showMessage(t("entrypoints.note-revision-created"));
}
async saveNamedRevisionCommand() {
const noteId = appContext.tabManager.getActiveContextNoteId();
if (!noteId) return;
const name = await dialog.prompt({
title: t("entrypoints.save-named-revision-title"),
message: t("entrypoints.save-named-revision-message"),
defaultValue: ""
});
// null means the user cancelled
if (name === null) return;
await server.post(`notes/${noteId}/revision`, { description: name || undefined });
toastService.showMessage(t("entrypoints.note-revision-created"));
}
}

View File

@@ -25,15 +25,6 @@ export type GetTextEditorCallback = (editor: CKTextEditor) => void;
export type SaveState = "saved" | "saving" | "unsaved" | "error";
const READ_ONLY_CAPABLE_TYPES: string[] = [
"text",
"code",
"mermaid",
"canvas",
"mindMap",
"spreadsheet"
];
export interface NoteContextDataMap {
toc: HeadingContext;
pdfPages: {
@@ -312,12 +303,8 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
return false;
}
if (!this.note) {
return false;
}
// Note types that support a read-only state (via the #readOnly label, source view, or auto-readonly).
if (!READ_ONLY_CAPABLE_TYPES.includes(this.note.type)) {
// "readOnly" is a state valid only for text/code notes
if (!this.note || (this.note.type !== "text" && this.note.type !== "code")) {
return false;
}
@@ -333,11 +320,6 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
return true;
}
// Auto read-only based on content size is only configurable for text/code.
if (this.note.type !== "text" && this.note.type !== "code") {
return false;
}
// Store the initial decision about read-only status in the viewScope
// This will be "remembered" until the viewScope is refreshed
if (!this.viewScope) {

View File

@@ -148,19 +148,6 @@ export default class RootCommandExecutor extends Component {
}
}
async showNoteOCRTextCommand() {
const notePath = appContext.tabManager.getActiveContextNotePath();
if (notePath) {
await appContext.tabManager.openTabWithNoteWithHoisting(notePath, {
activate: true,
viewScope: {
viewMode: "ocr"
}
});
}
}
async showAttachmentsCommand() {
const notePath = appContext.tabManager.getActiveContextNotePath();

View File

@@ -1,15 +1,16 @@
import Component from "./component.js";
import SpacedUpdate from "../services/spaced_update.js";
import server from "../services/server.js";
import options from "../services/options.js";
import froca from "../services/froca.js";
import treeService from "../services/tree.js";
import NoteContext from "./note_context.js";
import appContext from "./app_context.js";
import Mutex from "../utils/mutex.js";
import linkService from "../services/link.js";
import type { EventData } from "./app_context.js";
import type FNote from "../entities/fnote.js";
import { closeAllHeadlessAutocompletes } from "../services/autocomplete_core.js";
import froca from "../services/froca.js";
import linkService from "../services/link.js";
import options from "../services/options.js";
import server from "../services/server.js";
import SpacedUpdate from "../services/spaced_update.js";
import treeService from "../services/tree.js";
import Mutex from "../utils/mutex.js";
import type { EventData } from "./app_context.js";
import appContext from "./app_context.js";
import Component from "./component.js";
import NoteContext from "./note_context.js";
interface TabState {
contexts: NoteContext[];
@@ -429,10 +430,7 @@ export default class TabManager extends Component {
}
// close dangling autocompletes after closing the tab
const $autocompleteEl = $(".aa-input");
if ("autocomplete" in $autocompleteEl) {
$autocompleteEl.autocomplete("close");
}
closeAllHeadlessAutocompletes();
// close dangling tooltips
$("body > div.tooltip").remove();

View File

@@ -1,5 +1,3 @@
import "autocomplete.js/index_jquery.js";
import type ElectronRemote from "@electron/remote";
import type Electron from "electron";
@@ -54,7 +52,7 @@ function initOnElectron() {
const currentWindow = electronRemote.getCurrentWindow();
const style = window.getComputedStyle(document.body);
initDarkOrLightMode();
initDarkOrLightMode(style);
initTransparencyEffects(style, currentWindow);
initFullScreenDetection(currentWindow);
@@ -119,11 +117,11 @@ function initTransparencyEffects(style: CSSStyleDeclaration, currentWindow: Elec
*
* @param style the root CSS element to read variables from.
*/
function initDarkOrLightMode() {
function initDarkOrLightMode(style: CSSStyleDeclaration) {
let themeSource: typeof nativeTheme.themeSource = "system";
const themeStyle = window.glob.getThemeStyle();
if (themeStyle !== "auto") {
const themeStyle = style.getPropertyValue("--theme-style");
if (style.getPropertyValue("--theme-style-auto") !== "true" && (themeStyle === "light" || themeStyle === "dark")) {
themeSource = themeStyle;
}

View File

@@ -66,15 +66,7 @@ class FAttribute {
}
get isAutoLink() {
if (this.type === "relation") {
return ["internalLink", "imageLink", "relationMapLink", "includeNoteLink"].includes(this.name);
}
if (this.type === "label") {
return this.name === "internalBookmark";
}
return false;
return this.type === "relation" && ["internalLink", "imageLink", "relationMapLink", "includeNoteLink"].includes(this.name);
}
get toString() {

View File

@@ -1,6 +1,5 @@
import { getNoteIcon } from "@triliumnext/commons";
import bundleService from "../services/bundle.js";
import cssClassManager from "../services/css_class_manager.js";
import type { Froca } from "../services/froca-interface.js";
import noteAttributeCache from "../services/note_attribute_cache.js";
@@ -19,7 +18,7 @@ const RELATION = "relation";
* end user. Those types should be used only for checking against, they are
* not for direct use.
*/
export type NoteType = "file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code" | "mindMap" | "spreadsheet" | "llmChat";
export type NoteType = "file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code" | "mindMap" | "spreadsheet";
export interface NotePathRecord {
isArchived: boolean;
@@ -236,16 +235,6 @@ export default class FNote {
return this.hasAttribute("label", "archived");
}
/**
* Returns true if the note's metadata (title, icon) should not be editable.
* This applies to system notes like options, help, and launch bar configuration.
*/
get isMetadataReadOnly() {
return utils.isLaunchBarConfig(this.noteId)
|| this.noteId.startsWith("_help_")
|| this.noteId.startsWith("_options");
}
getChildNoteIds() {
return this.children;
}
@@ -1025,6 +1014,7 @@ export default class FNote {
const env = this.getScriptEnv();
if (env === "frontend") {
const bundleService = (await import("../services/bundle.js")).default;
return await bundleService.getAndExecuteBundle(this.noteId);
} else if (env === "backend") {
await server.post(`script/run/${this.noteId}`);
@@ -1069,10 +1059,6 @@ export default class FNote {
return this.mime === "text/x-sqlite;schema=trilium";
}
isMarkdown() {
return this.type === "code" && (this.mime === "text/markdown" || this.mime === "text/x-markdown" || this.mime === "text/x-gfm");
}
isTriliumScript() {
return this.mime.startsWith("application/javascript");
}

View File

@@ -1,5 +1,3 @@
import { getThemeStyle } from "./services/theme";
async function bootstrap() {
showSplash();
await setupGlob();
@@ -18,17 +16,6 @@ async function initJQuery() {
const $ = (await import("jquery")).default;
window.$ = $;
window.jQuery = $;
// Polyfill removed jQuery methods for autocomplete.js compatibility
($ as any).isArray = Array.isArray;
($ as any).isFunction = function(obj: any) { return typeof obj === 'function'; };
($ as any).isPlainObject = function(obj: any) {
if (obj == null || typeof obj !== 'object') { return false; }
const proto = Object.getPrototypeOf(obj);
if (proto === null) { return true; }
const Ctor = Object.prototype.hasOwnProperty.call(proto, 'constructor') && proto.constructor;
return typeof Ctor === 'function' && Ctor === Object;
};
}
async function setupGlob() {
@@ -40,7 +27,6 @@ async function setupGlob() {
...json,
activeDialog: null
};
window.glob.getThemeStyle = getThemeStyle;
}
async function loadBootstrapCss() {
@@ -52,65 +38,31 @@ async function loadBootstrapCss() {
}
}
type StylesheetRef = {
href: string;
media?: string;
};
function getConfiguredThemeStylesheets(stylesheetsPath: string, theme: string, customThemeCssUrl?: string) {
if (theme === "auto") {
return [{ href: `${stylesheetsPath}/theme-dark.css`, media: "(prefers-color-scheme: dark)" }];
}
if (theme === "dark") {
return [{ href: `${stylesheetsPath}/theme-dark.css` }];
}
if (theme === "next") {
return [
{ href: `${stylesheetsPath}/theme-next-light.css` },
{ href: `${stylesheetsPath}/theme-next-dark.css`, media: "(prefers-color-scheme: dark)" }
];
}
if (theme === "next-light") {
return [{ href: `${stylesheetsPath}/theme-next-light.css` }];
}
if (theme === "next-dark") {
return [{ href: `${stylesheetsPath}/theme-next-dark.css` }];
}
if (theme !== "light" && customThemeCssUrl) {
return [{ href: customThemeCssUrl }];
}
return [];
}
function loadStylesheets() {
const { device, assetPath, theme, themeBase, customThemeCssUrl } = window.glob;
const stylesheetsPath = `${assetPath}/stylesheets`;
const { device, assetPath, themeCssUrl, themeUseNextAsBase } = window.glob;
const cssToLoad: StylesheetRef[] = [];
const cssToLoad: string[] = [];
if (device !== "print") {
cssToLoad.push({ href: `${stylesheetsPath}/ckeditor-theme.css` });
cssToLoad.push({ href: `api/fonts` });
cssToLoad.push({ href: `${stylesheetsPath}/theme-light.css` });
cssToLoad.push(...getConfiguredThemeStylesheets(stylesheetsPath, theme, customThemeCssUrl));
if (themeBase) {
cssToLoad.push(...getConfiguredThemeStylesheets(stylesheetsPath, themeBase));
cssToLoad.push(`${assetPath}/stylesheets/ckeditor-theme.css`);
cssToLoad.push(`api/fonts`);
cssToLoad.push(`${assetPath}/stylesheets/theme-light.css`);
if (themeCssUrl) {
cssToLoad.push(themeCssUrl);
}
cssToLoad.push({ href: `${stylesheetsPath}/style.css` });
if (themeUseNextAsBase === "next") {
cssToLoad.push(`${assetPath}/stylesheets/theme-next.css`);
} else if (themeUseNextAsBase === "next-dark") {
cssToLoad.push(`${assetPath}/stylesheets/theme-next-dark.css`);
} else if (themeUseNextAsBase === "next-light") {
cssToLoad.push(`${assetPath}/stylesheets/theme-next-light.css`);
}
cssToLoad.push(`${assetPath}/stylesheets/style.css`);
}
for (const { href, media } of cssToLoad) {
for (const href of cssToLoad) {
const linkEl = document.createElement("link");
linkEl.href = href;
linkEl.rel = "stylesheet";
if (media) {
linkEl.media = media;
}
document.head.appendChild(linkEl);
}
}

View File

@@ -24,7 +24,6 @@ import InfoDialog from "../widgets/dialogs/info.js";
import IncorrectCpuArchDialog from "../widgets/dialogs/incorrect_cpu_arch.js";
import CallToActionDialog from "../widgets/dialogs/call_to_action.jsx";
import PopupEditorDialog from "../widgets/dialogs/PopupEditor.jsx";
import PrintPreviewDialog from "../widgets/dialogs/print_preview.jsx";
import ToastContainer from "../widgets/Toast.jsx";
export function applyModals(rootContainer: RootContainer) {
@@ -52,7 +51,6 @@ export function applyModals(rootContainer: RootContainer) {
.child(<PromptDialog />)
.child(<IncorrectCpuArchDialog />)
.child(<PopupEditorDialog />)
.child(<PrintPreviewDialog />)
.child(<CallToActionDialog />)
.child(<ToastContainer />);
}

View File

@@ -39,7 +39,6 @@ export interface MenuCommandItem<T> {
title: string;
command?: T;
type?: string;
mime?: string;
/**
* The icon to display in the menu item.
*

View File

@@ -1,14 +1,12 @@
import type { BrowserWindow } from "electron";
import type { CommandNames } from "../components/app_context.js";
import appContext from "../components/app_context.js";
import zoomService from "../components/zoom.js";
import * as clipboardExt from "../services/clipboard_ext.js";
import { t } from "../services/i18n.js";
import options from "../services/options.js";
import server from "../services/server.js";
import utils from "../services/utils.js";
import options from "../services/options.js";
import zoomService from "../components/zoom.js";
import contextMenu, { type MenuItem } from "./context_menu.js";
import { t } from "../services/i18n.js";
import server from "../services/server.js";
import * as clipboardExt from "../services/clipboard_ext.js";
import type { BrowserWindow } from "electron";
import type { CommandNames, AppContext } from "../components/app_context.js";
function setupContextMenu() {
const electron = utils.dynamicRequire("electron");
@@ -17,6 +15,8 @@ function setupContextMenu() {
// FIXME: Remove typecast once Electron is properly integrated.
const { webContents } = remote.getCurrentWindow() as BrowserWindow;
let appContext: AppContext;
webContents.on("context-menu", (event, params) => {
const { editFlags } = params;
const hasText = params.selectionText.trim().length > 0;
@@ -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: () => electron.ipcRenderer.send("add-word-to-dictionary", params.misspelledWord)
handler: () => webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord)
});
items.push({ kind: "separator" });
@@ -141,7 +141,7 @@ function setupContextMenu() {
}
// Replace the placeholder with the real search keyword.
const searchUrl = searchEngineUrl.replace("{keyword}", encodeURIComponent(params.selectionText));
let searchUrl = searchEngineUrl.replace("{keyword}", encodeURIComponent(params.selectionText));
items.push({ kind: "separator" });
@@ -155,6 +155,10 @@ function setupContextMenu() {
title: t("electron_context_menu.search_in_trilium", { term: shortenedSelection }),
uiIcon: "bx bx-search",
handler: async () => {
if (!appContext) {
appContext = (await import("../components/app_context.js")).default;
}
await appContext.triggerCommand("searchNotes", {
searchString: params.selectionText
});

View File

@@ -1,101 +0,0 @@
import type { ToggleInParentResponse } from "@triliumnext/commons";
import type FNote from "../entities/fnote.js";
import branchService from "../services/branches.js";
import { t } from "../services/i18n.js";
import server from "../services/server.js";
import toast from "../services/toast.js";
import contextMenu, { type ContextMenuEvent, type MenuItem } from "./context_menu.js";
const VISIBLE_LAUNCHER_PARENTS = ["_lbVisibleLaunchers", "_lbMobileVisibleLaunchers"];
function getVisibleLauncherBranch(launcherNote: FNote) {
return launcherNote.getParentBranches().find((b) => VISIBLE_LAUNCHER_PARENTS.includes(b.parentNoteId));
}
function getBookmarkBranch(launcherNote: FNote) {
return launcherNote.getParentBranches().find((b) => b.parentNoteId === "_lbBookmarks");
}
async function removeFromLaunchBar(launcherNote: FNote) {
const bookmarkBranch = getBookmarkBranch(launcherNote);
if (bookmarkBranch) {
// Individual bookmarks are represented via a branch under `_lbBookmarks`; removing them
// from the launch bar is the same as unbookmarking the note.
const resp = await server.put<ToggleInParentResponse>(
`notes/${launcherNote.noteId}/toggle-in-parent/_lbBookmarks/false`
);
if (!resp.success && resp.message) {
toast.showError(resp.message);
}
return;
}
const launcherBranch = getVisibleLauncherBranch(launcherNote);
if (!launcherBranch) return;
const isMobileLauncher = launcherBranch.parentNoteId === "_lbMobileVisibleLaunchers";
// Branch IDs in the hidden subtree follow the `${parentNoteId}_${noteId}` convention,
// so the branch linking `_lb(Mobile)?Root` to the "available" launchers root is predictable.
const targetBranchId = isMobileLauncher
? "_lbMobileRoot__lbMobileAvailableLaunchers"
: "_lbRoot__lbAvailableLaunchers";
await branchService.moveToParentNote([launcherBranch.branchId], targetBranchId);
}
export function canRemoveFromLaunchBar(launcherNote: FNote | null | undefined) {
if (!launcherNote) return false;
return !!(getVisibleLauncherBranch(launcherNote) || getBookmarkBranch(launcherNote));
}
export interface ShowLauncherContextMenuOptions<T extends string> {
/** Menu items specific to this launcher (e.g. "Open in new tab" for note-based launchers). They appear above the "Remove from launch bar" item. */
extraItems?: MenuItem<T>[];
/** Handler for the {@link extraItems}. The "Remove from launch bar" item is handled internally and will not be forwarded. */
onCommand?: (command: T | undefined) => void;
}
const REMOVE_COMMAND = "__removeFromLaunchBar__";
/**
* Displays the launch bar icon context menu. When the launcher can be removed (i.e. it is a direct
* child of the visible launchers root or of `_lbBookmarks`), a "Remove from launch bar" entry is
* appended. Extra items can be supplied to preserve launcher-specific actions (e.g. "Open in new tab").
*/
export async function showLauncherContextMenu<T extends string>(
launcherNote: FNote | null | undefined,
e: ContextMenuEvent,
options: ShowLauncherContextMenuOptions<T> = {}
) {
e.preventDefault();
const items = [...(options.extraItems ?? [])] as MenuItem<string>[];
if (canRemoveFromLaunchBar(launcherNote)) {
if (items.length > 0) {
items.push({ kind: "separator" });
}
items.push({
title: t("launcher_button_context_menu.remove_from_launch_bar"),
command: REMOVE_COMMAND,
uiIcon: "bx bx-x-circle"
});
}
if (items.length === 0) return;
contextMenu.show<string>({
x: e.pageX ?? 0,
y: e.pageY ?? 0,
items,
selectMenuItemHandler: ({ command }) => {
if (command === REMOVE_COMMAND) {
if (launcherNote) {
void removeFromLaunchBar(launcherNote);
}
return;
}
options.onCommand?.(command as T | undefined);
}
});
}

View File

@@ -288,7 +288,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
return items.filter((row) => row !== null) as MenuItem<TreeCommandNames>[];
}
async selectMenuItemHandler({ command, type, mime, templateNoteId }: MenuCommandItem<TreeCommandNames>) {
async selectMenuItemHandler({ command, type, templateNoteId }: MenuCommandItem<TreeCommandNames>) {
const notePath = treeService.getNotePath(this.node);
if (utils.isMobile()) {
@@ -305,7 +305,6 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
target: "after",
targetBranchId: this.node.data.branchId,
type,
mime,
isProtected,
templateNoteId
});
@@ -314,7 +313,6 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
noteCreateService.createNote(parentNotePath, {
type,
mime,
isProtected: this.node.data.isProtected,
templateNoteId
});

View File

@@ -1,5 +1,3 @@
import "autocomplete.js/index_jquery.js";
import appContext from "./components/app_context.js";
import glob from "./services/glob.js";
import noteAutocompleteService from "./services/note_autocomplete.js";

View File

@@ -2,17 +2,18 @@
:root {
--print-font-size: 11pt;
--ck-content-color-image-caption-background: transparent !important;
}
html,
body {
--print-font-family: var(--detail-font-family, sans-serif);
width: 100%;
height: 100%;
color: black;
font-family: var(--print-font-family);
}
@page {
margin: 2cm;
}
.note-list-widget.full-height,
@@ -25,12 +26,6 @@ body {
}
body[data-note-type="text"] .ck-content {
--ck-content-font-family: var(--print-font-family);
--ck-content-font-size: var(--print-font-size);
--ck-content-font-color: black;
--ck-content-line-height: 1.5;
--ck-content-color-image-caption-background: transparent;
font-size: var(--print-font-size);
text-align: justify;
}
@@ -159,4 +154,4 @@ span[style] {
.page-break::after {
display: none !important;
}
/* #endregion */
/* #endregion */

View File

@@ -4,7 +4,6 @@ import { useCallback, useLayoutEffect, useRef } from "preact/hooks";
import FNote from "./entities/fnote";
import content_renderer from "./services/content_renderer";
import { applyInlineMermaid } from "./services/content_renderer_text";
import froca from "./services/froca";
import { dynamicRequire, isElectron } from "./services/utils";
import { CustomNoteList, useNoteViewType } from "./widgets/collections/NoteList";
@@ -31,21 +30,7 @@ async function main() {
if (!noteId) return;
await import("./print.css");
// Browser printing relies on @page margins since there's no programmatic control.
// Electron uses printToPDF() margins instead, so we only inject this for the browser path.
if (!isElectron()) {
const style = document.createElement("style");
style.textContent = "@page { margin: 2cm; }";
document.head.appendChild(style);
}
// Load the user's font preferences so that --detail-font-family is available.
const fontLink = document.createElement("link");
fontLink.rel = "stylesheet";
fontLink.href = "api/fonts";
document.head.appendChild(fontLink);
const froca = (await import("./services/froca")).default;
const note = await froca.getNote(noteId);
const bodyWrapper = document.createElement("div");
@@ -120,9 +105,6 @@ function SingleNoteRenderer({ note, onReady }: RendererProps) {
// Check custom CSS.
await loadCustomCss(note);
// Wait for all fonts (including those from custom CSS) to finish loading.
await document.fonts.ready;
}
load().then(() => requestAnimationFrame(() => onReady({
@@ -148,7 +130,6 @@ function CollectionRenderer({ note, onReady, onProgressChanged }: RendererProps)
media="print"
onReady={async (data: PrintReport) => {
await loadCustomCss(note);
await document.fonts.ready;
onReady(data);
}}
onProgressChanged={onProgressChanged}

View File

@@ -8,17 +8,6 @@ async function loadBootstrap() {
}
}
// Polyfill removed jQuery methods for autocomplete.js compatibility
($ as any).isArray = Array.isArray;
($ as any).isFunction = function(obj: any) { return typeof obj === 'function'; };
($ as any).isPlainObject = function(obj: any) {
if (obj == null || typeof obj !== 'object') { return false; }
const proto = Object.getPrototypeOf(obj);
if (proto === null) { return true; }
const Ctor = Object.prototype.hasOwnProperty.call(proto, 'constructor') && proto.constructor;
return typeof Ctor === 'function' && Ctor === Object;
};
(window as any).$ = $;
(window as any).jQuery = $;
await loadBootstrap();

View File

@@ -0,0 +1,47 @@
import { describe, expect, it } from "vitest";
import { shouldAutocompleteHandleEnterKey } from "./attribute_autocomplete.js";
describe("attribute autocomplete enter handling", () => {
it("delegates plain Enter when the panel is open and an item is active", () => {
expect(shouldAutocompleteHandleEnterKey(
{ key: "Enter", ctrlKey: false, metaKey: false },
{ isPanelOpen: true, hasActiveItem: true }
)).toBe(true);
});
it("does not delegate plain Enter when there is no active suggestion", () => {
expect(shouldAutocompleteHandleEnterKey(
{ key: "Enter", ctrlKey: false, metaKey: false },
{ isPanelOpen: true, hasActiveItem: false }
)).toBe(false);
});
it("does not delegate plain Enter when the panel is closed", () => {
expect(shouldAutocompleteHandleEnterKey(
{ key: "Enter", ctrlKey: false, metaKey: false },
{ isPanelOpen: false, hasActiveItem: false }
)).toBe(false);
});
it("does not delegate Ctrl+Enter even when an item is active", () => {
expect(shouldAutocompleteHandleEnterKey(
{ key: "Enter", ctrlKey: true, metaKey: false },
{ isPanelOpen: true, hasActiveItem: true }
)).toBe(false);
});
it("does not delegate Cmd+Enter even when an item is active", () => {
expect(shouldAutocompleteHandleEnterKey(
{ key: "Enter", ctrlKey: false, metaKey: true },
{ isPanelOpen: true, hasActiveItem: true }
)).toBe(false);
});
it("ignores non-Enter keys", () => {
expect(shouldAutocompleteHandleEnterKey(
{ key: "ArrowDown", ctrlKey: false, metaKey: false },
{ isPanelOpen: false, hasActiveItem: false }
)).toBe(true);
});
});

View File

@@ -1,114 +1,450 @@
import type { AutocompleteApi as CoreAutocompleteApi, BaseItem } from "@algolia/autocomplete-core";
import { createAutocomplete } from "@algolia/autocomplete-core";
import type { AttributeType } from "../entities/fattribute.js";
import { bindAutocompleteInput, createHeadlessPanelController, registerHeadlessAutocompleteCloser, withHeadlessSourceDefaults } from "./autocomplete_core.js";
import server from "./server.js";
interface InitOptions {
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
interface NameItem extends BaseItem {
name: string;
}
export function shouldAutocompleteHandleEnterKey(
event: Pick<KeyboardEvent, "key" | "ctrlKey" | "metaKey">,
{ isPanelOpen, hasActiveItem }: { isPanelOpen: boolean; hasActiveItem: boolean }
) {
if (event.key !== "Enter") {
return true;
}
if (event.ctrlKey || event.metaKey) {
return false;
}
return isPanelOpen && hasActiveItem;
}
interface InitAttributeNameOptions {
/** The <input> element where the user types */
$el: JQuery<HTMLElement>;
attributeType?: AttributeType | (() => AttributeType);
open: boolean;
nameCallback?: () => string;
/** Called when the user selects a value or the panel closes */
onValueChange?: (value: string) => void;
}
/**
* @param $el - element on which to init autocomplete
* @param attributeType - "relation" or "label" or callback providing one of those values as a type of autocompleted attributes
* @param open - should the autocomplete be opened after init?
*/
function initAttributeNameAutocomplete({ $el, attributeType, open }: InitOptions) {
if (!$el.hasClass("aa-input")) {
$el.autocomplete(
{
appendTo: document.querySelector("body"),
hint: false,
openOnFocus: true,
minLength: 0,
tabAutocomplete: false
},
[
{
displayKey: "name",
// disabling cache is important here because otherwise cache can stay intact when switching between attribute type which will lead to autocomplete displaying attribute names for incorrect attribute type
cache: false,
source: async (term, cb) => {
const type = typeof attributeType === "function" ? attributeType() : attributeType;
// ---------------------------------------------------------------------------
// Instance tracking
// ---------------------------------------------------------------------------
const names = await server.get<string[]>(`attribute-names/?type=${type}&query=${encodeURIComponent(term)}`);
const result = names.map((name) => ({ name }));
interface ManagedInstance {
autocomplete: CoreAutocompleteApi<NameItem>;
panelEl: HTMLElement;
cleanup: () => void;
}
cb(result);
}
}
]
);
const instanceMap = new WeakMap<HTMLElement, ManagedInstance>();
$el.on("autocomplete:opened", () => {
if ($el.attr("readonly")) {
$el.autocomplete("close");
function renderItems(
panelEl: HTMLElement,
items: NameItem[],
activeItemId: number | null,
onSelect: (item: NameItem) => void,
onActivate: (index: number) => void,
onDeactivate: () => void
): void {
panelEl.innerHTML = "";
if (items.length === 0) {
panelEl.style.display = "none";
return;
}
const list = document.createElement("ul");
list.className = "aa-core-list";
items.forEach((item, index) => {
const li = document.createElement("li");
li.className = "aa-core-item";
if (index === activeItemId) {
li.classList.add("aa-core-item--active");
}
li.textContent = item.name;
li.addEventListener("mousemove", () => {
if (activeItemId === index) {
return;
}
onActivate(index);
});
}
li.addEventListener("mouseleave", (event) => {
const relatedTarget = event.relatedTarget;
if (relatedTarget instanceof HTMLElement && li.contains(relatedTarget)) {
return;
}
if (open) {
$el.autocomplete("open");
}
onDeactivate();
});
li.addEventListener("mousedown", (e) => {
e.preventDefault(); // prevent input blur
e.stopPropagation();
onSelect(item);
});
list.appendChild(li);
});
panelEl.appendChild(list);
}
async function initLabelValueAutocomplete({ $el, open, nameCallback }: InitOptions) {
if ($el.hasClass("aa-input")) {
// we reinit every time because autocomplete seems to have a bug where it retains state from last
// open even though the value was reset
$el.autocomplete("destroy");
}
// ---------------------------------------------------------------------------
// Attribute name autocomplete — new (autocomplete-core, headless)
// ---------------------------------------------------------------------------
let attributeName = "";
if (nameCallback) {
attributeName = nameCallback();
}
function initAttributeNameAutocomplete({ $el, attributeType, open, onValueChange }: InitAttributeNameOptions) {
const inputEl = $el[0] as HTMLInputElement;
const syncQueryFromInputValue = (autocomplete: CoreAutocompleteApi<NameItem>) => {
autocomplete.setQuery(inputEl.value || "");
};
if (attributeName.trim() === "") {
// Already initialized — just open if requested
if (instanceMap.has(inputEl)) {
if (open) {
const inst = instanceMap.get(inputEl)!;
syncQueryFromInputValue(inst.autocomplete);
inst.autocomplete.setIsOpen(true);
inst.autocomplete.refresh();
}
return;
}
const attributeValues = (await server.get<string[]>(`attribute-values/${encodeURIComponent(attributeName)}`)).map((attribute) => ({ value: attribute }));
const panelController = createHeadlessPanelController({ inputEl });
const { panelEl } = panelController;
if (attributeValues.length === 0) {
return;
}
let isPanelOpen = false;
let hasActiveItem = false;
$el.autocomplete(
{
appendTo: document.querySelector("body"),
hint: false,
openOnFocus: false, // handled manually
minLength: 0,
tabAutocomplete: false
const autocomplete = createAutocomplete<NameItem>({
openOnFocus: true,
defaultActiveItemId: 0,
shouldPanelOpen() {
return true;
},
[
{
displayKey: "value",
cache: false,
source: async function (term, cb) {
term = term.toLowerCase();
const filtered = attributeValues.filter((attr) => attr.value.toLowerCase().includes(term));
getSources({ query }) {
return [
withHeadlessSourceDefaults({
sourceId: "attribute-names",
getItems() {
const type = typeof attributeType === "function" ? attributeType() : attributeType;
return server
.get<string[]>(`attribute-names/?type=${type}&query=${encodeURIComponent(query)}`)
.then((names) => names.map((name) => ({ name })));
},
getItemInputValue({ item }) {
return item.name;
},
onSelect({ item }) {
inputEl.value = item.name;
autocomplete.setQuery(item.name);
autocomplete.setIsOpen(false);
onValueChange?.(item.name);
},
}),
];
},
cb(filtered);
}
onStateChange({ state }) {
isPanelOpen = state.isOpen;
hasActiveItem = state.activeItemId !== null;
// Render items
const collections = state.collections;
const items = collections.length > 0 ? (collections[0].items as NameItem[]) : [];
const activeId = state.activeItemId ?? null;
if (state.isOpen && items.length > 0) {
renderItems(
panelEl,
items,
activeId,
(item) => {
inputEl.value = item.name;
autocomplete.setQuery(item.name);
autocomplete.setIsOpen(false);
onValueChange?.(item.name);
},
(index) => {
autocomplete.setActiveItemId(index);
},
() => {
autocomplete.setActiveItemId(null);
}
);
panelController.startPositioning();
} else {
panelController.hide();
}
]
);
$el.on("autocomplete:opened", () => {
if ($el.attr("readonly")) {
$el.autocomplete("close");
if (!state.isOpen) {
panelController.hide();
}
},
});
const unregisterGlobalCloser = registerHeadlessAutocompleteCloser(() => {
autocomplete.setIsOpen(false);
panelController.hide();
});
const cleanupInputBindings = bindAutocompleteInput<NameItem>({
inputEl,
autocomplete,
onInput(e, handlers) {
handlers.onChange(e as any);
},
onFocus(e, handlers) {
syncQueryFromInputValue(autocomplete);
handlers.onFocus(e as any);
},
onBlur() {
// Delay to allow mousedown on panel items
setTimeout(() => {
autocomplete.setIsOpen(false);
panelController.hide();
onValueChange?.(inputEl.value);
}, 50);
},
onKeyDown(e, handlers) {
if (!shouldAutocompleteHandleEnterKey(e, { isPanelOpen, hasActiveItem })) {
return;
}
if (e.key === "Enter") {
// Prevent the enter key from propagating to parent dialogs
// (which might interpret it as "submit" or "save and close")
e.stopPropagation();
}
handlers.onKeyDown(e as any);
}
});
const cleanup = () => {
unregisterGlobalCloser();
cleanupInputBindings();
panelController.destroy();
};
instanceMap.set(inputEl, { autocomplete, panelEl, cleanup });
if (open) {
$el.autocomplete("open");
syncQueryFromInputValue(autocomplete);
autocomplete.setIsOpen(true);
autocomplete.refresh();
panelController.startPositioning();
}
}
// ---------------------------------------------------------------------------
// Label value autocomplete (headless autocomplete-core)
// ---------------------------------------------------------------------------
interface LabelValueInitOptions {
$el: JQuery<HTMLElement>;
open: boolean;
nameCallback?: () => string;
onValueChange?: (value: string) => void;
}
function initLabelValueAutocomplete({ $el, open, nameCallback, onValueChange }: LabelValueInitOptions) {
const inputEl = $el[0] as HTMLInputElement;
const syncQueryFromInputValue = (autocomplete: CoreAutocompleteApi<NameItem>) => {
autocomplete.setQuery(inputEl.value || "");
};
if (instanceMap.has(inputEl)) {
if (open) {
const inst = instanceMap.get(inputEl)!;
syncQueryFromInputValue(inst.autocomplete);
inst.autocomplete.setIsOpen(true);
inst.autocomplete.refresh();
}
return;
}
const panelController = createHeadlessPanelController({ inputEl });
const { panelEl } = panelController;
let isPanelOpen = false;
let hasActiveItem = false;
let isSelecting = false;
let cachedAttributeName = "";
let cachedAttributeValues: NameItem[] = [];
const handleSelect = (item: NameItem) => {
isSelecting = true;
inputEl.value = item.name;
inputEl.dispatchEvent(new Event("input", { bubbles: true }));
autocomplete.setQuery(item.name);
autocomplete.setIsOpen(false);
onValueChange?.(item.name);
isSelecting = false;
setTimeout(() => {
// Preserve the legacy contract: several consumers still commit the
// selected value from their existing Enter key handlers instead of
// listening to the autocomplete selection event directly.
inputEl.dispatchEvent(new KeyboardEvent("keydown", {
key: "Enter",
code: "Enter",
keyCode: 13,
which: 13,
bubbles: true,
cancelable: true
}));
}, 0);
};
const autocomplete = createAutocomplete<NameItem>({
openOnFocus: true,
defaultActiveItemId: null,
shouldPanelOpen() {
return true;
},
getSources({ query }) {
return [
withHeadlessSourceDefaults({
sourceId: "attribute-values",
async getItems() {
const attributeName = nameCallback ? nameCallback() : "";
if (!attributeName.trim()) {
return [];
}
if (attributeName !== cachedAttributeName || cachedAttributeValues.length === 0) {
cachedAttributeName = attributeName;
const values = await server.get<string[]>(`attribute-values/${encodeURIComponent(attributeName)}`);
cachedAttributeValues = values.map((name) => ({ name }));
}
const q = query.toLowerCase();
return cachedAttributeValues.filter((attr) => attr.name.toLowerCase().includes(q));
},
getItemInputValue({ item }) {
return item.name;
},
onSelect({ item }) {
handleSelect(item);
},
}),
];
},
onStateChange({ state }) {
isPanelOpen = state.isOpen;
hasActiveItem = state.activeItemId !== null;
const collections = state.collections;
const items = collections.length > 0 ? (collections[0].items as NameItem[]) : [];
const activeId = state.activeItemId ?? null;
if (state.isOpen && items.length > 0) {
renderItems(
panelEl,
items,
activeId,
handleSelect,
(index) => {
autocomplete.setActiveItemId(index);
},
() => {
autocomplete.setActiveItemId(null);
}
);
panelController.startPositioning();
} else {
panelController.hide();
}
if (!state.isOpen) {
panelController.hide();
}
},
});
const unregisterGlobalCloser = registerHeadlessAutocompleteCloser(() => {
autocomplete.setIsOpen(false);
panelController.hide();
});
const cleanupInputBindings = bindAutocompleteInput<NameItem>({
inputEl,
autocomplete,
onInput(e, handlers) {
if (!isSelecting) {
handlers.onChange(e as any);
}
},
onFocus(e, handlers) {
const attributeName = nameCallback ? nameCallback() : "";
if (attributeName !== cachedAttributeName) {
cachedAttributeName = "";
cachedAttributeValues = [];
}
syncQueryFromInputValue(autocomplete);
handlers.onFocus(e as any);
},
onBlur() {
setTimeout(() => {
autocomplete.setIsOpen(false);
panelController.hide();
onValueChange?.(inputEl.value);
}, 50);
},
onKeyDown(e, handlers) {
if (!shouldAutocompleteHandleEnterKey(e, { isPanelOpen, hasActiveItem })) {
return;
}
if (e.key === "Enter") {
e.stopPropagation();
}
handlers.onKeyDown(e as any);
}
});
const cleanup = () => {
unregisterGlobalCloser();
cleanupInputBindings();
panelController.destroy();
};
instanceMap.set(inputEl, { autocomplete, panelEl, cleanup });
if (open) {
syncQueryFromInputValue(autocomplete);
autocomplete.setIsOpen(true);
autocomplete.refresh();
panelController.startPositioning();
}
}
export function destroyAutocomplete($el: JQuery<HTMLElement> | HTMLElement) {
const inputEl = $el instanceof HTMLElement ? $el : $el[0] as HTMLInputElement;
const instance = instanceMap.get(inputEl);
if (instance) {
instance.cleanup();
instanceMap.delete(inputEl);
}
}
export default {
initAttributeNameAutocomplete,
initLabelValueAutocomplete
destroyAutocomplete,
initLabelValueAutocomplete,
};

View File

@@ -7,10 +7,6 @@ async function renderAttribute(attribute: FAttribute, renderIsInheritable: boole
const isInheritable = renderIsInheritable && attribute.isInheritable ? `(inheritable)` : "";
const $attr = $("<span>");
if (attribute.isAutoLink) {
return $attr;
}
if (attribute.type === "label") {
$attr.append(document.createTextNode(`#${attribute.name}${isInheritable}`));
@@ -19,6 +15,9 @@ async function renderAttribute(attribute: FAttribute, renderIsInheritable: boole
$attr.append(document.createTextNode(formatValue(attribute.value)));
}
} else if (attribute.type === "relation") {
if (attribute.isAutoLink) {
return $attr;
}
// when the relation has just been created, then it might not have a value
if (attribute.value) {

View File

@@ -6,8 +6,10 @@ import froca from "./froca";
import server from "./server.js";
// Spy on server methods to track calls
server.put = vi.fn(async () => ({})) as typeof server.put;
server.remove = vi.fn(async () => ({})) as typeof server.remove;
// @ts-expect-error the generic typing is causing issues here
server.put = vi.fn(async <T> (url: string, data?: T) => ({} as T));
// @ts-expect-error the generic typing is causing issues here
server.remove = vi.fn(async <T> (url: string) => ({} as T));
describe("Set boolean with inheritance", () => {
beforeEach(() => {

View File

@@ -0,0 +1,93 @@
import $ from "jquery";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const {
showSpy,
hideSpy,
updateDisplayedShortcutsSpy,
saveFocusedElementSpy,
focusSavedElementSpy
} = vi.hoisted(() => ({
showSpy: vi.fn(),
hideSpy: vi.fn(),
updateDisplayedShortcutsSpy: vi.fn(),
saveFocusedElementSpy: vi.fn(),
focusSavedElementSpy: vi.fn()
}));
vi.mock("bootstrap", () => ({
Modal: {
getOrCreateInstance: vi.fn(() => ({
show: showSpy,
hide: hideSpy
}))
}
}));
vi.mock("./keyboard_actions.js", () => ({
default: {
updateDisplayedShortcuts: updateDisplayedShortcutsSpy
}
}));
vi.mock("./focus.js", () => ({
saveFocusedElement: saveFocusedElementSpy,
focusSavedElement: focusSavedElementSpy
}));
import { closeAllHeadlessAutocompletes, registerHeadlessAutocompleteCloser } from "./autocomplete_core.js";
import { openDialog } from "./dialog.js";
describe("headless autocomplete closing", () => {
const unregisterClosers: Array<() => void> = [];
beforeEach(() => {
vi.clearAllMocks();
(window as any).glob = {
...(window as any).glob,
activeDialog: null
};
});
afterEach(() => {
while (unregisterClosers.length > 0) {
unregisterClosers.pop()?.();
}
});
it("closes every registered closer and skips unregistered ones", () => {
const closer1 = vi.fn();
const closer2 = vi.fn();
const closer3 = vi.fn();
unregisterClosers.push(registerHeadlessAutocompleteCloser(closer1));
const unregister2 = registerHeadlessAutocompleteCloser(closer2);
unregisterClosers.push(unregister2);
unregisterClosers.push(registerHeadlessAutocompleteCloser(closer3));
unregister2();
closeAllHeadlessAutocompletes();
expect(closer1).toHaveBeenCalledTimes(1);
expect(closer2).not.toHaveBeenCalled();
expect(closer3).toHaveBeenCalledTimes(1);
});
it("closes registered autocompletes when a dialog finishes hiding", async () => {
const closer = vi.fn();
unregisterClosers.push(registerHeadlessAutocompleteCloser(closer));
const dialogEl = document.createElement("div");
const $dialog = $(dialogEl);
await openDialog($dialog, false);
$dialog.trigger("hidden.bs.modal");
expect(showSpy).toHaveBeenCalledTimes(1);
expect(updateDisplayedShortcutsSpy).toHaveBeenCalledWith($dialog);
expect(saveFocusedElementSpy).toHaveBeenCalledTimes(1);
expect(closer).toHaveBeenCalledTimes(1);
expect(focusSavedElementSpy).toHaveBeenCalledTimes(1);
});
});

View File

@@ -0,0 +1,195 @@
import type { AutocompleteApi, AutocompleteSource, BaseItem } from "@algolia/autocomplete-core";
export const HEADLESS_AUTOCOMPLETE_PANEL_SELECTOR = ".aa-core-panel";
type HeadlessSourceDefaults = Required<Pick<AutocompleteSource<any>, "getItemUrl" | "onActive" | "onResolve">>;
const headlessAutocompleteClosers = new Set<() => void>();
export function withHeadlessSourceDefaults<TSource extends AutocompleteSource<any>>(
source: TSource
): TSource & HeadlessSourceDefaults {
return {
getItemUrl() {
return undefined;
},
onActive() {
// Headless consumers handle highlight side effects themselves.
},
onResolve() {
// Headless consumers resolve and render items manually.
},
...source
} as TSource & HeadlessSourceDefaults;
}
export function registerHeadlessAutocompleteCloser(close: () => void) {
headlessAutocompleteClosers.add(close);
return () => {
headlessAutocompleteClosers.delete(close);
};
}
export function closeAllHeadlessAutocompletes() {
for (const close of Array.from(headlessAutocompleteClosers)) {
close();
}
}
interface HeadlessPanelControllerOptions {
inputEl: HTMLElement;
container?: HTMLElement | null;
className?: string;
containedClassName?: string;
}
export function createHeadlessPanelController({
inputEl,
container,
className = "aa-core-panel",
containedClassName = "aa-core-panel--contained"
}: HeadlessPanelControllerOptions) {
const panelEl = document.createElement("div");
panelEl.className = className;
const isContained = Boolean(container);
if (isContained) {
panelEl.classList.add(containedClassName);
container!.appendChild(panelEl);
} else {
document.body.appendChild(panelEl);
}
panelEl.style.display = "none";
let rafId: number | null = null;
const positionPanel = () => {
if (isContained) {
panelEl.style.position = "static";
panelEl.style.top = "";
panelEl.style.left = "";
panelEl.style.width = "100%";
panelEl.style.display = "block";
return;
}
const rect = inputEl.getBoundingClientRect();
panelEl.style.position = "fixed";
panelEl.style.top = `${rect.bottom}px`;
panelEl.style.left = `${rect.left}px`;
panelEl.style.width = `${rect.width}px`;
panelEl.style.display = "block";
};
const stopPositioning = () => {
if (rafId !== null) {
cancelAnimationFrame(rafId);
rafId = null;
}
};
const startPositioning = () => {
if (isContained) {
positionPanel();
return;
}
if (rafId !== null) {
return;
}
const update = () => {
positionPanel();
rafId = requestAnimationFrame(update);
};
update();
};
const hide = () => {
panelEl.style.display = "none";
stopPositioning();
};
const destroy = () => {
hide();
panelEl.remove();
};
return {
panelEl,
hide,
destroy,
startPositioning,
stopPositioning
};
}
type InputHandlers<TItem extends BaseItem> = ReturnType<AutocompleteApi<TItem>["getInputProps"]>;
interface InputBinding<TEvent extends Event = Event> {
type: string;
listener: (event: TEvent) => void;
}
interface BindAutocompleteInputOptions<TItem extends BaseItem> {
inputEl: HTMLInputElement;
autocomplete: AutocompleteApi<TItem>;
onInput?: (event: Event, handlers: InputHandlers<TItem>) => void;
onFocus?: (event: Event, handlers: InputHandlers<TItem>) => void;
onBlur?: (event: Event, handlers: InputHandlers<TItem>) => void;
onKeyDown?: (event: KeyboardEvent, handlers: InputHandlers<TItem>) => void;
extraBindings?: InputBinding[];
}
export function bindAutocompleteInput<TItem extends BaseItem>({
inputEl,
autocomplete,
onInput,
onFocus,
onBlur,
onKeyDown,
extraBindings = []
}: BindAutocompleteInputOptions<TItem>) {
const handlers = autocomplete.getInputProps({ inputElement: inputEl });
const bindings: InputBinding[] = [
{
type: "input",
listener: (event: Event) => {
onInput?.(event, handlers);
}
},
{
type: "focus",
listener: (event: Event) => {
onFocus?.(event, handlers);
}
},
{
type: "blur",
listener: (event: Event) => {
onBlur?.(event, handlers);
}
},
{
type: "keydown",
listener: (event: Event) => {
onKeyDown?.(event as KeyboardEvent, handlers);
}
},
...extraBindings
];
bindings.forEach(({ type, listener }) => {
inputEl.addEventListener(type, listener as EventListener);
});
return () => {
bindings.forEach(({ type, listener }) => {
inputEl.removeEventListener(type, listener as EventListener);
});
};
}

View File

@@ -120,7 +120,7 @@ async function deleteNotes(branchIdsToDelete: string[], forceDeleteAllClones = f
if (moveToParent) {
try {
await activateParentNotePath(branchIdsToDelete);
await activateParentNotePath();
} catch (e) {
console.error(e);
}
@@ -152,28 +152,13 @@ async function deleteNotes(branchIdsToDelete: string[], forceDeleteAllClones = f
return true;
}
async function activateParentNotePath(branchIdsToDelete: string[]) {
async function activateParentNotePath() {
// this is not perfect, maybe we should find the next/previous sibling, but that's more complex
const activeContext = appContext.tabManager.getActiveContext();
const activeNotePath = activeContext?.notePathArray ?? [];
const parentNotePathArr = activeContext?.notePathArray.slice(0, -1);
// Find the deleted branch that appears earliest in the active note's path
let earliestIndex = activeNotePath.length;
for (const branchId of branchIdsToDelete) {
const branch = froca.getBranch(branchId);
if (branch) {
const index = activeNotePath.indexOf(branch.noteId);
if (index !== -1 && index < earliestIndex) {
earliestIndex = index;
}
}
}
// Navigate to the parent of the highest deleted ancestor
if (earliestIndex < activeNotePath.length) {
const parentPath = activeNotePath.slice(0, earliestIndex);
if (parentPath.length > 0) {
await activeContext?.setNote(parentPath.join("/"));
}
if (parentNotePathArr && parentNotePathArr.length > 0) {
activeContext?.setNote(parentNotePathArr.join("/"));
}
}

View File

@@ -26,7 +26,7 @@ type WithNoteId<T> = T & {
};
export type Widget = WithNoteId<(LegacyWidget | WidgetDefinitionWithType)>;
async function getAndExecuteBundle(noteId: string, originEntity: Entity | null = null, script: string | null = null, params: string | null = null) {
async function getAndExecuteBundle(noteId: string, originEntity = null, script = null, params = null) {
const bundle = await server.post<Bundle>(`script/bundle/${noteId}`, {
script,
params

View File

@@ -1,6 +1,3 @@
import { t } from "./i18n.js";
import toast from "./toast.js";
export function copyText(text: string) {
if (!text) {
return;
@@ -9,26 +6,29 @@ export function copyText(text: string) {
if (navigator.clipboard) {
navigator.clipboard.writeText(text);
return true;
}
// Fallback method: https://stackoverflow.com/a/72239825
const textArea = document.createElement("textarea");
textArea.value = text;
try {
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
return document.execCommand('copy');
} finally {
document.body.removeChild(textArea);
} else {
// Fallback method: https://stackoverflow.com/a/72239825
const textArea = document.createElement("textarea");
textArea.value = text;
try {
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
return document.execCommand('copy');
} finally {
document.body.removeChild(textArea);
}
}
} catch (e) {
console.warn(e);
return false;
}
}
export function copyTextWithToast(text: string) {
export async function copyTextWithToast(text: string) {
const t = (await import("./i18n.js")).t;
const toast = (await import("./toast.js")).default;
if (copyText(text)) {
toast.showMessage(t("clipboard.copy_success"));
} else {

View File

@@ -1,7 +1,6 @@
import "./content_renderer.css";
import { normalizeMimeTypeForCKEditor, renderToHtml, type TextRepresentationResponse } from "@triliumnext/commons";
import DOMPurify from "dompurify";
import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
import { h, render } from "preact";
import WheelZoom from 'vanilla-js-wheel-zoom';
@@ -9,14 +8,13 @@ import FAttachment from "../entities/fattachment.js";
import FNote from "../entities/fnote.js";
import imageContextMenuService from "../menus/image_context_menu.js";
import { t } from "../services/i18n.js";
import renderText, { postProcessRichContent, renderChildrenList } from "./content_renderer_text.js";
import renderText from "./content_renderer_text.js";
import renderDoc from "./doc_renderer.js";
import { loadElkIfNeeded, postprocessMermaidSvg } from "./mermaid.js";
import openService from "./open.js";
import protectedSessionService from "./protected_session.js";
import protectedSessionHolder from "./protected_session_holder.js";
import renderService from "./render.js";
import server from "./server.js";
import { applySingleBlockSyntaxHighlight } from "./syntax_highlight.js";
import utils, { getErrorMessage } from "./utils.js";
@@ -34,7 +32,6 @@ export interface RenderOptions {
includeArchivedNotes?: boolean;
/** Set of note IDs that have already been seen during rendering to prevent infinite recursion. */
seenNoteIds?: Set<string>;
showTextRepresentation?: boolean;
}
const CODE_MIME_TYPES = new Set(["application/json"]);
@@ -55,14 +52,12 @@ export async function getRenderedContent(this: {} | { ctx: string }, entity: FNo
if (type === "text" || type === "book") {
await renderText(entity, $renderedContent, options);
} else if (type === "markdown") {
await renderMarkdown(entity, $renderedContent, options);
} else if (type === "code") {
await renderCode(entity, $renderedContent);
} else if (["image", "canvas", "mindMap", "spreadsheet"].includes(type)) {
await renderImage(entity, $renderedContent, options);
renderImage(entity, $renderedContent, options);
} else if (!options.tooltip && ["file", "pdf", "audio", "video"].includes(type)) {
await renderFile(entity, type, $renderedContent, options);
await renderFile(entity, type, $renderedContent);
} else if (type === "mermaid") {
await renderMermaid(entity, $renderedContent);
} else if (type === "render" && entity instanceof FNote) {
@@ -122,31 +117,6 @@ export async function getRenderedContent(this: {} | { ctx: string }, entity: FNo
};
}
/**
* Renders a markdown note by converting its source to CKEditor-compatible HTML,
* then running the same post-render pipeline as text notes (included notes,
* math, reference links, Mermaid, code highlight) so the preview matches what
* the user sees in the Markdown note type's preview pane.
*/
async function renderMarkdown(note: FNote | FAttachment, $renderedContent: JQuery<HTMLElement>, options: RenderOptions) {
const blob = await note.getBlob();
const source = blob?.content ?? "";
if (!source.trim()) {
if (note instanceof FNote && !options.noChildrenList) {
await renderChildrenList($renderedContent, note, options.includeArchivedNotes ?? false);
}
return;
}
const html = renderToHtml(source, note.title, {
sanitize: (dirty) => DOMPurify.sanitize(dirty),
wikiLink: { formatHref: (id) => `#root/${id}` }
});
$renderedContent.append($('<div class="ck-content">').html(html));
await postProcessRichContent(note, $renderedContent, options);
}
/**
* Renders a code note, by displaying its content and applying syntax highlighting based on the selected MIME type.
*/
@@ -168,7 +138,7 @@ async function renderCode(note: FNote | FAttachment, $renderedContent: JQuery<HT
await applySingleBlockSyntaxHighlight($codeBlock, normalizeMimeTypeForCKEditor(note.mime));
}
async function renderImage(entity: FNote | FAttachment, $renderedContent: JQuery<HTMLElement>, options: RenderOptions = {}) {
function renderImage(entity: FNote | FAttachment, $renderedContent: JQuery<HTMLElement>, options: RenderOptions = {}) {
const encodedTitle = encodeURIComponent(entity.title);
let url;
@@ -176,14 +146,13 @@ async function renderImage(entity: FNote | FAttachment, $renderedContent: JQuery
if (entity instanceof FNote) {
url = `api/images/${entity.noteId}/${encodedTitle}?${Math.random()}`;
} else if (entity instanceof FAttachment) {
url = `api/attachments/${entity.attachmentId}/image/${encodedTitle}?${entity.utcDateModified}`;
url = `api/attachments/${entity.attachmentId}/image/${encodedTitle}?${entity.utcDateModified}">`;
}
$renderedContent // styles needed for the zoom to work well
.css("display", "flex")
.css("align-items", "center")
.css("justify-content", "center")
.css("flex-direction", "column"); // OCR text is displayed below the image.
.css("justify-content", "center");
const $img = $("<img>")
.attr("src", url || "")
@@ -209,35 +178,9 @@ async function renderImage(entity: FNote | FAttachment, $renderedContent: JQuery
}
imageContextMenuService.setupContextMenu($img);
if (entity instanceof FNote && options.showTextRepresentation) {
await addOCRTextIfAvailable(entity, $renderedContent);
}
}
async function addOCRTextIfAvailable(note: FNote, $content: JQuery<HTMLElement>) {
try {
const data = await server.get<TextRepresentationResponse>(`ocr/notes/${note.noteId}/text`);
if (data.success && data.hasOcr && data.text) {
const $ocrSection = $(`
<div class="ocr-text-section">
<div class="ocr-header">
<span class="bx bx-text"></span> ${t("ocr.extracted_text")}
</div>
<div class="ocr-content"></div>
</div>
`);
$ocrSection.find('.ocr-content').text(data.text);
$content.append($ocrSection);
}
} catch (error) {
// Silently fail if OCR API is not available
console.debug('Failed to fetch OCR text:', error);
}
}
async function renderFile(entity: FNote | FAttachment, type: string, $renderedContent: JQuery<HTMLElement>, options: RenderOptions = {}) {
async function renderFile(entity: FNote | FAttachment, type: string, $renderedContent: JQuery<HTMLElement>) {
let entityType, entityId;
if (entity instanceof FNote) {
@@ -277,10 +220,6 @@ async function renderFile(entity: FNote | FAttachment, type: string, $renderedCo
$content.append($videoPreview);
}
if (entity instanceof FNote && options.showTextRepresentation) {
await addOCRTextIfAvailable(entity, $content);
}
if (entityType === "notes" && "noteId" in entity) {
// TODO: we should make this available also for attachments, but there's a problem with "Open externally" support
// in attachment list
@@ -358,8 +297,6 @@ function getRenderingType(entity: FNote | FAttachment) {
if (type === "file" && mime === "application/pdf") {
type = "pdf";
} else if (type === "code" && entity instanceof FNote && entity.isMarkdown()) {
type = "markdown";
} else if ((type === "file" || type === "viewConfig") && mime && CODE_MIME_TYPES.has(mime) && !isIconPack) {
type = "code";
} else if (type === "file" && mime && mime.startsWith("audio/")) {

View File

@@ -15,47 +15,37 @@ export default async function renderText(note: FNote | FAttachment, $renderedCon
if (blob && !isHtmlEmpty(blob.content)) {
$renderedContent.append($('<div class="ck-content">').html(blob.content));
await postProcessRichContent(note, $renderedContent, options);
const seenNoteIds = options.seenNoteIds ?? new Set<string>();
seenNoteIds.add("noteId" in note ? note.noteId : note.attachmentId);
if (!options.noIncludedNotes) {
await renderIncludedNotes($renderedContent[0], seenNoteIds);
} else {
$renderedContent.find("section.include-note").remove();
}
if ($renderedContent.find("span.math-tex").length > 0) {
renderMathInElement($renderedContent[0], { trust: true });
}
const getNoteIdFromLink = (el: HTMLElement) => tree.getNoteIdFromUrl($(el).attr("href") || "");
const referenceLinks = $renderedContent.find<HTMLAnchorElement>("a.reference-link");
const noteIdsToPrefetch = referenceLinks.map((i, el) => getNoteIdFromLink(el));
await froca.getNotes(noteIdsToPrefetch);
for (const el of referenceLinks) {
const innerSpan = document.createElement("span");
await link.loadReferenceLinkTitle($(innerSpan), el.href);
el.replaceChildren(innerSpan);
}
await rewriteMermaidDiagramsInContainer($renderedContent[0] as HTMLDivElement);
await formatCodeBlocks($renderedContent);
} else if (note instanceof FNote && !options.noChildrenList) {
await renderChildrenList($renderedContent, note, options.includeArchivedNotes ?? false);
}
}
/**
* Apply the post-render passes that make CKEditor-compatible HTML fully
* interactive: expand `<section class="include-note">`, render inline math and
* Mermaid diagrams, rewrite reference-link titles, and highlight code blocks.
* Assumes the caller has already appended the HTML inside a `.ck-content` child
* of `$renderedContent`.
*/
export async function postProcessRichContent(note: FNote | FAttachment, $renderedContent: JQuery<HTMLElement>, options: RenderOptions = {}) {
const seenNoteIds = options.seenNoteIds ?? new Set<string>();
seenNoteIds.add("noteId" in note ? note.noteId : note.attachmentId);
if (!options.noIncludedNotes) {
await renderIncludedNotes($renderedContent[0], seenNoteIds);
} else {
$renderedContent.find("section.include-note").remove();
}
if ($renderedContent.find("span.math-tex").length > 0) {
renderMathInElement($renderedContent[0], { trust: true });
}
const getNoteIdFromLink = (el: HTMLElement) => tree.getNoteIdFromUrl($(el).attr("href") || "");
const referenceLinks = $renderedContent.find<HTMLAnchorElement>("a.reference-link");
const noteIdsToPrefetch = referenceLinks.map((i, el) => getNoteIdFromLink(el));
await froca.getNotes(noteIdsToPrefetch);
await Promise.all(referenceLinks.toArray().map(async (el) => {
const innerSpan = document.createElement("span");
await link.loadReferenceLinkTitle($(innerSpan), el.getAttribute("href"));
el.replaceChildren(innerSpan);
}));
await rewriteMermaidDiagramsInContainer($renderedContent[0] as HTMLDivElement);
await formatCodeBlocks($renderedContent);
}
async function renderIncludedNotes(contentEl: HTMLElement, seenNoteIds: Set<string>) {
// TODO: Consider duplicating with server's share/content_renderer.ts.
const includeNoteEls = contentEl.querySelectorAll("section.include-note");
@@ -111,107 +101,19 @@ export async function rewriteMermaidDiagramsInContainer(container: HTMLDivElemen
}
}
/**
* Per-container cache of rendered mermaid SVG keyed by diagram source text.
* Populated after each successful render; reused on subsequent renders to
* avoid flicker when the preview HTML is regenerated (e.g. live markdown
* editing). Entries for diagrams no longer present in the container are
* evicted on each run so the cache can't grow unbounded.
*/
const mermaidSvgCache = new WeakMap<HTMLElement, Map<string, string>>();
/**
* Per-container, ordered snapshot of the most recently rendered SVGs. Used as
* a positional placeholder so edits to a diagram's source keep the previous
* SVG visible while the new one renders offscreen.
*/
const mermaidLastRenderedByPosition = new WeakMap<HTMLElement, string[]>();
export async function applyInlineMermaid(container: HTMLDivElement) {
const nodes = Array.from(container.querySelectorAll<HTMLElement>("div.mermaid-diagram"));
if (!nodes.length) {
mermaidLastRenderedByPosition.delete(container);
return;
}
let cache = mermaidSvgCache.get(container);
if (!cache) {
cache = new Map();
mermaidSvgCache.set(container, cache);
}
const lastRendered = mermaidLastRenderedByPosition.get(container) ?? [];
// Decide per node: exact cache hit → paint final SVG; source changed →
// paint the previous SVG (by position) as a placeholder and queue an
// offscreen re-render. This way the user keeps seeing the old diagram
// until mermaid has finished producing the new one.
const pending: Array<{ visible: HTMLElement; source: string }> = [];
const seenSources = new Set<string>();
for (const [ index, node ] of nodes.entries()) {
const source = (node.textContent ?? "").trim();
seenSources.add(source);
const cached = cache.get(source);
if (cached) {
node.innerHTML = cached;
node.setAttribute("data-processed", "true");
continue;
}
pending.push({ visible: node, source });
const placeholder = lastRendered[index];
if (placeholder) {
node.innerHTML = placeholder;
}
}
// Evict cache entries whose source is no longer present.
for (const key of [ ...cache.keys() ]) {
if (!seenSources.has(key)) cache.delete(key);
}
if (!pending.length) {
mermaidLastRenderedByPosition.set(container, nodes.map((n) => n.innerHTML));
return;
}
// Initialize mermaid
const mermaid = (await import("mermaid")).default;
mermaid.initialize(getMermaidConfig());
// Render clones offscreen so the visible nodes keep showing the placeholder
// until the new SVG is ready. Keeps mermaid away from our placeholder SVG
// (which would otherwise confuse its text-based parser).
const offscreen = document.createElement("div");
offscreen.style.cssText = "position:absolute;left:-9999px;top:-9999px;width:0;height:0;overflow:hidden;visibility:hidden;";
document.body.appendChild(offscreen);
const pairs = pending.map(({ visible, source }) => {
const clone = document.createElement("div");
clone.className = "mermaid-diagram";
clone.textContent = source;
offscreen.appendChild(clone);
return { visible, clone, source };
});
const nodes = Array.from(container.querySelectorAll<HTMLElement>("div.mermaid-diagram"));
try {
await mermaid.run({ nodes: pairs.map((p) => p.clone) });
for (const { visible, clone, source } of pairs) {
if (clone.getAttribute("data-processed") !== "true") continue;
const svg = clone.innerHTML;
visible.innerHTML = svg;
visible.setAttribute("data-processed", "true");
cache.set(source, svg);
}
await mermaid.run({ nodes });
} catch (e) {
console.error(e);
} finally {
offscreen.remove();
console.log(e);
}
mermaidLastRenderedByPosition.set(container, nodes.map((n) => n.innerHTML));
}
export async function renderChildrenList($renderedContent: JQuery<HTMLElement>, note: FNote, includeArchivedNotes: boolean) {
async function renderChildrenList($renderedContent: JQuery<HTMLElement>, note: FNote, includeArchivedNotes: boolean) {
let childNoteIds = note.getChildNoteIds();
if (!childNoteIds.length) {

View File

@@ -84,55 +84,6 @@ async function createSearchNote(opts = {}) {
return await froca.getNote(note.noteId);
}
async function createLlmChat() {
const note = await server.post<FNoteRow>("special-notes/llm-chat");
await ws.waitForMaxKnownEntityChangeId();
return await froca.getNote(note.noteId);
}
/**
* Gets the most recently modified LLM chat.
* Returns null if no chat exists.
*/
async function getMostRecentLlmChat() {
const note = await server.get<FNoteRow | null>("special-notes/most-recent-llm-chat");
if (!note) {
return null;
}
await ws.waitForMaxKnownEntityChangeId();
return await froca.getNote(note.noteId);
}
/**
* Gets the most recent LLM chat, or creates a new one if none exists.
* Used by sidebar chat for persistent conversations across page refreshes.
*/
async function getOrCreateLlmChat() {
const note = await server.get<FNoteRow>("special-notes/get-or-create-llm-chat");
await ws.waitForMaxKnownEntityChangeId();
return await froca.getNote(note.noteId);
}
export interface RecentLlmChat {
noteId: string;
title: string;
dateModified: string;
}
/**
* Gets a list of recent LLM chats for the history popup.
*/
async function getRecentLlmChats(limit: number = 10): Promise<RecentLlmChat[]> {
return await server.get<RecentLlmChat[]>(`special-notes/recent-llm-chats?limit=${limit}`);
}
export default {
getInboxNote,
getTodayNote,
@@ -143,9 +94,5 @@ export default {
getMonthNote,
getYearNote,
createSqlConsole,
createSearchNote,
createLlmChat,
getMostRecentLlmChat,
getOrCreateLlmChat,
getRecentLlmChats
createSearchNote
};

View File

@@ -4,8 +4,8 @@ import appContext from "../components/app_context.js";
import type { ConfirmDialogOptions, ConfirmDialogResult, ConfirmWithMessageOptions, MessageType } from "../widgets/dialogs/confirm.js";
import { InfoExtraProps } from "../widgets/dialogs/info.jsx";
import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js";
import { closeAllHeadlessAutocompletes } from "./autocomplete_core.js";
import { focusSavedElement, saveFocusedElement } from "./focus.js";
import keyboardActionsService from "./keyboard_actions.js";
export async function openDialog($dialog: JQuery<HTMLElement>, closeActDialog = true, config?: Partial<Modal.Options>) {
if (closeActDialog) {
@@ -17,16 +17,14 @@ export async function openDialog($dialog: JQuery<HTMLElement>, closeActDialog =
Modal.getOrCreateInstance($dialog[0], config).show();
$dialog.on("hidden.bs.modal", () => {
const $autocompleteEl = $(".aa-input");
if ("autocomplete" in $autocompleteEl) {
$autocompleteEl.autocomplete("close");
}
closeAllHeadlessAutocompletes();
if (!glob.activeDialog || glob.activeDialog === $dialog) {
focusSavedElement();
}
});
const keyboardActionsService = (await import("./keyboard_actions.js")).default;
keyboardActionsService.updateDisplayedShortcuts($dialog);
return $dialog;

View File

@@ -1,30 +0,0 @@
import { describe, expect, it } from "vitest";
import { isValidDocName } from "./doc_renderer.js";
describe("isValidDocName", () => {
it("accepts valid docNames", () => {
expect(isValidDocName("launchbar_intro")).toBe(true);
expect(isValidDocName("User Guide/Quick Start")).toBe(true);
expect(isValidDocName("User Guide/User Guide/Quick Start")).toBe(true);
expect(isValidDocName("Quick Start Guide")).toBe(true);
expect(isValidDocName("quick_start_guide")).toBe(true);
expect(isValidDocName("quick-start-guide")).toBe(true);
});
it("rejects path traversal attacks", () => {
expect(isValidDocName("..")).toBe(false);
expect(isValidDocName("../etc/passwd")).toBe(false);
expect(isValidDocName("foo/../bar")).toBe(false);
expect(isValidDocName("../../../../api/notes/_malicious/open")).toBe(false);
expect(isValidDocName("..\\etc\\passwd")).toBe(false);
expect(isValidDocName("foo\\bar")).toBe(false);
});
it("rejects URL manipulation attacks", () => {
expect(isValidDocName("../../../../api/notes/_malicious/open?x=")).toBe(false);
expect(isValidDocName("foo#bar")).toBe(false);
expect(isValidDocName("%2e%2e")).toBe(false);
expect(isValidDocName("%2e%2e%2f%2e%2e%2fapi")).toBe(false);
});
});

View File

@@ -3,39 +3,22 @@ import { applyReferenceLinks } from "../widgets/type_widgets/text/read_only_help
import { getCurrentLanguage } from "./i18n.js";
import { formatCodeBlocks } from "./syntax_highlight.js";
/**
* Validates a docName to prevent path traversal attacks.
* Allows forward slashes for subdirectories (e.g., "User Guide/Quick Start")
* but blocks traversal sequences and URL manipulation characters.
*/
export function isValidDocName(docName: string): boolean {
// Allow alphanumeric characters, spaces, underscores, hyphens, and forward slashes.
const validDocNameRegex = /^[a-zA-Z0-9_/\- ]+$/;
return validDocNameRegex.test(docName);
}
export default function renderDoc(note: FNote) {
return new Promise<JQuery<HTMLElement>>((resolve) => {
const docName = note.getLabelValue("docName");
let docName = note.getLabelValue("docName");
const $content = $("<div>");
// find doc based on language
const url = getUrl(docName, getCurrentLanguage());
if (url) {
if (docName) {
// find doc based on language
const url = getUrl(docName, getCurrentLanguage());
$content.load(url, async (response, status) => {
// fallback to english doc if no translation available
if (status === "error") {
const fallbackUrl = getUrl(docName, "en");
if (fallbackUrl) {
$content.load(fallbackUrl, async () => {
await processContent(fallbackUrl, $content);
resolve($content);
});
} else {
$content.load(fallbackUrl, async () => {
await processContent(fallbackUrl, $content)
resolve($content);
}
});
return;
}
@@ -45,6 +28,8 @@ export default function renderDoc(note: FNote) {
} else {
resolve($content);
}
return $content;
});
}
@@ -54,7 +39,7 @@ async function processContent(url: string, $content: JQuery<HTMLElement>) {
// Images are relative to the docnote but that will not work when rendered in the application since the path breaks.
$content.find("img").each((i, el) => {
const $img = $(el);
$img.attr("src", `${dir}/${$img.attr("src")}`);
$img.attr("src", dir + "/" + $img.attr("src"));
});
formatCodeBlocks($content);
@@ -63,17 +48,10 @@ async function processContent(url: string, $content: JQuery<HTMLElement>) {
await applyReferenceLinks($content[0]);
}
function getUrl(docNameValue: string | null, language: string) {
if (!docNameValue) return;
if (!isValidDocName(docNameValue)) {
console.error(`Invalid docName: ${docNameValue}`);
return null;
}
function getUrl(docNameValue: string, language: string) {
// Cannot have spaces in the URL due to how JQuery.load works.
docNameValue = docNameValue.replaceAll(" ", "%20");
const basePath = window.glob.isDev ? `${window.glob.assetPath }/..` : window.glob.assetPath;
const basePath = window.glob.isDev ? window.glob.assetPath + "/.." : window.glob.assetPath;
return `${basePath}/doc_notes/${language}/${docNameValue}.html`;
}

View File

@@ -13,11 +13,6 @@ export const experimentalFeatures = [
id: "new-layout",
name: t("experimental_features.new_layout_name"),
description: t("experimental_features.new_layout_description"),
},
{
id: "llm",
name: t("experimental_features.llm_name"),
description: t("experimental_features.llm_description"),
}
] as const satisfies ExperimentalFeature[];

View File

@@ -1,16 +1,14 @@
import type { OptionNames } from "@triliumnext/commons";
import appContext from "../components/app_context.js";
import FAttachment, { type FAttachmentRow } from "../entities/fattachment.js";
import FAttribute, { type FAttributeRow } from "../entities/fattribute.js";
import LoadResults from "./load_results.js";
import froca from "./froca.js";
import utils from "./utils.js";
import options from "./options.js";
import noteAttributeCache from "./note_attribute_cache.js";
import FBranch, { type FBranchRow } from "../entities/fbranch.js";
import FAttribute, { type FAttributeRow } from "../entities/fattribute.js";
import FAttachment, { type FAttachmentRow } from "../entities/fattachment.js";
import type { default as FNote, FNoteRow } from "../entities/fnote.js";
import type { EntityChange } from "../server_types.js";
import froca from "./froca.js";
import LoadResults from "./load_results.js";
import noteAttributeCache from "./note_attribute_cache.js";
import options from "./options.js";
import utils from "./utils.js";
import type { OptionNames } from "@triliumnext/commons";
async function processEntityChanges(entityChanges: EntityChange[]) {
const loadResults = new LoadResults(entityChanges);
@@ -65,7 +63,7 @@ async function processEntityChanges(entityChanges: EntityChange[]) {
if (entityName === "branches" && !((entity as FBranchRow).parentNoteId in froca.notes)) {
missingNoteIds.push((entity as FBranchRow).parentNoteId);
} else if (entityName === "attributes") {
const attributeEntity = entity as FAttributeRow;
let attributeEntity = entity as FAttributeRow;
if (attributeEntity.type === "relation" && (attributeEntity.name === "template" || attributeEntity.name === "inherit") && !(attributeEntity.value in froca.notes)) {
missingNoteIds.push(attributeEntity.value);
}
@@ -81,6 +79,7 @@ async function processEntityChanges(entityChanges: EntityChange[]) {
noteAttributeCache.invalidate();
}
const appContext = (await import("../components/app_context.js")).default;
await appContext.triggerEvent("entitiesReloaded", { loadResults });
}
}

View File

@@ -1,4 +1,4 @@
import { createContext, Fragment, h, VNode } from "preact";
import { Fragment, h, VNode } from "preact";
import * as hooks from "preact/hooks";
import ActionButton from "../widgets/react/ActionButton";
@@ -47,7 +47,6 @@ export const preactAPI = Object.freeze({
// Core
h,
Fragment,
createContext,
/**
* Method that must be run for widget scripts that run on Preact, using JSX. The method just returns the same definition, reserved for future typechecking and perhaps validation purposes.

View File

@@ -1,10 +1,10 @@
import { findDuplicateJsonKeys, LOCALES } from "@triliumnext/commons";
import { LOCALES } from "@triliumnext/commons";
import { readFileSync } from "fs";
import { join } from "path";
import { describe, expect, it } from "vitest";
describe("i18n", () => {
it("translations are valid JSON with no duplicate keys", () => {
it("translations are valid JSON", () => {
for (const locale of LOCALES) {
if (locale.contentOnly || locale.id === "en_rtl") {
continue;
@@ -14,13 +14,6 @@ describe("i18n", () => {
const translationFile = readFileSync(translationPath, { encoding: "utf-8" });
expect(() => JSON.parse(translationFile), `JSON error while parsing locale '${locale.id}' at "${translationPath}"`)
.not.toThrow();
const duplicates = findDuplicateJsonKeys(translationFile);
expect(
duplicates,
`Duplicate keys in locale '${locale.id}' at "${translationPath}":\n${
duplicates.map((d) => ` - "${d.key}" (line ${d.line})`).join("\n")}`
).toEqual([]);
}
});
});

View File

@@ -1,14 +1,21 @@
import { LOCALE_IDS, LOCALES, setDayjsLocale } from "@triliumnext/commons";
import options from "./options.js";
import i18next from "i18next";
import i18nextHttpBackend from "i18next-http-backend";
import server from "./server.js";
import { LOCALE_IDS, setDayjsLocale, type Locale } from "@triliumnext/commons";
import { initReactI18next } from "react-i18next";
let locales: Locale[] | null;
/**
* A deferred promise that resolves when translations are initialized.
*/
export const translationsInitializedPromise = $.Deferred();
export let translationsInitializedPromise = $.Deferred();
export async function initLocale(locale: LOCALE_IDS = "en") {
export async function initLocale() {
const locale = ((options.get("locale") as string) || "en") as LOCALE_IDS;
locales = await server.get<Locale[]>("options/locales");
i18next.use(initReactI18next);
await i18next.use(i18nextHttpBackend).init({
@@ -17,7 +24,8 @@ export async function initLocale(locale: LOCALE_IDS = "en") {
backend: {
loadPath: `${window.glob.assetPath}/translations/{{lng}}/{{ns}}.json`
},
returnEmptyString: false
returnEmptyString: false,
showSupportNotice: false
});
await setDayjsLocale(locale);
@@ -25,7 +33,11 @@ export async function initLocale(locale: LOCALE_IDS = "en") {
}
export function getAvailableLocales() {
return LOCALES;
if (!locales) {
throw new Error("Tried to load list of locales, but localization is not yet initialized.")
}
return locales;
}
/**
@@ -36,7 +48,7 @@ export function getAvailableLocales() {
*/
export function getLocaleById(localeId: string | null | undefined) {
if (!localeId) return null;
return LOCALES.find((l) => l.id === localeId) ?? null;
return locales?.find((l) => l.id === localeId) ?? null;
}
export const t = i18next.t;

View File

@@ -19,8 +19,7 @@ export const byNoteType: Record<Exclude<NoteType, "book">, string | null> = {
search: null,
text: null,
webView: null,
spreadsheet: null,
llmChat: null
spreadsheet: null
};
export const byBookType: Record<ViewTypeOptions, string | null> = {

View File

@@ -1,9 +1,10 @@
import server from "./server.js";
import appContext from "../components/app_context.js";
import shortcutService, { ShortcutBinding } from "./shortcuts.js";
import type Component from "../components/component.js";
import type { ActionKeyboardShortcut } from "@triliumnext/commons";
import appContext from "../components/app_context.js";
import type Component from "../components/component.js";
import server from "./server.js";
import shortcutService, { ShortcutBinding } from "./shortcuts.js";
const keyboardActionRepo: Record<string, ActionKeyboardShortcut> = {};
const keyboardActionsLoaded = server.get<ActionKeyboardShortcut[]>("keyboard-actions").then((actions) => {
@@ -51,7 +52,10 @@ async function setupActionsForElement(scope: string, $el: JQuery<HTMLElement>, c
getActionsForScope("window").then((actions) => {
for (const action of actions) {
for (const shortcut of action.effectiveShortcuts ?? []) {
shortcutService.bindGlobalShortcut(shortcut, () => appContext.triggerCommand(action.actionName, { ntxId: appContext.tabManager.activeNtxId }));
shortcutService.bindGlobalShortcut(shortcut, () => {
const ntxId = appContext.tabManager?.activeNtxId ?? null;
appContext.triggerCommand(action.actionName, { ntxId });
});
}
}
});

View File

@@ -4,8 +4,6 @@ import appContext, { type NoteCommandData } from "../components/app_context.js";
import { openInCurrentNoteContext } from "../components/note_context.js";
import linkContextMenuService from "../menus/link_context_menu.js";
import froca from "./froca.js";
import { t } from "./i18n.js";
import { showError } from "./toast.js";
import treeService from "./tree.js";
import utils from "./utils.js";
@@ -30,7 +28,7 @@ async function getLinkIcon(noteId: string, viewMode: ViewMode | undefined) {
return icon;
}
export type ViewMode = "default" | "source" | "attachments" | "contextual-help" | "note-map" | "ocr";
export type ViewMode = "default" | "source" | "attachments" | "contextual-help" | "note-map";
export interface ViewScope {
/**
@@ -60,8 +58,6 @@ export interface ViewScope {
*/
tocPreviousVisible?: boolean;
tocCollapsedHeadings?: Set<string>;
/** When set, scrolls to a bookmark anchor within the note after navigation. */
bookmark?: string;
}
interface CreateLinkOptions {
@@ -246,7 +242,7 @@ export function parseNavigationStateFromUrl(url: string | undefined) {
hoistedNoteId = value;
} else if (name === "searchString") {
searchString = value; // supports triggering search from URL, e.g. #?searchString=blabla
} else if (["viewMode", "attachmentId", "bookmark"].includes(name)) {
} else if (["viewMode", "attachmentId"].includes(name)) {
(viewScope as any)[name] = value;
} else if (name === "popup") {
openInPopup = true;
@@ -337,30 +333,15 @@ export function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDo
if (openInNewTab || openInNewWindow || (isLeftClick && (withinEditLink || outsideOfCKEditor))) {
if (hrefLink.toLowerCase().startsWith("http") || hrefLink.startsWith("api/")) {
window.open(hrefLink, "_blank");
} else if (ALLOWED_PROTOCOLS.some((protocol) => hrefLink.toLowerCase().startsWith(`${protocol}:`))) {
} else {
// Enable protocols supported by CKEditor 5 to be clickable.
if (utils.isElectron()) {
const electron = utils.dynamicRequire("electron");
const reportLinkError = (e: unknown) => {
const message = e instanceof Error ? e.message : String(e);
logError(`Failed to open link '${hrefLink}': ${message}`);
showError(t("link.failed_to_open", { href: hrefLink, message }));
};
if (hrefLink.toLowerCase().startsWith("file:")) {
// shell.openExternal mishandles Unicode file:// URLs on Windows;
// convert to a filesystem path and use shell.openPath instead.
// Normalize file://c:/... (2 slashes — drive read as host) to file:///c:/...
const normalized = hrefLink.replace(/^file:\/\/(?=[a-zA-Z]:)/i, "file:///");
const { fileURLToPath } = utils.dynamicRequire("url");
electron.shell.openPath(fileURLToPath(normalized)).then((err: string) => {
if (err) reportLinkError(new Error(err));
}).catch(reportLinkError);
if (ALLOWED_PROTOCOLS.some((protocol) => hrefLink.toLowerCase().startsWith(`${protocol}:`))) {
if ( utils.isElectron()) {
const electron = utils.dynamicRequire("electron");
electron.shell.openExternal(hrefLink);
} else {
electron.shell.openExternal(hrefLink).catch(reportLinkError);
window.open(hrefLink, "_blank");
}
} else {
window.open(hrefLink, "_blank");
}
}
}
@@ -434,13 +415,6 @@ async function loadReferenceLinkTitle($el: JQuery<HTMLElement>, href: string | n
const title = await getReferenceLinkTitle(href);
$el.text(title);
if (viewScope?.bookmark) {
$el.append($("<small>").append(
$("<span>").addClass("bx bx-bookmark"),
document.createTextNode(viewScope.bookmark)
));
}
if (note) {
const icon = await getLinkIcon(noteId, viewScope.viewMode);
@@ -466,8 +440,8 @@ async function getReferenceLinkTitle(href: string) {
return attachment ? attachment.title : "[missing attachment]";
}
return note.title;
}
function getReferenceLinkTitleSync(href: string) {
@@ -490,12 +464,8 @@ function getReferenceLinkTitleSync(href: string) {
return attachment ? attachment.title : "[missing attachment]";
}
if (viewScope?.bookmark) {
return `${note.title} - ${viewScope.bookmark}`;
}
return note.title;
}
if (glob.device !== "print") {

View File

@@ -1,116 +0,0 @@
import type { LlmChatConfig, LlmCitation, LlmMessage, LlmModelInfo,LlmUsage } from "@triliumnext/commons";
import server from "./server.js";
/**
* Fetch available models from all configured providers.
*/
export async function getAvailableModels(): Promise<LlmModelInfo[]> {
const response = await server.get<{ models?: LlmModelInfo[] }>("llm-chat/models");
return response.models ?? [];
}
export interface StreamCallbacks {
onChunk: (text: string) => void;
onThinking?: (text: string) => void;
onToolUse?: (toolName: string, input: Record<string, unknown>) => void;
onToolResult?: (toolName: string, result: string, isError?: boolean) => void;
onCitation?: (citation: LlmCitation) => void;
onUsage?: (usage: LlmUsage) => void;
onError: (error: string) => void;
onDone: () => void;
}
/**
* Stream a chat completion from the LLM API using Server-Sent Events.
*/
export async function streamChatCompletion(
messages: LlmMessage[],
config: LlmChatConfig,
callbacks: StreamCallbacks,
abortSignal?: AbortSignal
): Promise<void> {
const headers = await server.getHeaders();
const response = await fetch(`${window.glob.baseApiUrl}llm-chat/stream`, {
method: "POST",
headers: {
...headers,
"Content-Type": "application/json"
} as HeadersInit,
body: JSON.stringify({ messages, config }),
signal: abortSignal
});
if (!response.ok) {
callbacks.onError(`HTTP ${response.status}: ${response.statusText}`);
return;
}
const reader = response.body?.getReader();
if (!reader) {
callbacks.onError("No response body");
return;
}
const decoder = new TextDecoder();
let buffer = "";
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
if (line.startsWith("data: ")) {
try {
const data = JSON.parse(line.slice(6));
switch (data.type) {
case "text":
callbacks.onChunk(data.content);
break;
case "thinking":
callbacks.onThinking?.(data.content);
break;
case "tool_use":
callbacks.onToolUse?.(data.toolName, data.toolInput);
// Yield to force Preact to commit the pending tool call
// state before we process the result.
await new Promise((r) => setTimeout(r, 1));
break;
case "tool_result":
callbacks.onToolResult?.(data.toolName, data.result, data.isError);
await new Promise((r) => setTimeout(r, 1));
break;
case "citation":
if (data.citation) {
callbacks.onCitation?.(data.citation);
}
break;
case "usage":
if (data.usage) {
callbacks.onUsage?.(data.usage);
}
break;
case "error":
callbacks.onError(data.error);
break;
case "done":
callbacks.onDone();
break;
}
} catch (e) {
console.error("Failed to parse SSE data line:", line, e);
}
}
}
}
} finally {
reader.releaseLock();
}
}

View File

@@ -32,9 +32,4 @@ describe("Mermaid", () => {
`;
expect(postprocessMermaidSvg(before)).toBe(after);
});
it("replaces &nbsp; with numeric entity for valid XML", () => {
expect(postprocessMermaidSvg("<text>a&nbsp;b&nbsp;&nbsp;c</text>"))
.toBe("<text>a&#160;b&#160;&#160;c</text>");
});
});

View File

@@ -49,15 +49,11 @@ export async function loadElkIfNeeded(mermaid: Mermaid, mermaidContent: string)
* Processes the output of a Mermaid SVG render before it should be delivered to the user.
*
* <p>
* Currently this fixes <br> to <br/> and replaces named HTML entities like &nbsp; with their
* numeric equivalents, both of which would otherwise cause invalid XML when the SVG is saved
* as an attachment.
* Currently this fixes <br> to <br/> which would otherwise cause an invalid XML.
*
* @param svg the Mermaid SVG to process.
* @returns the processed SVG.
*/
export function postprocessMermaidSvg(svg: string) {
return svg
.replaceAll(/<br\s*>/ig, "<br/>")
.replaceAll(/&nbsp;/g, "&#160;");
return svg.replaceAll(/<br\s*>/ig, "<br/>");
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,6 @@
import type { NoteType } from "../entities/fnote.js";
import type { MenuCommandItem, MenuItem, MenuItemBadge, MenuSeparatorItem } from "../menus/context_menu.js";
import type { TreeCommandNames } from "../menus/tree_context_menu.js";
import { isExperimentalFeatureEnabled } from "./experimental_features.js";
import froca from "./froca.js";
import { t } from "./i18n.js";
import server from "./server.js";
@@ -27,7 +26,7 @@ export const NOTE_TYPES: NoteTypeMapping[] = [
// The default note type (always the first item)
{ type: "text", mime: "text/html", title: t("note_types.text"), icon: "bx-note" },
{ type: "spreadsheet", mime: "application/json", title: t("note_types.spreadsheet"), icon: "bx-table", isBeta: true, isNew: true },
{ type: "spreadsheet", mime: "application/json", title: t("note_types.spreadsheet"), icon: "bx-table", isBeta: true },
// Text notes group
{ type: "book", mime: "", title: t("note_types.book"), icon: "bx-book" },
@@ -42,14 +41,12 @@ export const NOTE_TYPES: NoteTypeMapping[] = [
{ type: "relationMap", mime: "application/json", title: t("note_types.relation-map"), icon: "bxs-network-chart" },
// Misc note types
{ type: "llmChat", mime: "application/json", title: t("note_types.llm-chat"), icon: "bx-message-square-dots", isBeta: true },
{ type: "render", mime: "", title: t("note_types.render-note"), icon: "bx-extension" },
{ type: "search", title: t("note_types.saved-search"), icon: "bx-file-find", static: true },
{ type: "webView", mime: "", title: t("note_types.web-view"), icon: "bx-globe-alt" },
// Code notes
{ type: "code", mime: "text/plain", title: t("note_types.code"), icon: "bx-code" },
{ type: "code", mime: "text/x-markdown", title: t("note_types.markdown"), icon: "bxl-markdown", isNew: true },
// Reserved types (cannot be created by the user)
{ type: "contentWidget", mime: "", title: t("note_types.widget"), reserved: true },
@@ -95,13 +92,11 @@ async function getNoteTypeItems(command?: TreeCommandNames) {
function getBlankNoteTypes(command?: TreeCommandNames): MenuItem<TreeCommandNames>[] {
return NOTE_TYPES
.filter((nt) => !nt.reserved && nt.type !== "book")
.filter((nt) => nt.type !== "llmChat" || isExperimentalFeatureEnabled("llm"))
.map((nt) => {
const menuItem: MenuCommandItem<TreeCommandNames> = {
title: nt.title,
command,
type: nt.type,
mime: nt.mime,
uiIcon: `bx ${nt.icon}`,
badges: []
};

View File

@@ -18,10 +18,6 @@ async function render(note: FNote, $el: JQuery<HTMLElement>, onError?: ErrorHand
for (const renderNoteId of renderNoteIds) {
const bundle = await server.postWithSilentInternalServerError<Bundle>(`script/bundle/${renderNoteId}`);
if (!bundle) {
throw new Error(`Script note '${renderNoteId}' could not be loaded. It may be protected and require an active protected session.`);
}
const $scriptContainer = $("<div>");
$el.append($scriptContainer);

View File

@@ -1,4 +1,3 @@
import { t } from "./i18n.js";
import utils, { isShare } from "./utils.js";
import ValidationError from "./validation_error.js";
@@ -33,7 +32,8 @@ async function getHeaders(headers?: Headers) {
return {};
}
const activeNoteContext = glob.appContext?.tabManager ? glob.appContext.tabManager.getActiveContext() : null;
const appContext = (await import("../components/app_context.js")).default;
const activeNoteContext = appContext.tabManager ? appContext.tabManager.getActiveContext() : null;
// headers need to be lowercase because node.js automatically converts them to lower case
// also avoiding using underscores instead of dashes since nginx filters them out by default
@@ -270,11 +270,7 @@ function ajax(url: string, method: string, data: unknown, headers: Headers, opts
} else if (opts.silentInternalServerError && jqXhr.status === 500) {
// report nothing
} else {
try {
await reportError(method, url, jqXhr.status, jqXhr.responseText);
} catch {
// reportError may throw (e.g. ValidationError); ensure rej() is still called below.
}
await reportError(method, url, jqXhr.status, jqXhr.responseText);
}
rej(jqXhr.responseText);
@@ -344,7 +340,6 @@ async function reportError(method: string, url: string, statusCode: number, resp
} catch (e) {}
}
// Dynamic import to avoid circular dependency (toast → app_context → options → server).
const toastService = (await import("./toast.js")).default;
const messageStr = (typeof message === "string" ? message : JSON.stringify(message)) || "-";
@@ -358,6 +353,7 @@ async function reportError(method: string, url: string, statusCode: number, resp
...response
});
} else {
const { t } = await import("./i18n.js");
if (statusCode === 400 && (url.includes("%23") || url.includes("%2F"))) {
toastService.showPersistent({
id: "trafik-blocked",
@@ -371,7 +367,8 @@ async function reportError(method: string, url: string, statusCode: number, resp
t("server.unknown_http_error_content", { statusCode, method, url, message: messageStr }),
15_000);
}
window.logError(`${statusCode} ${method} ${url} - ${message}`);
const { logError } = await import("./ws.js");
logError(`${statusCode} ${method} ${url} - ${message}`);
}
}

View File

@@ -1,87 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import SpacedUpdate from "./spaced_update";
// Mock logError which is a global in Trilium
vi.stubGlobal("logError", vi.fn());
describe("SpacedUpdate", () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it("should only call updater once per interval even with multiple pending callbacks", async () => {
const updater = vi.fn(async () => {
// Simulate a slow network request - this is where the race condition occurs
await new Promise((resolve) => setTimeout(resolve, 100));
});
const spacedUpdate = new SpacedUpdate(updater, 50);
// Simulate rapid typing - each keystroke calls scheduleUpdate()
// This queues multiple setTimeout callbacks due to recursive scheduleUpdate() calls
for (let i = 0; i < 10; i++) {
spacedUpdate.scheduleUpdate();
// Small delay between keystrokes
await vi.advanceTimersByTimeAsync(5);
}
// Advance time past the update interval to trigger the update
await vi.advanceTimersByTimeAsync(100);
// Let the "network request" complete and any pending callbacks run
await vi.advanceTimersByTimeAsync(200);
// The updater should have been called only ONCE, not multiple times
// With the bug, multiple pending setTimeout callbacks would all pass the time check
// during the async updater call and trigger multiple concurrent requests
expect(updater).toHaveBeenCalledTimes(1);
});
it("should call updater again if changes occur during the update", async () => {
const updater = vi.fn(async () => {
await new Promise((resolve) => setTimeout(resolve, 50));
});
const spacedUpdate = new SpacedUpdate(updater, 30);
// First update
spacedUpdate.scheduleUpdate();
await vi.advanceTimersByTimeAsync(40);
// Schedule another update while the first one is in progress
spacedUpdate.scheduleUpdate();
// Let first update complete
await vi.advanceTimersByTimeAsync(60);
// Advance past the interval again for the second update
await vi.advanceTimersByTimeAsync(100);
// Should have been called twice - once for each distinct change period
expect(updater).toHaveBeenCalledTimes(2);
});
it("should restore changed flag on error so retry can happen", async () => {
const updater = vi.fn()
.mockRejectedValueOnce(new Error("Network error"))
.mockResolvedValue(undefined);
const spacedUpdate = new SpacedUpdate(updater, 50);
spacedUpdate.scheduleUpdate();
// Advance to trigger first update (which will fail)
await vi.advanceTimersByTimeAsync(60);
// The error should have restored the changed flag, so scheduling again should work
spacedUpdate.scheduleUpdate();
await vi.advanceTimersByTimeAsync(60);
expect(updater).toHaveBeenCalledTimes(2);
});
});

View File

@@ -77,22 +77,16 @@ export default class SpacedUpdate {
}
if (Date.now() - this.lastUpdated > this.updateInterval) {
// Update these BEFORE the async call to prevent race conditions.
// Multiple setTimeout callbacks may be pending from recursive scheduleUpdate() calls.
// Without this, they would all pass the time check during the await and trigger multiple requests.
this.lastUpdated = Date.now();
this.changed = false;
this.onStateChanged("saving");
try {
await this.updater();
this.onStateChanged("saved");
this.changed = false;
} catch (e) {
// Restore changed flag on error so a retry can happen
this.changed = true;
this.onStateChanged("error");
logError(getErrorMessage(e));
}
this.lastUpdated = Date.now();
} else {
// update isn't triggered but changes are still pending, so we need to schedule another check
this.scheduleUpdate();

View File

@@ -33,14 +33,6 @@ export async function formatCodeBlocks($container: JQuery<HTMLElement>) {
applySingleBlockSyntaxHighlight($(codeBlock), normalizedMimeType);
}
}
// Add click-to-copy for inline code (code elements not inside pre)
if (glob.device !== "print") {
const inlineCodeElements = $container.find("code:not(pre code)");
for (const inlineCode of inlineCodeElements) {
applyInlineCodeCopy($(inlineCode));
}
}
}
export function applyCopyToClipboardButton($codeBlock: JQuery<HTMLElement>) {
@@ -59,23 +51,6 @@ export function applyCopyToClipboardButton($codeBlock: JQuery<HTMLElement>) {
$codeBlock.parent().append($copyButton);
}
export function applyInlineCodeCopy($inlineCode: JQuery<HTMLElement>) {
$inlineCode
.addClass("copyable-inline-code")
.attr("title", t("code_block.click_to_copy"))
.off("click")
.on("click", (e) => {
e.stopPropagation();
const text = $inlineCode.text();
if (!isShare) {
copyTextWithToast(text);
} else {
copyText(text);
}
});
}
/**
* Applies syntax highlight to the given code block (assumed to be <pre><code>), using highlight.js.
*/

View File

@@ -1,35 +0,0 @@
export function getThemeStyle(): "auto" | "light" | "dark" {
const configuredTheme = window.glob?.theme;
if (configuredTheme === "auto" || configuredTheme === "next") {
return "auto";
}
if (configuredTheme === "light" || configuredTheme === "dark") {
return configuredTheme;
}
if (configuredTheme === "next-light") {
return "light";
}
if (configuredTheme === "next-dark") {
return "dark";
}
const style = window.getComputedStyle(document.body);
const themeStyle = style.getPropertyValue("--theme-style");
if (style.getPropertyValue("--theme-style-auto") !== "true" && (themeStyle === "light" || themeStyle === "dark")) {
return themeStyle as "light" | "dark";
}
return "auto";
}
export function getEffectiveThemeStyle(): "light" | "dark" {
const themeStyle = getThemeStyle();
if (themeStyle === "auto") {
return window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}
return themeStyle === "dark" ? "dark" : "light";
}

View File

@@ -360,8 +360,7 @@ function copySelectionToClipboard() {
type dynamicRequireMappings = {
"@electron/remote": typeof import("@electron/remote"),
"electron": typeof import("electron"),
"child_process": typeof import("child_process"),
"url": typeof import("url")
"child_process": typeof import("child_process")
};
export function dynamicRequire<T extends keyof dynamicRequireMappings>(moduleName: T): Awaited<dynamicRequireMappings[T]>{
@@ -456,7 +455,9 @@ export function openInAppHelpFromUrl(inAppHelpPage: string) {
export async function openInReusableSplit(targetNoteId: string, targetViewMode: ViewMode, openOpts: {
hoistedNoteId?: string;
} = {}) {
const activeContext = glob.appContext?.tabManager?.getActiveContext();
// Dynamic import to avoid import issues in tests.
const appContext = (await import("../components/app_context.js")).default;
const activeContext = appContext.tabManager.getActiveContext();
if (!activeContext) {
return;
}
@@ -466,7 +467,7 @@ export async function openInReusableSplit(targetNoteId: string, targetViewMode:
if (!existingSubcontext) {
// The target split is not already open, open a new split with it.
const { ntxId } = subContexts[subContexts.length - 1];
glob.appContext?.triggerCommand("openNewNoteSplit", {
appContext.triggerCommand("openNewNoteSplit", {
ntxId,
notePath: targetNoteId,
hoistedNoteId: openOpts.hoistedNoteId,
@@ -921,7 +922,6 @@ export default {
parseDate,
formatDateISO,
formatDateTime,
formatTime,
formatTimeInterval,
formatSize,
localNowDateTime,

View File

@@ -1,15 +1,13 @@
import { WebSocketMessage } from "@triliumnext/commons";
import appContext from "../components/app_context.js";
import type { EntityChange } from "../server_types.js";
import bundleService from "./bundle.js";
import froca from "./froca.js";
import frocaUpdater from "./froca_updater.js";
import { t } from "./i18n.js";
import options from "./options.js";
import server from "./server.js";
import toast from "./toast.js";
import utils from "./utils.js";
import toastService from "./toast.js";
import server from "./server.js";
import options from "./options.js";
import frocaUpdater from "./froca_updater.js";
import appContext from "../components/app_context.js";
import { t } from "./i18n.js";
import type { EntityChange } from "../server_types.js";
import { WebSocketMessage } from "@triliumnext/commons";
import toast from "./toast.js";
type MessageHandler = (message: WebSocketMessage) => void;
let messageHandlers: MessageHandler[] = [];
@@ -128,14 +126,20 @@ async function handleMessage(event: MessageEvent<any>) {
} else if (message.type === "frontend-update") {
await executeFrontendUpdate(message.data.entityChanges);
} else if (message.type === "sync-hash-check-failed") {
toast.showError(t("ws.sync-check-failed"), 60000);
toastService.showError(t("ws.sync-check-failed"), 60000);
} else if (message.type === "consistency-checks-failed") {
toast.showError(t("ws.consistency-checks-failed"), 50 * 60000);
toastService.showError(t("ws.consistency-checks-failed"), 50 * 60000);
} else if (message.type === "api-log-messages") {
appContext.triggerEvent("apiLogMessages", { noteId: message.noteId, messages: message.messages });
} else if (message.type === "toast") {
toast.showMessage(message.message, message.timeout);
toastService.showMessage(message.message);
} else if (message.type === "execute-script") {
// TODO: Remove after porting the file
// @ts-ignore
const bundleService = (await import("./bundle.js")).default as any;
// TODO: Remove after porting the file
// @ts-ignore
const froca = (await import("./froca.js")).default as any;
const originEntity = message.originEntityId ? await froca.getNote(message.originEntityId) : null;
bundleService.getAndExecuteBundle(message.currentNoteId, originEntity, message.script, message.params);
@@ -157,7 +161,7 @@ function waitForEntityChangeId(desiredEntityChangeId: number) {
return new Promise<void>((res, rej) => {
entityChangeIdReachedListeners.push({
desiredEntityChangeId,
desiredEntityChangeId: desiredEntityChangeId,
resolvePromise: res,
start: Date.now()
});
@@ -201,7 +205,7 @@ async function consumeFrontendUpdateData() {
} else {
console.log("nonProcessedEntityChanges causing the timeout", nonProcessedEntityChanges);
toast.showError(t("ws.encountered-error", { message: e.message }));
toastService.showError(t("ws.encountered-error", { message: e.message }));
}
}

View File

@@ -99,7 +99,7 @@ class SetupController {
}
private async finish() {
const syncServerHost = this.syncServerHostInput.value.trim().replace(/\/+$/, "");
const syncServerHost = this.syncServerHostInput.value.trim();
const syncProxy = this.syncProxyInput.value.trim();
const password = this.passwordInput.value;

View File

@@ -835,7 +835,6 @@ table.promoted-attributes-in-tooltip th {
text-align: start;
color: var(--main-text-color) !important;
max-width: 500px;
white-space: pre-line;
box-shadow: 10px 10px 93px -25px #aaaaaa;
}
@@ -893,33 +892,6 @@ table.promoted-attributes-in-tooltip th {
opacity: 1;
}
.algolia-autocomplete {
width: calc(100% - 30px);
z-index: 2000 !important;
}
.algolia-autocomplete-container .aa-dropdown-menu {
position: inherit !important;
overflow: auto;
}
.algolia-autocomplete .aa-input,
.algolia-autocomplete .aa-hint {
width: 100%;
}
.algolia-autocomplete .aa-dropdown-menu {
width: 100%;
background-color: var(--main-background-color);
border: 1px solid var(--main-border-color);
border-top: none;
z-index: 2000 !important;
max-height: 500px;
overflow: auto;
padding: 0;
margin: 0;
}
.aa-dropdown-menu .aa-suggestion {
cursor: pointer;
padding: 6px 16px;
@@ -961,17 +933,160 @@ table.promoted-attributes-in-tooltip th {
background-color: var(--active-item-background-color);
}
.help-button,
.custom-title-bar-button {
float: inline-end;
background: none;
font-weight: 900;
/* ===== @algolia/autocomplete-core (headless, custom panel) ===== */
.aa-core-panel {
z-index: 10000;
background-color: var(--main-background-color);
border: 1px solid var(--main-border-color);
border-top: none;
max-height: 500px;
overflow: auto;
padding: 0;
margin: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.aa-core-panel.aa-dropdown-menu {
width: 100%;
}
.aa-core-panel--contained {
position: static !important;
border: 0;
background: transparent;
box-shadow: none;
}
.aa-core-list {
list-style: none;
padding: 0;
margin: 0;
}
.aa-core-item {
cursor: pointer;
padding: 7px 16px;
margin: 0;
white-space: normal;
}
.aa-core-item--active {
color: var(--active-item-text-color);
background-color: var(--active-item-background-color);
}
.aa-core-item .note-suggestion {
display: flex;
align-items: flex-start;
gap: 8px;
width: 100%;
}
.aa-core-item .icon,
.aa-core-item .command-icon {
flex-shrink: 0;
line-height: 1.4;
margin-top: 1px;
}
.aa-core-item .text {
min-width: 0;
flex: 1;
}
.aa-core-item .aa-core-primary-row {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
}
.aa-core-item .search-result-title {
display: block;
min-width: 0;
line-height: 1.35;
word-break: break-word;
font-size: 1.02em;
}
.aa-core-item .search-result-attributes {
display: block;
margin-top: 1px;
font-size: 0.8em;
color: var(--muted-text-color);
opacity: 0.65;
line-height: 1.2;
word-break: break-word;
}
.aa-core-item .search-result-attributes {
padding-inline-start: 14px;
}
.aa-core-item .aa-core-shortcut,
.aa-core-item kbd.command-shortcut {
flex-shrink: 0;
padding: 0;
border: 0;
background: transparent;
color: var(--muted-text-color);
font-family: inherit !important;
font-size: 0.8em;
opacity: 0.85;
}
.aa-core-item .command-suggestion {
display: flex;
align-items: center;
gap: 0.75rem;
width: 100%;
font-size: 0.9em;
}
.aa-core-item .command-content {
flex-grow: 1;
min-width: 0;
}
.aa-core-item .command-name {
font-weight: bold;
line-height: 1.35;
}
.aa-core-item .command-description {
font-size: 0.8em;
line-height: 1.3;
opacity: 0.75;
}
.aa-core-item .search-result-title b,
.aa-core-item .search-result-path b,
.aa-core-item .search-result-attributes b,
.aa-core-item .command-name b,
.aa-core-item .command-description b {
color: var(--admonition-warning-accent-color);
text-decoration: underline;
}
.aa-core-item .aa-core-separator {
padding: 0 2px;
}
.jump-to-note-results .aa-core-panel--contained {
max-height: calc(80vh - 200px);
overflow-y: auto;
overflow-x: hidden;
text-overflow: ellipsis;
}
.help-button {
float: inline-end;
background: none;
font-weight: 900;
color: orange;
border: 0;
cursor: pointer;
}
.multiplicity {
@@ -1152,90 +1267,11 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href
padding: 0.5rem 1rem 0.5rem 1rem !important; /* make modal header padding slightly smaller */
}
.modal-header .help-button,
.modal-header .custom-title-bar-button {
.modal-header .help-button {
padding: 6px;
margin: 0 12px;
}
.modal-content-with-sidebar {
flex-direction: row !important;
}
.modal-content-with-sidebar > .modal-sidebar {
display: flex;
flex-direction: column;
border-right: 1px solid var(--main-border-color);
flex-shrink: 0;
min-height: 0;
}
.modal-content-with-sidebar .modal-sidebar-header {
padding: 0.75rem 1rem;
flex-shrink: 0;
text-align: center;
border-bottom: 1px solid var(--main-border-color);
}
.modal-content-with-sidebar .modal-sidebar-header h5 {
margin: 0;
font-size: 1em;
}
.modal-content-with-sidebar > .modal-main > .modal-header > .modal-title {
visibility: hidden;
flex-grow: 1;
width: 0;
padding: 0;
margin: 0;
overflow: hidden;
}
.modal-content-with-sidebar > .modal-main > .modal-header {
flex-wrap: nowrap;
}
.modal-content-with-sidebar > .modal-main {
display: flex;
flex-direction: column;
flex-grow: 1;
min-width: 0;
min-height: 0;
overflow: hidden;
}
.modal-content-with-sidebar > .modal-main > .modal-body {
overflow: auto;
flex-grow: 1;
min-height: 0;
}
body.mobile .modal-content-with-sidebar {
flex-direction: column !important;
}
body.mobile .modal-content-with-sidebar > .modal-sidebar {
border-right: none;
border-bottom: 1px solid var(--main-border-color);
height: 30vh;
flex-shrink: 0;
overflow: hidden;
order: 1;
}
body.mobile .modal-content-with-sidebar > .modal-main {
order: 0;
}
body.mobile .modal-content-with-sidebar .modal-sidebar-header {
display: none;
}
body.mobile .modal-content-with-sidebar > .modal-main > .modal-header > .modal-title {
visibility: visible;
width: auto;
}
.ck-mentions .ck-button {
font-size: var(--detail-font-size) !important;
padding: 5px;
@@ -1314,43 +1350,6 @@ body.mobile .modal-content-with-sidebar > .modal-main > .modal-header > .modal-t
width: 100%;
}
/* Expandable include note styles */
.include-note-title-row {
display: flex;
align-items: center;
gap: 5px;
cursor: pointer;
}
.include-note-title-row .include-note-title {
margin: 0;
}
.include-note-toggle {
background: none;
border: none;
padding: 2px;
cursor: pointer;
font-size: 1.2em;
color: var(--main-text-color);
transition: transform 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.include-note-toggle:hover {
color: var(--main-link-color);
}
.include-note-toggle.expanded {
transform: rotate(90deg);
}
.include-note[data-box-size="expandable"] .include-note-content {
margin-top: 10px;
}
.alert {
padding: 8px 14px;
width: auto;
@@ -1750,22 +1749,6 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
display: flex;
}
body.mobile .modal-dialog.modal-dialog-full-page-on-mobile {
width: 100%;
height: 100%;
max-height: unset;
max-width: unset;
.modal-content {
border-radius: 0;
border: 0;
.modal-body {
overflow: scroll;
}
}
}
body.mobile .modal-content {
overflow-y: auto;
border-radius: var(--bs-modal-border-radius) var(--bs-modal-border-radius) 0 0;
@@ -1780,7 +1763,7 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
display: inline !important;
}
body.mobile .options-section-card table {
body.mobile .options-section table {
word-break: break-all;
}
@@ -1887,11 +1870,8 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
justify-content: space-between;
align-items: baseline;
font-weight: bold;
color: var(--muted-text-color) !important;
}
#right-pane .card-header-title {
text-transform: uppercase;
color: var(--muted-text-color) !important;
}
#right-pane .card-header-buttons {
@@ -1960,12 +1940,12 @@ button.close:hover {
margin-bottom: 15px;
}
.options-section-card h5 {
.options-section h5 {
margin-top: 10px;
margin-bottom: 10px;
}
.options-section-card input[type="number"] {
.options-section input[type="number"] {
/* overriding settings from .form-control */
width: 10em !important;
flex-grow: 0 !important;
@@ -2778,30 +2758,3 @@ iframe.print-iframe {
min-height: 50px;
align-items: center;
}
.ocr-text-section {
padding: 10px;
background: var(--accented-background-color);
border-left: 3px solid var(--main-border-color);
text-align: left;
width: 100%;
}
.ocr-header {
font-weight: bold;
margin-bottom: 8px;
font-size: 0.9em;
color: var(--muted-text-color);
}
.ocr-content {
max-height: 150px;
overflow-y: auto;
font-size: 0.9em;
line-height: 1.4;
white-space: pre-wrap;
}
.ck-content pre code {
tab-size: var(--code-block-tab-width, 4);
}

View File

@@ -269,9 +269,9 @@
--timeline-connector-active-color: #ddd;
--timeline-connector-hover-blend-mode: multiply;
--tooltip-background-color: rgba(0, 0, 0, 0.818);
--tooltip-foreground-color: #ffffffeb;
--tooltip-shadow-color: rgba(0, 0, 0, 0.2);
--tooltip-background-color: rgba(255, 255, 255, 0.85);
--tooltip-foreground-color: #000000ba;
--tooltip-shadow-color: rgba(0, 0, 0, 0.15);
--help-background-color: #fffc;
--help-card-background: var(--card-background-color);

View File

@@ -0,0 +1,11 @@
/* Import the light color scheme.
* This is the base color scheme, always active and overridden by the dark
* color scheme stylesheet when necessary. */
@import url(./theme-next-light.css);
/* Import the dark color scheme when the system preference is set to dark mode */
@import url(./theme-next-dark.css) (prefers-color-scheme: dark);
:root {
--theme-style-auto: true;
}

View File

@@ -544,11 +544,14 @@ li.dropdown-item a.dropdown-item-button:focus-visible {
vertical-align: middle;
}
#toast-container .toast .toast-header .btn-close,
#toast-container .toast .toast-close .btn-close {
#toast-container .toast .toast-header .btn-close {
margin: 0 0 0 12px;
}
#toast-container .toast.no-title {
flex-direction: row;
}
#toast-container .toast .toast-body {
flex-grow: 1;
overflow: hidden;

View File

@@ -26,8 +26,7 @@
.modal .modal-header .btn-close,
.modal .modal-header .help-button,
.modal .modal-header .custom-title-bar-button,
#toast-container .toast .toast-header .btn-close,
#toast-container .toast .toast-close .btn-close {
#toast-container .toast .toast-header .btn-close {
display: flex;
justify-content: center;
align-items: center;
@@ -47,14 +46,12 @@
}
.modal .modal-header .btn-close,
#toast-container .toast .toast-header .btn-close,
#toast-container .toast .toast-close .btn-close {
#toast-container .toast .toast-header .btn-close {
--modal-control-button-hover-background: var(--modal-close-button-hover-background);
}
.modal .modal-header .btn-close::after,
#toast-container .toast .toast-header .btn-close::after,
#toast-container .toast .toast-close .btn-close::after {
#toast-container .toast .toast-header .btn-close::after {
content: "\ec8d";
font-family: boxicons;
}
@@ -70,8 +67,7 @@
.modal .modal-header .btn-close:hover,
.modal .modal-header .help-button:hover,
.modal .modal-header .custom-title-bar-button:hover,
#toast-container .toast .toast-header .btn-close:hover,
#toast-container .toast .toast-close .btn-close:hover {
#toast-container .toast .toast-header .btn-close:hover {
background: var(--modal-control-button-hover-background);
color: var(--modal-control-button-hover-color);
}
@@ -79,22 +75,19 @@
.modal .modal-header .btn-close:active,
.modal .modal-header .help-button:active,
.modal .modal-header .custom-title-bar-button:active,
#toast-container .toast .toast-header .btn-close:active,
#toast-container .toast .toast-close .btn-close:active {
#toast-container .toast .toast-header .btn-close:active {
transform: scale(.85);
}
.modal .modal-header .btn-close:focus,
.modal .modal-header .help-button:focus,
#toast-container .toast .toast-header .btn-close:focus,
#toast-container .toast .toast-close .btn-close:focus {
#toast-container .toast .toast-header .btn-close:focus {
box-shadow: none !important;
}
.modal .modal-header .btn-close:focus-visible,
.modal .modal-header .help-button:focus-visible,
#toast-container .toast .toast-header .btn-close:focus-visible,
#toast-container .toast .toast-close .btn-close:focus-visible {
#toast-container .toast .toast-header .btn-close:focus-visible {
outline: 2px solid var(--input-focus-outline-color);
outline-offset: 2px;
}

View File

@@ -21,20 +21,6 @@ button.ck.ck-button:is(.ck-button-action, .ck-button-save, .ck-button-cancel, .c
&.dropdown-toggle-split {
min-width: unset;
}
.btn-group > & {
border-radius: 0;
}
.btn-group > &:first-child {
border-start-start-radius: 6px;
border-end-start-radius: 6px;
}
.btn-group > &:last-child {
border-start-end-radius: 6px;
border-end-end-radius: 6px;
}
}
button.btn.btn-primary:hover,
@@ -131,16 +117,11 @@ button.tn-low-profile:hover {
color: var(--icon-button-color);
}
:root .btn-group .icon-action:not(:first-child) {
:root .btn-group .icon-action:last-child {
border-top-left-radius: unset !important;
border-bottom-left-radius: unset !important;
}
:root .btn-group .icon-action:not(:last-child) {
border-top-right-radius: unset !important;
border-bottom-right-radius: unset !important;
}
.btn-group .tn-tool-button + .tn-tool-button {
margin-inline-start: 4px !important;
}

View File

@@ -635,7 +635,6 @@ html .note-detail-editable-text :not(figure, .include-note, hr):first-child {
inset-inline-end: 0.35em;
}
.ck-content h1,
.ck-content h2,
.ck-content h3,
.ck-content h4,
@@ -695,7 +694,7 @@ html .note-detail-editable-text :not(figure, .include-note, hr):first-child {
outline: none;
box-shadow: 0 0 0 2px var(--link-selection-outline-color);
background: var(--link-hover-background);
}
}
/* Reference link */
@@ -714,15 +713,6 @@ html .note-detail-editable-text :not(figure, .include-note, hr):first-child {
text-decoration: underline;
}
.ck-content a.reference-link small {
margin-left: 0.25em;
opacity: 0.5;
>span {
font-size: 0.7em;
}
}
/*
* Read-only text content
*/

View File

@@ -128,8 +128,8 @@
margin-inline: auto;
}
/* The search results list */
.note-detail-empty span.aa-dropdown-menu {
/* The headless autocomplete panel rendered into the empty-note results container */
.note-detail-empty .aa-core-panel--contained {
margin-top: 1em;
border: unset;
}
@@ -186,7 +186,12 @@ body.experimental-feature-new-layout .note-detail-content-widget-content.options
}
.options-section:not(.tn-no-card) {
margin-bottom: 26px !important;
margin-bottom: calc(var(--options-title-offset) + 26px) !important;
box-shadow: var(--card-box-shadow);
border: 1px solid var(--card-border-color) !important;
border-radius: 8px;
background: var(--card-background-color);
padding: var(--options-card-padding);
}
body.desktop .options-section:not(.tn-no-card) {
@@ -194,70 +199,40 @@ body.desktop .options-section:not(.tn-no-card) {
max-width: var(--options-card-max-width);
}
.options-section-card {
box-shadow: var(--card-box-shadow);
border: 1px solid var(--card-border-color);
border-radius: 8px;
background: var(--card-background-color);
padding: var(--options-card-padding);
}
.note-detail-content-widget-content.options {
--default-padding: 15px;
padding-top: var(--default-padding);
padding-top: calc(var(--default-padding) + var(--options-title-offset) + var(--options-title-font-size));
padding-bottom: var(--default-padding);
}
.options-section:not(.tn-no-card) h4 {
.options-section:not(.tn-no-card) h4,
.options-section:not(.tn-no-card) h5 {
text-transform: uppercase;
letter-spacing: .4pt;
}
.options-section:not(.tn-no-card) h4 {
font-size: var(--options-title-font-size);
font-weight: 600;
color: var(--launcher-pane-text-color);
margin-top: 0;
margin-bottom: var(--options-title-offset);
margin-top: calc(-1 * var(--options-card-padding) - var(--options-title-font-size) - var(--options-title-offset)) !important;
margin-bottom: calc(var(--options-title-offset) + var(--options-card-padding)) !important;
margin-inline-start: calc(-1 * var(--options-card-padding));
}
.options-section:not(.tn-no-card) .options-section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.options-section:not(.tn-no-card) .options-section-header h4 {
margin: 0;
margin-bottom: 0;
}
.options-section:not(.tn-no-card) .options-section-header .icon-action {
margin-inline-start: auto;
}
.options-section-description {
color: var(--muted-text-color);
font-size: 13px;
margin-top: 0;
margin-bottom: 16px;
}
.options-section-card h5 {
text-transform: uppercase;
letter-spacing: .4pt;
.options-section:not(.tn-no-card) h5 {
font-size: var(--options-title-font-size);
font-weight: bold;
margin-top: 1em !important;
margin-bottom: unset !important;
}
.options-section-card h5:first-of-type {
.options-section:not(.tn-no-card) h5:first-of-type {
margin-top: unset !important;
}
.options-section-card hr {
.options-section hr {
--bs-border-width: 2px;
margin-inline-start: calc(var(--options-card-padding) * -1);
@@ -266,26 +241,27 @@ body.desktop .options-section:not(.tn-no-card) {
color: var(--root-background);
}
.options-section-card p:last-of-type:not(:first-of-type),
.options-section-card .existing-anonymized-databases {
.options-section p:last-of-type:not(:first-of-type),
.options-section h4 + p:last-child,
.options-section .existing-anonymized-databases {
margin-bottom: 0;
}
.options-section-card .form-group {
.options-section .form-group {
margin-bottom: 1em;
}
.options-section-card .form-group:last-child {
.options-section .form-group:last-child {
margin-bottom: 0;
}
.options-section-card ul {
.options-section ul {
margin: 0;
padding: 0;
margin-bottom: 1em;
}
.options-section-card label:not(.tn-checkbox):not(.tn-radio) {
.options-section label:not(.tn-checkbox):not(.tn-radio) {
margin-bottom: 6px;
}
@@ -299,7 +275,7 @@ nav.options-section-tabs .nav-tabs {
border-bottom: 0;
}
nav.options-section-tabs + .options-section .options-section-card {
nav.options-section-tabs + .options-section {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
@@ -353,9 +329,3 @@ nav.options-section-tabs + .options-section .options-section-card {
.etapi-options-section div {
height: auto !important;
}
/* BACKUP */
.options-section-card table a {
color: inherit;
}

View File

@@ -0,0 +1,11 @@
/* Import the light color scheme.
* This is the base color scheme, always active and overridden by the dark
* color scheme stylesheet when necessary. */
@import url(./theme-light.css);
/* Import the dark color scheme when the system preference is set to dark mode */
@import url(./theme-dark.css) (prefers-color-scheme: dark);
:root {
--theme-style-auto: true;
}

View File

@@ -1,6 +1,13 @@
{
"about": {
"data_directory": "مجلد البيانات:"
"title": "حول تريليوم للملاحظات",
"homepage": "الصفحة الرئيسية:",
"app_version": "اصدار التطبيق:",
"sync_version": "اصدار المزامنه:",
"build_date": "تاريخ الانشاء:",
"build_revision": "مراجعة الاصدار:",
"data_directory": "مجلد البيانات:",
"db_version": "اصدار قاعدة البيانات:"
},
"toast": {
"critical-error": {
@@ -366,6 +373,7 @@
"theme_defined": "النسق المحدد",
"main_font": "الخط الرئيسي",
"font_family": "عائلة الخطوط",
"reload_frontend": "اعادة تحميل الواجهة",
"generic-fonts": "الخطوط العامة",
"sans-serif": "خطوط بدون زوائد",
"system-default": "الاعداد الافتراضي للنظام",
@@ -385,7 +393,9 @@
},
"delete_notes": {
"close": "غلق",
"cancel": "الغاء"
"cancel": "الغاء",
"ok": "نعم",
"delete_notes_preview": "حذف معاينة الملاحظات"
},
"export": {
"close": "غلق",
@@ -477,6 +487,26 @@
"start_button": "بدء جلسة محمية",
"help_title": "مساعدة حول الملاحظات المحمية"
},
"revisions": {
"delete_button": "حذف",
"download_button": "تنزيل",
"restore_button": "أستعادة",
"note_revisions": "مراجعات الملاحظة",
"diff_on": "عرض الفروقات",
"diff_off": "عرض المحتوى",
"file_size": "حجم الملف:",
"mime": "MIME: ",
"delete_all_button": "حذف كل المراجعات",
"settings": "اعدادات مراجعة الملاحظة",
"diff_not_available": "المقارنة غير متوفرة.",
"help_title": "مساعدة حول مراجعات الملاحظة",
"diff_off_hint": "انقر لعرض محتويات الملاحظة",
"revisions_deleted": "تم حذف جميع نسخ المراجعات للملاحظة.",
"revision_restored": "تم استعادة نسخ المراجعة للملاحظة.",
"revision_deleted": "تم حذف مراجعة الملاحظة.",
"snapshot_interval": "فاصل زمني لحفظ لقطات اصدارات المراجعة: {{seconds}}",
"maximum_revisions": "حد عدد لقطات اصدارات الملاحظة: {{number}}"
},
"sort_child_notes": {
"title": "عنوان",
"ascending": "تصاعدي",
@@ -596,8 +626,7 @@
"date-and-time": "التاريخ والوقت",
"no_backup_yet": "لايوجد نسخة احتياطية لحد الان",
"enable_daily_backup": "تمكين النسخ الاحتياطي اليومي",
"backup_database_now": "نسخ اختياطي لقاعدة البيانات الان",
"download": "تنزيل"
"backup_database_now": "نسخ اختياطي لقاعدة البيانات الان"
},
"etapi": {
"created": "تم الأنشاء",
@@ -634,6 +663,8 @@
"default_shortcuts": "اختصارات افتراضية"
},
"sync_2": {
"timeout_unit": "ميلي ثانية",
"note": "ملاحظة",
"save": "حفظ",
"help": "المساعدة",
"config_title": "تهيئة المزامنة",
@@ -1098,7 +1129,9 @@
"spellcheck": {
"title": "التدقيق الاملائي",
"enable": "تفعيل التدقيق الاملائي",
"language_code_label": "رمز اللغة او رموز اللغات"
"language_code_label": "رمز اللغة او رموز اللغات",
"available_language_codes_label": "رموز اللغات المتاحة:",
"language_code_placeholder": "على سبيل المثال \"en-US\", \"de-AI\""
},
"note-map": {
"button-link-map": "خريطة الروابط",

View File

@@ -1,4 +1,10 @@
{
"about": {
"title": "Sobre Trilium Notes",
"homepage": "Pàgina principal:",
"app_version": "Versió de l'aplicació:",
"db_version": "Versió de la base de dades:"
},
"add_link": {
"note": "Nota"
},
@@ -19,7 +25,8 @@
},
"delete_notes": {
"close": "Tanca",
"cancel": "Cancel·la"
"cancel": "Cancel·la",
"ok": "OK"
},
"export": {
"close": "Tanca",
@@ -53,6 +60,12 @@
"recent_changes": {
"undelete_link": "recuperar"
},
"revisions": {
"restore_button": "Restaura",
"delete_button": "Suprimeix",
"download_button": "Descarrega",
"mime": "MIME: "
},
"sort_child_notes": {
"title": "títol",
"ascending": "ascendent",

View File

@@ -1,5 +1,12 @@
{
"about": {
"title": "关于 Trilium Notes",
"homepage": "项目主页:",
"app_version": "应用版本:",
"db_version": "数据库版本:",
"sync_version": "同步版本:",
"build_date": "编译日期:",
"build_revision": "编译版本:",
"data_directory": "数据目录:"
},
"toast": {
@@ -34,9 +41,7 @@
"link_title_mirrors": "链接标题跟随笔记标题变化",
"link_title_arbitrary": "链接标题可随意修改",
"link_title": "链接标题",
"button_add_link": "添加链接",
"anchor": "锚点(可选)",
"anchor_none": "无(笔记链接)"
"button_add_link": "添加链接"
},
"branch_prefix": {
"edit_branch_prefix": "编辑分支前缀",
@@ -83,22 +88,17 @@
"also_delete_note": "同时删除笔记"
},
"delete_notes": {
"delete_notes_preview": "删除笔记预览",
"close": "关闭",
"delete_all_clones_description": "同时删除所有克隆(可以在最近修改中撤消)",
"erase_notes_description": "立即删除笔记,而不是软删除。此操作无法撤销,并将强制应用程序重新加载。",
"erase_notes_description": "通常(软)删除仅标记笔记为已删除,可以在一段时间内通过最近修改对话框撤消。选中此选项将立即擦除笔记,不可撤销。",
"erase_notes_warning": "永久擦除笔记(无法撤销),包括所有克隆。这将强制应用程序重载。",
"notes_to_be_deleted": "删除笔记({{notesCount}}",
"notes_to_be_deleted": "删除以下笔记 ({{notesCount}})",
"no_note_to_delete": "没有笔记将被删除(仅克隆)。",
"broken_relations_to_be_deleted": "断开的关联({{relationCount}}",
"broken_relations_to_be_deleted": "将删除以下关系并断开连接 ({{ relationCount}})",
"cancel": "取消",
"title": "删除笔记",
"delete": "删除",
"clones_label": "克隆",
"delete_clones_description_other": "同时删除 {{count}} 个其他克隆。此操作可在最近的更改中撤销。",
"erase_notes_label": "永久擦除",
"table_note_with_relation": "关联笔记",
"table_relation": "关联",
"table_points_to": "指向(已删除)"
"ok": "确定",
"deleted_relation_text": "笔记 {{- note}} (将被删除的笔记) 被以下关系 {{- relation}} 引用, 来自 {{- source}}。"
},
"export": {
"export_note_title": "导出笔记",
@@ -209,8 +209,7 @@
"box_size_small": "小型 (显示大约10行)",
"box_size_medium": "中型 (显示大约30行)",
"box_size_full": "完整显示(完整文本框)",
"button_include": "包含笔记",
"box_size_expandable": "可展开(默认折叠)"
"button_include": "包含笔记"
},
"info": {
"modalTitle": "信息消息",
@@ -271,6 +270,33 @@
"undelete_link": "恢复删除",
"confirm_undelete": "您确定要恢复此笔记及其子笔记吗?"
},
"revisions": {
"note_revisions": "笔记历史版本",
"delete_all_revisions": "删除此笔记的所有修订版本",
"delete_all_button": "删除所有修订版本",
"help_title": "关于笔记修订版本的帮助",
"confirm_delete_all": "您是否要删除此笔记的所有修订版本?",
"no_revisions": "此笔记暂无修订版本...",
"restore_button": "恢复",
"confirm_restore": "您是否要恢复此修订版本?这将使用此修订版本覆盖笔记的当前标题和内容。",
"delete_button": "删除",
"confirm_delete": "您是否要删除此修订版本?",
"revisions_deleted": "笔记修订版本已删除。",
"revision_restored": "笔记修订版本已恢复。",
"revision_deleted": "笔记修订版本已删除。",
"snapshot_interval": "笔记快照保存间隔: {{seconds}}秒。",
"maximum_revisions": "当前笔记的最大历史数量: {{number}}。",
"settings": "笔记修订设置",
"download_button": "下载",
"mime": "MIME 类型: ",
"file_size": "文件大小:",
"preview_not_available": "无法预览此类型的笔记。",
"diff_on": "显示差异",
"diff_off": "显示内容",
"diff_on_hint": "点击以显示笔记源代码差异",
"diff_off_hint": "点击以显示笔记内容",
"diff_not_available": "差异不可用。"
},
"sort_child_notes": {
"sort_children_by": "按...排序子笔记",
"sorting_criteria": "排序条件",
@@ -342,7 +368,7 @@
"calendar_root": "标记应用作为每日笔记的根。只应标记一个笔记。",
"archived": "含有此标签的笔记默认在搜索结果中不可见(也适用于跳转到、添加链接对话框等)。",
"exclude_from_export": "笔记(及其子树)不会包含在任何笔记导出中",
"run": "定义脚本应运行的事件。可能的值包括:\n<ul>\n<li>frontendStartup - Trilium前端启动时或刷新时但不会在移动端执行。</li>\n<li>mobileStartup - Trilium前端启动时或刷新时 在移动端会执行。</li>\n<li>backendStartup - Trilium后端启动时</li>\n<li>hourly - 每小时运行一次。您可以使用附加标签<code>runAtHour</code>指定小时。</li>\n<li>daily - 每天运行一次</li>\n</ul>",
"run": "定义脚本应运行的事件。可能的值包括:\n<ul>\n<li>frontendStartup - Trilium前端启动时或刷新时但不会在移动端执行。</li>\n<li>mobileStartup - Trilium前端启动时或刷新时 在移动端会执行。</li>\n<li>backendStartup - Trilium后端启动时</li>\n<li>hourly - 每小时运行一次。您可以使用附加标签<code>runAtHour</code>指定小时。</li>\n<li>daily - 每天运行一次</li>\n</ul>",
"run_on_instance": "定义应在哪个Trilium实例上运行。默认为所有实例。",
"run_at_hour": "应在哪个小时运行。应与<code>#run=hourly</code>一起使用。可以多次定义,以便一天内运行多次。",
"disable_inclusion": "含有此标签的脚本不会包含在父脚本执行中。",
@@ -421,9 +447,7 @@
"print_landscape": "导出为 PDF 时,将页面方向更改为横向而不是纵向。",
"print_page_size": "导出为 PDF 时,更改页面大小。支持的值:<code>A0</code>、<code>A1</code>、<code>A2</code>、<code>A3</code>、<code>A4</code>、<code>A5</code>、<code>A6</code>、<code>Legal</code>、<code>Letter</code>、<code>Tabloid</code>、<code>Ledger</code>。",
"color_type": "颜色",
"textarea": "多行文本",
"print_scale": "导出为 PDF 时,更改渲染内容的比例。取值范围从 0.1 (10%) 到 2 (200%),默认值为 1 (100%)。",
"print_margins": "导出为 PDF 时,设置页面边距。可以使用 <code>default</code>、<code>none</code>、<code>minimum</code> 或以毫米为单位的自定义值,例如 <code>top,right,bottom,left</code>。"
"textarea": "多行文本"
},
"attribute_editor": {
"help_text_body1": "要添加标签,只需输入例如 <code>#rock</code> 或者如果您还想添加值,则例如 <code>#year = 2020</code>",
@@ -685,13 +709,7 @@
"advanced": "高级",
"export_as_image": "导出为图像",
"export_as_image_png": "PNG栅格",
"export_as_image_svg": "SVG矢量图",
"view_ocr_text": "查看 OCR 文本",
"word_wrap": "自动换行",
"word_wrap_auto": "自动",
"word_wrap_auto_description": "遵循全局设置",
"word_wrap_on": "开启",
"word_wrap_off": "关闭"
"export_as_image_svg": "SVG矢量图"
},
"onclick_button": {
"no_click_handler": "按钮组件'{{componentId}}'没有定义点击处理程序"
@@ -785,10 +803,7 @@
"expand_first_level": "展开直接子代",
"expand_nth_level": "展开 {{depth}} 层",
"expand_all_levels": "展开所有层级",
"hide_child_notes": "隐藏树中的子笔记",
"open_all_in_tabs": "全部打开",
"open_all_in_tabs_tooltip": "在新标签页中打开所有结果",
"open_all_confirm": "这将在新标签页中打开 {{count}} 个笔记。继续吗?"
"hide_child_notes": "隐藏树中的子笔记"
},
"edited_notes": {
"no_edited_notes_found": "今天还没有编辑过的笔记...",
@@ -842,8 +857,7 @@
"collapse": "折叠到正常大小",
"title": "笔记地图",
"fix-nodes": "固定节点",
"link-distance": "链接距离",
"too-many-notes": "此子树包含 {{count}} 个笔记,超过了笔记地图中可显示的 {{max}} 个笔记的限制。"
"link-distance": "链接距离"
},
"note_paths": {
"title": "笔记路径",
@@ -1048,8 +1062,7 @@
"note_already_in_diagram": "笔记 \"{{title}}\" 已经在图中。",
"enter_title_of_new_note": "输入新笔记的标题",
"default_new_note_title": "新笔记",
"click_on_canvas_to_place_new_note": "点击画布以放置新笔记",
"rename_relation": "重命名关系"
"click_on_canvas_to_place_new_note": "点击画布以放置新笔记"
},
"backend_log": {
"refresh": "刷新"
@@ -1058,17 +1071,15 @@
"title": "检查一致性",
"find_and_fix_button": "查找并修复一致性问题",
"finding_and_fixing_message": "正在查找并修复一致性问题...",
"issues_fixed_message": "一致性问题应该已被修复。",
"find_and_fix_label": "查找并修复一致性问题",
"find_and_fix_description": "扫描并自动修复数据库中的任何数据一致性问题。"
"issues_fixed_message": "一致性问题应该已被修复。"
},
"database_anonymization": {
"title": "数据库匿名化",
"full_anonymization": "完全匿名化",
"full_anonymization_description": "创建数据库副本,移除所有笔记内容,仅保留数据库结构和非敏感元数据。在调试问题时,可安全地在线共享。",
"full_anonymization_description": "此操作将创建一个新的数据库副本并进行匿名化处理(删除所有笔记内容,仅保留结构和一些非敏感元数据),用来分享到网上做调试而不用担心泄漏您的个人资料。",
"save_fully_anonymized_database": "保存完全匿名化的数据库",
"light_anonymization": "轻度匿名化",
"light_anonymization_description": "创建一个副本,其中移除笔记内容,但保留标题属性和自定义脚本/小部件。这有助于提供更多调试信息。",
"light_anonymization_description": "此操作将创建一个新的数据库副本,并对其进行轻度匿名化处理——仅删除所有笔记内容,但保留标题属性。此外,自定义 JS 前端/后端脚本笔记和自定义小部件将保留。这提供更多上下文以调试问题。",
"choose_anonymization": "您可以自行决定是提供完全匿名化还是轻度匿名化的数据库。即使是完全匿名化的数据库也非常有用,但在某些情况下,轻度匿名化的数据库可以加快错误识别和修复的过程。",
"save_lightly_anonymized_database": "保存轻度匿名化的数据库",
"existing_anonymized_databases": "现有的匿名化数据库",
@@ -1077,17 +1088,15 @@
"error_creating_anonymized_database": "无法创建匿名化数据库,请检查后端日志以获取详细信息",
"successfully_created_fully_anonymized_database": "成功创建完全匿名化的数据库,路径为 {{anonymizedFilePath}}",
"successfully_created_lightly_anonymized_database": "成功创建轻度匿名化的数据库,路径为 {{anonymizedFilePath}}",
"no_anonymized_database_yet": "尚无匿名化数据库。",
"description": "创建数据库的匿名副本,以便在调试问题时与开发人员共享,而不会泄露个人数据。"
"no_anonymized_database_yet": "尚无匿名化数据库。"
},
"database_integrity_check": {
"title": "数据库完整性检查",
"description": "检查 SQLite 数据库是否损坏。根据数据库的大小,可能会需要一些时间。",
"check_button": "检查数据库完整性",
"checking_integrity": "正在检查数据库完整性...",
"integrity_check_succeeded": "完整性检查成功 - 未发现问题。",
"integrity_check_failed": "完整性检查失败: {{results}}",
"check_integrity_label": "检查数据库完整性",
"check_integrity_description": "验证 SQLite 数据库是否已损坏。"
"integrity_check_failed": "完整性检查失败: {{results}}"
},
"sync": {
"title": "同步",
@@ -1097,31 +1106,28 @@
"filling_entity_changes": "正在填充实体变更行...",
"sync_rows_filled_successfully": "同步行填充成功",
"finished-successfully": "同步已完成。",
"failed": "同步失败:{{message}}",
"force_full_sync_label": "强制全量同步",
"force_full_sync_description": "触发与同步服务器的全量同步,重新上传所有更改。",
"fill_entity_changes_description": "重建实体变更记录。如果同步过程中缺少某些变更,请使用此功能。",
"fill_entity_changes_label": "填充实体变更"
"failed": "同步失败:{{message}}"
},
"vacuum_database": {
"title": "数据库清理",
"description": "这会重建数据库,通常会减少占用空间,不会删除数据。",
"button_text": "清理数据库",
"vacuuming_database": "正在清理数据库...",
"database_vacuumed": "数据库已清理",
"vacuum_label": "数据库清理",
"vacuum_description": "重建数据库以减小文件大小。数据不会发生任何变化。"
"database_vacuumed": "数据库已清理"
},
"fonts": {
"theme_defined": "跟随主题",
"fonts": "字体",
"main_font": "界面字体",
"main_font": "字体",
"font_family": "字体系列",
"size": "大小",
"note_tree_font": "笔记树字体",
"note_detail_font": "文档字体",
"monospace_font": "等宽字体",
"not_all_fonts_available": "并非所有列出的字体都可能在您的系统上可用",
"note_detail_font": "笔记详情字体",
"monospace_font": "等宽(代码)字体",
"note_tree_and_detail_font_sizing": "请注意,笔记树字体和详细字体的大小相对于主字体大小设置。",
"not_all_fonts_available": "并非所有列出的字体都可能在您的系统上可用。",
"apply_font_changes": "要应用字体更改,请点击",
"reload_frontend": "重载前端",
"generic-fonts": "通用字体",
"sans-serif-system-fonts": "无衬线系统字体",
"serif-system-fonts": "衬线系统字体",
@@ -1130,12 +1136,7 @@
"serif": "衬线",
"sans-serif": "无衬线",
"monospace": "等宽",
"system-default": "系统默认",
"custom_fonts": "使用自定义字体",
"preview": "预览",
"monospace_font_description": "用于代码注释和代码块",
"size_relative_to_general": "大小相对于一般字体大小",
"apply_changes": "重新加载以应用更改"
"system-default": "系统默认"
},
"max_content_width": {
"title": "内容宽度",
@@ -1155,23 +1156,20 @@
"edited_notes_message": "日记笔记自动打开编辑过的笔记功能区标签页"
},
"theme": {
"title": "用户界面",
"theme_label": "应用主题",
"title": "主题",
"theme_label": "主题",
"override_theme_fonts_label": "覆盖主题字体",
"triliumnext": "跟随系统颜色方案",
"triliumnext-light": "浅色",
"triliumnext-dark": "深色",
"triliumnext": "Trilium跟随系统颜色方案",
"triliumnext-light": "Trilium浅色",
"triliumnext-dark": "Trilium深色",
"layout": "布局",
"layout-vertical-title": "垂直",
"layout-horizontal-title": "水平",
"layout-vertical-description": "启动栏位于左侧(默认)",
"layout-horizontal-description": "启动栏位于标签页栏下方,标签页栏现在是全宽的。",
"auto_theme": "跟随系统配色方案",
"light_theme": "浅色",
"dark_theme": "深色",
"modern_themes": "现代",
"legacy_themes": "传统",
"custom_themes": "自定义"
"auto_theme": "传统(跟随系统配色方案",
"light_theme": "传统(浅色",
"dark_theme": "传统(深色"
},
"zoom_factor": {
"title": "缩放系数(仅桌面客户端有效)",
@@ -1180,7 +1178,7 @@
"code_auto_read_only_size": {
"title": "自动只读大小",
"description": "自动只读大小是指笔记超过设置的大小后自动设置为只读模式(为性能考虑)。",
"label": "自动只读大小",
"label": "自动只读大小(代码笔记)",
"unit": "字符"
},
"code_mime_types": {
@@ -1199,51 +1197,32 @@
},
"images": {
"images_section_title": "图片",
"download_images_automatically": "自动下载图片",
"download_images_description": "粘贴的 HTML 代码中下载引用的在线图片,以便离线使用。",
"enable_image_compression": "图片压缩",
"max_image_dimensions": "最大图像尺寸",
"jpeg_quality_description": "建议范围为 5085。较低的值可以减小文件大小较高的值可以保留细节。",
"max_image_dimensions_unit": "像素",
"enable_image_compression_description": "上传或粘贴图片时,对其进行压缩和调整大小。",
"max_image_dimensions_description": "超过此尺寸的图片将自动调整大小。",
"jpeg_quality": "JPEG质量",
"ocr_section_title": "文本提取OCR",
"ocr_related_content_languages": "内容语言(用于文本提取)",
"ocr_auto_process": "自动处理新文件",
"ocr_auto_process_description": "自动从新上传或粘贴的文件中提取文本。",
"ocr_min_confidence": "最低置信度",
"ocr_confidence_description": "仅提取置信度高于此阈值的文本。较低的置信度阈值会包含更多文本,但可能准确性较低。",
"batch_ocr_title": "处理现有文件",
"batch_ocr_description": "从笔记中的所有现有图像、PDF 和 Office 文档中提取文本。这可能需要一些时间,具体取决于文件数量。",
"batch_ocr_start": "开始批量处理",
"batch_ocr_starting": "开始批量处理...",
"batch_ocr_progress": "正在处理 {{processed}} 个文件,共 {{total}} 个文件...",
"batch_ocr_completed": "批量处理完成!已处理 {{processed}} 个文件。",
"batch_ocr_error": "批量处理过程中出错:{{error}}"
"download_images_automatically": "自动下载图片以供离线使用。",
"download_images_description": "粘贴的 HTML 可能包含在线图片的引用Trilium 会找到这些引用并下载图片,以便它们可以离线使用。",
"enable_image_compression": "启用图片压缩",
"max_image_dimensions": "图片的最大宽度/高度(超过此限制的图像将会被缩放)。",
"jpeg_quality_description": "JPEG 质量10 - 最差质量100 最佳质量,建议为 50 - 85",
"max_image_dimensions_unit": "像素"
},
"attachment_erasure_timeout": {
"attachment_erasure_timeout": "未使用的附件",
"erase_attachments_after": "在此时间后清理未使用的附件",
"manual_erasing_description": "手动触发清理,忽略上述超时。",
"erase_unused_attachments_now": "立即清理未使用的附件",
"unused_attachments_erased": "未使用的附件已被清理。",
"description": "不再被任何笔记引用的附件被视为未使用,可以在一段时间后自动清理。",
"erase_attachments_after_description": "未使用附件被永久清理前的时间。"
"attachment_erasure_timeout": "附件清理超时",
"attachment_auto_deletion_description": "如果附件在一段时间后不再被笔记引用,它们将自动被删除(并被清理)。",
"erase_attachments_after": "在此时间后删除未使用的附件:",
"manual_erasing_description": "您还可以手动触发清理(而不考虑上述定义的超时时间):",
"erase_unused_attachments_now": "立即清理未使用的附件笔记",
"unused_attachments_erased": "未使用的附件已被删除。"
},
"network_connections": {
"network_connections_title": "网络",
"check_for_updates": "自动检查更新",
"check_for_updates_description": "在 GitHub 上检查新版本,并在可用时在全局菜单中显示通知。"
"network_connections_title": "网络连接",
"check_for_updates": "自动检查更新"
},
"note_erasure_timeout": {
"note_erasure_timeout_title": "已删除的笔记",
"erase_notes_after": "在此时间后清理笔记",
"manual_erasing_description": "手动触发清理,忽略上述超时。",
"note_erasure_timeout_title": "笔记清理超时",
"note_erasure_description": "被删除的笔记(以及属性、修订历史等)最初仅被标记为“删除”,可以从“最近修改”对话框中恢复它们。经过一段时间后,已删除的笔记会被“清理”,这意味着它们的内容将无法恢复。此设置允许您配置从删除到清除笔记之间的时间长度。",
"erase_notes_after": "在此时间后删除笔记:",
"manual_erasing_description": "您还可以手动触发清理(不考虑上述定义的超时):",
"erase_deleted_notes_now": "立即清理已删除的笔记",
"deleted_notes_erased": "已删除的笔记已被清理。",
"description": "删除的笔记最初只是被标记为已删除,可以从“最近的笔记”中恢复。经过一段时间后,它们将被永久清理。",
"erase_notes_after_description": "删除的笔记被永久清理之前的时间。"
"deleted_notes_erased": "已删除的笔记已被清理。"
},
"revisions_snapshot_interval": {
"note_revisions_snapshot_interval_title": "笔记修订快照间隔",
@@ -1318,6 +1297,7 @@
"first-week-contains-first-thursday": "第一周包含一年的第一个周四",
"first-week-has-minimum-days": "第一周有最小天数",
"min-days-in-first-week": "第一周的最小天数",
"first-week-info": "第一周包含一年的第一个周四,基于 <a href=\"https://en.wikipedia.org/wiki/ISO_week_date#First_week\">ISO 8601</a> 标准。",
"first-week-warning": "更改第一周选项可能会导致与现有周笔记重复,已创建的周笔记将不会相应更新。",
"formatting-locale": "日期和数字格式",
"tuesday": "周二",
@@ -1340,8 +1320,7 @@
"date-and-time": "日期和时间",
"path": "路径",
"database_backed_up_to": "数据库已备份到 {{backupFilePath}}",
"no_backup_yet": "尚无备份",
"download": "下载"
"no_backup_yet": "尚无备份"
},
"etapi": {
"title": "ETAPI",
@@ -1439,28 +1418,28 @@
"spellcheck": {
"title": "拼写检查",
"description": "这些选项仅适用于桌面版本,浏览器将使用其原生的拼写检查功能。",
"enable": "拼写检查",
"language_code_label": "拼写检查语言",
"restart-required": "拼写检查选项的更改将在应用重启后生效。",
"custom_dictionary_title": "自定义词典",
"custom_dictionary_description": "添加到词典中的单词会在您的所有设备上同步。",
"custom_dictionary_edit": "自定义词",
"custom_dictionary_edit_description": "编辑拼写检查器不应标记的单词列表。更改将在重启后生效。",
"custom_dictionary_open": "编辑词典",
"related_description": "配置拼写检查语言和自定义词典。"
"enable": "启用拼写检查",
"language_code_label": "语言代码",
"language_code_placeholder": "例如 \"en-US\", \"de-AT\"",
"multiple_languages_info": "多种语言可以用逗号分隔,例如 \"en-US, de-DE, cs\"。 ",
"available_language_codes_label": "可用的语言代码:",
"restart-required": "拼写检查选项的更改将在应用重启后生效。"
},
"sync_2": {
"config_title": "同步配置",
"server_address": "服务器实例地址",
"timeout": "同步超时(单位:毫秒)",
"proxy_label": "同步代理服务器(可选)",
"note": "注意",
"note_description": "代理设置留空则使用系统代理(仅桌面客户端有效)。",
"special_value_description": "另一个特殊值是 <code>noproxy</code>,它强制忽略系统代理并遵守 <code>NODE_TLS_REJECT_UNAUTHORIZED</code>。",
"save": "保存",
"help": "帮助",
"test_title": "同步测试",
"test_description": "测试和同步服务器之间的连接。如果同步服务器没有初始化,会将本地文档同步到同步服务器上。",
"test_button": "测试同步",
"handshake_failed": "同步服务器握手失败,错误:{{message}}",
"timeout_description": "同步连接速度慢时,应该等待多久才放弃?如果网络不稳定,请增加等待时间。"
"timeout_unit": "毫秒"
},
"api_log": {
"close": "关闭"
@@ -1556,9 +1535,8 @@
"new-feature": "新建",
"collections": "集合",
"book": "集合",
"ai-chat": "AI对话",
"spreadsheet": "电子表格",
"llm-chat": "AI对话"
"ai-chat": "AI聊天",
"spreadsheet": "电子表格"
},
"protect_note": {
"toggle-on": "保护笔记",
@@ -1865,9 +1843,7 @@
}
},
"code-editor-options": {
"title": "编辑器",
"tab_width": "制表符宽度",
"tab_width_unit": "空格"
"title": "编辑器"
},
"custom_date_time_format": {
"title": "自定义日期/时间格式",
@@ -1885,7 +1861,7 @@
},
"content_language": {
"title": "内容语言",
"description": "在只读或可编辑文本笔记的“基本属性”部分,选择一种或多种语言,这些语言将显示在语言选择列表中。这将启用拼写检查从右到左的阅读支持和文本提取OCR功能。"
"description": "选择一种或多种语言出现在只读或可编辑文本注释的基本属性,这将支持拼写检查从右向左之类的功能。"
},
"switch_layout_button": {
"title_vertical": "将编辑面板移至底部",
@@ -2026,11 +2002,11 @@
},
"ui-performance": {
"title": "性能",
"enable-motion": "过渡和动画",
"enable-shadows": "阴影",
"enable-backdrop-effects": "菜单、弹窗和面板的背景效果",
"enable-smooth-scroll": "平滑滚动",
"app-restart-required": "需要重启应用"
"enable-motion": "启用过渡和动画",
"enable-shadows": "启用阴影",
"enable-backdrop-effects": "启用菜单、弹窗和面板的背景效果",
"enable-smooth-scroll": "启用平滑滚动",
"app-restart-required": "(需重启程序以应用更改)"
},
"pagination": {
"total_notes": "{{count}} 篇笔记",
@@ -2070,9 +2046,7 @@
"title": "实验选项",
"disclaimer": "这些选项处于实验阶段,可能导致系统不稳定。请谨慎使用。",
"new_layout_name": "新布局",
"new_layout_description": "尝试全新布局,呈现更现代的外观并提升易用性。后续版本将进行重大调整。",
"llm_name": "AI/大语言模型对话",
"llm_description": "启用由大语言模型驱动的 AI对话侧边栏和大语言模型对话笔记。"
"new_layout_description": "尝试全新布局,呈现更现代的外观并提升易用性。后续版本将进行重大调整。"
},
"tab_history_navigation_buttons": {
"go-back": "返回前一笔记",
@@ -2240,124 +2214,6 @@
"sample_xy": "散点图",
"sample_venn": "韦恩图",
"sample_ishikawa": "鱼骨图",
"placeholder": "输入你的美人鱼图的内容,或者使用下面的示例图之一。",
"sample_treeview": "树形视图",
"sample_wardley": "沃德利地图"
},
"llm_chat": {
"placeholder": "输入消息…",
"send": "发送",
"sending": "正在发送...",
"empty_state": "在下方输入消息,即可开始对话。",
"searching_web": "在网上搜索…",
"web_search": "联网搜索",
"sources": "来源",
"extended_thinking": "延伸思考",
"legacy_models": "传统模型",
"thinking": "正在思考...",
"thought_process": "思考过程",
"tool_calls": "{{count}} 次工具调用",
"input": "输入",
"result": "结果",
"error": "错误",
"tool_error": "失败",
"total_tokens": "{{total}} 个词元",
"tokens_detail": "{{prompt}} 提示词 + {{completion}} 补全",
"tokens_used": "{{prompt}} 提示词 + {{completion}} 补全 = {{total}} 个词元",
"tokens_used_with_cost": "{{prompt}} 提示词 + {{completion}} 补全 = {{total}} 个词元(约 ${{cost}}",
"tokens_used_with_model": "{{model}}: {{prompt}} 提示词 + {{completion}} 补全 = {{total}} 个词元",
"tokens_used_with_model_and_cost": "{{model}}: {{prompt}} 提示词 + {{completion}} 补全 = {{total}} 个词元(约 ${{cost}}",
"tokens": "词元",
"context_used": "{{percentage}}% 使用率",
"note_context_enabled": "点击即可禁用笔记上下文:{{title}}",
"note_context_disabled": "点击即可将当前注释添加到上下文中",
"no_provider_message": "未配置人工智能提供商。添加一个即可开始对话。",
"add_provider": "添加人工智能提供商",
"note_tools": "笔记访问",
"sources_summary": "来自 {{sites}} 个网站的 {{count}} 个来源"
},
"sidebar_chat": {
"title": "AI对话",
"launcher_title": "打开AI对话",
"new_chat": "开始新对话",
"save_chat": "将对话保存到笔记",
"empty_state": "开始对话",
"history": "对话历史",
"recent_chats": "最近对话",
"no_chats": "无历史对话"
},
"ocr": {
"extracted_text": "提取文本OCR",
"extracted_text_title": "提取文本OCR",
"loading_text": "正在加载OCR文本...",
"no_text_available": "暂无OCR文本",
"no_text_explanation": "该笔记未进行 OCR 文本提取处理,或未找到文本。",
"failed_to_load": "OCR文本加载失败",
"process_now": "处理 OCR",
"processing": "正在处理...",
"processing_started": "OCR识别已开始。请稍候片刻并刷新页面。",
"processing_failed": "OCR处理启动失败",
"view_extracted_text": "查看提取的文本OCR",
"processing_complete": "OCR识别处理完成。",
"text_filtered_low_confidence": "OCR 检测到文本,置信度为 {{confidence}}% ,但由于您的最小阈值为 {{threshold}}% ,因此该文本已被丢弃。",
"open_media_settings": "打开设置"
},
"mind-map": {
"addChild": "添加子节点",
"addParent": "添加父节点",
"addSibling": "添加同级节点",
"removeNode": "删除节点",
"focus": "专注模式",
"cancelFocus": "退出专注模式",
"moveUp": "上移",
"moveDown": "下移",
"link": "链接",
"linkBidirectional": "双向链接",
"clickTips": "请点击目标节点",
"summary": "总结"
},
"llm": {
"settings_description": "配置人工智能和大语言模型集成。",
"add_provider": "添加提供商",
"settings_title": "AI / LLM",
"feature_not_enabled": "在“设置”→“高级”→“实验性功能”中启用 LLM 实验性功能,即可使用 AI 集成。",
"add_provider_title": "添加AI供应商",
"configured_providers": "已配置的供应商",
"no_providers_configured": "尚未配置任何供应商。",
"provider_name": "名称",
"provider_type": "供应商"
},
"revisions": {
"note_revisions": "笔记修订",
"delete_all_revisions": "删除此笔记的所有修订版本",
"delete_all_button": "删除所有修订版本",
"help_title": "关于笔记修订的帮助",
"confirm_delete_all": "是否要删除此笔记的所有修订版本?",
"no_revisions": "这篇笔记目前还没有修改……",
"restore_button": "恢复",
"diff_on": "显示差异",
"diff_off": "显示内容",
"diff_on_hint": "点击显示笔记来源差异",
"diff_off_hint": "点击显示笔记内容",
"diff_not_available": "差异数据不可用。",
"confirm_restore": "是否恢复此版本?这将用此版本覆盖笔记的当前标题和内容。",
"delete_button": "删除",
"confirm_delete": "您要删除此修订吗?",
"revisions_deleted": "笔记修订已被删除。",
"revision_restored": "笔记修订已恢复。",
"revision_deleted": "笔记修订已删除。",
"snapshot_interval": "笔记修订快照间隔:{{seconds}}秒。",
"maximum_revisions": "笔记修订快照限制:{{number}}。",
"settings": "笔记修订设置",
"download_button": "下载",
"mime": "MIME 类型: ",
"file_size": "文件大小:",
"preview_not_available": "无法预览此类型的笔记。"
},
"database": {
"title": "数据库"
},
"auto_link_attribute_list": {
"title": "系统属性"
"placeholder": "输入你的美人鱼图的内容,或者使用下面的示例图之一。"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,12 @@
{
"about": {
"title": "Über Trilium Notes",
"homepage": "Startseite:",
"app_version": "App-Version:",
"db_version": "DB-Version:",
"sync_version": "Sync-Version:",
"build_date": "Build-Datum:",
"build_revision": "Build-Revision:",
"data_directory": "Datenverzeichnis:"
},
"toast": {
@@ -81,22 +88,17 @@
"also_delete_note": "Auch die Notiz löschen"
},
"delete_notes": {
"delete_notes_preview": "Vorschau der Notizen löschen",
"close": "Schließen",
"delete_all_clones_description": "auch alle Klone löschen (kann bei letzte Änderungen rückgängig gemacht werden)",
"erase_notes_description": "Beim normalen (vorläufigen) Löschen werden die Notizen nur als gelöscht markiert und sie können innerhalb eines bestimmten Zeitraums (im Dialogfeld „Letzte Änderungen“) wiederhergestellt werden. Wenn du diese Option aktivierst, werden die Notizen sofort gelöscht und es ist nicht möglich, die Notizen wiederherzustellen.",
"erase_notes_warning": "Notizen dauerhaft löschen (kann nicht rückgängig gemacht werden), einschließlich aller Klone. Dadurch wird ein Neuladen der Anwendung erzwungen.",
"notes_to_be_deleted": "({{notesCount}}) Notizen werden gelöscht",
"notes_to_be_deleted": "Folgende Notizen werden gelöscht ({{notesCount}})",
"no_note_to_delete": "Es werden keine Notizen gelöscht (nur Klone).",
"broken_relations_to_be_deleted": "Unterbrochene Beziehungen ({{relationCount}})",
"broken_relations_to_be_deleted": "Folgende Beziehungen werden gelöst und gelöscht ({{ relationCount}})",
"cancel": "Abbrechen",
"title": "Notizen löschen",
"clones_label": "Duplikate",
"delete_clones_description_one": "Löscht {{count}} weiteres Duplikat. Kann unter „Letzte Änderungen“ rückgängig gemacht werden.",
"delete_clones_description_other": "Löscht {{count}} weitere Duplikate. Kann unter „Letzte Änderungen“ rückgängig gemacht werden.",
"erase_notes_label": "Permanent löschen",
"table_note_with_relation": "Notiz mit Beziehung",
"table_relation": "Beziehung",
"delete": "Löschen"
"ok": "OK",
"deleted_relation_text": "Notiz {{- note}} (soll gelöscht werden) wird von Beziehung {{- relation}} ausgehend von {{- source}} referenziert."
},
"export": {
"export_note_title": "Notiz exportieren",
@@ -268,6 +270,33 @@
"undelete_link": "Wiederherstellen",
"confirm_undelete": "Möchten Sie diese Notiz und ihre Unternotizen wiederherstellen?"
},
"revisions": {
"note_revisions": "Notizrevisionen",
"delete_all_revisions": "Lösche alle Revisionen dieser Notiz",
"delete_all_button": "Alle Revisionen löschen",
"help_title": "Hilfe zu Notizrevisionen",
"confirm_delete_all": "Möchtest du alle Revisionen dieser Notiz löschen?",
"no_revisions": "Für diese Notiz gibt es noch keine Revisionen...",
"confirm_restore": "Möchtest du diese Revision wiederherstellen? Dadurch werden der aktuelle Titel und Inhalt der Notiz mit dieser Revision überschrieben.",
"confirm_delete": "Möchtest du diese Revision löschen?",
"revisions_deleted": "Notizrevisionen wurden gelöscht.",
"revision_restored": "Die Notizrevision wurde wiederhergestellt.",
"revision_deleted": "Notizrevision wurde gelöscht.",
"snapshot_interval": "Notizrevisionen-Snapshot Intervall: {{seconds}}s.",
"maximum_revisions": "Maximale Revisionen für aktuelle Notiz: {{number}}.",
"settings": "Einstellungen für Notizrevisionen",
"download_button": "Herunterladen",
"mime": "MIME: ",
"file_size": "Dateigröße:",
"preview_not_available": "Für diesen Notiztyp ist keine Vorschau verfügbar.",
"restore_button": "Wiederherstellen",
"delete_button": "Löschen",
"diff_on": "Zeige Differenz",
"diff_off": "Zeige Inhalt",
"diff_on_hint": "Klicke, um die Differenz des Notiz-Quellcodes zu zeigen",
"diff_off_hint": "Klicke, um den Notizinhalt zu zeigen",
"diff_not_available": "Differenz-Abgleich ist nicht verfügbar."
},
"sort_child_notes": {
"sort_children_by": "Unternotizen sortieren nach...",
"sorting_criteria": "Sortierkriterien",
@@ -339,7 +368,7 @@
"calendar_root": "Markiert eine Notiz, die als Basis für Tagesnotizen verwendet werden soll. Nur einer sollte als solcher gekennzeichnet sein.",
"archived": "Notizen mit dieser Bezeichnung werden standardmäßig nicht in den Suchergebnissen angezeigt (auch nicht in den Dialogen „Springen zu“, „Link hinzufügen“ usw.).",
"exclude_from_export": "Notizen (mit ihrem Unterbaum) werden nicht im Notizexport inkludiert",
"run": "Definiert bei welchen Ereignissen das Skript ausgeführt werden soll. Mögliche Werte sind:\n<ul>\n<li>frontendStartup - wenn das Trilium-Frontend startet (oder aktualisiert wird), außer auf mobilen Geräten.</li>\n<li>mobileStartup - wenn das Trilium-Frontend auf einem mobilen Gerät startet (oder aktualisiert wird).</li>\n<li>backendStartup - wenn das Trilium-Backend startet.</li>\n<li>hourly - einmal pro Stunde ausführen. Du kannst das zusätzliche Label <code>runAtHour</code> verwenden, um die genaue Stunde festzulegen.</li>\n<li>daily - einmal pro Tag ausführen</li>\n</ul>",
"run": "Definiert, bei welchen Ereignissen das Skript ausgeführt werden soll. Mögliche Werte sind:\n<ul>\n<li>frontendStartup - wenn das Trilium-Frontend startet (oder aktualisiert wird), außer auf mobilen Geräten.</li>\n<li>mobileStartup - wenn das Trilium-Frontend auf einem mobilen Gerät startet (oder aktualisiert wird).</li>\n<li>backendStartup - wenn das Trilium-Backend startet</li>\n<li>hourly - einmal pro Stunde ausführen. Du kannst das zusätzliche Label <code>runAtHour</code> verwenden, um die genaue Stunde festzulegen.</li>\n<li>daily - einmal pro Tag ausführen</li>\n</ul>",
"run_on_instance": "Definiere, auf welcher Trilium-Instanz dies ausgeführt werden soll. Standardmäßig alle Instanzen.",
"run_at_hour": "Zu welcher Stunde soll das laufen? Sollte zusammen mit <code>#runu003dhourly</code> verwendet werden. Kann für mehr Läufe im Laufe des Tages mehrfach definiert werden.",
"disable_inclusion": "Skripte mit dieser Bezeichnung werden nicht in die Ausführung des übergeordneten Skripts einbezogen.",
@@ -677,8 +706,7 @@
"export_as_image_svg": "SVG (Vektor)",
"note_map": "Notizen Karte",
"view_revisions": "Notizrevisionen...",
"advanced": "Erweitert",
"view_ocr_text": "OCR text anzeigen"
"advanced": "Erweitert"
},
"onclick_button": {
"no_click_handler": "Das Schaltflächen-Widget „{{componentId}}“ hat keinen definierten Klick-Handler"
@@ -774,10 +802,7 @@
"expand_tooltip": "Erweitert die direkten Unterelemente dieser Sammlung (eine Ebene tiefer). Für weitere Optionen auf den Pfeil rechts klicken.",
"expand_first_level": "Direkte Unterelemente erweitern",
"expand_nth_level": "{{depth}} Ebenen erweitern",
"hide_child_notes": "Untergeordnete Notizen im Baum ausblenden",
"open_all_in_tabs": "Alle öffnen",
"open_all_in_tabs_tooltip": "Alle Resultate in neuen Tabs öffnen",
"open_all_confirm": "Dies öffnet {{count}} Notizen in neuen Tabs. Fortfahren?"
"hide_child_notes": "Untergeordnete Notizen im Baum ausblenden"
},
"edited_notes": {
"no_edited_notes_found": "An diesem Tag wurden noch keine Notizen bearbeitet...",
@@ -831,8 +856,7 @@
"collapse": "Auf normale Größe reduzieren",
"title": "Notizkarte",
"fix-nodes": "Knoten fixieren",
"link-distance": "Verbindungslänge",
"too-many-notes": "Dieser Unterbaum enthält {{count}} Notizen, welche die Limitierung von {{max}} überschreitet die in der Notizkarte dargestellt werden können."
"link-distance": "Verbindungslänge"
},
"note_paths": {
"title": "Notizpfade",
@@ -880,7 +904,7 @@
"limit": "Limit",
"limit_description": "Begrenze die Anzahl der Ergebnisse",
"debug": "debuggen",
"debug_description": "Debug gibt zusätzliche Protokolleinträge in der Konsole aus, um die Fehlerdiagnose komplexer Abfragen zu erleichtern",
"debug_description": "Debug gibt zusätzliche Debuginformationen in die Konsole aus, um das Debuggen komplexer Abfragen zu erleichtern",
"action": "Aktion",
"search_button": "Suchen",
"search_execute": "Aktionen suchen und ausführen",
@@ -912,7 +936,7 @@
},
"debug": {
"debug": "Debuggen",
"debug_info": "Debug gibt zusätzliche Protokolleinträge in der Konsole aus, um die Fehlerdiagnose komplexer Abfragen zu erleichtern.",
"debug_info": "Debug gibt zusätzliche Debuginformationen in die Konsole aus, um das Debuggen komplexer Abfragen zu erleichtern.",
"access_info": "Um auf die Debug-Informationen zuzugreifen, führe die Abfrage aus und klicke oben links auf \"Backend-Log anzeigen\"."
},
"fast_search": {
@@ -1037,8 +1061,7 @@
"note_already_in_diagram": "Die Notiz \"{{title}}\" ist schon im Diagram.",
"enter_title_of_new_note": "Gebe den Titel der neuen Notiz ein",
"default_new_note_title": "neue Notiz",
"click_on_canvas_to_place_new_note": "Klicke auf den Canvas, um eine neue Notiz zu platzieren",
"rename_relation": "Beziehung umbenennen"
"click_on_canvas_to_place_new_note": "Klicke auf den Canvas, um eine neue Notiz zu platzieren"
},
"backend_log": {
"refresh": "Aktualisieren"
@@ -1047,17 +1070,15 @@
"title": "Konsistenzprüfungen",
"find_and_fix_button": "Finde und behebe die Konsistenzprobleme",
"finding_and_fixing_message": "Konsistenzprobleme finden und beheben...",
"issues_fixed_message": "Konsistenzprobleme sollten behoben werden.",
"find_and_fix_label": "Finde und behebe Konsistenzprobleme",
"find_and_fix_description": "Suche nach Probleme mit der Datenkonsistenz in der Datenbank und repariere diese automatisch."
"issues_fixed_message": "Konsistenzprobleme sollten behoben werden."
},
"database_anonymization": {
"title": "Datenbankanonymisierung",
"full_anonymization": "Vollständige Anonymisierung",
"full_anonymization_description": "Erstellt eine Kopie der Datenbank, wo der Inhalt der Notizen entfernt wurde. Es vergleicht lediglich die Struktur sowie nicht-kritische Metadaten. Sicher zum teilen für die Fehlerdiagnose.",
"full_anonymization_description": "Durch diese Aktion wird eine neue Kopie der Datenbank erstellt und anonymisiert (der gesamte Notizinhalt wird entfernt und nur die Struktur und einige nicht vertrauliche Metadaten bleiben übrig), sodass sie zu Debugging-Zwecken online geteilt werden kann, ohne befürchten zu müssen, dass Ihre persönlichen Daten verloren gehen.",
"save_fully_anonymized_database": "Speichere eine vollständig anonymisierte Datenbank",
"light_anonymization": "Leichte Anonymisierung",
"light_anonymization_description": "Erstellt eine Kopie wo der Inhalt der Notiz entfernt ist, jedoch Title, Attribute und benutzerdefinierte Skripte erhalten bleiben. Hilfreich zur Fehlerdiagnose.",
"light_anonymization_description": "Durch diese Aktion wird eine neue Kopie der Datenbank erstellt und eine leichte Anonymisierung vorgenommen insbesondere wird nur der Inhalt aller Notizen entfernt, Titel und Attribute bleiben jedoch erhalten. Darüber hinaus bleiben benutzerdefinierte JS-Frontend-/Backend-Skriptnotizen und benutzerdefinierte Widgets erhalten. Dies bietet mehr Kontext zum Debuggen der Probleme.",
"choose_anonymization": "Du kannst selbst entscheiden, ob du eine vollständig oder leicht anonymisierte Datenbank bereitstellen möchten. Selbst eine vollständig anonymisierte Datenbank ist sehr nützlich. In einigen Fällen kann jedoch eine leicht anonymisierte Datenbank den Prozess der Fehlererkennung und -behebung beschleunigen.",
"save_lightly_anonymized_database": "Speichere eine leicht anonymisierte Datenbank",
"existing_anonymized_databases": "Vorhandene anonymisierte Datenbanken",
@@ -1066,17 +1087,15 @@
"error_creating_anonymized_database": "Die anonymisierte Datenbank konnte nicht erstellt werden. Überprüfe die Backend-Protokolle auf Details",
"successfully_created_fully_anonymized_database": "Vollständig anonymisierte Datenbank in {{anonymizedFilePath}} erstellt",
"successfully_created_lightly_anonymized_database": "Leicht anonymisierte Datenbank in {{anonymizedFilePath}} erstellt",
"no_anonymized_database_yet": "Noch keine anonymisierte Datenbank.",
"description": "Erstellt eine anonymisierte Kopie deiner Datenbank für den Austausch mit Entwicklern zur Fehlerdiagnose, ohne persönliche Daten preiszugeben."
"no_anonymized_database_yet": "Noch keine anonymisierte Datenbank."
},
"database_integrity_check": {
"title": "Datenbankintegritätsprüfung",
"description": "Dadurch wird überprüft, ob die Datenbank auf SQLite-Ebene nicht beschädigt ist. Abhängig von der DB-Größe kann es einige Zeit dauern.",
"check_button": "Überprüfe die Datenbankintegrität",
"checking_integrity": "Datenbankintegrität prüfen...",
"integrity_check_succeeded": "Integritätsprüfung erfolgreich keine Probleme gefunden.",
"integrity_check_failed": "Integritätsprüfung fehlgeschlagen: {{results}}",
"check_integrity_label": "Überprüfe die Datenbankintegrität",
"check_integrity_description": "Überprüfe die Datenbank auf SQLite Ebene auf Fehler."
"integrity_check_failed": "Integritätsprüfung fehlgeschlagen: {{results}}"
},
"sync": {
"title": "Synchronisieren",
@@ -1086,18 +1105,14 @@
"filling_entity_changes": "Entitätsänderungszeilen werden gefüllt...",
"sync_rows_filled_successfully": "Synchronisierungszeilen erfolgreich gefüllt",
"finished-successfully": "Synchronisierung erfolgreich beendet.",
"failed": "Synchronisierung fehlgeschlagen: {{message}}",
"force_full_sync_label": "Vollständige Synchronisierung erzwingen",
"force_full_sync_description": "Vollständige Synchronisation mit dem Sync-Server auslösen, lädt alle Änderungen erneut hoch."
"failed": "Synchronisierung fehlgeschlagen: {{message}}"
},
"vacuum_database": {
"title": "Datenbank aufräumen",
"description": "Dadurch wird die Datenbank neu erstellt, was normalerweise zu einer kleineren Datenbankdatei führt. Es werden keine Daten tatsächlich geändert.",
"button_text": "Datenbank aufräumen",
"vacuuming_database": "Datenbank wird geleert...",
"database_vacuumed": "Die Datenbank wurde geleert",
"vacuum_label": "Datenbank bereinigen",
"vacuum_description": "Datenbank neu aufbauen um die Dateigröße zu reduzieren. Keine Daten werden verändert."
"database_vacuumed": "Die Datenbank wurde geleert"
},
"fonts": {
"theme_defined": "Thema definiert",
@@ -1108,7 +1123,10 @@
"note_tree_font": "Notizbaum-Schriftart",
"note_detail_font": "Notiz-Detail-Schriftart",
"monospace_font": "Minivan (Code) Schriftart",
"not_all_fonts_available": "Möglicherweise sind nicht alle aufgelisteten Schriftarten auf Ihrem System verfügbar",
"note_tree_and_detail_font_sizing": "Beachte, dass die Größe der Baum- und Detailschriftarten relativ zur Hauptschriftgrößeneinstellung ist.",
"not_all_fonts_available": "Möglicherweise sind nicht alle aufgelisteten Schriftarten auf Ihrem System verfügbar.",
"apply_font_changes": "Um Schriftartänderungen zu übernehmen, klicke auf",
"reload_frontend": "Frontend neu laden",
"generic-fonts": "Generische Schriftarten",
"sans-serif-system-fonts": "Sans-serif Systemschriftarten",
"serif-system-fonts": "Serif Systemschriftarten",
@@ -1117,9 +1135,7 @@
"serif": "Serif",
"sans-serif": "Sans Serif",
"monospace": "Monospace",
"system-default": "System Standard",
"custom_fonts": "Benutzerdefinierte Schriftarten verwenden",
"preview": "Vorschau"
"system-default": "System Standard"
},
"max_content_width": {
"title": "Inhaltsbreite",
@@ -1152,10 +1168,7 @@
"layout-horizontal-description": "Startleiste ist unter der Tableiste. Die Tableiste wird dadurch auf die ganze Breite erweitert.",
"auto_theme": "Alt (Folge dem Farbschema des Systems)",
"light_theme": "Alt (Hell)",
"dark_theme": "Alt (Dunkel)",
"modern_themes": "Modern",
"legacy_themes": "Veraltet",
"custom_themes": "Benutzerdefiniert"
"dark_theme": "Alt (Dunkel)"
},
"zoom_factor": {
"title": "Zoomfaktor (nur Desktop-Build)",
@@ -1183,25 +1196,18 @@
},
"images": {
"images_section_title": "Bilder",
"download_images_automatically": "Bilder automatisch herunterladen",
"download_images_description": "Referenzierte online Bilder vom eingefügten HTML-Code herunterladen, damit diese offline zur Verfügung stehen.",
"enable_image_compression": "Bildkomprimierung",
"max_image_dimensions": "Maximale Maße eines Bildes",
"jpeg_quality_description": "Empfohlener Bereich liegt zwischen 50-85. Niedrige Werte reduzieren die Dateigröße, höhere Werte bewahren Details.",
"max_image_dimensions_unit": "Pixel",
"enable_image_compression_description": "Komprimieren und skalieren Bilder, wenn diese hochgeladen oder eingefügt werden.",
"max_image_dimensions_description": "Bilder, die diese Größe überschreiten, werden automatisch verkleinert.",
"jpeg_quality": "JPEG Qualität",
"ocr_section_title": "Text Extrahierung (OCR)",
"ocr_related_content_languages": "Inhaltssprachen (für Textextrahierung verwendet)",
"ocr_auto_process": "Neue Dateien automatisch verarbeiten",
"ocr_auto_process_description": "Text automatisch extrahieren von neu hochgeladenen oder eingefügten Dateien.",
"ocr_min_confidence": "Minimale Sicherheit"
"download_images_automatically": "Lade Bilder automatisch herunter, um sie offline zu verwenden.",
"download_images_description": "Eingefügter HTML-Code kann Verweise auf Online-Bilder enthalten. Trilium findet diese Verweise und lädt die Bilder herunter, sodass sie offline verfügbar sind.",
"enable_image_compression": "Bildkomprimierung aktivieren",
"max_image_dimensions": "Maximale Breite/Höhe eines Bildes in Pixel (die Größe des Bildes wird geändert, wenn es diese Einstellung überschreitet).",
"jpeg_quality_description": "JPEG-Qualität (10 schlechteste Qualität, 100 beste Qualität, 50 85 wird empfohlen)",
"max_image_dimensions_unit": "Pixel"
},
"attachment_erasure_timeout": {
"attachment_erasure_timeout": "Zeitüberschreitung beim Löschen von Anhängen",
"erase_attachments_after": "Nicht verwendete Anhänge löschen nach",
"manual_erasing_description": "Manuelle Löschung aktivieren, ignoriert den obigen Zeitpunkt.",
"attachment_auto_deletion_description": "Anhänge werden automatisch gelöscht (und gelöscht), wenn sie nach einer definierten Zeitspanne nicht mehr in ihrer Notiz referenziert werden.",
"erase_attachments_after": "Nicht verwendete Anhänge löschen nach:",
"manual_erasing_description": "Du kannst das Löschen auch manuell auslösen (ohne Berücksichtigung des oben definierten Timeouts):",
"erase_unused_attachments_now": "Lösche jetzt nicht verwendete Anhangnotizen",
"unused_attachments_erased": "Nicht verwendete Anhänge wurden gelöscht."
},
@@ -1211,29 +1217,28 @@
},
"note_erasure_timeout": {
"note_erasure_timeout_title": "Beachte das Zeitlimit für die Löschung",
"erase_notes_after": "Notizen löschen nach",
"manual_erasing_description": "Manuelle Löschung aktivieren, ignoriert den obigen Zeitpunkt.",
"note_erasure_description": "Gelöschte Notizen (und Attribute, Notizrevisionen...) werden zunächst nur als gelöscht markiert und können über den Dialog „Zuletzt verwendete Notizen” wiederhergestellt werden. Nach einer bestimmten Zeit werden gelöschte Notizen „gelöscht”, was bedeutet, dass ihr Inhalt nicht mehr wiederhergestellt werden kann. Mit dieser Einstellung können Sie die Zeitspanne zwischen dem Löschen und dem endgültigen Löschen der Notiz festlegen.",
"erase_notes_after": "Notizen löschen nach:",
"manual_erasing_description": "Du kannst das Löschen auch manuell auslösen (ohne Berücksichtigung des oben definierten Timeouts):",
"erase_deleted_notes_now": "Jetzt gelöschte Notizen löschen",
"deleted_notes_erased": "Gelöschte Notizen wurden gelöscht."
},
"revisions_snapshot_interval": {
"note_revisions_snapshot_interval_title": "Snapshot-Intervall für Notizrevisionen",
"note_revisions_snapshot_description": "Das Snapshot-Zeitintervall für Notizrevisionen ist die Zeit, nach der eine neue Notizrevision erstellt wird. Weitere Informationen findest du im <doc>Wiki</doc>.",
"snapshot_time_interval_label": "Snapshot Intervall"
"snapshot_time_interval_label": "Zeitintervall für Notiz-Revisions-Snapshot:"
},
"revisions_snapshot_limit": {
"note_revisions_snapshot_limit_title": "Limit für Notizrevision-Snapshots",
"note_revisions_snapshot_limit_description": "Das Limit für Notizrevision-Snapshots bezieht sich auf die maximale Anzahl von Revisionen, die für jede Notiz gespeichert werden können. Dabei bedeutet -1, dass es kein Limit gibt, und 0 bedeutet, dass alle Revisionen gelöscht werden. Du kannst das maximale Limit für Revisionen einer einzelnen Notiz über das Label #versioningLimit festlegen.",
"snapshot_number_limit_label": "Maximale Revisionen",
"snapshot_number_limit_label": "Limit der Notizrevision-Snapshots:",
"erase_excess_revision_snapshots": "Überschüssige Revision-Snapshots jetzt löschen",
"erase_excess_revision_snapshots_prompt": "Überschüssige Revision-Snapshots wurden gelöscht.",
"snapshot_number_limit_unit": "Momentaufnahmen",
"note_revisions_snapshot_limit_description_short": "Maximale Anzahl an Revisionen pro Notiz. Nutze -1 für unlimitiert oder 0 zum deaktivieren.",
"erase_excess_revision_snapshots_description": "Lösche Revisionen aller Notizen die das Limit überschreiten."
"snapshot_number_limit_unit": "Momentaufnahmen"
},
"search_engine": {
"title": "Suchmaschine",
"custom_search_engine_info": "Wird verwendet bei der Suche im Web für den ausgewählten Text. Wenn nicht konfiguriert, wird DuckDuckGo verwendet.",
"custom_search_engine_info": "Für eine benutzerdefinierte Suchmaschine müssen sowohl ein Name als auch eine URL festgelegt werden. Wenn keine dieser Optionen festgelegt ist, wird DuckDuckGo als Standardsuchmaschine verwendet.",
"predefined_templates_label": "Vordefinierte Suchmaschinenvorlagen",
"bing": "Bing",
"baidu": "Baidu",
@@ -1291,6 +1296,7 @@
"first-week-contains-first-thursday": "Erste Woche enthält den ersten Donnerstag des Jahres",
"first-week-has-minimum-days": "Erste Woche hat Mindestanzahl an Tagen",
"min-days-in-first-week": "Mindestanzahl an Tagen in erster Woche",
"first-week-info": "Die erste Woche, die den ersten Donnerstag des Jahres enthält, basiert auf dem Standard <a href=\"https://en.wikipedia.org/wiki/ISO_week_date#First_week\">ISO 8601</a>.",
"first-week-warning": "Das Ändern der Optionen für die erste Woche kann zu Duplikaten mit bestehenden Wochen-Notizen führen. Bestehende Wochen-Notizen werden nicht entsprechend aktualisiert.",
"formatting-locale": "Datums- und Zahlenformat",
"tuesday": "Dienstag",
@@ -1313,9 +1319,7 @@
"date-and-time": "Datum & Uhrzeit",
"path": "Pfad",
"database_backed_up_to": "Die Datenbank wurde gesichert unter {{backupFilePath}}",
"no_backup_yet": "noch kein Backup",
"title": "Sicherung",
"download": "Download"
"no_backup_yet": "noch kein Backup"
},
"etapi": {
"title": "ETAPI",
@@ -1354,17 +1358,14 @@
"protected_session_timeout_description": "Das Zeitlimit für geschützte Sitzungen ist ein Zeitraum, nach dem die geschützte Sitzung aus dem Speicher des Browsers gelöscht wird. Dies wird ab der letzten Interaktion mit geschützten Notizen gemessen. Sehen",
"wiki": "Wiki",
"for_more_info": "für weitere Informationen.",
"protected_session_timeout_label": "Automatisches beenden der Sitzung nach",
"protected_session_timeout_label": "Zeitüberschreitung der geschützten Sitzung:",
"reset_confirmation": "Durch das Zurücksetzen des Passworts verlierst du für immer den Zugriff auf alle Ihre bestehenden geschützten Notizen. Möchtest du das Passwort wirklich zurücksetzen?",
"reset_success_message": "Das Passwort wurde zurückgesetzt. Bitte lege ein neues Passwort fest",
"change_password_heading": "Kennwort ändern",
"set_password_heading": "Passwort festlegen",
"set_password": "Passwort festlegen",
"password_mismatch": "Neue Passwörter sind nicht dasselbe.",
"password_changed_success": "Das Passwort wurde geändert. Trilium wird neu geladen, nachdem du auf OK geklickt hast.",
"change_password_description": "Aktualisiere dein aktuelles Passwort",
"reset_password": "Passwort zurücksetzen",
"cancel": "Abbrechen"
"password_changed_success": "Das Passwort wurde geändert. Trilium wird neu geladen, nachdem du auf OK geklickt hast."
},
"shortcuts": {
"keyboard_shortcuts": "Tastaturkürzel",
@@ -1385,22 +1386,26 @@
"description": "Diese Optionen gelten nur für Desktop-Builds. Browser verwenden ihre eigene native Rechtschreibprüfung.",
"enable": "Aktiviere die Rechtschreibprüfung",
"language_code_label": "Sprachcode(s)",
"restart-required": "Änderungen an den Rechtschreibprüfungsoptionen werden nach dem Neustart der Anwendung wirksam.",
"custom_dictionary_title": "Benutzerdefiniertes Wörterbuch",
"custom_dictionary_edit": "Benutzerdefiniert Wörter",
"custom_dictionary_open": "Wörterbuch bearbeiten"
"language_code_placeholder": "zum Beispiel \"en-US\", \"de-AT\"",
"multiple_languages_info": "Mehrere Sprachen können mit einem Komma getrennt werden z.B. \"en-US, de-DE, cs\". ",
"available_language_codes_label": "Verfügbare Sprachcodes:",
"restart-required": "Änderungen an den Rechtschreibprüfungsoptionen werden nach dem Neustart der Anwendung wirksam."
},
"sync_2": {
"config_title": "Synchronisierungskonfiguration",
"server_address": "Adresse der Serverinstanz",
"timeout": "Synchronisierungs-Timeout (Millisekunden)",
"proxy_label": "Proxyserver synchronisieren (optional)",
"note": "Notiz",
"note_description": "Wenn du die Proxy-Einstellung leer lässt, wird der System-Proxy verwendet (gilt nur für Desktop-/Electron-Build).",
"special_value_description": "Ein weiterer besonderer Wert ist <code>noproxy</code>, der das Ignorieren sogar des System-Proxys erzwingt und <code>NODE_TLS_REJECT_UNAUTHORIZED</code> respektiert.",
"save": "Speichern",
"help": "Helfen",
"test_title": "Synchronisierungstest",
"test_description": "Dadurch werden die Verbindung und der Handshake zum Synchronisierungsserver getestet. Wenn der Synchronisierungsserver nicht initialisiert ist, wird er dadurch für die Synchronisierung mit dem lokalen Dokument eingerichtet.",
"test_button": "Teste die Synchronisierung",
"handshake_failed": "Handshake des Synchronisierungsservers fehlgeschlagen, Fehler: {{message}}"
"handshake_failed": "Handshake des Synchronisierungsservers fehlgeschlagen, Fehler: {{message}}",
"timeout_unit": "Millisekunden"
},
"api_log": {
"close": "Schließen"
@@ -1497,8 +1502,7 @@
"task-list": "Aufgabenliste",
"new-feature": "Neu",
"collections": "Sammlungen",
"spreadsheet": "Tabelle",
"llm-chat": "KI-Chat"
"spreadsheet": "Tabelle"
},
"protect_note": {
"toggle-on": "Notiz schützen",
@@ -1700,8 +1704,7 @@
"theme_none": "Keine Syntaxhervorhebung",
"theme_group_light": "Helle Themen",
"theme_group_dark": "Dunkle Themen",
"copy_title": "Kopiere in Zwischenablage",
"click_to_copy": "Klicke zum kopieren"
"copy_title": "Kopiere in Zwischenablage"
},
"classic_editor_toolbar": {
"title": "Format"
@@ -1720,7 +1723,7 @@
"title": "Fixiert",
"description": "Werkzeuge erscheinen im \"Format\" Tab."
},
"multiline-toolbar": "Toolbar wenn nötig in mehreren Zeilen darstellen"
"multiline-toolbar": "Toolbar wenn nötig in mehreren Zeilen darstellen."
}
},
"electron_context_menu": {
@@ -1811,9 +1814,8 @@
"custom_date_time_format": {
"title": "Benutzerdefiniertes Datums-/Zeitformat",
"description": "Passe das Format des Datums und der Uhrzeit an, die über <shortcut /> oder die Symbolleiste eingefügt werden. Die verfügbaren Format-Tokens sind unter <doc>Day.js docs</doc> zu finden.",
"format_string": "Format Zeichenfolge",
"formatted_time": "Formatiertes Datum/Uhrzeit",
"preview": "Vorschau: {{preview}}"
"format_string": "Format Zeichenfolge:",
"formatted_time": "Formatiertes Datum/Uhrzeit:"
},
"multi_factor_authentication": {
"title": "Multi-Faktor-Authentifizierung",
@@ -2044,9 +2046,7 @@
"title": "Experimentelle Optionen",
"disclaimer": "Diese Optionen sind experimentell und können Instabilitäten verursachen. Achtsam zu verwenden.",
"new_layout_name": "Neues Layout",
"new_layout_description": "Probiere das neue Layout für eine modernere Darstellung und verbesserte Benutzbarkeit aus. Kann sich in Zukunft stark ändern.",
"llm_name": "KI- / LLM-Chat",
"llm_description": "Aktiviert die KI-Chat Seitenleiste sowie LLM-Chat Notizen die von einem Sprachmodell betrieben werden."
"new_layout_description": "Probiere das neue Layout für eine modernere Darstellung und verbesserte Benutzbarkeit aus. Kann sich in Zukunft stark ändern."
},
"server": {
"unknown_http_error_title": "Kommunikationsfehler mit dem Server",
@@ -2230,79 +2230,5 @@
"sample_xy": "XY",
"sample_venn": "Mengen",
"sample_ishikawa": "Ursache-Wirkung"
},
"database": {
"title": "Datenbank"
},
"search": {
"title": "Suche",
"fuzzy_matching_label": "Fehlertoleranz bei Suche"
},
"text_editor": {
"title": "Editor"
},
"llm_chat": {
"placeholder": "Schreibe eine Nachricht...",
"send": "Senden",
"sending": "Sende...",
"empty_state": "Starte eine Unterhaltung indem du unten eine Nachricht schreibst.",
"searching_web": "Suche im Web...",
"web_search": "Websuche",
"note_tools": "Notizzugriff",
"sources": "Quellen",
"legacy_models": "Veraltete Modelle",
"thinking": "Denke...",
"thought_process": "Denkprozess",
"result": "Ergebnis",
"error": "Fehler",
"tool_error": "Fehlgeschlagen",
"total_tokens": "{{total}} Token",
"tokens": "Token",
"context_used": "{{percentage}}% verwendet",
"no_provider_message": "Kein KI-Anbieter konfiguriert. Fügen einen hinzu, um zu chatten.",
"add_provider": "KI-Anbieter hinzufügen",
"stop": "Stopp"
},
"sidebar_chat": {
"title": "KI-Chat",
"launcher_title": "KI-Chat öffnen",
"new_chat": "Neuen Chat starten",
"save_chat": "Chat in Notiz sichern",
"empty_state": "Starte Unterhaltung",
"history": "Chathistorie",
"recent_chats": "Kürzliche Chats",
"no_chats": "Keine vorherigen Chats"
},
"llm": {
"tools": {
"clone_note": "Notiz duplizieren",
"move_note": "Notiz verschieben",
"delete_note": "Notiz löschen",
"rename_note": "Notiz umbenennen",
"web_search": "Websuche",
"create_note": "Notiz erstellen",
"append_to_note": "An Notiz anhängen",
"get_note": "Notiz beziehen",
"search_notes": "Notizen durchsuchen"
},
"cancel": "Abbrechen",
"api_key": "API Schlüssel",
"delete_provider": "Löschen",
"actions": "Aktionen",
"provider_type": "Anbieter",
"provider_name": "Name",
"settings_title": "KI / LLM"
},
"mind-map": {
"summary": "Zusammenfassung",
"link": "Link"
},
"ocr": {
"view_extracted_text": "Extrahierten Text anzeigen (OCR)",
"open_media_settings": "Öffne Einstellungen",
"processing": "Verarbeite...",
"loading_text": "Lädt OCR Text...",
"extracted_text_title": "Extrahierter Text (OCR)",
"extracted_text": "Extrahierter Text (OCR)"
}
}

View File

@@ -1,5 +1,12 @@
{
"about": {
"title": "Σχετικά με το Trilium Notes",
"homepage": "Αρχική Σελίδα:",
"app_version": "Έκδοση εφαρμογής:",
"db_version": "Έκδοση βάσης δεδομένων:",
"sync_version": "Έκδοση πρωτοκόλου συγχρονισμού:",
"build_date": "Ημερομηνία χτισίματος εφαρμογής:",
"build_revision": "Αριθμός αναθεώρησης χτισίματος:",
"data_directory": "Φάκελος δεδομένων:"
},
"toast": {

View File

@@ -66,172 +66,6 @@
"set-custom-color": "Set custom note colour"
},
"about": {
"data_directory": "Data directory:"
},
"toast": {
"critical-error": {
"title": "Critical error",
"message": "A critical error has occurred which prevents the client application from starting:\n\n{{message}}\n\nThis is most likely caused by a script failing in an unexpected way. Try starting the application in safe mode and addressing the issue."
},
"widget-error": {
"title": "Failed to initialise a widget",
"message-custom": "Custom widget from note with ID \"{{id}}\", titled \"{{title}}\" could not be initialised due to:\n\n{{message}}",
"message-unknown": "Unknown widget could not be initialised due to:\n\n{{message}}"
},
"bundle-error": {
"title": "Failed to load a custom script",
"message": "Script could not be executed due to:\n\n{{message}}"
},
"widget-list-error": {
"title": "Failed to obtain the list of widgets from the server"
},
"widget-render-error": {
"title": "Failed to render a custom React widget"
},
"widget-missing-parent": "Custom widget does not have mandatory '{{property}}' property defined.\n\nIf this script is meant to be run without a UI element, use '#run=frontendStartup' instead.",
"open-script-note": "Open script note",
"scripting-error": "Custom script error: {{title}}"
},
"add_link": {
"add_link": "Add link",
"help_on_links": "Help on links",
"note": "Note",
"search_note": "search for note by its name",
"link_title_mirrors": "link title mirrors the note's current title",
"link_title_arbitrary": "link title can be changed arbitrarily",
"link_title": "Link title",
"anchor": "Anchor (optional)",
"anchor_none": "None (link to note)",
"button_add_link": "Add link"
},
"delete_notes": {
"delete_clones_description_one": "Also delete {{count}} other clone. Can be undone in recent changes.",
"delete_clones_description_other": "Also delete {{count}} other clones. Can be undone in recent changes.",
"title": "Delete notes",
"close": "Close",
"clones_label": "Clones",
"delete_all_clones_description": "Delete also all clones (can be undone in recent changes)",
"erase_notes_label": "Erase permanently",
"erase_notes_description": "Erase notes immediately instead of soft deletion. This cannot be undone and will force application reload.",
"erase_notes_warning": "Erase notes permanently (can't be undone), including all clones. This will force application reload.",
"notes_to_be_deleted": "Notes to be deleted ({{notesCount}})",
"no_note_to_delete": "No note will be deleted (only clones).",
"broken_relations_to_be_deleted": "Broken relations ({{relationCount}})",
"table_note_with_relation": "Note with relation",
"table_relation": "Relation",
"table_points_to": "Points to (deleted)",
"cancel": "Cancel",
"delete": "Delete"
},
"branch_prefix": {
"edit_branch_prefix": "Edit branch prefix",
"edit_branch_prefix_multiple": "Edit branch prefix for {{count}} branches",
"help_on_tree_prefix": "Help on Tree prefix",
"prefix": "Prefix: ",
"save": "Save",
"branch_prefix_saved": "Branch prefix has been saved.",
"branch_prefix_saved_multiple": "Branch prefix has been saved for {{count}} branches.",
"affected_branches": "Affected branches ({{count}}):"
},
"bulk_actions": {
"bulk_actions": "Bulk actions",
"affected_notes": "Affected notes",
"include_descendants": "Include descendants of the selected notes",
"available_actions": "Available actions",
"chosen_actions": "Chosen actions",
"execute_bulk_actions": "Execute bulk actions",
"bulk_actions_executed": "Bulk actions have been executed successfully.",
"none_yet": "None yet... add an action by clicking one of the available ones above.",
"labels": "Labels",
"relations": "Relations",
"notes": "Notes",
"other": "Other"
},
"clone_to": {
"clone_notes_to": "Clone notes to...",
"help_on_links": "Help on links",
"notes_to_clone": "Notes to clone",
"target_parent_note": "Target parent note",
"search_for_note_by_its_name": "search for note by its name",
"cloned_note_prefix_title": "Cloned note will be shown in note tree with given prefix",
"prefix_optional": "Prefix (optional)",
"clone_to_selected_note": "Clone to selected note",
"no_path_to_clone_to": "No path to clone to.",
"note_cloned": "Note \"{{clonedTitle}}\" has been cloned into \"{{targetTitle}}\""
},
"confirm": {
"confirmation": "Confirmation",
"cancel": "Cancel",
"ok": "OK",
"are_you_sure_remove_note": "Are you sure you want to remove the note \"{{title}}\" from relation map? ",
"if_you_dont_check": "If you don't check this, the note will be only removed from the relation map.",
"also_delete_note": "Also delete the note"
},
"export": {
"export_note_title": "Export note",
"close": "Close",
"export_type_subtree": "This note and all of its descendants",
"format_html": "HTML - recommended as it keeps all format",
"format_html_zip": "HTML in ZIP archive - this is recommended since this preserves all the formatting.",
"format_markdown": "Markdown - this preserves most of the formatting.",
"format_opml": "OPML - outliner interchange format for text only. Formatting, images and files are not included.",
"opml_version_1": "OPML v1.0 - plain text only",
"opml_version_2": "OPML v2.0 - allows also HTML",
"export_type_single": "Only this note without its descendants",
"export": "Export",
"choose_export_type": "Choose export type first please",
"export_status": "Export status",
"export_in_progress": "Export in progress: {{progressCount}}",
"export_finished_successfully": "Export finished successfully.",
"format_pdf": "PDF - for printing or sharing purposes.",
"share-format": "HTML for web publishing - uses the same theme that is used shared notes, but can be published as a static website."
},
"help": {
"title": "Cheatsheet",
"collapseSubTree": "collapse sub-tree",
"activateNextTab": "activate next tab",
"editShortcuts": "Edit keyboard shortcuts",
"noteNavigation": "Note navigation",
"goUpDown": "go up/down in the list of notes",
"collapseExpand": "collapse/expand node",
"notSet": "not set",
"goBackForwards": "go back / forwards in the history",
"showJumpToNoteDialog": "show <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/note-navigation.html#jump-to-note\">\"Jump to\" dialog</a>",
"scrollToActiveNote": "scroll to active note",
"jumpToParentNote": "jump to parent note",
"collapseWholeTree": "collapse whole note tree",
"tabShortcuts": "Tab shortcuts",
"newTabNoteLink": "on note link opens note in a new tab",
"newTabWithActivationNoteLink": "on note link opens and activates the note in a new tab",
"onlyInDesktop": "Only in desktop (Electron build)",
"openEmptyTab": "open empty tab",
"closeActiveTab": "close active tab",
"activatePreviousTab": "activate previous tab",
"creatingNotes": "Creating notes",
"createNoteAfter": "create new note after the active note",
"createNoteInto": "create new sub-note into active note",
"editBranchPrefix": "edit <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/tree-concepts.html#prefix\">prefix</a> of active note clone",
"movingCloningNotes": "Moving / cloning notes",
"moveNoteUpDown": "move note up/down in the note list",
"moveNoteUpHierarchy": "move note up in the hierarchy",
"multiSelectNote": "multi-select note above/below",
"selectAllNotes": "select all notes in the current level",
"selectNote": "select note",
"copyNotes": "copy active note (or current selection) into clipboard (used for <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/cloning-notes.html#cloning-notes\">cloning</a>)",
"cutNotes": "cut current note (or current selection) into clipboard (used for moving notes)",
"pasteNotes": "paste note(s) as sub-note into active note (which is either move or clone depending on whether it was copied or cut into clipboard)",
"deleteNotes": "delete note / sub-tree",
"editingNotes": "Editing notes",
"editNoteTitle": "in tree pane will switch from tree pane into note title. Enter from note title will switch focus to text editor. <kbd>Ctrl+.</kbd> will switch back from editor to tree pane.",
"createEditLink": "create / edit external link",
"createInternalLink": "create internal link",
"followLink": "follow link under cursor",
"insertDateTime": "insert current date and time at caret position",
"jumpToTreePane": "jump away to the tree pane and scroll to active note",
"markdownAutoformat": "Markdown-like autoformatting",
"headings": "<code>##</code>, <code>###</code>, <code>####</code> etc. followed by space for headings",
"bulletList": "<code>*</code> or <code>-</code> followed by space for bullet list",
"numberedList": "<code>1.</code> or <code>1)</code> followed by space for numbered list",
"blockQuote": "start a line with <code>></code> followed by space for block quote"
"title": "About Trilium Notes"
}
}

View File

@@ -1,23 +1,13 @@
{
"about": {
"version_label": "Version:",
"version": "{{appVersion}} (database: {{dbVersion}}, sync protocol: {{syncVersion}})",
"build_info": "Build: {{buildDate}}, revision: <buildRevision />",
"contributors_label": "Contributors:",
"contributor_roles": {
"lead-dev": "lead developer",
"original-dev": "original developer"
},
"role_brief_history": {
"lead-dev": "Elian Doran founded TriliumNext in 2024, a community fork created after Zadam stepped back from the project. Zadam later transferred the original repository to the TriliumNext team, merging both projects back into one.",
"original-dev": "On 25th December 2017, Zadam released the first beta of Trilium (written with a single \"l\", unlike the flower). Dissatisfied with existing note organizers, he built a powerful self-hosted hierarchical knowledge base that gathered over 22,000 GitHub stars. In 2024, as life got busier, he placed the project into maintenance mode."
},
"contributor_full_list": "See the entire community",
"data_directory": "Data directory:",
"github_tooltip": "Report bugs, suggest features, or contribute on GitHub",
"license_tooltip": "View license",
"donate": "Donate",
"donate_tooltip": "Donate to support this project"
"about": {
"title": "About Trilium Notes",
"homepage": "Homepage:",
"app_version": "App version:",
"db_version": "DB version:",
"sync_version": "Sync version:",
"build_date": "Build date:",
"build_revision": "Build revision:",
"data_directory": "Data directory:"
},
"toast": {
"critical-error": {
@@ -51,8 +41,6 @@
"link_title_mirrors": "link title mirrors the note's current title",
"link_title_arbitrary": "link title can be changed arbitrarily",
"link_title": "Link title",
"anchor": "Anchor (optional)",
"anchor_none": "None (link to note)",
"button_add_link": "Add link"
},
"branch_prefix": {
@@ -100,23 +88,17 @@
"also_delete_note": "Also delete the note"
},
"delete_notes": {
"title": "Delete notes",
"delete_notes_preview": "Delete notes preview",
"close": "Close",
"clones_label": "Clones",
"delete_clones_description_one": "Also delete {{count}} other clone. Can be undone in recent changes.",
"delete_clones_description_other": "Also delete {{count}} other clones. Can be undone in recent changes.",
"delete_all_clones_description": "Delete also all clones (can be undone in recent changes)",
"erase_notes_label": "Erase permanently",
"erase_notes_description": "Erase notes immediately instead of soft deletion. This cannot be undone and will force application reload.",
"erase_notes_description": "Normal (soft) deletion only marks the notes as deleted and they can be undeleted (in recent changes dialog) within a period of time. Checking this option will erase the notes immediately and it won't be possible to undelete the notes.",
"erase_notes_warning": "Erase notes permanently (can't be undone), including all clones. This will force application reload.",
"notes_to_be_deleted": "Notes to be deleted ({{notesCount}})",
"notes_to_be_deleted": "Following notes will be deleted ({{notesCount}})",
"no_note_to_delete": "No note will be deleted (only clones).",
"broken_relations_to_be_deleted": "Broken relations ({{relationCount}})",
"table_note_with_relation": "Note with relation",
"table_relation": "Relation",
"table_points_to": "Points to (deleted)",
"broken_relations_to_be_deleted": "Following relations will be broken and deleted ({{ relationCount}})",
"cancel": "Cancel",
"delete": "Delete"
"ok": "OK",
"deleted_relation_text": "Note {{- note}} (to be deleted) is referenced by relation {{- relation}} originating from {{- source}}."
},
"export": {
"export_note_title": "Export note",
@@ -227,7 +209,6 @@
"box_size_small": "small (~ 10 lines)",
"box_size_medium": "medium (~ 30 lines)",
"box_size_full": "full (box shows complete text)",
"box_size_expandable": "expandable (collapsed by default)",
"button_include": "Include note"
},
"info": {
@@ -297,7 +278,6 @@
"confirm_delete_all": "Do you want to delete all revisions of this note?",
"no_revisions": "No revisions for this note yet...",
"restore_button": "Restore",
"highlight_changes": "Highlight changes",
"diff_on": "Show diff",
"diff_off": "Show content",
"diff_on_hint": "Click to show note source diff",
@@ -311,39 +291,11 @@
"revision_deleted": "Note revision has been deleted.",
"snapshot_interval": "Note Revision Snapshot Interval: {{seconds}}s.",
"maximum_revisions": "Note Revision Snapshot Limit: {{number}}.",
"save_revision_now": "Save a revision now",
"save_named_revision": "Save named revision...",
"snapshot_header": "Note revision snapshot",
"snapshot_interval_value": "Interval: {{seconds}}s",
"snapshot_limit_value": "Limit: {{number}}",
"settings": "Note Revision Settings",
"menu_tooltip": "Revision options",
"download_button": "Download",
"mime": "MIME: ",
"file_size": "File size:",
"preview_not_available": "Preview isn't available for this note type.",
"save_revision": "Save revision",
"save_revision_tooltip": "Manually save a snapshot of the current note",
"description_placeholder": "Name this revision",
"revision_saved": "Note revision has been saved.",
"edit_description": "Edit name",
"description_updated": "Revision name has been updated.",
"source_auto": "Auto-save",
"source_manual": "Manual save",
"source_etapi": "ETAPI",
"source_llm": "LLM",
"source_restore": "Restore",
"source_unknown": "Snapshot",
"date_today": "Today",
"date_yesterday": "Yesterday",
"date_this_week": "This week",
"date_this_month": "This month",
"source_description_auto": "Automatically saved by the system at regular intervals",
"source_description_manual": "Manually saved by the user",
"source_description_etapi": "Created via the External Trilium API",
"source_description_llm": "Created by the AI assistant",
"source_description_restore": "Saved before restoring a previous revision",
"source_description_unknown": "Source not available"
"preview_not_available": "Preview isn't available for this note type."
},
"sort_child_notes": {
"sort_children_by": "Sort children by...",
@@ -417,7 +369,7 @@
"calendar_root": "marks note which should be used as root for day notes. Only one should be marked as such.",
"archived": "notes with this label won't be visible by default in search results (also in Jump To, Add Link dialogs etc).",
"exclude_from_export": "notes (with their sub-tree) won't be included in any note export",
"run": "defines on which events script should run. Possible values are:\n<ul>\n<li>frontendStartup - when Trilium frontend starts up (or is refreshed), but not on mobile.</li>\n<li>mobileStartup - when Trilium frontend starts up (or is refreshed), on mobile.</li>\n<li>backendStartup - when Trilium backend starts up.</li>\n<li>hourly - run once an hour. You can use additional label <code>runAtHour</code> to specify at which hour.</li>\n<li>daily - run once a day.</li>\n</ul>",
"run": "defines on which events script should run. Possible values are:\n<ul>\n<li>frontendStartup - when Trilium frontend starts up (or is refreshed), but not on mobile.</li>\n<li>mobileStartup - when Trilium frontend starts up (or is refreshed), on mobile.</li>\n<li>backendStartup - when Trilium backend starts up</li>\n<li>hourly - run once an hour. You can use additional label <code>runAtHour</code> to specify at which hour.</li>\n<li>daily - run once a day</li>\n</ul>",
"run_on_instance": "Define which trilium instance should run this on. Default to all instances.",
"run_at_hour": "On which hour should this run. Should be used together with <code>#run=hourly</code>. Can be defined multiple times for more runs during the day.",
"disable_inclusion": "scripts with this label won't be included into parent script execution.",
@@ -495,8 +447,6 @@
"and_more": "... and {{count}} more.",
"print_landscape": "When exporting to PDF, changes the orientation of the page to landscape instead of portrait.",
"print_page_size": "When exporting to PDF, changes the size of the page. Supported values: <code>A0</code>, <code>A1</code>, <code>A2</code>, <code>A3</code>, <code>A4</code>, <code>A5</code>, <code>A6</code>, <code>Legal</code>, <code>Letter</code>, <code>Tabloid</code>, <code>Ledger</code>.",
"print_scale": "When exporting to PDF, changes the scale of the rendered content. Values range from 0.1 (10%) to 2 (200%), default is 1 (100%).",
"print_margins": "When exporting to PDF, sets page margins. Use <code>default</code>, <code>none</code>, <code>minimum</code>, or custom values as <code>top,right,bottom,left</code> in millimeters.",
"color_type": "Color"
},
"attribute_editor": {
@@ -736,17 +686,11 @@
"move_right": "Move right"
},
"note_actions": {
"word_wrap": "Word wrap",
"word_wrap_auto": "Auto",
"word_wrap_auto_description": "Follow the global setting",
"word_wrap_on": "On",
"word_wrap_off": "Off",
"convert_into_attachment": "Convert into attachment",
"re_render_note": "Re-render note",
"search_in_note": "Search in note",
"note_source": "Note source",
"note_attachments": "Note attachments",
"view_ocr_text": "View OCR text",
"open_note_externally": "Open note externally",
"open_note_externally_title": "File will be open in an external application and watched for changes. You'll then be able to upload the modified version back to Trilium.",
"open_note_custom": "Open note custom",
@@ -757,7 +701,6 @@
"print_note": "Print note",
"view_revisions": "Note revisions...",
"save_revision": "Save revision",
"save_named_revision": "Save named revision...",
"advanced": "Advanced",
"convert_into_attachment_failed": "Converting note '{{title}}' failed.",
"convert_into_attachment_successful": "Note '{{title}}' has been converted to attachment.",
@@ -862,11 +805,7 @@
"board": "Board",
"presentation": "Presentation",
"include_archived_notes": "Show archived notes",
"hide_child_notes": "Hide child notes in tree",
"open_all_in_tabs": "Open all",
"open_all_in_tabs_tooltip": "Open all results in new tabs",
"open_all_confirm": "This will open {{count}} notes in new tabs. Continue?",
"open_all_too_many": "Too many results ({{count}}). Maximum is {{max}}."
"hide_child_notes": "Hide child notes in tree"
},
"edited_notes": {
"no_edited_notes_found": "No edited notes on this day yet...",
@@ -902,9 +841,6 @@
"no_inherited_attributes": "No inherited attributes.",
"none": "none"
},
"auto_link_attribute_list": {
"title": "System Attributes"
},
"note_info_widget": {
"note_id": "Note ID",
"created": "Created",
@@ -923,8 +859,7 @@
"collapse": "Collapse to normal size",
"title": "Note Map",
"fix-nodes": "Fix nodes",
"link-distance": "Link distance",
"too-many-notes": "This subtree contains {{count}} notes, which exceeds the limit of {{max}} that can be displayed in the note map."
"link-distance": "Link distance"
},
"note_paths": {
"title": "Note Paths",
@@ -1138,7 +1073,6 @@
"edit_title": "Edit title",
"rename_note": "Rename note",
"enter_new_title": "Enter new note title:",
"rename_relation": "Rename relation",
"remove_relation": "Remove relation",
"confirm_remove_relation": "Are you sure you want to remove the relation?",
"specify_new_relation_name": "Specify new relation name (allowed characters: alphanumeric, colon and underscore):",
@@ -1173,20 +1107,17 @@
},
"consistency_checks": {
"title": "Consistency Checks",
"find_and_fix_label": "Find and fix consistency issues",
"find_and_fix_description": "Scan for and automatically repair any data consistency issues in the database.",
"find_and_fix_button": "Find and fix consistency issues",
"finding_and_fixing_message": "Finding and fixing consistency issues...",
"issues_fixed_message": "Any consistency issue which may have been found is now fixed."
},
"database_anonymization": {
"title": "Database Anonymization",
"description": "Create an anonymized copy of your database for sharing with developers when debugging issues, without exposing personal data.",
"full_anonymization": "Full anonymization",
"full_anonymization_description": "Creates a copy of the database with all note content removed, leaving only structure and non-sensitive metadata. Safe for sharing online when debugging issues.",
"full_anonymization": "Full Anonymization",
"full_anonymization_description": "This action will create a new copy of the database and anonymize it (remove all note content and leave only structure and some non-sensitive metadata) for sharing online for debugging purposes without fear of leaking your personal data.",
"save_fully_anonymized_database": "Save fully anonymized database",
"light_anonymization": "Light anonymization",
"light_anonymization_description": "Creates a copy with note content removed, but titles, attributes, and custom scripts/widgets remain. Provides more context for debugging.",
"light_anonymization": "Light Anonymization",
"light_anonymization_description": "This action will create a new copy of the database and do a light anonymization on it — specifically only content of all notes will be removed, but titles and attributes will remain. Additionally, custom JS frontend/backend script notes and custom widgets will remain. This provides more context to debug the issues.",
"choose_anonymization": "You can decide yourself if you want to provide a fully or lightly anonymized database. Even fully anonymized DB is very useful, however in some cases lightly anonymized database can speed up the process of bug identification and fixing.",
"save_lightly_anonymized_database": "Save lightly anonymized database",
"existing_anonymized_databases": "Existing anonymized databases",
@@ -1199,8 +1130,7 @@
},
"database_integrity_check": {
"title": "Database Integrity Check",
"check_integrity_label": "Check database integrity",
"check_integrity_description": "Verify that the database is not corrupted on the SQLite level.",
"description": "This will check that the database is not corrupted on the SQLite level. It might take some time, depending on the DB size.",
"check_button": "Check database integrity",
"checking_integrity": "Checking database integrity...",
"integrity_check_succeeded": "Integrity check succeeded - no problems found.",
@@ -1208,11 +1138,7 @@
},
"sync": {
"title": "Sync",
"force_full_sync_label": "Force full sync",
"force_full_sync_description": "Trigger a complete synchronization with the sync server, re-uploading all changes.",
"force_full_sync_button": "Force full sync",
"fill_entity_changes_label": "Fill entity changes",
"fill_entity_changes_description": "Rebuild entity change records. Use this if sync is missing some changes.",
"fill_entity_changes_button": "Fill entity changes records",
"full_sync_triggered": "Full sync triggered",
"filling_entity_changes": "Filling entity changes rows...",
@@ -1220,13 +1146,8 @@
"finished-successfully": "Sync finished successfully.",
"failed": "Sync failed: {{message}}"
},
"database": {
"title": "Database"
},
"vacuum_database": {
"title": "Vacuum Database",
"vacuum_label": "Vacuum database",
"vacuum_description": "Rebuild the database to reduce file size. No data will be changed.",
"description": "This will rebuild the database which will typically result in a smaller database file. No data will be actually changed.",
"button_text": "Vacuum database",
"vacuuming_database": "Vacuuming database...",
@@ -1236,25 +1157,21 @@
"title": "Experimental Options",
"disclaimer": "These options are experimental and may cause instability. Use with caution.",
"new_layout_name": "New Layout",
"new_layout_description": "Try out the new layout for a more modern look and improved usability. Subject to heavy change in the upcoming releases.",
"llm_name": "AI / LLM Chat",
"llm_description": "Enable the AI chat sidebar and LLM chat notes powered by large language models."
"new_layout_description": "Try out the new layout for a more modern look and improved usability. Subject to heavy change in the upcoming releases."
},
"fonts": {
"theme_defined": "Theme defined",
"fonts": "Fonts",
"custom_fonts": "Use custom fonts",
"main_font": "Interface text",
"main_font": "Main Font",
"font_family": "Font family",
"size": "Size",
"preview": "Preview",
"note_tree_font": "Note tree text",
"note_detail_font": "Document text",
"monospace_font": "Monospace text",
"monospace_font_description": "Used for code notes and code blocks",
"size_relative_to_general": "Size is relative to the general font size",
"not_all_fonts_available": "Not all listed fonts may be available on your system",
"apply_changes": "Reload to apply changes",
"note_tree_font": "Note Tree Font",
"note_detail_font": "Note Detail Font",
"monospace_font": "Monospace (code) Font",
"note_tree_and_detail_font_sizing": "Note that tree and detail font sizing is relative to the main font size setting.",
"not_all_fonts_available": "Not all listed fonts may be available on your system.",
"apply_font_changes": "To apply font changes, click on",
"reload_frontend": "reload frontend",
"generic-fonts": "Generic fonts",
"sans-serif-system-fonts": "Sans-serif system fonts",
"serif-system-fonts": "Serif system fonts",
@@ -1283,18 +1200,15 @@
"edited_notes_message": "Edited Notes ribbon tab will automatically open on day notes"
},
"theme": {
"title": "User Interface",
"theme_label": "Application theme",
"title": "Application Theme",
"theme_label": "Theme",
"override_theme_fonts_label": "Override theme fonts",
"auto_theme": "Follow system color scheme",
"light_theme": "Light",
"dark_theme": "Dark",
"triliumnext": "Follow system color scheme",
"triliumnext-light": "Light",
"triliumnext-dark": "Dark",
"modern_themes": "Modern",
"legacy_themes": "Legacy",
"custom_themes": "Custom",
"auto_theme": "Legacy (Follow system color scheme)",
"light_theme": "Legacy (Light)",
"dark_theme": "Legacy (Dark)",
"triliumnext": "Trilium (Follow system color scheme)",
"triliumnext-light": "Trilium (Light)",
"triliumnext-dark": "Trilium (Dark)",
"layout": "Layout",
"layout-vertical-title": "Vertical",
"layout-horizontal-title": "Horizontal",
@@ -1303,11 +1217,11 @@
},
"ui-performance": {
"title": "Performance",
"enable-motion": "Transitions and animations",
"enable-shadows": "Shadows",
"enable-backdrop-effects": "Background effects for menus, popups and panels",
"enable-smooth-scroll": "Smooth scrolling",
"app-restart-required": "Requires app restart"
"enable-motion": "Enable transitions and animations",
"enable-shadows": "Enable shadows",
"enable-backdrop-effects": "Enable background effects for menus, popups and panels",
"enable-smooth-scroll": "Enable smooth scrolling",
"app-restart-required": "(a restart of the application is required for the change to take effect)"
},
"zoom_factor": {
"title": "Zoom Factor (desktop build only)",
@@ -1316,13 +1230,11 @@
"code_auto_read_only_size": {
"title": "Automatic Read-Only Size",
"description": "Automatic read-only note size is the size after which notes will be displayed in a read-only mode (for performance reasons).",
"label": "Automatic read-only size",
"label": "Automatic read-only size (code notes)",
"unit": "characters"
},
"code-editor-options": {
"title": "Editor",
"tab_width": "Tab width",
"tab_width_unit": "spaces"
"title": "Editor"
},
"code_mime_types": {
"title": "Available MIME types in the dropdown",
@@ -1340,104 +1252,66 @@
},
"images": {
"images_section_title": "Images",
"download_images_automatically": "Download images automatically",
"download_images_description": "Download referenced online images from pasted HTML so they are available offline.",
"enable_image_compression": "Image compression",
"enable_image_compression_description": "Compress and resize images when they are uploaded or pasted.",
"max_image_dimensions": "Max image dimensions",
"max_image_dimensions_description": "Images exceeding this size will be resized automatically.",
"download_images_automatically": "Download images automatically for offline use.",
"download_images_description": "Pasted HTML can contain references to online images, Trilium will find those references and download the images so that they are available offline.",
"enable_image_compression": "Enable image compression",
"max_image_dimensions": "Max width / height of an image (image will be resized if it exceeds this setting).",
"max_image_dimensions_unit": "pixels",
"jpeg_quality": "JPEG quality",
"jpeg_quality_description": "Recommended range is 5085. Lower values reduce file size, higher values preserve detail.",
"ocr_section_title": "Text Extraction (OCR)",
"ocr_related_content_languages": "Content languages (used for text extraction)",
"ocr_auto_process": "Auto-process new files",
"ocr_auto_process_description": "Automatically extract text from newly uploaded or pasted files.",
"ocr_min_confidence": "Minimum confidence",
"ocr_confidence_description": "Only extract text above this confidence threshold. Lower values include more text but may be less accurate.",
"batch_ocr_title": "Process Existing Files",
"batch_ocr_description": "Extract text from all existing images, PDFs, and Office documents in your notes. This may take some time depending on the number of files.",
"batch_ocr_start": "Start Batch Processing",
"batch_ocr_starting": "Starting batch processing...",
"batch_ocr_progress": "Processing {{processed}} of {{total}} files...",
"batch_ocr_completed": "Batch processing completed! Processed {{processed}} files.",
"batch_ocr_error": "Error during batch processing: {{error}}"
"jpeg_quality_description": "JPEG quality (10 - worst quality, 100 - best quality, 50 - 85 is recommended)"
},
"attachment_erasure_timeout": {
"attachment_erasure_timeout": "Unused Attachments",
"description": "Attachments that are no longer referenced by any note are considered unused and can be automatically erased after a period of time.",
"erase_attachments_after": "Erase unused attachments after",
"erase_attachments_after_description": "Time before unused attachments are permanently erased.",
"manual_erasing_description": "Trigger erasing manually, ignoring the timeout above.",
"erase_unused_attachments_now": "Erase unused attachments now",
"attachment_erasure_timeout": "Attachment Erasure Timeout",
"attachment_auto_deletion_description": "Attachments get automatically deleted (and erased) if they are not referenced by their note anymore after a defined time out.",
"erase_attachments_after": "Erase unused attachments after:",
"manual_erasing_description": "You can also trigger erasing manually (without considering the timeout defined above):",
"erase_unused_attachments_now": "Erase unused attachment notes now",
"unused_attachments_erased": "Unused attachments have been erased."
},
"network_connections": {
"network_connections_title": "Network",
"check_for_updates": "Check for updates automatically",
"check_for_updates_description": "Checks for new versions on GitHub and shows a notification in the global menu when available."
"network_connections_title": "Network Connections",
"check_for_updates": "Check for updates automatically"
},
"note_erasure_timeout": {
"note_erasure_timeout_title": "Deleted Notes",
"description": "Deleted notes are first only marked as deleted and can be recovered from Recent Notes. After a period of time, they are permanently erased.",
"erase_notes_after": "Erase notes after",
"erase_notes_after_description": "Time before deleted notes are permanently erased.",
"manual_erasing_description": "Trigger erasing manually, ignoring the timeout above.",
"note_erasure_timeout_title": "Note Erasure Timeout",
"note_erasure_description": "Deleted notes (and attributes, revisions...) are at first only marked as deleted and it is possible to recover them from Recent Notes dialog. After a period of time, deleted notes are \"erased\" which means their content is not recoverable anymore. This setting allows you to configure the length of the period between deleting and erasing the note.",
"erase_notes_after": "Erase notes after:",
"manual_erasing_description": "You can also trigger erasing manually (without considering the timeout defined above):",
"erase_deleted_notes_now": "Erase deleted notes now",
"deleted_notes_erased": "Deleted notes have been erased."
},
"revisions_snapshot": {
"title": "Note Revisions"
},
"revisions_snapshot_interval": {
"note_revisions_snapshot_interval_title": "Note Revision Snapshot Interval",
"note_revisions_snapshot_description": "The Note revision snapshot interval is the time after which a new note revision will be created for the note. See <doc>wiki</doc> for more info.",
"note_revisions_snapshot_description_short": "Time after which a new note revision will be created.",
"snapshot_time_interval_label": "Snapshot interval"
"snapshot_time_interval_label": "Note revision snapshot time interval:"
},
"revisions_snapshot_limit": {
"note_revisions_snapshot_limit_title": "Note Revision Snapshot Limit",
"note_revisions_snapshot_limit_description": "The note revision snapshot number limit refers to the maximum number of revisions that can be saved for each note. Where -1 means no limit, 0 means delete all revisions. You can set the maximum revisions for a single note through the #versioningLimit label.",
"note_revisions_snapshot_limit_description_short": "Max revisions per note. Use -1 for unlimited, 0 to disable.",
"snapshot_number_limit_label": "Maximum revisions",
"snapshot_number_limit_label": "Note revision snapshot number limit:",
"snapshot_number_limit_unit": "snapshots",
"erase_excess_revision_snapshots": "Erase excess revision snapshots now",
"erase_excess_revision_snapshots_description": "Delete revisions exceeding the limit for all notes.",
"erase_excess_revision_snapshots_prompt": "Excess revision snapshots have been erased."
},
"search": {
"title": "Search",
"fuzzy_matching_label": "Typo tolerance in search",
"fuzzy_matching_description": "Affects quick search and full search. Finds similar words when exact matches are insufficient.",
"autocomplete_fuzzy_label": "Typo tolerance in autocomplete",
"autocomplete_fuzzy_description": "Affects jump-to-note and note selectors. Slower but tolerates typos."
},
"search_engine": {
"title": "Search Engine",
"custom_search_engine_info": "Used when searching the web for selected text. If not configured, DuckDuckGo will be used.",
"predefined_templates_label": "Presets",
"custom_search_engine_info": "Custom search engine requires both a name and a URL to be set. If either of these is not set, DuckDuckGo will be used as the default search engine.",
"predefined_templates_label": "Predefined search engine templates",
"bing": "Bing",
"baidu": "Baidu",
"duckduckgo": "DuckDuckGo",
"google": "Google",
"custom_name_label": "Name",
"custom_name_placeholder": "Search engine name",
"custom_url_label": "URL",
"custom_url_description": "Use {keyword} as a placeholder for the search term.",
"custom_url_placeholder": "Search engine URL",
"custom_name_label": "Custom search engine name",
"custom_name_placeholder": "Customize search engine name",
"custom_url_label": "Custom search engine URL should include {keyword} as a placeholder for the search term.",
"custom_url_placeholder": "Customize search engine url",
"save_button": "Save"
},
"tray": {
"title": "System Tray",
"enable_tray": "Tray icon",
"enable_tray_description": "Trilium needs to be restarted for this change to take effect."
},
"text_editor": {
"title": "Editor"
"enable_tray": "Enable tray (Trilium needs to be restarted for this change to take effect)"
},
"heading_style": {
"title": "Heading style",
"description": "Visual style for headings in text notes.",
"title": "Heading Style",
"plain": "Plain",
"underline": "Underline",
"markdown": "Markdown-style"
@@ -1464,16 +1338,14 @@
"text_auto_read_only_size": {
"title": "Automatic Read-Only Size",
"description": "Automatic read-only note size is the size after which notes will be displayed in a read-only mode (for performance reasons).",
"label": "Automatic read-only size",
"label": "Automatic read-only size (text notes)",
"unit": "characters"
},
"custom_date_time_format": {
"title": "Date/time format",
"title": "Custom Date/Time Format",
"description": "Customize the format of the date and time inserted via <shortcut /> or the toolbar. See <doc>Day.js docs</doc> for available format tokens.",
"description_short": "Customize the format of the date and time inserted via the toolbar.",
"preview": "Preview: {{preview}}",
"format_string": "Format string",
"formatted_time": "Formatted date/time"
"format_string": "Format string:",
"formatted_time": "Formatted date/time:"
},
"i18n": {
"title": "Localization",
@@ -1488,20 +1360,20 @@
"sunday": "Sunday",
"first-week-of-the-year": "First week of the year",
"first-week-contains-first-day": "First week contains first day of the year",
"first-week-contains-first-thursday": "First week contains first Thursday (ISO 8601)",
"first-week-contains-first-thursday": "First week contains first Thursday of the year",
"first-week-has-minimum-days": "First week has minimum days",
"min-days-in-first-week": "Minimum days in first week",
"first-week-warning": "Changing this may cause duplicates with existing Week Notes.",
"first-week-info": "First week contains first Thursday of the year is based on <a href=\"https://en.wikipedia.org/wiki/ISO_week_date#First_week\">ISO 8601</a> standard.",
"first-week-warning": "Changing first week options may cause duplicate with existing Week Notes and the existing Week Notes will not be updated accordingly.",
"formatting-locale": "Date & number format",
"formatting-locale-auto": "Based on the application's language"
},
"backup": {
"title": "Backup",
"automatic_backup": "Automatic backup",
"automatic_backup_description": "Trilium can back up the database automatically:",
"enable_daily_backup": "Backup daily",
"enable_weekly_backup": "Backup weekly",
"enable_monthly_backup": "Backup monthly",
"enable_daily_backup": "Enable daily backup",
"enable_weekly_backup": "Enable weekly backup",
"enable_monthly_backup": "Enable monthly backup",
"backup_recommendation": "It's recommended to keep the backup turned on, but this can make application startup slow with large databases and/or slow storage devices.",
"backup_now": "Backup now",
"backup_database_now": "Backup database now",
@@ -1509,8 +1381,7 @@
"date-and-time": "Date & time",
"path": "Path",
"database_backed_up_to": "Database has been backed up to {{backupFilePath}}",
"no_backup_yet": "no backup yet",
"download": "Download"
"no_backup_yet": "no backup yet"
},
"etapi": {
"title": "ETAPI",
@@ -1545,15 +1416,11 @@
"new_password": "New password",
"new_password_confirmation": "New password confirmation",
"change_password": "Change password",
"change_password_description": "Update your current password",
"reset_password": "Reset password",
"reset_password_description": "Permanently lose access to protected notes",
"cancel": "Cancel",
"protected_session_timeout": "Protected Session",
"protected_session_timeout_description": "Time of inactivity before the session is cleared from browser memory. See",
"protected_session_timeout": "Protected Session Timeout",
"protected_session_timeout_description": "Protected session timeout is a time period after which the protected session is wiped from the browser's memory. This is measured from the last interaction with protected notes. See",
"wiki": "wiki",
"for_more_info": "for more info.",
"protected_session_timeout_label": "Auto-close session after",
"protected_session_timeout_label": "Protected session timeout:",
"reset_confirmation": "By resetting the password you will forever lose access to all your existing protected notes. Do you really want to reset the password?",
"reset_success_message": "Password has been reset. Please set new password",
"change_password_heading": "Change Password",
@@ -1612,28 +1479,26 @@
"spellcheck": {
"title": "Spell Check",
"description": "These options apply only for desktop builds, browsers will use their own native spell check.",
"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."
"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."
},
"sync_2": {
"config_title": "Sync Server",
"server_address": "Server address",
"server_address_description": "URL of the Trilium server to sync with.",
"timeout": "Connection timeout",
"timeout_description": "Time to wait before giving up on a slow connection.",
"proxy_label": "Proxy server",
"proxy_description": "Leave blank to use system proxy (desktop only). Use \"noproxy\" to bypass all proxies.",
"config_title": "Sync Configuration",
"server_address": "Server instance address",
"timeout": "Sync timeout",
"timeout_unit": "milliseconds",
"proxy_label": "Sync proxy server (optional)",
"note": "Note",
"note_description": "If you leave the proxy setting blank, the system proxy will be used (applies to desktop/electron build only).",
"special_value_description": "Another special value is <code>noproxy</code> which forces ignoring even the system proxy and respects <code>NODE_TLS_REJECT_UNAUTHORIZED</code>.",
"save": "Save",
"help": "Help",
"test_title": "Test Connection",
"test_description": "Test the connection to the sync server. If not initialized, this will set up syncing.",
"test_title": "Sync Test",
"test_description": "This will test the connection and handshake to the sync server. If the sync server isn't initialized, this will set it up to sync with the local document.",
"test_button": "Test sync",
"handshake_failed": "Sync server handshake failed, error: {{message}}"
},
@@ -1716,7 +1581,6 @@
"note_types": {
"text": "Text",
"code": "Code",
"markdown": "Markdown",
"saved-search": "Saved Search",
"relation-map": "Relation Map",
"note-map": "Note Map",
@@ -1735,7 +1599,6 @@
"geo-map": "Geo Map",
"beta-feature": "Beta",
"ai-chat": "AI Chat",
"llm-chat": "AI Chat",
"task-list": "Task List",
"new-feature": "New",
"collections": "Collections",
@@ -1747,49 +1610,6 @@
"toggle-on-hint": "Note is not protected, click to make it protected",
"toggle-off-hint": "Note is protected, click to make it unprotected"
},
"llm_chat": {
"placeholder": "Type a message...",
"send": "Send",
"sending": "Sending...",
"empty_state": "Start a conversation by typing a message below.",
"searching_web": "Searching the web...",
"web_search": "Web search",
"note_tools": "Note access",
"sources": "Sources",
"sources_summary": "{{count}} sources from {{sites}} sites",
"extended_thinking": "Extended thinking",
"legacy_models": "Legacy models",
"thinking": "Thinking...",
"thought_process": "Thought process",
"tool_calls": "{{count}} tool call(s)",
"input": "Input",
"result": "Result",
"error": "Error",
"tool_error": "failed",
"total_tokens": "{{total}} tokens",
"tokens_detail": "{{prompt}} prompt + {{completion}} completion",
"tokens_used": "{{prompt}} prompt + {{completion}} completion = {{total}} tokens",
"tokens_used_with_cost": "{{prompt}} prompt + {{completion}} completion = {{total}} tokens (~${{cost}})",
"tokens_used_with_model": "{{model}}: {{prompt}} prompt + {{completion}} completion = {{total}} tokens",
"tokens_used_with_model_and_cost": "{{model}}: {{prompt}} prompt + {{completion}} completion = {{total}} tokens (~${{cost}})",
"tokens": "tokens",
"context_used": "{{percentage}}% used",
"note_context_enabled": "Click to disable note context: {{title}}",
"note_context_disabled": "Click to include current note in context",
"no_provider_message": "No AI provider configured. Add one to start chatting.",
"add_provider": "Add AI Provider",
"stop": "Stop"
},
"sidebar_chat": {
"title": "AI Chat",
"launcher_title": "Open AI Chat",
"new_chat": "Start new chat",
"save_chat": "Save chat to notes",
"empty_state": "Start a conversation",
"history": "Chat history",
"recent_chats": "Recent chats",
"no_chats": "No previous chats"
},
"shared_switch": {
"shared": "Shared",
"toggle-on-title": "Share the note",
@@ -1859,7 +1679,7 @@
"note_detail": {
"could_not_find_typewidget": "Could not find typeWidget for type '{{type}}'",
"printing": "Printing in progress...",
"printing_pdf": "Preparing print preview...",
"printing_pdf": "Exporting to PDF in progress...",
"print_report_title": "Print report",
"print_report_error_title": "Failed to print",
"print_report_stack_trace": "Stack trace",
@@ -1937,8 +1757,6 @@
},
"entrypoints": {
"note-revision-created": "Note revision has been created.",
"save-named-revision-title": "Save named revision",
"save-named-revision-message": "Enter a name for this revision (leave empty for no name):",
"note-executed": "Note executed.",
"sql-error": "Error occurred while executing SQL query: {{message}}"
},
@@ -1976,9 +1794,6 @@
"move-to-available-launchers": "Move to available launchers",
"duplicate-launcher": "Duplicate launcher <kbd data-command=\"duplicateSubtree\">"
},
"launcher_button_context_menu": {
"remove_from_launch_bar": "Remove from launch bar"
},
"highlighting": {
"title": "Code Blocks",
"description": "Controls the syntax highlighting for code blocks inside text notes, code notes will not be affected.",
@@ -1989,10 +1804,7 @@
"theme_none": "No syntax highlighting",
"theme_group_light": "Light themes",
"theme_group_dark": "Dark themes",
"copy_title": "Copy to clipboard",
"click_to_copy": "Click to copy",
"tab_width": "Tab width",
"tab_width_unit": "spaces"
"copy_title": "Copy to clipboard"
},
"classic_editor_toolbar": {
"title": "Formatting"
@@ -2003,7 +1815,6 @@
"editing": {
"editor_type": {
"label": "Formatting toolbar",
"toolbar_style": "Toolbar style",
"floating": {
"title": "Floating",
"description": "editing tools appear near the cursor;"
@@ -2012,7 +1823,7 @@
"title": "Fixed",
"description": "editing tools appear in the \"Formatting\" ribbon tab."
},
"multiline-toolbar": "Display the toolbar on multiple lines if it doesn't fit"
"multiline-toolbar": "Display the toolbar on multiple lines if it doesn't fit."
}
},
"electron_context_menu": {
@@ -2030,9 +1841,6 @@
"copy_reference_to_clipboard": "Copy reference to clipboard",
"copy_image_to_clipboard": "Copy image to clipboard"
},
"link": {
"failed_to_open": "Failed to open link '{{- href}}': {{- message}}"
},
"link_context_menu": {
"open_note_in_new_tab": "Open note in a new tab",
"open_note_in_new_split": "Open note in a new split",
@@ -2082,7 +1890,7 @@
"days": "Days"
},
"share": {
"title": "Share",
"title": "Share Settings",
"redirect_bare_domain": "Redirect bare domain to Share page",
"redirect_bare_domain_description": "Redirect anonymous users to the Share page instead of showing Login",
"show_login_link": "Show Login link in Share theme",
@@ -2113,7 +1921,7 @@
},
"content_language": {
"title": "Content languages",
"description": "Select one or more languages that should appear in the language selection in the Basic Properties section of a read-only or editable text note. This will allow features such as spell-checking, right-to-left support and text extraction (OCR)."
"description": "Select one or more languages that should appear in the language selection in the Basic Properties section of a read-only or editable text note. This will allow features such as spell-checking or right-to-left support."
},
"switch_layout_button": {
"title_vertical": "Move editing pane to the bottom",
@@ -2123,11 +1931,6 @@
"unlock-editing": "Unlock editing",
"lock-editing": "Lock editing"
},
"display_mode": {
"source": "Source view",
"split": "Split view",
"preview": "Preview"
},
"png_export_button": {
"button_title": "Export diagram as PNG"
},
@@ -2151,12 +1954,12 @@
},
"editorfeatures": {
"title": "Features",
"emoji_completion_enabled": "Emoji auto-completion",
"emoji_completion_description": "Emojis can be easily inserted into text by typing `:`, followed by the name of an emoji.",
"note_completion_enabled": "Note auto-completion",
"note_completion_description": "Links to notes can be created by typing `@` followed by the title of a note.",
"slash_commands_enabled": "Slash commands",
"slash_commands_description": "Editing commands such as inserting line breaks or headings can be toggled by typing `/`."
"emoji_completion_enabled": "Enable Emoji auto-completion",
"emoji_completion_description": "If enabled, emojis can be easily inserted into text by typing `:`, followed by the name of an emoji.",
"note_completion_enabled": "Enable note auto-completion",
"note_completion_description": "If enabled, links to notes can be created by typing `@` followed by the title of a note.",
"slash_commands_enabled": "Enable slash commands",
"slash_commands_description": "If enabled, editing commands such as inserting line breaks or headings can be toggled by typing `/`."
},
"table_view": {
"new-row": "New row",
@@ -2218,22 +2021,6 @@
"calendar_view": {
"delete_note": "Delete note..."
},
"ocr": {
"extracted_text": "Extracted Text (OCR)",
"extracted_text_title": "Extracted Text (OCR)",
"loading_text": "Loading OCR text...",
"no_text_available": "No OCR text available",
"no_text_explanation": "This note has not been processed for OCR text extraction or no text was found.",
"failed_to_load": "Failed to load OCR text",
"process_now": "Process OCR",
"processing": "Processing...",
"processing_started": "OCR processing has been started. Please wait a moment and refresh.",
"processing_complete": "OCR processing complete.",
"processing_failed": "Failed to start OCR processing",
"text_filtered_low_confidence": "OCR detected text with {{confidence}}% confidence, but it was discarded because your minimum threshold is {{threshold}}%.",
"open_media_settings": "Open Settings",
"view_extracted_text": "View extracted text (OCR)"
},
"command_palette": {
"tree-action-name": "Tree: {{name}}",
"export_note_title": "Export Note",
@@ -2275,8 +2062,6 @@
"related_code_blocks": "Color scheme for code blocks in text notes",
"related_code_notes": "Color scheme for code notes",
"ui": "User interface",
"ui_layout_style": "Layout style",
"ui_layout_orientation": "Launcher bar orientation",
"ui_old_layout": "Old layout",
"ui_new_layout": "New layout"
},
@@ -2359,19 +2144,7 @@
"note_paths_one": "{{count}} path",
"note_paths_other": "{{count}} paths",
"note_paths_title": "Note paths",
"code_note_switcher": "Change language mode",
"tab_width": "Tab Width: {{width}}",
"tab_width_title": "Change tab width",
"tab_width_spaces": "{{count}} spaces",
"tab_width_spaces_short": "Spaces: {{width}}",
"tab_width_tabs": "Tabs: {{width}}",
"tab_width_use_default": "Use default ({{width}})",
"tab_width_use_default_style": "Use default ({{style}})",
"tab_width_display_header": "Display width",
"tab_width_reindent_header": "Re-indent content to",
"tab_width_style_header": "Indent using",
"tab_width_style_spaces": "Spaces",
"tab_width_style_tabs": "Tabs"
"code_note_switcher": "Change language mode"
},
"attributes_panel": {
"title": "Note Attributes"
@@ -2382,37 +2155,6 @@
"toggle": "Toggle right panel",
"custom_widget_go_to_source": "Go to source code"
},
"print_preview": {
"title": "Print preview",
"close": "Close",
"save": "Save as PDF",
"print": "Print",
"export_pdf": "Export as PDF",
"system_print": "Print using system dialog",
"destination": "Destination",
"destination_pdf": "Save as PDF",
"destination_printers": "Printers",
"destination_default": "Default",
"orientation": "Orientation",
"portrait": "Portrait",
"landscape": "Landscape",
"page_size": "Page size",
"scale": "Scale",
"margins": "Margins",
"render_error": "Unable to render PDF with the current settings. Please check the margins and scale.",
"margins_default": "Default",
"margins_none": "None",
"margins_minimum": "Minimum",
"margins_custom": "Custom",
"margin_top": "Top",
"margin_right": "Right",
"margin_bottom": "Bottom",
"margin_left": "Left",
"page_ranges": "Pages",
"page_ranges_hint": "Leave empty to print all pages.",
"page_ranges_invalid": "Invalid format. Use e.g. 1-5, 8, 11-13.",
"page_ranges_placeholder": "e.g. 1-5, 8, 11-13"
},
"pdf": {
"attachments_one": "{{count}} attachment",
"attachments_other": "{{count}} attachments",
@@ -2487,71 +2229,6 @@
"sample_user_journey": "User Journey",
"sample_xy": "XY",
"sample_venn": "Venn",
"sample_ishikawa": "Ishikawa",
"sample_treeview": "TreeView",
"sample_wardley": "Wardley Map"
},
"mind-map": {
"addChild": "Add child",
"addParent": "Add parent",
"addSibling": "Add sibling",
"removeNode": "Remove node",
"focus": "Focus Mode",
"cancelFocus": "Cancel Focus Mode",
"moveUp": "Move up",
"moveDown": "Move down",
"link": "Link",
"linkBidirectional": "Bidirectional Link",
"clickTips": "Please click the target node",
"summary": "Summary"
},
"llm": {
"settings_title": "AI / LLM",
"settings_description": "Configure AI and Large Language Model integrations.",
"feature_not_enabled": "Enable the LLM experimental feature in Settings → Advanced → Experimental features to use AI integrations.",
"add_provider": "Add Provider",
"add_provider_title": "Add AI Provider",
"configured_providers": "Configured Providers",
"no_providers_configured": "No providers configured yet.",
"provider_name": "Name",
"provider_type": "Provider",
"actions": "Actions",
"delete_provider": "Delete",
"delete_provider_confirmation": "Are you sure you want to delete the provider \"{{name}}\"?",
"api_key": "API Key",
"api_key_placeholder": "Enter your API key",
"cancel": "Cancel",
"mcp_title": "MCP (Model Context Protocol)",
"mcp_enabled": "MCP server",
"mcp_enabled_description": "Expose a Model Context Protocol (MCP) endpoint so that AI coding assistants (e.g. Claude Code, GitHub Copilot) can read and modify your notes. The endpoint is only accessible from localhost.",
"mcp_endpoint_title": "Endpoint URL",
"mcp_endpoint_description": "Add this URL to your AI assistant's MCP configuration",
"tools": {
"search_notes": "Search notes",
"get_note": "Get note",
"get_note_content": "Get note content",
"update_note_content": "Update note content",
"append_to_note": "Append to note",
"create_note": "Create note",
"get_attributes": "Get attributes",
"get_attribute": "Get attribute",
"set_attribute": "Set attribute",
"delete_attribute": "Delete attribute",
"get_child_notes": "Get child notes",
"get_subtree": "Get subtree",
"load_skill": "Load skill",
"web_search": "Web search",
"note_in_parent": "<Note/> in <Parent/>",
"get_attachment": "Get attachment",
"get_attachment_content": "Read attachment content",
"rename_note": "Rename note",
"delete_note": "Delete note",
"move_note": "Move note",
"clone_note": "Clone note"
}
},
"common": {
"save": "Save",
"cancel": "Cancel"
"sample_ishikawa": "Ishikawa"
}
}

View File

@@ -1,5 +1,12 @@
{
"about": {
"title": "Acerca de Trilium Notes",
"homepage": "Página principal:",
"app_version": "Versión de la aplicación:",
"db_version": "Versión de base de datos:",
"sync_version": "Versión de sincronización:",
"build_date": "Fecha de creación:",
"build_revision": "Revisión de compilación:",
"data_directory": "Directorio de datos:"
},
"toast": {
@@ -81,24 +88,17 @@
"also_delete_note": "También eliminar la nota"
},
"delete_notes": {
"delete_notes_preview": "Eliminar vista previa de notas",
"close": "Cerrar",
"delete_all_clones_description": "Eliminar también todos los clones (se puede deshacer en cambios recientes)",
"erase_notes_description": "Borrar notas inmediatamente en lugar de la eliminación con respaldo. Esto no se puede deshacer y forzará la recarga de la aplicación.",
"erase_notes_description": "La eliminación normal (suave) solo marca las notas como eliminadas y se pueden recuperar (en el cuadro de diálogo de cambios recientes) dentro de un periodo de tiempo. Al marcar esta opción se borrarán las notas inmediatamente y no será posible recuperarlas.",
"erase_notes_warning": "Eliminar notas permanentemente (no se puede deshacer), incluidos todos los clones. Esto forzará la recarga de la aplicación.",
"notes_to_be_deleted": "Notas a ser eliminadas ({{notesCount}})",
"notes_to_be_deleted": "Las siguientes notas serán eliminadas ({{notesCount}})",
"no_note_to_delete": "No se eliminará ninguna nota (solo clones).",
"broken_relations_to_be_deleted": "Relaciones rotas ({{ relationCount}})",
"broken_relations_to_be_deleted": "Las siguientes relaciones se romperán y serán eliminadas ({{ relationCount}})",
"cancel": "Cancelar",
"title": "Eliminar notas",
"clones_label": "Clones",
"delete_clones_description_one": "También eliminar otro clon. Puede deshacerse en los cambios recientes.",
"delete_clones_description_many": "También eliminar otros {{count}} clones. Puede deshacerse en los cambios recientes.",
"delete_clones_description_other": "También eliminar otros {{count}} clones. Puede deshacerse en los cambios recientes.",
"erase_notes_label": "Borrar permanentemente",
"table_note_with_relation": "Nota con relación",
"table_relation": "Relación",
"table_points_to": "Apunta a (eliminado)",
"delete": "Eliminar"
"ok": "Aceptar",
"deleted_relation_text": "Nota {{- note}} (para ser eliminada) está referenciado por la relación {{- relation}} que se origina en {{- source}}."
},
"export": {
"export_note_title": "Exportar nota",
@@ -209,8 +209,7 @@
"box_size_small": "pequeño (~ 10 líneas)",
"box_size_medium": "medio (~ 30 líneas)",
"box_size_full": "completo (el cuadro muestra el texto completo)",
"button_include": "Incluir nota",
"box_size_expandable": "expandible (colapsado por defecto)"
"button_include": "Incluir nota"
},
"info": {
"modalTitle": "Mensaje informativo",
@@ -271,6 +270,33 @@
"undelete_link": "recuperar",
"confirm_undelete": "¿Quiere recuperar esta nota y sus subnotas?"
},
"revisions": {
"note_revisions": "Revisiones de nota",
"delete_all_revisions": "Eliminar todas las revisiones de esta nota",
"delete_all_button": "Eliminar todas las revisiones",
"help_title": "Ayuda sobre revisiones de notas",
"confirm_delete_all": "¿Quiere eliminar todas las revisiones de esta nota?",
"no_revisions": "Aún no hay revisiones para esta nota...",
"restore_button": "Restaurar",
"confirm_restore": "¿Quiere restaurar esta revisión? Esto sobrescribirá el título actual y el contenido de la nota con esta revisión.",
"delete_button": "Eliminar",
"confirm_delete": "¿Quieres eliminar esta revisión?",
"revisions_deleted": "Se han eliminado las revisiones de nota.",
"revision_restored": "Se ha restaurado la revisión de nota.",
"revision_deleted": "Se ha eliminado la revisión de la nota.",
"snapshot_interval": "Intervalo de respaldo de revisiones de nota: {{seconds}}s.",
"maximum_revisions": "Máximo de revisiones para la nota actual: {{number}}.",
"settings": "Ajustes para revisiones de nota",
"download_button": "Descargar",
"mime": "MIME: ",
"file_size": "Tamaño del archivo:",
"preview_not_available": "La vista previa no está disponible para este tipo de notas.",
"diff_off": "Mostrar contenido",
"diff_on": "Mostrar diferencia",
"diff_off_hint": "Haga clic para mostrar el contenido de la nota",
"diff_not_available": "Diferencias no disponibles.",
"diff_on_hint": "Haga clic para ver las diferencias"
},
"sort_child_notes": {
"sort_children_by": "Ordenar hijos por...",
"sorting_criteria": "Criterios de ordenamiento",
@@ -342,7 +368,7 @@
"calendar_root": "marca la nota que debe usarse como raíz para las notas del día. Sólo uno debe estar marcado como tal.",
"archived": "las notas con esta etiqueta no serán visibles de forma predeterminada en los resultados de búsqueda (tampoco en los cuadros de diálogo Saltar a, Agregar vínculo, etc.).",
"exclude_from_export": "las notas (con su subárbol) no se incluirán en ninguna exportación de notas",
"run": "define en qué eventos debe ejecutarse el script. Los valores posibles son:\n<ul>\n<li>frontendStartup - cuando el frontend de Trilium se inicia (o se actualiza), pero no en móvil.</li>\n<li>mobileStartup - cuando el frontend de Trilium se inicia (o se actualiza), en móvil.</li>\n<li>backendStartup - cuando el backend de Trilium se inicia</li>\n<li>hourly - se ejecuta una vez por hora. Se puede usar la etiqueta adicional <code>runAtHour</code> para especificar a qué hora.</li>\n<li>daily - se ejecuta una vez al día</li>\n</ul>",
"run": "define en qué eventos debe ejecutarse el script. Los valores posibles son:\n<ul>\n<li>frontendStartup - cuando Trilium frontend se inicia (o se actualiza), pero no en móvil.</li>\n<li>mobileStartup - cuando Trilium frontend se inicia (o se actualiza), en móvil.</li>\n<li>backendStartup - cuando Trilium backend se inicia</li>\n<li>hourly - se ejecuta una vez por hora. Se puede usar la etiqueta adicional <code>runAtHour</code> para especificar a qué hora.</li>\n<li>daily - se ejecuta una vez al día</li>\n</ul>",
"run_on_instance": "Definir en qué instancia de Trilium se debe ejecutar esto. Predeterminado para todas las instancias.",
"run_at_hour": "¿A qué hora debería funcionar? Debe usarse junto con <code>#run=hourly</code>. Se puede definir varias veces para varias ejecuciones durante el día.",
"disable_inclusion": "los scripts con esta etiqueta no se incluirán en la ejecución del script principal.",
@@ -420,10 +446,7 @@
"and_more": "... y {{count}} más.",
"print_landscape": "Al exportar a PDF, cambia la orientación de la página a paisaje en lugar de retrato.",
"print_page_size": "Al exportar a PDF, cambia el tamaño de la página. Valores soportados: <code>A0</code>, <code>A1</code>, <code>A2</code>, <code>A3</code>, <code>A4</code>, <code>A5</code>, <code>A6</code>, <code>Legal</code>, <code>Letter</code>, <code>Tabloid</code>, <code>Ledger</code>.",
"color_type": "Color",
"textarea": "Texto multilínea",
"print_scale": "Al exportar a PDF, cambia la escala del contenido renderizado. Los valores varían de 0.1 (10%) a 2 (200%), por defecto es 1 (100%).",
"print_margins": "Al exportar a PDF, establece márgenes de página. Use <code>default</code>, <code>none</code>, <code>minimum</code>, o valores personalizados como <code>top,right,bottom,left</code> en milímetros."
"color_type": "Color"
},
"attribute_editor": {
"help_text_body1": "Para agregar una etiqueta, simplemente escriba, por ejemplo. <code>#rock</code> o si desea agregar también valor, p.e. <code>#año = 2020</code>",
@@ -685,12 +708,7 @@
"export_as_image": "Exportar como imagen",
"export_as_image_png": "PNG (ráster)",
"export_as_image_svg": "SVG (vectorial)",
"note_map": "Mapa de la nota",
"view_ocr_text": "Ver texto OCR",
"word_wrap_auto": "Automático",
"word_wrap_auto_description": "Seguir el ajuste global",
"word_wrap_on": "Encendido",
"word_wrap_off": "Apagado"
"note_map": "Mapa de la nota"
},
"onclick_button": {
"no_click_handler": "El widget de botón '{{componentId}}' no tiene un controlador de clics definido"
@@ -788,13 +806,7 @@
"expand_first_level": "Expandir hijos inmediatos",
"expand_nth_level": "Expandir {{depth}} niveles",
"expand_all_levels": "Expandir todos los niveles",
"hide_child_notes": "Ocultar subnotas en el árbol",
"open_all_in_tabs": "Abrir todo",
"open_all_in_tabs_tooltip": "Abrir todos los resultados en nuevas pestañas",
"open_all_confirm": "Esto abrirá {{count}} notas en nuevas pestañas. ¿Continuar?",
"open_all_too_one": " ",
"open_all_too_many": " ",
"open_all_too_other": " "
"hide_child_notes": "Ocultar subnotas en el árbol"
},
"edited_notes": {
"no_edited_notes_found": "Aún no hay notas editadas en este día...",
@@ -848,8 +860,7 @@
"collapse": "Contraer al tamaño normal",
"title": "Mapa de notas",
"fix-nodes": "Fijar nodos",
"link-distance": "Distancia de enlace",
"too-many-notes": "Este subárbol contiene {{count}} notas, lo que excede el límite de {{max}} que se puede mostrar en el mapa de notas."
"link-distance": "Distancia de enlace"
},
"note_paths": {
"title": "Rutas de nota",
@@ -1054,8 +1065,7 @@
"note_already_in_diagram": "La nota \"{{title}}\" ya está en el diagrama.",
"enter_title_of_new_note": "Ingrese el título de la nueva nota",
"default_new_note_title": "nueva nota",
"click_on_canvas_to_place_new_note": "Haga clic en el lienzo para colocar una nueva nota",
"rename_relation": "Renombrar relación"
"click_on_canvas_to_place_new_note": "Haga clic en el lienzo para colocar una nueva nota"
},
"backend_log": {
"refresh": "Refrescar"
@@ -1064,17 +1074,15 @@
"title": "Comprobación de coherencia",
"find_and_fix_button": "Buscar y solucionar problemas de coherencia",
"finding_and_fixing_message": "Buscando y solucionando problemas de coherencia...",
"issues_fixed_message": "Los problemas de coherencia han sido solucionados.",
"find_and_fix_label": "Buscar y solucionar problemas de coherencia",
"find_and_fix_description": "Escanear y reparar automáticamente cualquier problema de consistencia de datos en la base de datos."
"issues_fixed_message": "Los problemas de coherencia han sido solucionados."
},
"database_anonymization": {
"title": "Anonimización de bases de datos",
"full_anonymization": "Anonimización total",
"full_anonymization_description": "Crea una copia de la base de datos con todo el contenido eliminado, dejando solo la estructura y algunos metadatos no confidenciales. Seguro para compartirla en línea con fines de depuración de problemas.",
"full_anonymization_description": "Esta acción crea una nueva copia de la base de datos y la anonimizará (eliminará todo el contenido de las notas y deja solo la estructura y algunos metadatos no confidenciales) para compartirla en línea con fines de depuración sin temor a filtrar sus datos personales.",
"save_fully_anonymized_database": "Guarde la base de datos completamente anónima",
"light_anonymization": "Anonimización ligera",
"light_anonymization_description": "Crea una copia con el contenido de las notas eliminado, excepto los títulos, atributos y scripts/widgets personalizados que permanecerán. Esto proporciona más contexto para el depurado.",
"light_anonymization_description": "Esta acción crea una nueva copia de la base de datos y realizará una ligera anonimización en ella; específicamente, solo se eliminará el contenido de todas las notas, pero los títulos y atributos permanecerán. Además, se mantendrán las notas de script JS frontend/backend personalizadas y los widgets personalizados. Esto proporciona más contexto para depurar los problemas.",
"choose_anonymization": "Puede decidir usted mismo si desea proporcionar una base de datos total o ligeramente anónima. Incluso una base de datos totalmente anónima es muy útil; sin embargo, en algunos casos, una base de datos ligeramente anónima puede acelerar el proceso de identificación y corrección de errores.",
"save_lightly_anonymized_database": "Guarde una base de datos ligeramente anónima",
"existing_anonymized_databases": "Bases de datos anónimas existentes",
@@ -1083,17 +1091,15 @@
"error_creating_anonymized_database": "No se pudo crear una base de datos anónima; consulte los registros de backend para obtener más detalles",
"successfully_created_fully_anonymized_database": "Se creó una base de datos completamente anónima en {{anonymizedFilePath}}",
"successfully_created_lightly_anonymized_database": "Se creó una base de datos ligeramente anónima en {{anonymizedFilePath}}",
"no_anonymized_database_yet": "Aún no hay base de datos anónima.",
"description": "Crear una copia anónima de su base de datos para compartir con los desarrolladores cuando depuren problemas, sin exponer datos personales."
"no_anonymized_database_yet": "Aún no hay base de datos anónima."
},
"database_integrity_check": {
"title": "Verificación de integridad de la base de datos",
"description": "Esto verificará que la base de datos no esté dañada en el nivel SQLite. Puede que tarde algún tiempo, dependiendo del tamaño de la base de datos.",
"check_button": "Verificar la integridad de la base de datos",
"checking_integrity": "Comprobando la integridad de la base de datos...",
"integrity_check_succeeded": "La verificación de integridad fue exitosa; no se encontraron problemas.",
"integrity_check_failed": "La verificación de integridad falló: {{results}}",
"check_integrity_label": "Verificar la integridad de la base de datos",
"check_integrity_description": "Verifcar que la base de datos no está dañada en el nivel SQLite."
"integrity_check_failed": "La verificación de integridad falló: {{results}}"
},
"sync": {
"title": "Sincronizar",
@@ -1103,10 +1109,7 @@
"filling_entity_changes": "Rellenar filas de cambios de entidad...",
"sync_rows_filled_successfully": "Sincronizar filas completadas correctamente",
"finished-successfully": "La sincronización finalizó exitosamente.",
"failed": "La sincronización falló: {{message}}",
"force_full_sync_label": "Forzar sincronización completa",
"force_full_sync_description": "Activa una sincronización completa con el servidor de sincronización, resubiendo todos los cambios.",
"fill_entity_changes_label": "Llenar cambios de entidad"
"failed": "La sincronización falló: {{message}}"
},
"vacuum_database": {
"title": "Limpiar base de datos",
@@ -1124,7 +1127,10 @@
"note_tree_font": "Fuente del árbol de notas",
"note_detail_font": "Fuente de detalle de nota",
"monospace_font": "Fuente Monospace (código)",
"not_all_fonts_available": "Es posible que no todas las fuentes enumeradas estén disponibles en su sistema",
"note_tree_and_detail_font_sizing": "Tenga en cuenta que el tamaño de fuente del árbol y de los detalles es relativo a la configuración del tamaño de fuente principal.",
"not_all_fonts_available": "Es posible que no todas las fuentes enumeradas estén disponibles en su sistema.",
"apply_font_changes": "Para aplicar cambios de fuente, haga clic en",
"reload_frontend": "recargar la interfaz",
"generic-fonts": "Fuentes genéricas",
"sans-serif-system-fonts": "Fuentes Sans-serif del sistema",
"serif-system-fonts": "Fuentes Serif del sistema",
@@ -1197,20 +1203,18 @@
},
"images": {
"images_section_title": "Imágenes",
"download_images_automatically": "Descargar imágenes automáticamente",
"download_images_automatically": "Descargar imágenes automáticamente para usarlas sin conexión.",
"download_images_description": "El HTML pegado puede contener referencias a imágenes en línea; Trilium encontrará esas referencias y descargará las imágenes para que estén disponibles sin conexión.",
"enable_image_compression": "Habilitar la compresión de imágenes",
"max_image_dimensions": "Dimensiones máximas de imagen",
"max_image_dimensions": "Ancho/alto máximo de una imagen en píxeles (la imagen cambiará de tamaño si excede esta configuración).",
"max_image_dimensions_unit": "píxeles",
"jpeg_quality_description": "Se recomienda un rango 50-85. Valores más pequeños reducen el tamaño del archivo, valores más altos preservan los detalles.",
"enable_image_compression_description": "Comprimir y ajustar el tamaño de las imágenes cuando son subidas o pegadas.",
"max_image_dimensions_description": "Las imágenes que excedan el límite de tamaño serán ajustadas automáticamente.",
"jpeg_quality": "Calidad JPEG"
"jpeg_quality_description": "Calidad JPEG (10 - peor calidad, 100 - mejor calidad, se recomienda 50 - 85)"
},
"attachment_erasure_timeout": {
"attachment_erasure_timeout": "Tiempo de espera para borrar archivos adjuntos",
"attachment_auto_deletion_description": "Los archivos adjuntos se eliminan (y borran) automáticamente si ya no se hace referencia a ellos en su nota después de un tiempo de espera definido.",
"erase_attachments_after": "Borrar archivos adjuntos después de:",
"manual_erasing_description": "Activar el borrado manualmente, ignorando el tiempo de espera definido anteriormente.",
"manual_erasing_description": "También puede activar el borrado manualmente (sin considerar el tiempo de espera definido anteriormente):",
"erase_unused_attachments_now": "Borrar ahora los archivos adjuntos no utilizados en la nota",
"unused_attachments_erased": "Los archivos adjuntos no utilizados se han eliminado."
},
@@ -1220,8 +1224,9 @@
},
"note_erasure_timeout": {
"note_erasure_timeout_title": "Tiempo de espera de borrado de notas",
"note_erasure_description": "Las notas eliminadas (y los atributos, las revisiones ...) en principio solo están marcadas como eliminadas y es posible recuperarlas del diálogo de Notas recientes. Después de un período de tiempo, las notas eliminadas son \" borradas\", lo que significa que su contenido ya no es recuperable. Esta configuración le permite configurar la longitud del período entre eliminar y borrar la nota.",
"erase_notes_after": "Borrar notas después de:",
"manual_erasing_description": "Activar el borrado manualmente, ignorando el tiempo de espera definido anteriormente.",
"manual_erasing_description": "También puede activar el borrado manualmente (sin considerar el tiempo de espera definido anteriormente):",
"erase_deleted_notes_now": "Borrar notas eliminadas ahora",
"deleted_notes_erased": "Las notas eliminadas han sido borradas."
},
@@ -1240,7 +1245,7 @@
},
"search_engine": {
"title": "Motor de búsqueda",
"custom_search_engine_info": "Utilizado al buscar la web para el texto seleccionado. Si no está configurado, DuckDuckGo se utilizado.",
"custom_search_engine_info": "El motor de búsqueda personalizado requiere que se establezcan un nombre y una URL. Si alguno de estos no está configurado, DuckDuckGo se utilizará como motor de búsqueda predeterminado.",
"predefined_templates_label": "Plantillas de motor de búsqueda predefinidas",
"bing": "Bing",
"baidu": "Baidu",
@@ -1304,6 +1309,7 @@
"first-week-contains-first-thursday": "Primer semana que contiene al primer jueves del año",
"first-week-has-minimum-days": "Primer semana que contiene un mínimo de días",
"min-days-in-first-week": "Días mínimos en la primer semana",
"first-week-info": "Primer semana que contiene al primer jueves del año está basado en el estándar<a href=\"https://en.wikipedia.org/wiki/ISO_week_date#First_week\">ISO 8601</a>.",
"first-week-warning": "Cambiar las opciones de primer semana puede causar duplicados con las Notas Semanales existentes y las Notas Semanales existentes no serán actualizadas respectivamente.",
"formatting-locale": "Fecha y formato de número",
"tuesday": "Martes",
@@ -1326,8 +1332,7 @@
"date-and-time": "Fecha y hora",
"path": "Ruta",
"database_backed_up_to": "Se ha realizado una copia de seguridad de la base de datos en {{backupFilePath}}",
"no_backup_yet": "no hay copia de seguridad todavía",
"download": "Descargar"
"no_backup_yet": "no hay copia de seguridad todavía"
},
"etapi": {
"title": "ETAPI",
@@ -1427,13 +1432,20 @@
"description": "Estas opciones se aplican sólo para compilaciones de escritorio; los navegadores utilizarán su corrector ortográfico nativo.",
"enable": "Habilitar corrector ortográfico",
"language_code_label": "Código(s) de idioma",
"language_code_placeholder": "por ejemplo \"en-US\", \"de-AT\"",
"multiple_languages_info": "Múltiples idiomas se pueden separar por coma, por ejemplo \"en-US, de-DE, cs\". ",
"available_language_codes_label": "Códigos de idioma disponibles:",
"restart-required": "Los cambios en las opciones de corrección ortográfica entrarán en vigor después del reinicio de la aplicación."
},
"sync_2": {
"config_title": "Configuración de sincronización",
"server_address": "Dirección de la instancia del servidor",
"timeout": "Tiempo de espera de sincronización (milisegundos)",
"timeout_unit": "milisegundos",
"proxy_label": "Sincronizar servidor proxy (opcional)",
"note": "Nota",
"note_description": "Si deja la configuración del proxy en blanco, se utilizará el proxy del sistema (se aplica únicamente a la compilación de escritorio/electron).",
"special_value_description": "Otro valor especial es <code>noproxy</code> que obliga a ignorar incluso al proxy del sistema y respeta <code>NODE_TLS_REJECT_UNAUTHORIZED</code>.",
"save": "Guardar",
"help": "Ayuda",
"test_title": "Prueba de sincronización",
@@ -1615,7 +1627,7 @@
"note_detail": {
"could_not_find_typewidget": "No se pudo encontrar typeWidget para el tipo '{{type}}'",
"printing": "Impresión en curso...",
"printing_pdf": "Preparando vista preliminar de impresión...",
"printing_pdf": "Exportando a PDF en curso..",
"print_report_collection_content_one": "{{count}} nota en la colección no se puede imprimir porque no son compatibles o está protegida.",
"print_report_collection_content_many": "{{count}} notas en la colección no se pueden imprimir porque no son compatibles o están protegidas.",
"print_report_collection_content_other": "{{count}} notas en la colección no se pueden imprimir porque no son compatibles o están protegidas.",
@@ -1760,7 +1772,7 @@
"title": "Fijo",
"description": "las herramientas de edición aparecen en la pestaña de la cinta \"Formato\")."
},
"multiline-toolbar": "Mostrar la barra de herramientas en múltiples líneas si no cabe"
"multiline-toolbar": "Mostrar la barra de herramientas en múltiples líneas si no cabe."
}
},
"electron_context_menu": {

View File

@@ -1,5 +1,12 @@
{
"about": {
"title": "درباره Trilium Notes",
"homepage": "صفحه اصلی:",
"app_version": "نسخه برنامه:",
"db_version": "نسخه پایگاه داده:",
"sync_version": "نسخه منطبق:",
"build_date": "تاریخ ساخت:",
"build_revision": "نسخه بازنگری شده:",
"data_directory": "دایرکتوری داده:"
},
"toast": {

Some files were not shown because too many files have changed in this diff Show More