Merge branch 'main' into siriusbcd_close_split

This commit is contained in:
SiriusXT
2025-11-17 21:03:54 +08:00
34 changed files with 1036 additions and 656 deletions

View File

@@ -91,6 +91,7 @@ export default function CalendarView({ note, noteIds }: ViewModeProps<CalendarVi
const [ hideWeekends ] = useNoteLabelBoolean(note, "calendar:hideWeekends");
const [ weekNumbers ] = useNoteLabelBoolean(note, "calendar:weekNumbers");
const [ calendarView, setCalendarView ] = useNoteLabel(note, "calendar:view");
const [ initialDate ] = useNoteLabel(note, "calendar:initialDate");
const initialView = useRef(calendarView);
const viewSpacedUpdate = useSpacedUpdate(() => setCalendarView(initialView.current));
useResizeObserver(containerRef, () => calendarRef.current?.updateSize());
@@ -134,6 +135,7 @@ export default function CalendarView({ note, noteIds }: ViewModeProps<CalendarVi
height="90%"
nowIndicator
handleWindowResize={false}
initialDate={initialDate || undefined}
locale={locale}
{...editingProps}
eventDidMount={eventDidMount}

View File

@@ -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<DISPLAYABLE_LOCALE_IDS, Options["locale"] | null> = {
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<MindElixirInstance>(null);
const containerRef = useRef<HTMLDivElement>(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<HTMLDivElement>(externalContainerRef, null);
const apiRef = useRef<MindElixirInstance>(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(() => {

View File

@@ -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,

View File

@@ -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<string>();
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}.`)
}
}
});
});

View File

@@ -0,0 +1,19 @@
import type { DISPLAYABLE_LOCALE_IDS } from "@triliumnext/commons";
export const LANGUAGE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, string | null> = {
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"
};

View File

@@ -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<HTMLProps<HTMLDivElement>, "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<HTMLDivElement>(externalContainerRef, null);
const watchdogRef = useRef<EditorWatchdog>(null);
const [ uiLanguage ] = useTriliumOption("locale");
const [ editor, setEditor ] = useState<CKTextEditor>();
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 ]);

View File

@@ -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);
}
}
});
});

View File

@@ -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): Promise<EditorConfi
htmlSupport: {
allow: JSON.parse(options.get("allowedHtmlTags"))
},
// This value must be kept in sync with the language defined in webpack.config.js.
language: "en",
removePlugins: getDisabledPlugins()
removePlugins: getDisabledPlugins(),
...await getCkLocale(opts.uiLanguage)
};
// Set up content language.