Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.dev.example
Original file line number Diff line number Diff line change
Expand Up @@ -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)
# =============================================================================
Expand Down
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
56 changes: 54 additions & 2 deletions components/providers/intl-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>;
Expand All @@ -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
}, []);
Expand All @@ -80,4 +132,4 @@ export function IntlProvider({ locale: initialLocale, children }: IntlProviderPr
{children}
</NextIntlClientProvider>
);
}
}