feat(ckeditor5): set up multi-language support

This commit is contained in:
Elian Doran 2025-11-16 14:24:07 +02:00
parent d014ae4fcf
commit a10d99f938
No known key found for this signature in database
4 changed files with 101 additions and 7 deletions

View File

@ -1,9 +1,10 @@
import { HTMLProps, RefObject, useEffect, useImperativeHandle, useRef, useState } from "preact/compat"; import { HTMLProps, RefObject, useEffect, useImperativeHandle, useRef, useState } from "preact/compat";
import { PopupEditor, ClassicEditor, EditorWatchdog, type WatchdogConfig, CKTextEditor, TemplateDefinition } from "@triliumnext/ckeditor5"; import { PopupEditor, ClassicEditor, EditorWatchdog, type WatchdogConfig, CKTextEditor, TemplateDefinition } from "@triliumnext/ckeditor5";
import { buildConfig, BuildEditorOptions } from "./config"; 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 link from "../../../services/link";
import froca from "../../../services/froca"; import froca from "../../../services/froca";
import { DISPLAYABLE_LOCALE_IDS } from "@triliumnext/commons";
export type BoxSize = "small" | "medium" | "full"; 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) { 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 containerRef = useSyncedRef<HTMLDivElement>(externalContainerRef, null);
const watchdogRef = useRef<EditorWatchdog>(null); const watchdogRef = useRef<EditorWatchdog>(null);
const [ uiLanguage ] = useTriliumOption("locale");
const [ editor, setEditor ] = useState<CKTextEditor>(); const [ editor, setEditor ] = useState<CKTextEditor>();
const { parentComponent } = useNoteContext(); const { parentComponent } = useNoteContext();
@ -156,6 +158,7 @@ export default function CKEditorWithWatchdog({ containerRef: externalContainerRe
const editor = await buildEditor(container, !!isClassicEditor, { const editor = await buildEditor(container, !!isClassicEditor, {
forceGplLicense: false, forceGplLicense: false,
isClassicEditor: !!isClassicEditor, isClassicEditor: !!isClassicEditor,
uiLanguage: uiLanguage as DISPLAYABLE_LOCALE_IDS,
contentLanguage: contentLanguage ?? null, contentLanguage: contentLanguage ?? null,
templates templates
}); });
@ -180,7 +183,7 @@ export default function CKEditorWithWatchdog({ containerRef: externalContainerRe
watchdog.create(container); watchdog.create(container);
return () => watchdog.destroy(); return () => watchdog.destroy();
}, [ contentLanguage, templates ]); }, [ contentLanguage, templates, uiLanguage ]);
// React to content changes. // React to content changes.
useEffect(() => editor?.setData(content ?? ""), [ editor, content ]); useEffect(() => editor?.setData(content ?? ""), [ editor, content ]);

View File

@ -1,5 +1,5 @@
import { ALLOWED_PROTOCOLS, MIME_TYPE_AUTO } from "@triliumnext/commons"; import { ALLOWED_PROTOCOLS, DISPLAYABLE_LOCALE_IDS, MIME_TYPE_AUTO } from "@triliumnext/commons";
import { buildExtraCommands, type EditorConfig, PREMIUM_PLUGINS, TemplateDefinition } from "@triliumnext/ckeditor5"; import { buildExtraCommands, type EditorConfig, getCkLocale, PREMIUM_PLUGINS, TemplateDefinition } from "@triliumnext/ckeditor5";
import { getHighlightJsNameForMime } from "../../../services/mime_types.js"; import { getHighlightJsNameForMime } from "../../../services/mime_types.js";
import options from "../../../services/options.js"; import options from "../../../services/options.js";
import { ensureMimeTypesForHighlighting, isSyntaxHighlightEnabled } from "../../../services/syntax_highlight.js"; import { ensureMimeTypesForHighlighting, isSyntaxHighlightEnabled } from "../../../services/syntax_highlight.js";
@ -17,6 +17,7 @@ export const OPEN_SOURCE_LICENSE_KEY = "GPL";
export interface BuildEditorOptions { export interface BuildEditorOptions {
forceGplLicense: boolean; forceGplLicense: boolean;
isClassicEditor: boolean; isClassicEditor: boolean;
uiLanguage: DISPLAYABLE_LOCALE_IDS;
contentLanguage: string | null; contentLanguage: string | null;
templates: TemplateDefinition[]; templates: TemplateDefinition[];
} }
@ -161,9 +162,8 @@ export async function buildConfig(opts: BuildEditorOptions): Promise<EditorConfi
htmlSupport: { htmlSupport: {
allow: JSON.parse(options.get("allowedHtmlTags")) allow: JSON.parse(options.get("allowedHtmlTags"))
}, },
// This value must be kept in sync with the language defined in webpack.config.js. removePlugins: getDisabledPlugins(),
language: "en", ...await getCkLocale(opts.uiLanguage)
removePlugins: getDisabledPlugins()
}; };
// Set up content language. // Set up content language.

