Skip to content
Merged
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
16 changes: 12 additions & 4 deletions app/dashboard/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export default function SettingsPage() {
const { t } = useTranslation()
const router = useRouter()
const { user, signOut, signOutPending } = useAuth()
const { settings, updateTheme, isUpdating: isUpdatingTheme } = useUserSettings()
const { settings, updateTheme, isUpdating: isUpdatingTheme, updateLanguage, isUpdatingLanguage } = useUserSettings()
const { isInstallable, isInstalled, install: installPWA } = usePWAInstall()

// Modal states
Expand All @@ -50,8 +50,16 @@ export default function SettingsPage() {
}
}

const handleLanguageChange = (lang: string) => {
const handleLanguageChange = async (lang: string) => {
// Optimistically update UI immediately
i18n.changeLanguage(lang)

// Persist to database
try {
await updateLanguage(lang)
} catch (error) {
console.error("Error updating language:", error)
}
}

const currentTheme = settings?.theme || "system"
Expand Down Expand Up @@ -232,7 +240,7 @@ export default function SettingsPage() {
</div>
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" className="gap-2">
<Button variant="outline" className="gap-2" disabled={isUpdatingLanguage}>
<span className="text-lg">{currentLanguage.flag}</span>
{t("settings.account.change")}
</Button>
Expand All @@ -242,7 +250,7 @@ export default function SettingsPage() {
{languages.map((lang) => (
<button
key={lang.code}
disabled={lang.disabled}
disabled={lang.disabled || isUpdatingLanguage}
onClick={() => handleLanguageChange(lang.code)}
className={cn(
"flex items-center gap-3 w-full px-3 py-2 text-sm font-medium rounded-md transition-colors group",
Expand Down
38 changes: 38 additions & 0 deletions components/providers/LanguageSync.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
'use client';

import { useEffect } from 'react';
import { useUserSettings } from '@/hooks/useUserSettings';
import i18n from '@/i18n';

/**
* LanguageSync component
*
* This component synchronizes the language from the database with i18next.
* It runs once when settings are loaded to ensure the language from DB is applied.
*
* This is a separate component to ensure it runs at the app level and syncs
* language immediately when the user logs in.
*/
export function LanguageSync() {
const { settings } = useUserSettings();

useEffect(() => {
// Only sync when settings are loaded and user is authenticated
if (!settings?.language) {
return;
}

// Get current language (normalize to base code)
const currentLang = (i18n.language || 'en').split('-')[0];

// If the language from DB is different from current language, sync it
// This handles initial load when user logs in
if (settings.language !== currentLang) {
console.log(`[LanguageSync] Syncing language from DB: ${settings.language}`);
i18n.changeLanguage(settings.language);
}
}, [settings?.language]); // Only depend on settings.language to run once when loaded

// This component doesn't render anything
return null;
}
2 changes: 2 additions & 0 deletions components/providers/Providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ThemeProvider as NextThemesProvider } from 'next-themes';
import { Toaster } from '@/components/ui/sonner';
import { ServiceWorkerProvider } from './ServiceWorkerProvider';
import { ThemeSync } from './ThemeSync';
import { LanguageSync } from './LanguageSync';
import { TopLoadingBar } from './TopLoadingBar';
import { NetworkStatusProvider } from './NetworkStatusProvider';

Expand Down Expand Up @@ -64,6 +65,7 @@ export function Providers({ children }: { children: React.ReactNode }) {
disableTransitionOnChange
>
<ThemeSync />
<LanguageSync />
<TopLoadingBar />
<ServiceWorkerProvider>
<NetworkStatusProvider>
Expand Down
60 changes: 60 additions & 0 deletions hooks/useUserSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type UserSettings = {
id: string;
user_id: string;
theme: 'light' | 'dark' | 'system';
language: string;
created_at: string;
updated_at: string;
};
Expand Down Expand Up @@ -85,6 +86,7 @@ export function useUserSettings() {
.insert({
user_id: user.id,
theme: 'system',
language: 'en',
})
.select()
.single();
Expand Down Expand Up @@ -141,6 +143,35 @@ export function useUserSettings() {
},
});

// Mutation to update language
const updateLanguageMutation = useMutation({
mutationFn: async (language: string) => {
if (!user?.id) {
throw new Error('User must be authenticated');
}

const supabase = createBrowserClient();

// Update language in database
const { data, error: updateError } = await supabase
.from('user_settings')
.update({ language })
.eq('user_id', user.id)
.select()
.single();

if (updateError) {
throw updateError;
}

return data as UserSettings;
},
onSuccess: (data) => {
// Update cache with new settings
queryClient.setQueryData(userSettingsKeys.user(user?.id ?? null), data);
},
});

// Sync theme from database to next-themes when settings load
// This effect ensures next-themes is synchronized with the database theme
useEffect(() => {
Expand All @@ -157,6 +188,32 @@ export function useUserSettings() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [settings?.theme, currentTheme]); // Include currentTheme to detect when it changes

// Sync language from database to i18next when settings load
// This effect ensures i18next is synchronized with the database language
// Runs on login and whenever settings change
useEffect(() => {
if (!settings?.language || !user?.id) {
// Don't sync if settings aren't loaded or user isn't authenticated
return;
}

// Import i18n dynamically to avoid circular dependencies
import('@/i18n').then((i18nModule) => {
const i18n = i18nModule.default;

// Always sync language from database when settings load
// This ensures language is restored on login
const currentLang = (i18n.language || 'en').split('-')[0];

if (settings.language !== currentLang) {
console.log(`[useUserSettings] Syncing language from DB: ${settings.language}`);
i18n.changeLanguage(settings.language);
}
}).catch((error) => {
console.error('[useUserSettings] Failed to sync language:', error);
});
}, [settings?.language, user?.id]);

// Invalidate settings query when auth state changes
useEffect(() => {
if (!user?.id) {
Expand All @@ -175,6 +232,9 @@ export function useUserSettings() {
updateTheme: updateThemeMutation.mutateAsync,
isUpdating: updateThemeMutation.isPending,
updateError: updateThemeMutation.error as PostgrestError | null,
updateLanguage: updateLanguageMutation.mutateAsync,
isUpdatingLanguage: updateLanguageMutation.isPending,
updateLanguageError: updateLanguageMutation.error as PostgrestError | null,
};
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
-- ============================================================================
-- Migration: Add language column to user_settings table
-- ============================================================================
-- Adds language preference column to store user's selected language
-- Includes check constraint for valid language codes
-- ============================================================================

-- Add language column with default value
ALTER TABLE public.user_settings
ADD COLUMN language TEXT NOT NULL DEFAULT 'en';

-- Add check constraint to validate language codes
ALTER TABLE public.user_settings
ADD CONSTRAINT user_settings_language_check
CHECK (language IN ('en', 'es', 'pt', 'fr', 'de'));

-- Backfill existing records with default language (already done by DEFAULT, but explicit for clarity)
UPDATE public.user_settings
SET language = 'en'
WHERE language IS NULL;
Loading