diff --git a/apps/server/spec/setup.ts b/apps/server/spec/setup.ts index 4585f0f8c4..10ee99569b 100644 --- a/apps/server/spec/setup.ts +++ b/apps/server/spec/setup.ts @@ -9,7 +9,7 @@ import NodejsZipProvider from "../src/zip_provider.js"; import ServerPlatformProvider from "../src/platform_provider.js"; import BetterSqlite3Provider from "../src/sql_provider.js"; import NodejsInAppHelpProvider from "../src/in_app_help_provider.js"; -import { initializeTranslations } from "../src/services/i18n.js"; +import { initializeTranslationsWithParams } from "../src/services/i18n.js"; // Initialize environment variables. process.env.TRILIUM_DATA_DIR = join(__dirname, "db"); @@ -41,7 +41,7 @@ beforeAll(async () => { executionContext: new ClsHookedExecutionContext(), schema: readFileSync(require.resolve("@triliumnext/core/src/assets/schema.sql"), "utf-8"), platform: new ServerPlatformProvider(), - translations: initializeTranslations, + translations: initializeTranslationsWithParams, inAppHelp: new NodejsInAppHelpProvider() }); }); diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index 51011d4049..1905ea7102 100644 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -71,7 +71,7 @@ async function startApplication() { messaging: new WebSocketMessagingProvider(), schema: loadCoreSchema(), platform: new ServerPlatformProvider(), - translations: (await import("./services/i18n.js")).initializeTranslations, + translations: (await import("./services/i18n.js")).initializeTranslationsWithParams, // demo.zip is a server-owned asset; src/assets is copied to dist/assets // by the build script, so the same RESOURCE_DIR-relative path works in // both source and bundled-production modes. diff --git a/apps/server/src/services/i18n.ts b/apps/server/src/services/i18n.ts index cba0dae96c..c427444377 100644 --- a/apps/server/src/services/i18n.ts +++ b/apps/server/src/services/i18n.ts @@ -1,10 +1,17 @@ -import { LOCALE_IDS, setDayjsLocale } from "@triliumnext/commons"; -import type i18next from "i18next"; +import { dayjs, LOCALES, LOCALE_IDS, setDayjsLocale, type Dayjs } from "@triliumnext/commons"; +import i18next from "i18next"; import { join } from "path"; import { getResourceDir } from "./utils"; +import options from "./options.js"; +import sql_init from "./sql_init.js"; +import hidden_subtree from "./hidden_subtree.js"; -export async function initializeTranslations(i18nextInstance: typeof i18next, locale: LOCALE_IDS) { +/** + * Initialize translations with explicit i18next instance and locale. + * Used as a TranslationProvider callback for initializeCore(). + */ +export async function initializeTranslationsWithParams(i18nextInstance: typeof i18next, locale: LOCALE_IDS) { const resourceDir = getResourceDir(); const Backend = (await import("i18next-fs-backend/cjs")).default; @@ -21,3 +28,42 @@ export async function initializeTranslations(i18nextInstance: typeof i18next, lo // Initialize dayjs locale. await setDayjsLocale(locale); } + +/** + * Initialize translations using the global i18next instance and locale from options. + * Convenience function for scripts that don't use initializeCore(). + */ +export async function initializeTranslations() { + const locale = getCurrentLanguage(); + await initializeTranslationsWithParams(i18next, locale); +} + +export function ordinal(date: Dayjs) { + return dayjs(date).format("Do"); +} + +function getCurrentLanguage(): LOCALE_IDS { + let language: string | null = null; + if (sql_init.isDbInitialized()) { + language = options.getOptionOrNull("locale"); + } + + if (!language) { + console.info("Language option not found, falling back to en."); + language = "en"; + } + + return language as LOCALE_IDS; +} + +export async function changeLanguage(locale: string) { + await i18next.changeLanguage(locale); + hidden_subtree.checkHiddenSubtree(true, { restoreNames: true }); +} + +export function getCurrentLocale() { + const localeId = options.getOptionOrNull("locale") ?? "en"; + const currentLocale = LOCALES.find(l => l.id === localeId); + if (!currentLocale) return LOCALES.find(l => l.id === "en")!; + return currentLocale; +} diff --git a/packages/trilium-core/src/services/backend_script_api.ts b/packages/trilium-core/src/services/backend_script_api.ts index ef877a79bd..e1c0a7a494 100644 --- a/packages/trilium-core/src/services/backend_script_api.ts +++ b/packages/trilium-core/src/services/backend_script_api.ts @@ -1,7 +1,6 @@ import { type AttributeRow, dayjs, formatLogMessage } from "@triliumnext/commons"; import AbstractBeccaEntity from "../becca/entities/abstract_becca_entity"; import Becca from "../becca/becca-interface"; -import axios from "axios"; import * as cheerio from "cheerio"; import * as htmlParser from "node-html-parser"; import xml2js from "xml2js"; diff --git a/apps/server/src/services/options_init.spec.ts b/packages/trilium-core/src/services/options_init.spec.ts similarity index 100% rename from apps/server/src/services/options_init.spec.ts rename to packages/trilium-core/src/services/options_init.spec.ts diff --git a/packages/trilium-core/src/services/options_init.ts b/packages/trilium-core/src/services/options_init.ts index 9b5001a4fb..8d814b752f 100644 --- a/packages/trilium-core/src/services/options_init.ts +++ b/packages/trilium-core/src/services/options_init.ts @@ -67,14 +67,49 @@ export async function initNotSyncedOptions(initialized: boolean, opts: NotSynced optionService.createOption("textNoteEditorType", "ckeditor-classic", true); optionService.createOption("syncServerHost", opts.syncServerHost || "", false); - optionService.createOption("syncServerTimeout", "120000", false); + optionService.createOption("syncServerTimeout", "120", false); // 120 seconds (2 minutes) optionService.createOption("syncProxy", opts.syncProxy || "", false); } +/** + * Migrates a sync timeout value from milliseconds to seconds. + * Values >= 1000 are assumed to be in milliseconds (since 1000+ seconds = 16+ minutes is unlikely). + * TimeSelector stores values in seconds; the scale is only used for display. + * + * @returns The value in seconds and preferred display scale, or null if no migration is needed. + */ +export function migrateSyncTimeoutFromMilliseconds(milliseconds: number): { value: number; scale: number } | null { + if (isNaN(milliseconds) || milliseconds < 1000) { + return null; + } + + const seconds = Math.round(milliseconds / 1000); + + // Value is always stored in seconds; scale determines display unit + if (seconds >= 60 && seconds % 60 === 0) { + return { value: seconds, scale: 60 }; // display as minutes + } + return { value: seconds, scale: 1 }; // display as seconds +} + /** * Contains all the default options that must be initialized on new and existing databases (at startup). The value can also be determined based on other options, provided they have already been initialized. */ const defaultOptions: DefaultOption[] = [ + { + name: "syncServerTimeoutTimeScale", + value: (optionsMap) => { + const timeout = parseInt(optionsMap.syncServerTimeout || "120", 10); + const migrated = migrateSyncTimeoutFromMilliseconds(timeout); + if (migrated) { + optionService.setOption("syncServerTimeout", String(migrated.value)); + getLog().info(`Migrated syncServerTimeout from ${timeout}ms to ${migrated.value}s`); + return String(migrated.scale); + } + return "60"; // default to minutes + }, + isSynced: false + }, { name: "revisionSnapshotTimeInterval", value: "600", isSynced: true }, { name: "revisionSnapshotTimeIntervalTimeScale", value: "60", isSynced: true }, // default to Minutes { name: "revisionSnapshotNumberLimit", value: "-1", isSynced: true }, @@ -189,6 +224,7 @@ const defaultOptions: DefaultOption[] = [ { name: "textNoteEmojiCompletionEnabled", value: "true", isSynced: true }, { name: "textNoteCompletionEnabled", value: "true", isSynced: true }, { name: "textNoteSlashCommandsEnabled", value: "true", isSynced: true }, + { name: "includeNoteDefaultBoxSize", value: "medium", isSynced: true }, // HTML import configuration { name: "layoutOrientation", value: "vertical", isSynced: false },