Skip to content

[Bug]: UI always displays in English regardless of browser language or URL locale prefix #309

Description

@dealerweb

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:

  1. proxy.ts → next-intl sets x-next-intl-locale: de
  2. i18n/request.tsrequestLocale: de
  3. app/[locale]/layout.tsx → loads German messages
  4. locale-store.ts → overwrites everything with 'en'

Steps to Reproduce

  1. Clear browser localStorage (or use a fresh browser profile)
  2. Set browser language to any non-English language (e.g. German)
  3. 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

  1. Clear browser localStorage (DevTools → Application → localStorage → delete 'locale-storage')
  2. Set browser language to any non-English language (e.g. de-DE)
  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions