From 40e986b188944826dd71778622dc58ab1c39b368 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 6 Apr 2026 11:41:35 +0300 Subject: [PATCH 1/8] refactor(client): ineffective dynamic imports --- apps/client/src/components/app_context.ts | 2 +- apps/client/src/entities/fnote.ts | 2 +- .../client/src/menus/electron_context_menu.ts | 26 +++++++-------- apps/client/src/print.tsx | 2 +- apps/client/src/services/bundle.ts | 2 +- apps/client/src/services/clipboard_ext.ts | 32 +++++++++---------- apps/client/src/services/dialog.ts | 5 +-- apps/client/src/services/froca_updater.ts | 21 ++++++------ apps/client/src/services/server.ts | 8 ++--- apps/client/src/services/utils.ts | 2 +- apps/client/src/services/ws.ts | 27 +++++++--------- 11 files changed, 62 insertions(+), 67 deletions(-) diff --git a/apps/client/src/components/app_context.ts b/apps/client/src/components/app_context.ts index d731f03382..1508ade084 100644 --- a/apps/client/src/components/app_context.ts +++ b/apps/client/src/components/app_context.ts @@ -5,6 +5,7 @@ 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"; @@ -578,7 +579,6 @@ export class AppContext extends Component { this.tabManager.loadTabs(); - const bundleService = (await import("../services/bundle.js")).default; setTimeout(() => bundleService.executeStartupBundles(), 2000); } diff --git a/apps/client/src/entities/fnote.ts b/apps/client/src/entities/fnote.ts index 12fd311bec..5fe7caf33c 100644 --- a/apps/client/src/entities/fnote.ts +++ b/apps/client/src/entities/fnote.ts @@ -1,5 +1,6 @@ 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"; @@ -1014,7 +1015,6 @@ 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}`); diff --git a/apps/client/src/menus/electron_context_menu.ts b/apps/client/src/menus/electron_context_menu.ts index 6baba6a951..547a26d3d8 100644 --- a/apps/client/src/menus/electron_context_menu.ts +++ b/apps/client/src/menus/electron_context_menu.ts @@ -1,12 +1,14 @@ -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"; + +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 contextMenu, { type MenuItem } from "./context_menu.js"; function setupContextMenu() { const electron = utils.dynamicRequire("electron"); @@ -15,8 +17,6 @@ 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; @@ -141,7 +141,7 @@ function setupContextMenu() { } // Replace the placeholder with the real search keyword. - let searchUrl = searchEngineUrl.replace("{keyword}", encodeURIComponent(params.selectionText)); + const searchUrl = searchEngineUrl.replace("{keyword}", encodeURIComponent(params.selectionText)); items.push({ kind: "separator" }); @@ -155,10 +155,6 @@ 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 }); diff --git a/apps/client/src/print.tsx b/apps/client/src/print.tsx index dc7817d9b5..c943413c49 100644 --- a/apps/client/src/print.tsx +++ b/apps/client/src/print.tsx @@ -4,6 +4,7 @@ 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"; @@ -30,7 +31,6 @@ async function main() { if (!noteId) return; await import("./print.css"); - const froca = (await import("./services/froca")).default; const note = await froca.getNote(noteId); const bodyWrapper = document.createElement("div"); diff --git a/apps/client/src/services/bundle.ts b/apps/client/src/services/bundle.ts index 7cee01812b..fdee38ba34 100644 --- a/apps/client/src/services/bundle.ts +++ b/apps/client/src/services/bundle.ts @@ -26,7 +26,7 @@ type WithNoteId = T & { }; export type Widget = WithNoteId<(LegacyWidget | WidgetDefinitionWithType)>; -async function getAndExecuteBundle(noteId: string, originEntity = null, script = null, params = null) { +async function getAndExecuteBundle(noteId: string, originEntity: Entity | null = null, script: string | null = null, params: string | null = null) { const bundle = await server.post(`script/bundle/${noteId}`, { script, params diff --git a/apps/client/src/services/clipboard_ext.ts b/apps/client/src/services/clipboard_ext.ts index 9ab98af68f..16eea4ca49 100644 --- a/apps/client/src/services/clipboard_ext.ts +++ b/apps/client/src/services/clipboard_ext.ts @@ -1,3 +1,6 @@ +import { t } from "./i18n.js"; +import toast from "./toast.js"; + export function copyText(text: string) { if (!text) { return; @@ -6,29 +9,26 @@ export function copyText(text: string) { if (navigator.clipboard) { navigator.clipboard.writeText(text); return true; - } 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); - } + } + // 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 async function copyTextWithToast(text: string) { - const t = (await import("./i18n.js")).t; - const toast = (await import("./toast.js")).default; - +export function copyTextWithToast(text: string) { if (copyText(text)) { toast.showMessage(t("clipboard.copy_success")); } else { diff --git a/apps/client/src/services/dialog.ts b/apps/client/src/services/dialog.ts index 8711ec1751..6888870fc9 100644 --- a/apps/client/src/services/dialog.ts +++ b/apps/client/src/services/dialog.ts @@ -1,9 +1,11 @@ import { Modal } from "bootstrap"; + 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 { focusSavedElement, saveFocusedElement } from "./focus.js"; -import { InfoExtraProps } from "../widgets/dialogs/info.jsx"; +import keyboardActionsService from "./keyboard_actions.js"; export async function openDialog($dialog: JQuery, closeActDialog = true, config?: Partial) { if (closeActDialog) { @@ -25,7 +27,6 @@ export async function openDialog($dialog: JQuery, closeActDialog = } }); - const keyboardActionsService = (await import("./keyboard_actions.js")).default; keyboardActionsService.updateDisplayedShortcuts($dialog); return $dialog; diff --git a/apps/client/src/services/froca_updater.ts b/apps/client/src/services/froca_updater.ts index ca6c792746..8be1eceada 100644 --- a/apps/client/src/services/froca_updater.ts +++ b/apps/client/src/services/froca_updater.ts @@ -1,14 +1,16 @@ -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 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 FBranch, { type FBranchRow } from "../entities/fbranch.js"; import type { default as FNote, FNoteRow } from "../entities/fnote.js"; import type { EntityChange } from "../server_types.js"; -import type { OptionNames } from "@triliumnext/commons"; +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"; async function processEntityChanges(entityChanges: EntityChange[]) { const loadResults = new LoadResults(entityChanges); @@ -63,7 +65,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") { - let attributeEntity = entity as FAttributeRow; + const attributeEntity = entity as FAttributeRow; if (attributeEntity.type === "relation" && (attributeEntity.name === "template" || attributeEntity.name === "inherit") && !(attributeEntity.value in froca.notes)) { missingNoteIds.push(attributeEntity.value); } @@ -79,7 +81,6 @@ async function processEntityChanges(entityChanges: EntityChange[]) { noteAttributeCache.invalidate(); } - const appContext = (await import("../components/app_context.js")).default; await appContext.triggerEvent("entitiesReloaded", { loadResults }); } } diff --git a/apps/client/src/services/server.ts b/apps/client/src/services/server.ts index 010b57cae1..1db7454182 100644 --- a/apps/client/src/services/server.ts +++ b/apps/client/src/services/server.ts @@ -1,5 +1,8 @@ +import { t } from "./i18n.js"; +import toastService from "./toast.js"; import utils, { isShare } from "./utils.js"; import ValidationError from "./validation_error.js"; +import { logError } from "./ws.js"; type Headers = Record; @@ -32,6 +35,7 @@ async function getHeaders(headers?: Headers) { return {}; } + // Dynamic import to avoid circular dependency (app_context imports froca which imports server). const appContext = (await import("../components/app_context.js")).default; const activeNoteContext = appContext.tabManager ? appContext.tabManager.getActiveContext() : null; @@ -344,8 +348,6 @@ async function reportError(method: string, url: string, statusCode: number, resp } catch (e) {} } - const toastService = (await import("./toast.js")).default; - const messageStr = (typeof message === "string" ? message : JSON.stringify(message)) || "-"; if ([400, 404].includes(statusCode) && response && typeof response === "object") { @@ -357,7 +359,6 @@ 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 +372,6 @@ async function reportError(method: string, url: string, statusCode: number, resp t("server.unknown_http_error_content", { statusCode, method, url, message: messageStr }), 15_000); } - const { logError } = await import("./ws.js"); logError(`${statusCode} ${method} ${url} - ${message}`); } } diff --git a/apps/client/src/services/utils.ts b/apps/client/src/services/utils.ts index 30b5e4eaf7..bcd817b6e2 100644 --- a/apps/client/src/services/utils.ts +++ b/apps/client/src/services/utils.ts @@ -455,7 +455,7 @@ export function openInAppHelpFromUrl(inAppHelpPage: string) { export async function openInReusableSplit(targetNoteId: string, targetViewMode: ViewMode, openOpts: { hoistedNoteId?: string; } = {}) { - // Dynamic import to avoid import issues in tests. + // Dynamic import to avoid circular dependency (app_context imports utils). const appContext = (await import("../components/app_context.js")).default; const activeContext = appContext.tabManager.getActiveContext(); if (!activeContext) { diff --git a/apps/client/src/services/ws.ts b/apps/client/src/services/ws.ts index 1002abe9f7..a68734a6c3 100644 --- a/apps/client/src/services/ws.ts +++ b/apps/client/src/services/ws.ts @@ -1,13 +1,16 @@ -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 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 toastService from "./toast.js"; import toast from "./toast.js"; +import utils from "./utils.js"; type MessageHandler = (message: WebSocketMessage) => void; let messageHandlers: MessageHandler[] = []; @@ -134,12 +137,6 @@ async function handleMessage(event: MessageEvent) { } else if (message.type === "toast") { 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); @@ -161,7 +158,7 @@ function waitForEntityChangeId(desiredEntityChangeId: number) { return new Promise((res, rej) => { entityChangeIdReachedListeners.push({ - desiredEntityChangeId: desiredEntityChangeId, + desiredEntityChangeId, resolvePromise: res, start: Date.now() }); From 09258179f021be3f233f22684e713e24484ed4cb Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 6 Apr 2026 11:46:35 +0300 Subject: [PATCH 2/8] refactor(client): one more ineffective dynamic import due to appContext --- apps/client/src/services/server.ts | 4 +--- apps/client/src/services/utils.ts | 6 ++---- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/apps/client/src/services/server.ts b/apps/client/src/services/server.ts index 1db7454182..bb32b37ac9 100644 --- a/apps/client/src/services/server.ts +++ b/apps/client/src/services/server.ts @@ -35,9 +35,7 @@ async function getHeaders(headers?: Headers) { return {}; } - // Dynamic import to avoid circular dependency (app_context imports froca which imports server). - const appContext = (await import("../components/app_context.js")).default; - const activeNoteContext = appContext.tabManager ? appContext.tabManager.getActiveContext() : null; + const activeNoteContext = glob.appContext?.tabManager ? glob.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 diff --git a/apps/client/src/services/utils.ts b/apps/client/src/services/utils.ts index bcd817b6e2..09d71cc678 100644 --- a/apps/client/src/services/utils.ts +++ b/apps/client/src/services/utils.ts @@ -455,9 +455,7 @@ export function openInAppHelpFromUrl(inAppHelpPage: string) { export async function openInReusableSplit(targetNoteId: string, targetViewMode: ViewMode, openOpts: { hoistedNoteId?: string; } = {}) { - // Dynamic import to avoid circular dependency (app_context imports utils). - const appContext = (await import("../components/app_context.js")).default; - const activeContext = appContext.tabManager.getActiveContext(); + const activeContext = glob.appContext?.tabManager.getActiveContext(); if (!activeContext) { return; } @@ -467,7 +465,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]; - appContext.triggerCommand("openNewNoteSplit", { + glob.appContext?.triggerCommand("openNewNoteSplit", { ntxId, notePath: targetNoteId, hoistedNoteId: openOpts.hoistedNoteId, From a080b50c4538ee43a920a7f6e095ea2df02399a2 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 6 Apr 2026 12:06:27 +0300 Subject: [PATCH 3/8] refactor(client): duplicate toast import --- apps/client/src/services/ws.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/apps/client/src/services/ws.ts b/apps/client/src/services/ws.ts index a68734a6c3..47ed90341b 100644 --- a/apps/client/src/services/ws.ts +++ b/apps/client/src/services/ws.ts @@ -8,7 +8,6 @@ import frocaUpdater from "./froca_updater.js"; import { t } from "./i18n.js"; import options from "./options.js"; import server from "./server.js"; -import toastService from "./toast.js"; import toast from "./toast.js"; import utils from "./utils.js"; @@ -129,13 +128,13 @@ async function handleMessage(event: MessageEvent) { } else if (message.type === "frontend-update") { await executeFrontendUpdate(message.data.entityChanges); } else if (message.type === "sync-hash-check-failed") { - toastService.showError(t("ws.sync-check-failed"), 60000); + toast.showError(t("ws.sync-check-failed"), 60000); } else if (message.type === "consistency-checks-failed") { - toastService.showError(t("ws.consistency-checks-failed"), 50 * 60000); + toast.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") { - toastService.showMessage(message.message); + toast.showMessage(message.message); } else if (message.type === "execute-script") { const originEntity = message.originEntityId ? await froca.getNote(message.originEntityId) : null; @@ -202,7 +201,7 @@ async function consumeFrontendUpdateData() { } else { console.log("nonProcessedEntityChanges causing the timeout", nonProcessedEntityChanges); - toastService.showError(t("ws.encountered-error", { message: e.message })); + toast.showError(t("ws.encountered-error", { message: e.message })); } } From 42da1872e77421f60ca33bf4aabf5dbdbb5b23b0 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 6 Apr 2026 12:10:33 +0300 Subject: [PATCH 4/8] fix(client): crashing due to circular dependency --- apps/client/src/services/server.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/client/src/services/server.ts b/apps/client/src/services/server.ts index bb32b37ac9..a52fa0fd3f 100644 --- a/apps/client/src/services/server.ts +++ b/apps/client/src/services/server.ts @@ -1,8 +1,5 @@ -import { t } from "./i18n.js"; -import toastService from "./toast.js"; import utils, { isShare } from "./utils.js"; import ValidationError from "./validation_error.js"; -import { logError } from "./ws.js"; type Headers = Record; @@ -346,6 +343,10 @@ async function reportError(method: string, url: string, statusCode: number, resp } catch (e) {} } + // Dynamic imports to avoid circular dependency (toast/i18n → app_context → options → server). + const toastService = (await import("./toast.js")).default; + const { t } = await import("./i18n.js"); + const messageStr = (typeof message === "string" ? message : JSON.stringify(message)) || "-"; if ([400, 404].includes(statusCode) && response && typeof response === "object") { @@ -370,7 +371,7 @@ async function reportError(method: string, url: string, statusCode: number, resp t("server.unknown_http_error_content", { statusCode, method, url, message: messageStr }), 15_000); } - logError(`${statusCode} ${method} ${url} - ${message}`); + window.logError(`${statusCode} ${method} ${url} - ${message}`); } } From d212120f9b72b469a4ccc538cc367d512045d6c3 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 6 Apr 2026 12:16:32 +0300 Subject: [PATCH 5/8] refactor(client): read locales from common instead of going through the server --- apps/client/src/services/i18n.ts | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/apps/client/src/services/i18n.ts b/apps/client/src/services/i18n.ts index 5b5f38b762..8790bd6fc9 100644 --- a/apps/client/src/services/i18n.ts +++ b/apps/client/src/services/i18n.ts @@ -1,12 +1,9 @@ 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 { LOCALE_IDS, LOCALES, setDayjsLocale } from "@triliumnext/commons"; import { initReactI18next } from "react-i18next"; -let locales: Locale[] | null; - /** * A deferred promise that resolves when translations are initialized. */ @@ -15,8 +12,6 @@ export let translationsInitializedPromise = $.Deferred(); export async function initLocale() { const locale = ((options.get("locale") as string) || "en") as LOCALE_IDS; - locales = await server.get("options/locales"); - i18next.use(initReactI18next); await i18next.use(i18nextHttpBackend).init({ lng: locale, @@ -32,11 +27,7 @@ export async function initLocale() { } export function getAvailableLocales() { - if (!locales) { - throw new Error("Tried to load list of locales, but localization is not yet initialized.") - } - - return locales; + return LOCALES; } /** @@ -47,7 +38,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; From 19bb7f5ddbc5779027b9c47ae59af18e5d913e31 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 6 Apr 2026 12:18:36 +0300 Subject: [PATCH 6/8] refactor(server): remove unnecessary route --- apps/server/src/routes/api/options.ts | 9 ++------- apps/server/src/routes/routes.ts | 1 - apps/server/src/services/i18n.ts | 6 +----- 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/apps/server/src/routes/api/options.ts b/apps/server/src/routes/api/options.ts index ce5d7b9dc6..9e21dcb7b7 100644 --- a/apps/server/src/routes/api/options.ts +++ b/apps/server/src/routes/api/options.ts @@ -5,7 +5,7 @@ import type { Request } from "express"; import ValidationError from "../../errors/validation_error.js"; import config from "../../services/config.js"; -import { changeLanguage, getLocales } from "../../services/i18n.js"; +import { changeLanguage } from "../../services/i18n.js"; import log from "../../services/log.js"; import optionService from "../../services/options.js"; import searchService from "../../services/search/services/search.js"; @@ -192,10 +192,6 @@ function getUserThemes() { return ret; } -function getSupportedLocales() { - return getLocales(); -} - function isAllowed(name: string) { return (ALLOWED_OPTIONS as Set).has(name) || name.startsWith("keyboardShortcuts") @@ -207,6 +203,5 @@ export default { getOptions, updateOption, updateOptions, - getUserThemes, - getSupportedLocales + getUserThemes }; diff --git a/apps/server/src/routes/routes.ts b/apps/server/src/routes/routes.ts index 775a36e833..198fa5c22e 100644 --- a/apps/server/src/routes/routes.ts +++ b/apps/server/src/routes/routes.ts @@ -215,7 +215,6 @@ function register(app: express.Application) { apiRoute(PUT, "/api/options/:name/:value", optionsApiRoute.updateOption); apiRoute(PUT, "/api/options", optionsApiRoute.updateOptions); apiRoute(GET, "/api/options/user-themes", optionsApiRoute.getUserThemes); - apiRoute(GET, "/api/options/locales", optionsApiRoute.getSupportedLocales); apiRoute(PST, "/api/password/change", passwordApiRoute.changePassword); apiRoute(PST, "/api/password/reset", passwordApiRoute.resetPassword); diff --git a/apps/server/src/services/i18n.ts b/apps/server/src/services/i18n.ts index f77ec93ef6..51cec3e2ed 100644 --- a/apps/server/src/services/i18n.ts +++ b/apps/server/src/services/i18n.ts @@ -4,7 +4,7 @@ import sql_init from "./sql_init.js"; import { join } from "path"; import { getResourceDir } from "./utils.js"; import hidden_subtree from "./hidden_subtree.js"; -import { dayjs, LOCALES, setDayjsLocale, type Dayjs, type Locale, type LOCALE_IDS } from "@triliumnext/commons"; +import { dayjs, LOCALES, setDayjsLocale, type Dayjs, type LOCALE_IDS } from "@triliumnext/commons"; export async function initializeTranslations() { const resourceDir = getResourceDir(); @@ -30,10 +30,6 @@ export function ordinal(date: Dayjs) { .format("Do"); } -export function getLocales(): Locale[] { - return LOCALES; -} - function getCurrentLanguage(): LOCALE_IDS { let language: string | null = null; if (sql_init.isDbInitialized()) { From c4d3e776a15d06607539f3043f63e085c32690d3 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 6 Apr 2026 12:20:40 +0300 Subject: [PATCH 7/8] refactor(client): the last circular dependency --- apps/client/src/components/app_context.ts | 4 ++-- apps/client/src/services/i18n.ts | 8 +++----- apps/client/src/services/server.ts | 4 ++-- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/apps/client/src/components/app_context.ts b/apps/client/src/components/app_context.ts index 1508ade084..7019617714 100644 --- a/apps/client/src/components/app_context.ts +++ b/apps/client/src/components/app_context.ts @@ -1,6 +1,6 @@ import type { CKTextEditor } from "@triliumnext/ckeditor5"; import type CodeMirror from "@triliumnext/codemirror"; -import { SqlExecuteResponse } from "@triliumnext/commons"; +import { type LOCALE_IDS, SqlExecuteResponse } from "@triliumnext/commons"; import type { NativeImage, TouchBar } from "electron"; import { ColumnComponent } from "tabulator-tables"; @@ -564,7 +564,7 @@ export class AppContext extends Component { */ async earlyInit() { await options.initializedPromise; - await initLocale(); + await initLocale((options.get("locale") || "en") as LOCALE_IDS); } setLayout(layout: Layout) { diff --git a/apps/client/src/services/i18n.ts b/apps/client/src/services/i18n.ts index 8790bd6fc9..0743596731 100644 --- a/apps/client/src/services/i18n.ts +++ b/apps/client/src/services/i18n.ts @@ -1,16 +1,14 @@ -import options from "./options.js"; +import { LOCALE_IDS, LOCALES, setDayjsLocale } from "@triliumnext/commons"; import i18next from "i18next"; import i18nextHttpBackend from "i18next-http-backend"; -import { LOCALE_IDS, LOCALES, setDayjsLocale } from "@triliumnext/commons"; import { initReactI18next } from "react-i18next"; /** * A deferred promise that resolves when translations are initialized. */ -export let translationsInitializedPromise = $.Deferred(); +export const translationsInitializedPromise = $.Deferred(); -export async function initLocale() { - const locale = ((options.get("locale") as string) || "en") as LOCALE_IDS; +export async function initLocale(locale: LOCALE_IDS = "en") { i18next.use(initReactI18next); await i18next.use(i18nextHttpBackend).init({ diff --git a/apps/client/src/services/server.ts b/apps/client/src/services/server.ts index a52fa0fd3f..2d9d8273ae 100644 --- a/apps/client/src/services/server.ts +++ b/apps/client/src/services/server.ts @@ -1,3 +1,4 @@ +import { t } from "./i18n.js"; import utils, { isShare } from "./utils.js"; import ValidationError from "./validation_error.js"; @@ -343,9 +344,8 @@ async function reportError(method: string, url: string, statusCode: number, resp } catch (e) {} } - // Dynamic imports to avoid circular dependency (toast/i18n → app_context → options → server). + // Dynamic import to avoid circular dependency (toast → app_context → options → server). const toastService = (await import("./toast.js")).default; - const { t } = await import("./i18n.js"); const messageStr = (typeof message === "string" ? message : JSON.stringify(message)) || "-"; From dd4cab22c192c90d52dabbb90fe8ee7e61999b39 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 6 Apr 2026 12:22:00 +0300 Subject: [PATCH 8/8] chore(client): address requested changes --- apps/client/src/services/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/client/src/services/utils.ts b/apps/client/src/services/utils.ts index 09d71cc678..67bbdd0e26 100644 --- a/apps/client/src/services/utils.ts +++ b/apps/client/src/services/utils.ts @@ -455,7 +455,7 @@ export function openInAppHelpFromUrl(inAppHelpPage: string) { export async function openInReusableSplit(targetNoteId: string, targetViewMode: ViewMode, openOpts: { hoistedNoteId?: string; } = {}) { - const activeContext = glob.appContext?.tabManager.getActiveContext(); + const activeContext = glob.appContext?.tabManager?.getActiveContext(); if (!activeContext) { return; }