From 60ade0560e5c4007b0c459593e538ccd61ed5a9e Mon Sep 17 00:00:00 2001 From: DanielDerefaka Date: Tue, 6 Jan 2026 18:58:12 +0100 Subject: [PATCH 1/2] feat(status-page): add per-status-page language setting for anonymous visitors Adds a language dropdown in the status page settings that allows admins to set a specific display language for anonymous visitors. When configured, anonymous visitors will see the status page in the selected language instead of their browser's default language. Fixes #5836 Contribution by Gittensor, see my contribution statistics at https://gittensor.io/miners/details?githubId=101010297 --- ...026-01-06-0000-add-status-page-language.js | 11 +++++++ server/model/status_page.js | 2 ++ .../status-page-socket-handler.js | 5 +++ src/i18n.js | 4 +-- src/lang/en.json | 3 ++ src/mixins/lang.js | 27 +++++++++++++--- src/pages/StatusPage.vue | 31 +++++++++++++++++++ src/util-frontend.js | 8 ++--- 8 files changed, 81 insertions(+), 10 deletions(-) create mode 100644 db/knex_migrations/2026-01-06-0000-add-status-page-language.js diff --git a/db/knex_migrations/2026-01-06-0000-add-status-page-language.js b/db/knex_migrations/2026-01-06-0000-add-status-page-language.js new file mode 100644 index 0000000000..6fec261189 --- /dev/null +++ b/db/knex_migrations/2026-01-06-0000-add-status-page-language.js @@ -0,0 +1,11 @@ +exports.up = async function (knex) { + await knex.schema.alterTable("status_page", function (table) { + table.string("language", 20); + }); +}; + +exports.down = function (knex) { + return knex.schema.alterTable("status_page", function (table) { + table.dropColumn("language"); + }); +}; diff --git a/server/model/status_page.js b/server/model/status_page.js index e43fbe2ae8..587f570310 100644 --- a/server/model/status_page.js +++ b/server/model/status_page.js @@ -458,6 +458,7 @@ class StatusPage extends BeanModel { showCertificateExpiry: !!this.show_certificate_expiry, showOnlyLastHeartbeat: !!this.show_only_last_heartbeat, rssTitle: this.rss_title, + language: this.language, }; } @@ -485,6 +486,7 @@ class StatusPage extends BeanModel { showCertificateExpiry: !!this.show_certificate_expiry, showOnlyLastHeartbeat: !!this.show_only_last_heartbeat, rssTitle: this.rss_title, + language: this.language, }; } diff --git a/server/socket-handlers/status-page-socket-handler.js b/server/socket-handlers/status-page-socket-handler.js index 76f6775df1..2aecbc88e7 100644 --- a/server/socket-handlers/status-page-socket-handler.js +++ b/server/socket-handlers/status-page-socket-handler.js @@ -171,6 +171,11 @@ module.exports.statusPageSocketHandler = (socket) => { statusPage.analytics_id = config.analyticsId; statusPage.analytics_script_url = config.analyticsScriptUrl; statusPage.analytics_type = config.analyticsType; + if (typeof config.language === "string" && config.language.trim() !== "") { + statusPage.language = config.language.trim(); + } else { + statusPage.language = null; + } await R.store(statusPage); diff --git a/src/i18n.js b/src/i18n.js index fa5242ed8d..7c63d66162 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -95,8 +95,8 @@ export function currentLocale() { return "en"; } -export const localeDirection = () => { - return rtlLangs.includes(currentLocale()) ? "rtl" : "ltr"; +export const localeDirection = (locale = currentLocale()) => { + return rtlLangs.includes(locale) ? "rtl" : "ltr"; }; export const i18n = createI18n({ diff --git a/src/lang/en.json b/src/lang/en.json index b7763bc615..04c44db0cf 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -390,6 +390,9 @@ "Footer Text": "Footer Text", "RSS Title": "RSS Title", "Leave blank to use status page title": "Leave blank to use status page title", + "Status Page Language": "Status Page Language", + "Use browser language": "Use browser language", + "statusPageLanguageDescription": "Set the display language for anonymous visitors to this status page", "Refresh Interval": "Refresh Interval", "Refresh Interval Description": "The status page will do a full site refresh every {0} seconds", "Show Powered By": "Show Powered By", diff --git a/src/mixins/lang.js b/src/mixins/lang.js index 508e15c28b..39be8ba6ec 100644 --- a/src/mixins/lang.js +++ b/src/mixins/lang.js @@ -6,6 +6,7 @@ export default { data() { return { language: currentLocale(), + persistLanguage: true, }; }, @@ -17,23 +18,41 @@ export default { watch: { async language(lang) { - await this.changeLang(lang); + await this.changeLang(lang, { + persist: this.persistLanguage, + }); + this.persistLanguage = true; }, }, methods: { + /** + * Set the application language + * @param {string} lang Language code to switch to + * @param {{ persist?: boolean }} options Options for language change + * @returns {void} + */ + setLanguage(lang, options = {}) { + this.persistLanguage = options.persist !== false; + this.language = lang; + }, + /** * Change the application language * @param {string} lang Language code to switch to + * @param {{ persist?: boolean }} options Options for language change * @returns {Promise} */ - async changeLang(lang) { + async changeLang(lang, options = {}) { + const persist = options.persist !== false; let message = (await langModules["../lang/" + lang + ".json"]()) .default; this.$i18n.setLocaleMessage(lang, message); this.$i18n.locale = lang; - localStorage.locale = lang; - setPageLocale(); + if (persist) { + localStorage.locale = lang; + } + setPageLocale(lang); timeDurationFormatter.updateLocale(lang); }, }, diff --git a/src/pages/StatusPage.vue b/src/pages/StatusPage.vue index 81ae55e10a..cc36f4c54b 100644 --- a/src/pages/StatusPage.vue +++ b/src/pages/StatusPage.vue @@ -125,6 +125,24 @@ + +
+ + +
+ {{ $t("statusPageLanguageDescription") }} +
+
+
{{ $t("Custom CSS") }}
@@ -649,6 +667,10 @@ export default { if (res.ok) { this.config = res.config; + if (!("language" in this.config)) { + this.config.language = null; + } + if (!this.config.customCSS) { this.config.customCSS = "body {\n" + " \n" + @@ -737,10 +759,19 @@ export default { this.config.domainNameList = []; } + if (!("language" in this.config)) { + this.config.language = null; + } + if (this.config.icon) { this.imgDataUrl = this.config.icon; } + // Apply configured language if the visitor hasn't set their own preference + if (this.config.language && !localStorage.locale) { + this.$root.setLanguage(this.config.language, { persist: false }); + } + this.incident = res.data.incident; this.maintenanceList = res.data.maintenanceList; this.$root.publicGroupList = res.data.publicGroupList; diff --git a/src/util-frontend.js b/src/util-frontend.js index 5912620b54..7e42fbc4ce 100644 --- a/src/util-frontend.js +++ b/src/util-frontend.js @@ -62,12 +62,13 @@ export function timezoneList() { /** * Set the locale of the HTML page + * @param {string} locale The locale to use * @returns {void} */ -export function setPageLocale() { +export function setPageLocale(locale = currentLocale()) { const html = document.documentElement; - html.setAttribute("lang", currentLocale() ); - html.setAttribute("dir", localeDirection() ); + html.setAttribute("lang", locale); + html.setAttribute("dir", localeDirection(locale)); } /** @@ -289,4 +290,3 @@ class TimeDurationFormatter { } export const timeDurationFormatter = new TimeDurationFormatter(); - From 934874a94e486b4ea5d5cf15bdbaef5252c51a22 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 9 Jan 2026 04:14:43 +0000 Subject: [PATCH 2/2] [autofix.ci] apply automated fixes --- src/mixins/lang.js | 3 +-- src/pages/StatusPage.vue | 50 +++++++++++++++++++++------------------- 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/src/mixins/lang.js b/src/mixins/lang.js index 39be8ba6ec..e395a449f1 100644 --- a/src/mixins/lang.js +++ b/src/mixins/lang.js @@ -45,8 +45,7 @@ export default { */ async changeLang(lang, options = {}) { const persist = options.persist !== false; - let message = (await langModules["../lang/" + lang + ".json"]()) - .default; + let message = (await langModules["../lang/" + lang + ".json"]()).default; this.$i18n.setLocaleMessage(lang, message); this.$i18n.locale = lang; if (persist) { diff --git a/src/pages/StatusPage.vue b/src/pages/StatusPage.vue index f2ce4b81f0..96510e54e2 100644 --- a/src/pages/StatusPage.vue +++ b/src/pages/StatusPage.vue @@ -208,13 +208,14 @@
- - @@ -956,29 +957,30 @@ export default { this.slug = "default"; } - this.getData().then((res) => { - this.config = res.data.config; + this.getData() + .then((res) => { + this.config = res.data.config; - if (!this.config.domainNameList) { - this.config.domainNameList = []; - } + if (!this.config.domainNameList) { + this.config.domainNameList = []; + } - if (!("language" in this.config)) { - this.config.language = null; - } + if (!("language" in this.config)) { + this.config.language = null; + } - if (this.config.icon) { - this.imgDataUrl = this.config.icon; - } + if (this.config.icon) { + this.imgDataUrl = this.config.icon; + } - // Apply configured language if the visitor hasn't set their own preference - if (this.config.language && !localStorage.locale) { - this.$root.setLanguage(this.config.language, { persist: false }); - } + // Apply configured language if the visitor hasn't set their own preference + if (this.config.language && !localStorage.locale) { + this.$root.setLanguage(this.config.language, { persist: false }); + } - this.incident = res.data.incident; - this.maintenanceList = res.data.maintenanceList; - this.$root.publicGroupList = res.data.publicGroupList; + this.incident = res.data.incident; + this.maintenanceList = res.data.maintenanceList; + this.$root.publicGroupList = res.data.publicGroupList; if (!this.config.domainNameList) { this.config.domainNameList = [];