diff --git a/apps/client/src/widgets/type_widgets/MindMap.tsx b/apps/client/src/widgets/type_widgets/MindMap.tsx
index 4b3c8fe0d..b4ccdf371 100644
--- a/apps/client/src/widgets/type_widgets/MindMap.tsx
+++ b/apps/client/src/widgets/type_widgets/MindMap.tsx
@@ -1,15 +1,16 @@
import { useCallback, useEffect, useRef } from "preact/hooks";
import { TypeWidgetProps } from "./type_widget";
-import { MindElixirData, MindElixirInstance, Operation, default as VanillaMindElixir } from "mind-elixir";
+import { MindElixirData, MindElixirInstance, Operation, Options, default as VanillaMindElixir } from "mind-elixir";
import { HTMLAttributes, RefObject } from "preact";
// allow node-menu plugin css to be bundled by webpack
import nodeMenu from "@mind-elixir/node-menu";
import "mind-elixir/style";
import "@mind-elixir/node-menu/dist/style.css";
import "./MindMap.css";
-import { useEditorSpacedUpdate, useNoteLabelBoolean, useSyncedRef, useTriliumEvent, useTriliumEvents } from "../react/hooks";
+import { useEditorSpacedUpdate, useNoteLabelBoolean, useSyncedRef, useTriliumEvent, useTriliumEvents, useTriliumOption } from "../react/hooks";
import { refToJQuerySelector } from "../react/react_utils";
import utils from "../../services/utils";
+import { DISPLAYABLE_LOCALE_IDS } from "@triliumnext/commons";
const NEW_TOPIC_NAME = "";
@@ -21,6 +22,24 @@ interface MindElixirProps {
onChange?: () => void;
}
+const LOCALE_MAPPINGS: Record = {
+ ar: null,
+ cn: "zh_CN",
+ de: null,
+ en: "en",
+ en_rtl: "en",
+ es: "es",
+ fr: "fr",
+ it: "it",
+ ja: "ja",
+ pt: "pt",
+ pt_br: "pt",
+ ro: null,
+ ru: "ru",
+ tw: "zh_TW",
+ uk: null
+};
+
export default function MindMap({ note, ntxId, noteContext }: TypeWidgetProps) {
const apiRef = useRef(null);
const containerRef = useRef(null);
@@ -110,12 +129,14 @@ export default function MindMap({ note, ntxId, noteContext }: TypeWidgetProps) {
function MindElixir({ containerRef: externalContainerRef, containerProps, apiRef: externalApiRef, onChange, editable }: MindElixirProps) {
const containerRef = useSyncedRef(externalContainerRef, null);
const apiRef = useRef(null);
+ const [ locale ] = useTriliumOption("locale");
function reinitialize() {
if (!containerRef.current) return;
const mind = new VanillaMindElixir({
el: containerRef.current,
+ locale: LOCALE_MAPPINGS[locale as DISPLAYABLE_LOCALE_IDS] ?? undefined,
editable
});
@@ -143,7 +164,7 @@ function MindElixir({ containerRef: externalContainerRef, containerProps, apiRef
if (data) {
apiRef.current?.init(data);
}
- }, [ editable ]);
+ }, [ editable, locale ]);
// On change listener.
useEffect(() => {
diff --git a/apps/client/src/widgets/type_widgets/canvas/Canvas.tsx b/apps/client/src/widgets/type_widgets/canvas/Canvas.tsx
index 79f7f3795..6a5ea9377 100644
--- a/apps/client/src/widgets/type_widgets/canvas/Canvas.tsx
+++ b/apps/client/src/widgets/type_widgets/canvas/Canvas.tsx
@@ -1,7 +1,7 @@
import { Excalidraw } from "@excalidraw/excalidraw";
import { TypeWidgetProps } from "../type_widget";
import "@excalidraw/excalidraw/index.css";
-import { useNoteLabelBoolean } from "../../react/hooks";
+import { useNoteLabelBoolean, useTriliumOption } from "../../react/hooks";
import { useCallback, useMemo, useRef } from "preact/hooks";
import { type ExcalidrawImperativeAPI, type AppState } from "@excalidraw/excalidraw/types";
import options from "../../../services/options";
@@ -9,6 +9,8 @@ import "./Canvas.css";
import { NonDeletedExcalidrawElement } from "@excalidraw/excalidraw/element/types";
import { goToLinkExt } from "../../../services/link";
import useCanvasPersistence from "./persistence";
+import { LANGUAGE_MAPPINGS } from "./i18n";
+import { DISPLAYABLE_LOCALE_IDS } from "@triliumnext/commons";
// currently required by excalidraw, in order to allows self-hosting fonts locally.
// this avoids making excalidraw load the fonts from an external CDN.
@@ -21,6 +23,7 @@ export default function Canvas({ note, noteContext }: TypeWidgetProps) {
const documentStyle = window.getComputedStyle(document.documentElement);
return documentStyle.getPropertyValue("--theme-style")?.trim() as AppState["theme"];
}, []);
+ const [ locale ] = useTriliumOption("locale");
const persistence = useCanvasPersistence(note, noteContext, apiRef, themeStyle, isReadOnly);
/** Use excalidraw's native zoom instead of the global zoom. */
@@ -58,6 +61,7 @@ export default function Canvas({ note, noteContext }: TypeWidgetProps) {
detectScroll={false}
handleKeyboardGlobally={false}
autoFocus={false}
+ langCode={LANGUAGE_MAPPINGS[locale as DISPLAYABLE_LOCALE_IDS] ?? undefined}
UIOptions={{
canvasActions: {
saveToActiveFile: false,
diff --git a/apps/client/src/widgets/type_widgets/canvas/i18n.spec.ts b/apps/client/src/widgets/type_widgets/canvas/i18n.spec.ts
new file mode 100644
index 000000000..71eb3d18c
--- /dev/null
+++ b/apps/client/src/widgets/type_widgets/canvas/i18n.spec.ts
@@ -0,0 +1,29 @@
+import { LOCALES } from "@triliumnext/commons";
+import { readdirSync } from "fs";
+import { join } from "path";
+import { describe, expect, it } from "vitest";
+import { LANGUAGE_MAPPINGS } from "./i18n.js";
+
+const localeDir = join(__dirname, "../../../../../../node_modules/@excalidraw/excalidraw/dist/prod/locales");
+
+describe("Canvas i18n", () => {
+ it("all languages are mapped correctly", () => {
+ // Read the node_modules dir to obtain all the supported locales.
+ const supportedLanguageCodes = new Set();
+ for (const file of readdirSync(localeDir)) {
+ if (file.startsWith("percentages")) continue;
+ const match = file.match("^[a-z]{2,3}(?:-[A-Z]{2,3})?");
+ if (!match) continue;
+ supportedLanguageCodes.add(match[0]);
+ }
+
+ // Cross-check the locales.
+ for (const locale of LOCALES) {
+ if (locale.contentOnly || locale.devOnly) continue;
+ const languageCode = LANGUAGE_MAPPINGS[locale.id];
+ if (!supportedLanguageCodes.has(languageCode)) {
+ expect.fail(`Unable to find locale for ${locale.id} -> ${languageCode}.`)
+ }
+ }
+ });
+});
diff --git a/apps/client/src/widgets/type_widgets/canvas/i18n.ts b/apps/client/src/widgets/type_widgets/canvas/i18n.ts
new file mode 100644
index 000000000..43ee724cf
--- /dev/null
+++ b/apps/client/src/widgets/type_widgets/canvas/i18n.ts
@@ -0,0 +1,19 @@
+import type { DISPLAYABLE_LOCALE_IDS } from "@triliumnext/commons";
+
+export const LANGUAGE_MAPPINGS: Record = {
+ ar: "ar-SA",
+ cn: "zh-CN",
+ de: "de-DE",
+ en: "en",
+ en_rtl: "en",
+ es: "es-ES",
+ fr: "fr-FR",
+ it: "it-IT",
+ ja: "ja-JP",
+ pt: "pt-PT",
+ pt_br: "pt-BR",
+ ro: "ro-RO",
+ ru: "ru-RU",
+ tw: "zh-TW",
+ uk: "uk-UA"
+};
diff --git a/apps/client/src/widgets/type_widgets/text/CKEditorWithWatchdog.tsx b/apps/client/src/widgets/type_widgets/text/CKEditorWithWatchdog.tsx
index b7346dd9a..fd6814528 100644
--- a/apps/client/src/widgets/type_widgets/text/CKEditorWithWatchdog.tsx
+++ b/apps/client/src/widgets/type_widgets/text/CKEditorWithWatchdog.tsx
@@ -1,9 +1,10 @@
import { HTMLProps, RefObject, useEffect, useImperativeHandle, useRef, useState } from "preact/compat";
import { PopupEditor, ClassicEditor, EditorWatchdog, type WatchdogConfig, CKTextEditor, TemplateDefinition } from "@triliumnext/ckeditor5";
import { buildConfig, BuildEditorOptions } from "./config";
-import { useKeyboardShortcuts, useLegacyImperativeHandlers, useNoteContext, useSyncedRef } from "../../react/hooks";
+import { useKeyboardShortcuts, useLegacyImperativeHandlers, useNoteContext, useSyncedRef, useTriliumOption } from "../../react/hooks";
import link from "../../../services/link";
import froca from "../../../services/froca";
+import { DISPLAYABLE_LOCALE_IDS } from "@triliumnext/commons";
export type BoxSize = "small" | "medium" | "full";
@@ -37,6 +38,7 @@ interface CKEditorWithWatchdogProps extends Pick, "cla
export default function CKEditorWithWatchdog({ containerRef: externalContainerRef, content, contentLanguage, className, tabIndex, isClassicEditor, watchdogRef: externalWatchdogRef, watchdogConfig, onNotificationWarning, onWatchdogStateChange, onChange, onEditorInitialized, editorApi, templates }: CKEditorWithWatchdogProps) {
const containerRef = useSyncedRef(externalContainerRef, null);
const watchdogRef = useRef(null);
+ const [ uiLanguage ] = useTriliumOption("locale");
const [ editor, setEditor ] = useState();
const { parentComponent } = useNoteContext();
@@ -156,6 +158,7 @@ export default function CKEditorWithWatchdog({ containerRef: externalContainerRe
const editor = await buildEditor(container, !!isClassicEditor, {
forceGplLicense: false,
isClassicEditor: !!isClassicEditor,
+ uiLanguage: uiLanguage as DISPLAYABLE_LOCALE_IDS,
contentLanguage: contentLanguage ?? null,
templates
});
@@ -180,7 +183,7 @@ export default function CKEditorWithWatchdog({ containerRef: externalContainerRe
watchdog.create(container);
return () => watchdog.destroy();
- }, [ contentLanguage, templates ]);
+ }, [ contentLanguage, templates, uiLanguage ]);
// React to content changes.
useEffect(() => editor?.setData(content ?? ""), [ editor, content ]);
diff --git a/apps/client/src/widgets/type_widgets/text/config.spec.ts b/apps/client/src/widgets/type_widgets/text/config.spec.ts
new file mode 100644
index 000000000..5e85bab3b
--- /dev/null
+++ b/apps/client/src/widgets/type_widgets/text/config.spec.ts
@@ -0,0 +1,39 @@
+import { DISPLAYABLE_LOCALE_IDS, LOCALES } from "@triliumnext/commons";
+import { describe, expect, it, vi } from "vitest";
+
+vi.mock('../../../services/options.js', () => ({
+ default: {
+ get(name: string) {
+ if (name === "allowedHtmlTags") return "[]";
+ return undefined;
+ },
+ getJson: () => []
+ }
+}));
+
+describe("CK config", () => {
+ it("maps all languages correctly", async () => {
+ const { buildConfig } = await import("./config.js");
+ for (const locale of LOCALES) {
+ if (locale.contentOnly || locale.devOnly) continue;
+
+ const config = await buildConfig({
+ uiLanguage: locale.id as DISPLAYABLE_LOCALE_IDS,
+ contentLanguage: locale.id,
+ forceGplLicense: false,
+ isClassicEditor: false,
+ templates: []
+ });
+
+ let expectedLocale = locale.id.substring(0, 2);
+ if (expectedLocale === "cn") expectedLocale = "zh";
+ if (expectedLocale === "tw") expectedLocale = "zh-tw";
+
+ if (locale.id !== "en") {
+ expect((config.language as any).ui).toMatch(new RegExp(`^${expectedLocale}`));
+ expect(config.translations, locale.id).toBeDefined();
+ expect(config.translations, locale.id).toHaveLength(2);
+ }
+ }
+ });
+});
diff --git a/apps/client/src/widgets/type_widgets/text/config.ts b/apps/client/src/widgets/type_widgets/text/config.ts
index 7f39c4ea2..a12d384ef 100644
--- a/apps/client/src/widgets/type_widgets/text/config.ts
+++ b/apps/client/src/widgets/type_widgets/text/config.ts
@@ -1,5 +1,5 @@
-import { ALLOWED_PROTOCOLS, MIME_TYPE_AUTO } from "@triliumnext/commons";
-import { buildExtraCommands, type EditorConfig, PREMIUM_PLUGINS, TemplateDefinition } from "@triliumnext/ckeditor5";
+import { ALLOWED_PROTOCOLS, DISPLAYABLE_LOCALE_IDS, MIME_TYPE_AUTO } from "@triliumnext/commons";
+import { buildExtraCommands, type EditorConfig, getCkLocale, PREMIUM_PLUGINS, TemplateDefinition } from "@triliumnext/ckeditor5";
import { getHighlightJsNameForMime } from "../../../services/mime_types.js";
import options from "../../../services/options.js";
import { ensureMimeTypesForHighlighting, isSyntaxHighlightEnabled } from "../../../services/syntax_highlight.js";
@@ -17,6 +17,7 @@ export const OPEN_SOURCE_LICENSE_KEY = "GPL";
export interface BuildEditorOptions {
forceGplLicense: boolean;
isClassicEditor: boolean;
+ uiLanguage: DISPLAYABLE_LOCALE_IDS;
contentLanguage: string | null;
templates: TemplateDefinition[];
}
@@ -161,9 +162,8 @@ export async function buildConfig(opts: BuildEditorOptions): PromiseIn Trilium, attributes are key-value pairs assigned to notes, providing
additional metadata or functionality. There are two primary types of attributes:
-
+
Labels can
be used for a variety of purposes, such as storing metadata or configuring
the behavior of notes. Labels are also searchable, enhancing note retrieval.
For more information, including predefined labels, see Labels.
-
+
Relations define
connections between notes, similar to links. These can be used for metadata
and scripting purposes.
@@ -27,25 +27,24 @@
Conceptually there are two types of attributes (applying to both labels
and relations):
-
System attributes
+
System attributes As the name suggest, these attributes have a special meaning since they
are interpreted by Trilium. For example the color attribute
will change the color of the note as displayed in the Note Tree and
- links, and iconClass will change the icon of a note.
-
-
User-defined attributes
+ href="#root/_help_oPVyFC7WL2Lp">Note Tree and links, and iconClass will
+ change the icon of a note.
+
User-defined attributes These are free-form labels or relations that can be used by the user.
They can be used purely for categorization purposes (especially if combined
- with Search),
+ with Search),
or they can be given meaning through the use of Scripting.
+ href="#root/_help_CdNpE2pqjmI6">Scripting.
In practice, Trilium makes no direct distinction of whether an attribute
is a system one or a user-defined one. A label or relation is considered
a system attribute if it matches one of the built-in names (e.g. like the
aforementioned iconClass). Keep this in mind when creating
- Promoted Attributes in
+ Promoted Attributes in
order not to accidentally alter a system attribute (unless intended).
Viewing the list of attributes
Both the labels and relations for the current note are displayed in the Owned Attributes section
@@ -56,14 +55,13 @@
In the list of attributes, labels are prefixed with the # character
whereas relations are prefixed with the ~ character.
Promoted Attributes create
a form-like editing experience for attributes, which makes it easy to enhancing
the organization and management of attributes
Multiplicity
Attributes in Trilium can be "multi-valued", meaning multiple attributes
with the same name can co-exist. This can be combined with Promoted Attributes to
- easily add them.
+ href="#root/_help_OFXdgB2nNk1F">Promoted Attributes to easily add them.
Attribute Inheritance
Trilium supports attribute inheritance, allowing child notes to inherit
attributes from their parents. For more information, see
Select either Add new label definition or Add new relation definition.
-
Select the name which will be name of the label or relation that will
- be created when the promoted attribute is edited.
-
Ensure Promoted is checked in order to display it at the top of
- notes.
-
Optionally, choose an Alias which will be displayed next to the
- promoted attribute instead of the attribute name. Generally it's best to
- choose a “user-friendly” name since it can contain spaces and other characters
- which are not supported as attribute names.
-
Check Inheritable to apply it to this note and all its descendants.
- To keep it only for the current note, un-check it.
Select either Add new label definition or Add new relation definition.
+
Select the name which will be name of the label or relation that will
+ be created when the promoted attribute is edited.
+
Ensure Promoted is checked in order to display it at the top of
+ notes.
+
Optionally, choose an Alias which will be displayed next to the
+ promoted attribute instead of the attribute name. Generally it's best to
+ choose a “user-friendly” name since it can contain spaces and other characters
+ which are not supported as attribute names.
+
Check Inheritable to apply it to this note and all its descendants.
+ To keep it only for the current note, un-check it.
+
Press “Save & Close” to apply the changes.
How attribute definitions actually work
When a new promoted attribute definition is created, it creates a corresponding
@@ -54,37 +52,37 @@
The only purpose of the attribute definition is to set up a template.
If the attribute was marked as promoted, then it's also displayed to the
user for easy editing.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Notice how the promoted attribute definition only creates a “Due date”
- box above the text content.
-
-
-
-
-
-
-
-
Once a value is set by the user, a new label (or relation, depending on
- the type) is created. The name of the attribute matches one set when creating
- the promoted attribute.
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Notice how the promoted attribute definition only creates a “Due date”
+ box above the text content.
+
+
+
+
+
+
+
+
Once a value is set by the user, a new label (or relation, depending on
+ the type) is created. The name of the attribute matches one set when creating
+ the promoted attribute.
+
+
+
So there's one attribute for value and one for definition. But notice
how an definition attribute can be made Inheritable,
meaning that it's also applied to all descendant notes. In this case, the
@@ -95,22 +93,22 @@
to be able to easily alter them.
Collections already
make use of this practice, for example:
-
Calendars add “Start Date”, “End Date”, “Start Time” and “End Time” as
+
Calendars add “Start Date”, “End Date”, “Start Time” and “End Time” as
promoted attributes. These map to system attributes such as startDate which
are then interpreted by the calendar view.
Presentation adds
a “Background” promoted attribute for each of the slide to easily be able
to customize.
-
The Trilium documentation (which is edited in Trilium) uses a promoted
+
The Trilium documentation (which is edited in Trilium) uses a promoted
attribute to be able to easily edit the #shareAlias (see
Sharing) in order to form clean URLs.
-
If you always edit a particular system attribute such as #color,
+ class="reference-link" href="#root/_help_R9pX4DGra2Vt">Sharing) in order to form clean URLs.
+
If you always edit a particular system attribute such as #color,
simply create a promoted attribute for it to make it easier.
Right click on any note on the note tree and select Insert child note → Geo Map (beta).
-
-
-
2
-
-
-
-
-
-
By default the map will be empty and will show the entire world.
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
1
+
+
+
+
+
+
Right click on any note on the note tree and select Insert child note → Geo Map (beta).
+
+
+
2
+
+
+
+
+
+
By default the map will be empty and will show the entire world.
+
+
+
+
Repositioning the map
-
Click and drag the map in order to move across the map.
-
Use the mouse wheel, two-finger gesture on a touchpad or the +/- buttons
+
Click and drag the map in order to move across the map.
+
Use the mouse wheel, two-finger gesture on a touchpad or the +/- buttons
on the top-left to adjust the zoom.
The position on the map and the zoom are saved inside the map note and
restored when visiting again the note.
Adding a marker using the map
Adding a new note using the plus button
-
-
-
-
-
-
-
-
-
-
-
-
1
-
To create a marker, first navigate to the desired point on the map. Then
- press the
- button in the Floating buttons (top-right)
- area.
-
- If the button is not visible, make sure the button section is visible
- by pressing the chevron button (
- ) in the top-right of the map.
-
-
-
-
2
-
-
-
-
Once pressed, the map will enter in the insert mode, as illustrated by
- the notification.
-
- Simply click the point on the map where to place the marker, or the Escape
- key to cancel.
-
-
-
3
-
-
-
-
Enter the name of the marker/note to be created.
-
-
-
4
-
-
-
-
Once confirmed, the marker will show up on the map and it will also be
- displayed as a child note of the map.
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
1
+
To create a marker, first navigate to the desired point on the map. Then
+ press the
+ button in the Floating buttons (top-right)
+ area.
+
+ If the button is not visible, make sure the button section is visible
+ by pressing the chevron button (
+ ) in the top-right of the map.
+
+
+
+
2
+
+
+
+
Once pressed, the map will enter in the insert mode, as illustrated by
+ the notification.
+
+ Simply click the point on the map where to place the marker, or the Escape
+ key to cancel.
+
+
+
3
+
+
+
+
Enter the name of the marker/note to be created.
+
+
+
4
+
+
+
+
Once confirmed, the marker will show up on the map and it will also be
+ displayed as a child note of the map.
+
+
+
+
Adding a new note using the contextual menu
-
Right click anywhere on the map, where to place the newly created marker
+
Right click anywhere on the map, where to place the newly created marker
(and corresponding note).