From 2e50c652d81518a409b1ed88270b398b5e207667 Mon Sep 17 00:00:00 2001 From: lingxh Date: Mon, 6 Apr 2026 18:11:37 +0800 Subject: [PATCH] feat: add auto locale switch on first visit based on browser language --- .env.dev.example | 4 ++ .env.example | 4 ++ components/providers/intl-provider.tsx | 56 +++++++++++++++++++++++++- 3 files changed, 62 insertions(+), 2 deletions(-) diff --git a/.env.dev.example b/.env.dev.example index 4ab88223..81f5a3b3 100644 --- a/.env.dev.example +++ b/.env.dev.example @@ -25,6 +25,10 @@ JMAP_SERVER_URL=/api/dev-jmap APP_NAME=Bulwark Webmail (Dev) +# Auto-switch locale to browser language on first visit when no saved locale exists. +# Default is disabled; set to "true" to enable. +# NEXT_PUBLIC_AUTO_SWITCH_LOCALE_ON_FIRST_VISIT=true + # ============================================================================= # Session & Settings Sync (optional for dev) # ============================================================================= diff --git a/.env.example b/.env.example index 777871b6..b6b12f1f 100644 --- a/.env.example +++ b/.env.example @@ -9,6 +9,10 @@ # App name displayed in the UI APP_NAME=Bulwark Webmail +# Auto-switch locale to browser language on first visit when no saved locale exists. +# Default is disabled; set to "true" to enable. +# NEXT_PUBLIC_AUTO_SWITCH_LOCALE_ON_FIRST_VISIT=true + # URL of your JMAP-compatible mail server (required unless ALLOW_CUSTOM_JMAP_ENDPOINT is set) JMAP_SERVER_URL=https://your-jmap-server.com diff --git a/components/providers/intl-provider.tsx b/components/providers/intl-provider.tsx index a8dbc449..edb18b7c 100644 --- a/components/providers/intl-provider.tsx +++ b/components/providers/intl-provider.tsx @@ -32,6 +32,43 @@ const ALL_MESSAGES = { zh: zhMessages, }; +type SupportedLocale = keyof typeof ALL_MESSAGES; + +const AUTO_SWITCH_LOCALE_ON_FIRST_VISIT = + process.env.NEXT_PUBLIC_AUTO_SWITCH_LOCALE_ON_FIRST_VISIT === 'true'; + +function normalizeLocale(locale: string | undefined | null): SupportedLocale | null { + if (!locale) return null; + + const normalized = locale.trim().toLowerCase().replace(/_/g, '-'); + if (normalized in ALL_MESSAGES) { + return normalized as SupportedLocale; + } + + const primary = normalized.split('-')[0]; + if (primary in ALL_MESSAGES) { + return primary as SupportedLocale; + } + + return null; +} + +function detectBrowserLocale(): SupportedLocale { + if (typeof navigator === 'undefined') return 'en'; + + const preferred = [ + ...(navigator.languages ?? []), + navigator.language, + ].filter(Boolean); + + for (const locale of preferred) { + const match = normalizeLocale(locale); + if (match) return match; + } + + return 'en'; +} + interface IntlProviderProps { locale: string; messages: Record; @@ -58,8 +95,23 @@ export function IntlProvider({ locale: initialLocale, children }: IntlProviderPr // Sync initial locale with store on first mount only useEffect(() => { + try { + const persisted = localStorage.getItem('locale-storage'); + + if (!persisted && AUTO_SWITCH_LOCALE_ON_FIRST_VISIT) { + const detected = detectBrowserLocale(); + setLocale(detected); + setActiveLocale(detected); + return; + } + } catch { + // Ignore storage errors and fall back to server locale + } + if (!currentLocale) { - setLocale(initialLocale); + const fallback = normalizeLocale(initialLocale) ?? 'en'; + setLocale(fallback); + setActiveLocale(fallback); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -80,4 +132,4 @@ export function IntlProvider({ locale: initialLocale, children }: IntlProviderPr {children} ); -} +} \ No newline at end of file