View File

@ -0,0 +1,90 @@
import { DISPLAYABLE_LOCALE_IDS } from "@triliumnext/commons";
import { EditorConfig, Translations } from "ckeditor5";
interface LocaleMapping {
languageCode: string;
coreTranslation: () => Promise<{ default: Translations }>;
premiumFeaturesTranslation: () => Promise<{ default: Translations }>;
}
const LOCALE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, LocaleMapping | null> = {
en: null,
en_rtl: null,
ar: {
languageCode: "ar",
coreTranslation: () => import("ckeditor5/translations/ar.js"),
premiumFeaturesTranslation: () => import("ckeditor5-premium-features/translations/ar.js"),
},
cn: {
languageCode: "zh",
coreTranslation: () => import("ckeditor5/translations/zh-cn.js"),
premiumFeaturesTranslation: () => import("ckeditor5-premium-features/translations/zh-cn.js"),
},
de: {
languageCode: "de",
coreTranslation: () => import("ckeditor5/translations/de.js"),
premiumFeaturesTranslation: () => import("ckeditor5-premium-features/translations/de.js"),
},
es: {
languageCode: "es",
coreTranslation: () => import("ckeditor5/translations/es.js"),
premiumFeaturesTranslation: () => import("ckeditor5-premium-features/translations/es.js"),
},
fr: {
languageCode: "fr",
coreTranslation: () => import("ckeditor5/translations/fr.js"),
premiumFeaturesTranslation: () => import("ckeditor5-premium-features/translations/fr.js"),
},
it: {
languageCode: "it",
coreTranslation: () => import("ckeditor5/translations/it.js"),
premiumFeaturesTranslation: () => import("ckeditor5-premium-features/translations/it.js"),
},
ja: {
languageCode: "ja",
coreTranslation: () => import("ckeditor5/translations/ja.js"),
premiumFeaturesTranslation: () => import("ckeditor5-premium-features/translations/ja.js"),
},
pt: {
languageCode: "pt",
coreTranslation: () => import("ckeditor5/translations/pt.js"),
premiumFeaturesTranslation: () => import("ckeditor5-premium-features/translations/pt.js"),
},
pt_br: {
languageCode: "pt-br",
coreTranslation: () => import("ckeditor5/translations/pt-br.js"),
premiumFeaturesTranslation: () => import("ckeditor5-premium-features/translations/pt-br.js"),
},
ro: {
languageCode: "ro",
coreTranslation: () => import("ckeditor5/translations/ro.js"),
premiumFeaturesTranslation: () => import("ckeditor5-premium-features/translations/ro.js"),
},
tw: {
languageCode: "zh-tw",
coreTranslation: () => import("ckeditor5/translations/zh.js"),
premiumFeaturesTranslation: () => import("ckeditor5-premium-features/translations/zh.js"),
},
uk: {
languageCode: "uk",
coreTranslation: () => import("ckeditor5/translations/uk.js"),
premiumFeaturesTranslation: () => import("ckeditor5-premium-features/translations/uk.js"),
},
ru: {
languageCode: "ru",
coreTranslation: () => import("ckeditor5/translations/ru.js"),
premiumFeaturesTranslation: () => import("ckeditor5-premium-features/translations/ru.js")
},
};
export default async function getCkLocale(locale: DISPLAYABLE_LOCALE_IDS): Promise<Pick<EditorConfig, "language" | "translations">> {
const mapping = LOCALE_MAPPINGS[locale];
if (!mapping) return {};
const coreTranslation = (await (mapping.coreTranslation())).default;
const premiumFeaturesTranslation = (await (mapping.premiumFeaturesTranslation())).default;
return {
language: mapping.languageCode,
translations: [ coreTranslation, premiumFeaturesTranslation ]
};
}

View File

@ -8,6 +8,7 @@ export { PREMIUM_PLUGINS } from "./plugins.js";
export type { EditorConfig, MentionFeed, MentionFeedObjectItem, ModelNode, ModelPosition, ModelElement, WatchdogConfig, WatchdogState } from "ckeditor5"; export type { EditorConfig, MentionFeed, MentionFeedObjectItem, ModelNode, ModelPosition, ModelElement, WatchdogConfig, WatchdogState } from "ckeditor5";
export type { TemplateDefinition } from "ckeditor5-premium-features"; export type { TemplateDefinition } from "ckeditor5-premium-features";
export { default as buildExtraCommands } from "./extra_slash_commands.js"; export { default as buildExtraCommands } from "./extra_slash_commands.js";
export { default as getCkLocale } from "./i18n.js";
// Import with sideffects to ensure that type augmentations are present. // Import with sideffects to ensure that type augmentations are present.
import "@triliumnext/ckeditor5-math"; import "@triliumnext/ckeditor5-math";