- <% // TriliumNextTODO: move the css file to ${assetPath}/stylesheets/ %>
-
-
diff --git a/apps/server/src/assets/views/mobile.ejs b/apps/server/src/assets/views/mobile.ejs
index d210c5ca7..b5ea77a16 100644
--- a/apps/server/src/assets/views/mobile.ejs
+++ b/apps/server/src/assets/views/mobile.ejs
@@ -96,8 +96,13 @@
}
}
+
+
-
+
@@ -108,11 +113,9 @@
<%- include("./partials/windowGlobal.ejs", locals) %>
-
<% if (themeCssUrl) { %>
diff --git a/apps/server/src/assets/views/partials/windowGlobal.ejs b/apps/server/src/assets/views/partials/windowGlobal.ejs
index 1258030d1..3536d5265 100644
--- a/apps/server/src/assets/views/partials/windowGlobal.ejs
+++ b/apps/server/src/assets/views/partials/windowGlobal.ejs
@@ -18,6 +18,7 @@
appPath: "<%= appPath %>",
platform: "<%= platform %>",
hasNativeTitleBar: <%= hasNativeTitleBar %>,
- TRILIUM_SAFE_MODE: <%= !!process.env.TRILIUM_SAFE_MODE %>
+ TRILIUM_SAFE_MODE: <%= !!process.env.TRILIUM_SAFE_MODE %>,
+ isRtl: <%= !!currentLocale.rtl %>
};
\ No newline at end of file
diff --git a/apps/server/src/assets/views/set_password.ejs b/apps/server/src/assets/views/set_password.ejs
index 9dddfca50..261071647 100644
--- a/apps/server/src/assets/views/set_password.ejs
+++ b/apps/server/src/assets/views/set_password.ejs
@@ -6,13 +6,18 @@
- <% // TriliumNextTODO: move the css file to ${assetPath}/stylesheets/ %>
-
-
diff --git a/apps/server/src/assets/views/setup.ejs b/apps/server/src/assets/views/setup.ejs
index c11368867..68ee58b51 100644
--- a/apps/server/src/assets/views/setup.ejs
+++ b/apps/server/src/assets/views/setup.ejs
@@ -6,10 +6,11 @@
- <% // TriliumNextTODO: move the css file to ${assetPath}/stylesheets/ %>
-
@@ -168,7 +170,6 @@
-
diff --git a/apps/server/src/routes/index.ts b/apps/server/src/routes/index.ts
index 430c27d0f..6bede94d7 100644
--- a/apps/server/src/routes/index.ts
+++ b/apps/server/src/routes/index.ts
@@ -14,6 +14,7 @@ import { generateToken as generateCsrfToken } from "./csrf_protection.js";
import type { Request, Response } from "express";
import type BNote from "../becca/entities/bnote.js";
+import { getCurrentLocale } from "../services/i18n.js";
function index(req: Request, res: Response) {
const options = optionService.getOptionMap();
@@ -57,7 +58,8 @@ function index(req: Request, res: Response) {
maxContentWidth: Math.max(640, parseInt(options.maxContentWidth)),
triliumVersion: packageJson.version,
assetPath: assetPath,
- appPath: appPath
+ appPath: appPath,
+ currentLocale: getCurrentLocale()
});
}
diff --git a/apps/server/src/routes/login.ts b/apps/server/src/routes/login.ts
index 2acb3a4d5..1126d9a7f 100644
--- a/apps/server/src/routes/login.ts
+++ b/apps/server/src/routes/login.ts
@@ -11,9 +11,10 @@ import totp from '../services/totp.js';
import recoveryCodeService from '../services/encryption/recovery_codes.js';
import openID from '../services/open_id.js';
import openIDEncryption from '../services/encryption/open_id_encryption.js';
+import { getCurrentLocale } from "../services/i18n.js";
function loginPage(req: Request, res: Response) {
- // Login page is triggered twice. Once here, and another time if the password is failed.
+ // Login page is triggered twice. Once here, and another time (see sendLoginError) if the password is failed.
res.render('login', {
wrongPassword: false,
wrongTotp: false,
@@ -24,6 +25,7 @@ function loginPage(req: Request, res: Response) {
assetPath: assetPath,
assetPathFragment: assetUrlFragment,
appPath: appPath,
+ currentLocale: getCurrentLocale()
});
}
@@ -31,7 +33,8 @@ function setPasswordPage(req: Request, res: Response) {
res.render("set_password", {
error: false,
assetPath,
- appPath
+ appPath,
+ currentLocale: getCurrentLocale()
});
}
@@ -56,7 +59,8 @@ function setPassword(req: Request, res: Response) {
res.render("set_password", {
error,
assetPath,
- appPath
+ appPath,
+ currentLocale: getCurrentLocale()
});
return;
}
@@ -175,6 +179,7 @@ function sendLoginError(req: Request, res: Response, errorType: 'password' | 'to
assetPath: assetPath,
assetPathFragment: assetUrlFragment,
appPath: appPath,
+ currentLocale: getCurrentLocale()
});
}
diff --git a/apps/server/src/routes/setup.ts b/apps/server/src/routes/setup.ts
index 5bb0f56c9..4a2a8699a 100644
--- a/apps/server/src/routes/setup.ts
+++ b/apps/server/src/routes/setup.ts
@@ -6,6 +6,7 @@ import { isElectron } from "../services/utils.js";
import assetPath from "../services/asset_path.js";
import appPath from "../services/app_path.js";
import type { Request, Response } from "express";
+import { getCurrentLocale } from "../services/i18n.js";
function setupPage(req: Request, res: Response) {
if (sqlInit.isDbInitialized()) {
@@ -30,7 +31,8 @@ function setupPage(req: Request, res: Response) {
res.render("setup", {
syncInProgress: syncInProgress,
assetPath: assetPath,
- appPath: appPath
+ appPath: appPath,
+ currentLocale: getCurrentLocale()
});
}
@@ -39,7 +41,7 @@ async function handleElectronRedirect() {
const { app } = await import("electron");
// Wait for the main window to be created before closing the setup window to prevent triggering `window-all-closed`.
- await windowService.createMainWindow(app);
+ await windowService.createMainWindow(app);
windowService.closeSetupWindow();
const tray = (await import("../services/tray.js")).default;
diff --git a/apps/server/src/services/i18n.spec.ts b/apps/server/src/services/i18n.spec.ts
index a77368249..00dfd113b 100644
--- a/apps/server/src/services/i18n.spec.ts
+++ b/apps/server/src/services/i18n.spec.ts
@@ -6,7 +6,7 @@ import { DAYJS_LOADER } from "./i18n";
describe("i18n", () => {
it("translations are valid JSON", () => {
for (const locale of LOCALES) {
- if (locale.contentOnly) {
+ if (locale.contentOnly || locale.id === "en_rtl") {
continue;
}
diff --git a/apps/server/src/services/i18n.ts b/apps/server/src/services/i18n.ts
index 1f1db1ac6..a23ff7ea2 100644
--- a/apps/server/src/services/i18n.ts
+++ b/apps/server/src/services/i18n.ts
@@ -12,6 +12,7 @@ export const DAYJS_LOADER: Record Promise import("dayjs/locale/zh-cn.js"),
"de": () => import("dayjs/locale/de.js"),
"en": () => import("dayjs/locale/en.js"),
+ "en_rtl": () => import("dayjs/locale/en.js"),
"es": () => import("dayjs/locale/es.js"),
"fa": () => import("dayjs/locale/fa.js"),
"fr": () => import("dayjs/locale/fr.js"),
@@ -75,3 +76,10 @@ 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/apps/server/src/services/options_init.ts b/apps/server/src/services/options_init.ts
index 0908a0512..c6e0231c5 100644
--- a/apps/server/src/services/options_init.ts
+++ b/apps/server/src/services/options_init.ts
@@ -159,7 +159,7 @@ const defaultOptions: DefaultOption[] = [
// Internationalization
{ name: "locale", value: "en", isSynced: true },
- { name: "formattingLocale", value: "en", isSynced: true },
+ { name: "formattingLocale", value: "", isSynced: true }, // no value means auto-detect
{ name: "firstDayOfWeek", value: "1", isSynced: true },
{ name: "firstWeekOfYear", value: "0", isSynced: true },
{ name: "minDaysInFirstWeek", value: "4", isSynced: true },
diff --git a/packages/commons/src/lib/i18n.ts b/packages/commons/src/lib/i18n.ts
index 5970d2ab8..8e408f2e6 100644
--- a/packages/commons/src/lib/i18n.ts
+++ b/packages/commons/src/lib/i18n.ts
@@ -5,11 +5,13 @@ export interface Locale {
rtl?: boolean;
/** `true` if the language is not supported by the application as a display language, but it is selectable by the user for the content. */
contentOnly?: boolean;
+ /** `true` if the language should only be visible while in development mode, and not in production. */
+ devOnly?: boolean;
/** The value to pass to `--lang` for the Electron instance in order to set it as a locale. Not setting it will hide it from the list of supported locales. */
electronLocale?: "en" | "de" | "es" | "fr" | "zh_CN" | "zh_TW" | "ro" | "af" | "am" | "ar" | "bg" | "bn" | "ca" | "cs" | "da" | "el" | "en_GB" | "es_419" | "et" | "fa" | "fi" | "fil" | "gu" | "he" | "hi" | "hr" | "hu" | "id" | "it" | "ja" | "kn" | "ko" | "lt" | "lv" | "ml" | "mr" | "ms" | "nb" | "nl" | "pl" | "pt_BR" | "pt_PT" | "ru" | "sk" | "sl" | "sr" | "sv" | "sw" | "ta" | "te" | "th" | "tr" | "uk" | "ur" | "vi";
}
-const UNSORTED_LOCALES: Locale[] = [
+const UNSORTED_LOCALES = [
{ id: "cn", name: "简体中文", electronLocale: "zh_CN" },
{ id: "de", name: "Deutsch", electronLocale: "de" },
{ id: "en", name: "English", electronLocale: "en" },
@@ -23,6 +25,19 @@ const UNSORTED_LOCALES: Locale[] = [
{ id: "tw", name: "繁體中文", electronLocale: "zh_TW" },
{ id: "uk", name: "Українська", electronLocale: "uk" },
+ /**
+ * Development-only languages.
+ *
+ * These are only displayed while in dev mode, to test some language particularities (such as RTL) more easily.
+ */
+ {
+ id: "en_rtl",
+ name: "English (right-to-left) [dev]",
+ electronLocale: "en",
+ rtl: true,
+ devOnly: true
+ },
+
/*
* Right to left languages
*
@@ -32,7 +47,8 @@ const UNSORTED_LOCALES: Locale[] = [
id: "ar",
name: "اَلْعَرَبِيَّةُ",
rtl: true,
- contentOnly: true
+ devOnly: true,
+ electronLocale: "ar"
},
{ // Hebrew
id: "he",
@@ -57,4 +73,7 @@ const UNSORTED_LOCALES: Locale[] = [
export const LOCALES: Locale[] = Array.from(UNSORTED_LOCALES)
.sort((a, b) => a.name.localeCompare(b.name));
+/** A type containing a string union of all the supported locales, including those that are content-only. */
export type LOCALE_IDS = typeof UNSORTED_LOCALES[number]["id"];
+/** A type containing a string union of all the supported locales that are not content-only (i.e. can be used as the UI language). */
+export type DISPLAYABLE_LOCALE_IDS = Exclude["id"];