From 517abca7993361acae324b69f312fca7ad356408 Mon Sep 17 00:00:00 2001 From: Alexander Ackermann Date: Thu, 27 Aug 2026 14:10:01 +0200 Subject: [PATCH] fix(runtime): preserve selected language on plain layout pages --- .../web-runtime/src/container/bootstrap.ts | 6 +- packages/web-runtime/src/helpers/language.ts | 59 ++++++++++++--- .../tests/unit/helpers/language.spec.ts | 71 +++++++++++++++++++ 3 files changed, 126 insertions(+), 10 deletions(-) create mode 100644 packages/web-runtime/tests/unit/helpers/language.spec.ts diff --git a/packages/web-runtime/src/container/bootstrap.ts b/packages/web-runtime/src/container/bootstrap.ts index 6eccce16955..1315e19ecf1 100644 --- a/packages/web-runtime/src/container/bootstrap.ts +++ b/packages/web-runtime/src/container/bootstrap.ts @@ -89,7 +89,7 @@ import { onSSESpaceDeletedEvent, onSSESpaceEnabledEvent } from './sse' -import { loadAppTranslations } from '../helpers/language' +import { loadAppTranslations, resolveInitialLanguage } from '../helpers/language' import { urlJoin } from '@opencloud-eu/web-client' import { sha256 } from '@noble/hashes/sha2.js' import { bytesToHex } from '@noble/hashes/utils.js' @@ -440,7 +440,9 @@ export const announceGettext = ({ app: App } & Partial) => { const gettext = createGettext({ - defaultLanguage: navigator.language.substring(0, 2), + defaultLanguage: resolveInitialLanguage({ + browserLanguage: navigator.language + }), silent: true, ...options }) diff --git a/packages/web-runtime/src/helpers/language.ts b/packages/web-runtime/src/helpers/language.ts index d39b7ca887f..d87c8dfc061 100644 --- a/packages/web-runtime/src/helpers/language.ts +++ b/packages/web-runtime/src/helpers/language.ts @@ -2,21 +2,64 @@ import { ApplicationInformation } from '@opencloud-eu/web-pkg' import { merge } from 'lodash-es' import { Language, Translations } from 'vue3-gettext' +export const currentLanguageLocalStorageKey = 'oc_language' + +function normalizeLanguage(languageSetting: string): string { + const trimmed = languageSetting.trim() + if (!trimmed) { + return '' + } + return trimmed.includes('-') ? trimmed.split('-')[0] : trimmed +} + +function getStoredLanguage(): string { + const storedLanguage = window.localStorage.getItem(currentLanguageLocalStorageKey) ?? '' + if (!storedLanguage) { + return '' + } + return normalizeLanguage(storedLanguage) +} + +function storeLanguage(language: string): void { + window.localStorage.setItem(currentLanguageLocalStorageKey, language) +} + +function setDocumentLanguage(languageSetting: string): void { + const currentLanguage = normalizeLanguage(languageSetting) + if (!currentLanguage) { + return + } + + document.documentElement.lang = currentLanguage +} + +export const resolveInitialLanguage = ({ + browserLanguage +}: { + browserLanguage: string +}): string => { + const stored = getStoredLanguage() + const currentLanguage = stored || normalizeLanguage(browserLanguage) || 'en' + + setDocumentLanguage(currentLanguage) + return currentLanguage +} + export const setCurrentLanguage = ({ language, languageSetting = null }: { language: Language - languageSetting?: string + languageSetting?: string | null }): void => { - let currentLanguage = languageSetting - if (currentLanguage) { - if (currentLanguage.indexOf('-')) { - currentLanguage = currentLanguage.split('-')[0] - } - language.current = currentLanguage - document.documentElement.lang = currentLanguage + const currentLanguage = normalizeLanguage(languageSetting || language.current) + if (!currentLanguage) { + return } + + language.current = currentLanguage + setDocumentLanguage(currentLanguage) + storeLanguage(currentLanguage) } /** diff --git a/packages/web-runtime/tests/unit/helpers/language.spec.ts b/packages/web-runtime/tests/unit/helpers/language.spec.ts new file mode 100644 index 00000000000..e60ce503db8 --- /dev/null +++ b/packages/web-runtime/tests/unit/helpers/language.spec.ts @@ -0,0 +1,71 @@ +import { mock } from 'vitest-mock-extended' +import { nextTick } from 'vue' +import type { Language } from 'vue3-gettext' +import { + currentLanguageLocalStorageKey, + resolveInitialLanguage, + setCurrentLanguage +} from '../../../src/helpers/language' + +describe('language helpers', () => { + beforeEach(() => { + window.localStorage.clear() + document.documentElement.lang = '' + }) + + it('prefers stored language over browser language', () => { + window.localStorage.setItem(currentLanguageLocalStorageKey, 'en') + + const lang = resolveInitialLanguage({ + browserLanguage: 'de-DE' + }) + + expect(lang).toBe('en') + expect(document.documentElement.lang).toBe('en') + }) + + it('falls back to browser language when no stored language exists', () => { + const lang = resolveInitialLanguage({ + browserLanguage: 'de-DE' + }) + + expect(lang).toBe('de') + expect(document.documentElement.lang).toBe('de') + }) + + it('falls back to english when browser language is missing', () => { + const lang = resolveInitialLanguage({ + browserLanguage: '' + }) + + expect(lang).toBe('en') + expect(document.documentElement.lang).toBe('en') + }) + + it('sets, normalizes and stores the selected language', async () => { + const language = mock({ current: 'de' }) + + setCurrentLanguage({ + language, + languageSetting: 'en-US' + }) + await nextTick() + + expect(language.current).toBe('en') + expect(document.documentElement.lang).toBe('en') + expect(window.localStorage.getItem(currentLanguageLocalStorageKey)).toBe('en') + }) + + it('sets and stores current language when language setting is not provided', async () => { + const language = mock({ current: 'fr-FR' }) + + setCurrentLanguage({ + language + }) + await nextTick() + + expect(language.current).toBe('fr') + expect(document.documentElement.lang).toBe('fr') + expect(window.localStorage.getItem(currentLanguageLocalStorageKey)).toBe('fr') + }) +})