Skip to content

Commit 53bd9cf

Browse files
ralyodioclaude
andcommitted
fix(hydration): init theme and language stores with SSR-safe defaults
Both useThemeStore and currentLanguage were reading localStorage/navigator at module evaluation time on the client, producing different initial values than the server ('light'/'en'). This caused React error #418 (hydration mismatch) on every page load — the Navbar rendered different icons and translated strings than the server HTML. Fix: both stores now always initialize to 'light'/'en'. ClientLayout's existing mount useEffect already calls themeUtils.setTheme() to apply the stored theme; a new language-init block does the same for i18n. Non-English users still get their language applied immediately after hydration. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent bfca610 commit 53bd9cf

3 files changed

Lines changed: 17 additions & 5 deletions

File tree

src/app/client-layout.jsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react';
44
import { usePathname } from 'next/navigation';
55
import { useThemeStore, themeUtils } from '@/lib/stores/theme.js';
66
import { useI18n } from '@/lib/hooks/useI18n.js';
7+
import { i18nUtils, languages } from '@/lib/stores/i18n.js';
78
import Navbar from '@/lib/components/Navbar.jsx';
89
import Footer from '@/lib/components/Footer.jsx';
910
import PWAToastManager from '@/lib/components/PWAToastManager.jsx';
@@ -21,12 +22,22 @@ export default function ClientLayout({ children }) {
2122
useEffect(() => {
2223
setMounted(true);
2324

25+
// Apply stored or system theme
2426
let theme = localStorage.getItem('qrypt-theme');
2527
if (!theme) {
2628
theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
2729
}
2830
themeUtils.setTheme(theme);
2931

32+
// Apply stored or browser language (non-English only — 'en' is already loaded)
33+
const storedLang = localStorage.getItem('qrypt-language');
34+
const browserLang = navigator.language.split('-')[0];
35+
const lang = (storedLang && languages[storedLang]) ? storedLang
36+
: (languages[browserLang] ? browserLang : 'en');
37+
if (lang !== 'en') {
38+
i18nUtils.setLanguage(lang);
39+
}
40+
3041
if ('serviceWorker' in navigator) {
3142
navigator.serviceWorker
3243
.register('/sw.js')

src/lib/stores/i18n.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,9 @@ async function loadTranslations(languageCode) {
3838
}
3939
}
4040

41-
// Svelte writable store for the current language code
42-
export const currentLanguage = writable(
43-
typeof window === 'undefined' ? 'en' : getInitialLanguage()
44-
);
41+
// Always start with 'en' so server and client initial renders match.
42+
// ClientLayout's useEffect applies the stored/preferred language after mount.
43+
export const currentLanguage = writable('en');
4544

4645
// Svelte writable store holding all loaded translation objects keyed by language code
4746
export const _translations = writable({ en: enTranslations });

src/lib/stores/theme.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ export const themeUtils = {
8282
},
8383
};
8484

85+
// Always start with 'light' so server and client initial renders match.
86+
// ClientLayout's useEffect applies the stored/preferred theme after mount.
8587
export const useThemeStore = create(() => ({
86-
currentTheme: typeof window !== 'undefined' ? getInitialTheme() : 'light',
88+
currentTheme: 'light',
8789
}));

0 commit comments

Comments
 (0)