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 9952d56a8a..b8a037e5b1 100644 --- a/server/model/status_page.js +++ b/server/model/status_page.js @@ -448,6 +448,7 @@ class StatusPage extends BeanModel { showCertificateExpiry: !!this.show_certificate_expiry, showOnlyLastHeartbeat: !!this.show_only_last_heartbeat, rssTitle: this.rss_title, + language: this.language, }; } @@ -475,6 +476,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 a863076cca..c61b578c12 100644 --- a/server/socket-handlers/status-page-socket-handler.js +++ b/server/socket-handlers/status-page-socket-handler.js @@ -160,6 +160,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 5874e3bb35..9d8b8fd601 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 09a99998d6..502f97c435 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 8ab8bd11fe..e395a449f1 100644 --- a/src/mixins/lang.js +++ b/src/mixins/lang.js @@ -6,6 +6,7 @@ export default { data() { return { language: currentLocale(), + persistLanguage: true, }; }, @@ -17,22 +18,40 @@ 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 5704600999..96510e54e2 100644 --- a/src/pages/StatusPage.vue +++ b/src/pages/StatusPage.vue @@ -205,6 +205,25 @@ + +
+ + +
+ {{ $t("statusPageLanguageDescription") }} +
+
+
{{ $t("Custom CSS") }}
@@ -857,6 +876,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" + "}\n"; } @@ -942,6 +965,27 @@ 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; + + if (!this.config.domainNameList) { + this.config.domainNameList = []; + } + if (this.config.icon) { this.imgDataUrl = this.config.icon; } diff --git a/src/util-frontend.js b/src/util-frontend.js index e2a912202a..bab7bf4ddb 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)); } /**