From a10d99f93870d01e654c24c0b50dc464aecdb16e Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 16 Nov 2025 14:24:07 +0200 Subject: [PATCH] feat(ckeditor5): set up multi-language support --- .../text/CKEditorWithWatchdog.tsx | 7 +- .../src/widgets/type_widgets/text/config.ts | 10 +-- packages/ckeditor5/src/i18n.ts | 90 +++++++++++++++++++ packages/ckeditor5/src/index.ts | 1 + 4 files changed, 101 insertions(+), 7 deletions(-) create mode 100644 packages/ckeditor5/src/i18n.ts 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.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): Promise Promise<{ default: Translations }>; + premiumFeaturesTranslation: () => Promise<{ default: Translations }>; +} + +const LOCALE_MAPPINGS: Record = { + 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> { + 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 ] + }; +} diff --git a/packages/ckeditor5/src/index.ts b/packages/ckeditor5/src/index.ts index 1f7be24e4..2d73ecdea 100644 --- a/packages/ckeditor5/src/index.ts +++ b/packages/ckeditor5/src/index.ts @@ -8,6 +8,7 @@ export { PREMIUM_PLUGINS } from "./plugins.js"; export type { EditorConfig, MentionFeed, MentionFeedObjectItem, ModelNode, ModelPosition, ModelElement, WatchdogConfig, WatchdogState } from "ckeditor5"; export type { TemplateDefinition } from "ckeditor5-premium-features"; 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 "@triliumnext/ckeditor5-math";