Description
Bug Description
The UI is always displayed in English, even when:
- The browser's
Accept-Language header is set to a non-English locale (e.g. de-DE)
- The URL already contains the correct locale prefix (e.g.
/de/login)
- The server-side locale detection works correctly (
requestLocale: de confirmed in logs)
Root Cause
The bug is in stores/locale-store.ts. The Zustand store is initialized with a hardcoded 'en' default and persisted to localStorage:
export const useLocaleStore = create<LocaleStore>()(
persist(
(set) => ({
locale: 'en', // ← hardcoded English default
setLocale: (locale) => set({ locale }),
}),
{ name: 'locale-storage' }
)
);
In components/providers/intl-provider.tsx, the active locale is initialized from the store:
const [activeLocale, setActiveLocale] = useState(currentLocale || initialLocale);
On first visit, currentLocale from the store is 'en' (hardcoded default), which takes precedence over initialLocale (correctly resolved server-side locale). Since the store is persisted to localStorage, this 'en' value is sticky across sessions.
The full chain that works correctly but is overridden:
- ✅
proxy.ts → next-intl sets x-next-intl-locale: de
- ✅
i18n/request.ts → requestLocale: de
- ✅
app/[locale]/layout.tsx → loads German messages
- ❌
locale-store.ts → overwrites everything with 'en'
Steps to Reproduce
- Clear browser localStorage (or use a fresh browser profile)
- Set browser language to any non-English language (e.g. German)
- Open Bulwark — UI is displayed in English despite correct URL prefix (
/de/login) and correct Accept-Language header
Fix
stores/locale-store.ts — change default from 'en' to '':
persist(
(set) => ({
locale: '', // empty instead of hardcoded 'en'
setLocale: (locale) => set({ locale }),
}),
{ name: 'locale-storage' }
)
components/providers/intl-provider.tsx — initialize state from server-provided locale, not store:
// Before
const [activeLocale, setActiveLocale] = useState(currentLocale || initialLocale);
// After
const [activeLocale, setActiveLocale] = useState(initialLocale);
And update the first useEffect:
useEffect(() => {
if (!currentLocale) {
setLocale(initialLocale);
} else {
setActiveLocale(currentLocale);
}
}, []);
Environment
- Bulwark version: 1.6.7
- Deployment: native (non-Docker)
NEXT_PUBLIC_BASE_PATH=/webmail
NEXT_PUBLIC_LOCALE_PREFIX=always
- Browser: Chrome 148, Windows 10, language set to
de-DE
Steps to Reproduce
- Clear browser localStorage (DevTools → Application → localStorage → delete 'locale-storage')
- Set browser language to any non-English language (e.g. de-DE)
- Open Bulwark — UI is displayed in English despite correct URL prefix (/de/login)
Expected Behavior
The UI should be displayed in the language matching the browser's Accept-Language header and the URL locale prefix (e.g. German when browser language is de-DE).
Actual Behavior
The UI is always displayed in English. The server-side locale detection works correctly (requestLocale: de confirmed in logs, URL shows /de/login), but the client-side locale-store.ts has 'en' hardcoded as default and persisted in localStorage, which overrides the correctly resolved server locale in intl-provider.tsx.
Bulwark Version
1.6.7
Stalwart Mail Server Version
0.16.5
Browser
Chrome / Chromium
Operating System
Windows
Screenshots / Screen Recording
No response
Relevant Logs or Error Output
Additional Context
No response
Description
Bug Description
The UI is always displayed in English, even when:
Accept-Languageheader is set to a non-English locale (e.g.de-DE)/de/login)requestLocale: deconfirmed in logs)Root Cause
The bug is in
stores/locale-store.ts. The Zustand store is initialized with a hardcoded'en'default and persisted tolocalStorage:In
components/providers/intl-provider.tsx, the active locale is initialized from the store:On first visit,
currentLocalefrom the store is'en'(hardcoded default), which takes precedence overinitialLocale(correctly resolved server-side locale). Since the store is persisted tolocalStorage, this'en'value is sticky across sessions.The full chain that works correctly but is overridden:
proxy.ts→ next-intl setsx-next-intl-locale: dei18n/request.ts→requestLocale: deapp/[locale]/layout.tsx→ loads German messageslocale-store.ts→ overwrites everything with'en'Steps to Reproduce
/de/login) and correctAccept-LanguageheaderFix
stores/locale-store.ts— change default from'en'to'':components/providers/intl-provider.tsx— initialize state from server-provided locale, not store:And update the first
useEffect:Environment
NEXT_PUBLIC_BASE_PATH=/webmailNEXT_PUBLIC_LOCALE_PREFIX=alwaysde-DESteps to Reproduce
Expected Behavior
The UI should be displayed in the language matching the browser's Accept-Language header and the URL locale prefix (e.g. German when browser language is de-DE).
Actual Behavior
The UI is always displayed in English. The server-side locale detection works correctly (requestLocale: de confirmed in logs, URL shows /de/login), but the client-side locale-store.ts has 'en' hardcoded as default and persisted in localStorage, which overrides the correctly resolved server locale in intl-provider.tsx.
Bulwark Version
1.6.7
Stalwart Mail Server Version
0.16.5
Browser
Chrome / Chromium
Operating System
Windows
Screenshots / Screen Recording
No response
Relevant Logs or Error Output
Additional Context
No response