Configure web display language from profile settings, default is set to

This commit is contained in:
Indigo Tang 2025-06-09 03:36:02 +00:00
parent 261f21af55
commit 2bc4fcc77f
5 changed files with 87 additions and 16 deletions

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { useState } from 'react'; import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
@ -9,7 +9,8 @@ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Save } from 'lucide-react'; import { Save } from 'lucide-react';
import { useToast } from '@/hooks/use-toast'; import { useToast } from '@/hooks/use-toast';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { useI18n } from '@/locales/client'; import { useI18n, useCurrentLocale, useChangeLocale } from '@/locales/client';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
const mockUserProfile = { const mockUserProfile = {
name: 'Alice Wonderland', name: 'Alice Wonderland',
@ -23,6 +24,8 @@ const mockUserProfile = {
export default function ProfilePage() { export default function ProfilePage() {
const { toast } = useToast(); const { toast } = useToast();
const t = useI18n(); const t = useI18n();
const currentLocale = useCurrentLocale();
const changeLocale = useChangeLocale();
const [name, setName] = useState(mockUserProfile.name); const [name, setName] = useState(mockUserProfile.name);
const [email, setEmail] = useState(mockUserProfile.email); const [email, setEmail] = useState(mockUserProfile.email);
@ -33,39 +36,58 @@ export default function ProfilePage() {
const [currentPassword, setCurrentPassword] = useState(''); const [currentPassword, setCurrentPassword] = useState('');
const [newPassword, setNewPassword] = useState(''); const [newPassword, setNewPassword] = useState('');
const [confirmNewPassword, setConfirmNewPassword] = useState(''); const [confirmNewPassword, setConfirmNewPassword] = useState('');
const [selectedLanguage, setSelectedLanguage] = useState(currentLocale);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isPasswordLoading, setIsPasswordLoading] = useState(false); const [isPasswordLoading, setIsPasswordLoading] = useState(false);
const [isLanguageLoading, setIsLanguageLoading] = useState(false);
useEffect(() => {
setSelectedLanguage(currentLocale);
}, [currentLocale]);
const handleProfileUpdate = async (e: React.FormEvent<HTMLFormElement>) => { const handleProfileUpdate = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
setIsLoading(true); setIsLoading(true);
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise(resolve => setTimeout(resolve, 1000));
console.log("Updating profile:", { name, avatarUrl, bio, phone, location }); console.log("Updating profile:", { name, avatarUrl, bio, phone, location });
toast({ title: "Profile Updated", description: "Your profile information has been saved." }); // Translate toast({ title: t('dashboard.profile.save_button'), description: "Your profile information has been saved." });
setIsLoading(false); setIsLoading(false);
}; };
const handlePasswordChange = async (e: React.FormEvent<HTMLFormElement>) => { const handlePasswordChange = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
if (newPassword !== confirmNewPassword) { if (newPassword !== confirmNewPassword) {
toast({ title: "Password Mismatch", description: "New passwords do not match.", variant: "destructive" }); // Translate toast({ title: "Password Mismatch", description: "New passwords do not match.", variant: "destructive" });
return; return;
} }
if (newPassword.length < 6) { if (newPassword.length < 6) {
toast({ title: "Password Too Short", description: "Password must be at least 6 characters.", variant: "destructive" }); // Translate toast({ title: "Password Too Short", description: "Password must be at least 6 characters.", variant: "destructive" });
return; return;
} }
setIsPasswordLoading(true); setIsPasswordLoading(true);
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise(resolve => setTimeout(resolve, 1000));
console.log("Changing password"); console.log("Changing password");
toast({ title: "Password Updated", description: "Your password has been changed successfully." }); // Translate toast({ title: t('dashboard.profile.update_password_button'), description: "Your password has been changed successfully." });
setCurrentPassword(''); setCurrentPassword('');
setNewPassword(''); setNewPassword('');
setConfirmNewPassword(''); setConfirmNewPassword('');
setIsPasswordLoading(false); setIsPasswordLoading(false);
}; };
const handleLanguageChange = (newLang: 'en' | 'zh-TW') => {
setIsLanguageLoading(true);
changeLocale(newLang);
localStorage.setItem('userPreferredLanguage', newLang);
setSelectedLanguage(newLang);
toast({ title: t('dashboard.profile.language_settings_title'), description: t('dashboard.profile.language_updated_toast') });
// No need to manually set isLanguageLoading to false if the page reloads/re-renders due to locale change
// However, if changeLocale doesn't cause an immediate unmount, manage loading state appropriately.
// For simplicity, we assume changeLocale will lead to a re-render where currentLocale updates.
};
return ( return (
<div className="space-y-8 max-w-3xl mx-auto"> <div className="space-y-8 max-w-3xl mx-auto">
<div> <div>
@ -155,6 +177,32 @@ export default function ProfilePage() {
</CardFooter> </CardFooter>
</form> </form>
</Card> </Card>
<Card className="shadow-lg">
<CardHeader>
<CardTitle className="text-xl font-headline">{t('dashboard.profile.language_settings_title')}</CardTitle>
<CardDescription>{t('dashboard.profile.language_settings_description')}</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-2">
<Label htmlFor="language-select">{t('dashboard.profile.display_language_label')}</Label>
<Select
value={selectedLanguage}
onValueChange={(value) => handleLanguageChange(value as 'en' | 'zh-TW')}
disabled={isLanguageLoading}
>
<SelectTrigger id="language-select">
<SelectValue placeholder={t('lang.select_language')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="en">{t('lang.english')}</SelectItem>
<SelectItem value="zh-TW">{t('lang.traditional_chinese')}</SelectItem>
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
</div> </div>
); );
} }

View File

@ -15,23 +15,41 @@ import {
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { ThemeToggleButton } from '@/components/ui/theme-toggle'; import { ThemeToggleButton } from '@/components/ui/theme-toggle';
import LanguageSwitcher from './LanguageSwitcher'; import LanguageSwitcher from './LanguageSwitcher';
import { useI18n, useCurrentLocale } from '@/locales/client'; import { useI18n, useCurrentLocale, useChangeLocale } from '@/locales/client';
export default function Header() { export default function Header() {
const pathname = usePathname(); const pathname = usePathname();
const t = useI18n(); const t = useI18n();
const locale = useCurrentLocale(); const locale = useCurrentLocale();
const changeLocale = useChangeLocale();
const [isAuthenticated, setIsAuthenticated] = useState(false); const [isAuthenticated, setIsAuthenticated] = useState(false);
const [isMounted, setIsMounted] = useState(false);
useEffect(() => { useEffect(() => {
setIsMounted(true);
const authStatus = localStorage.getItem('isToyShareAuthenticated'); const authStatus = localStorage.getItem('isToyShareAuthenticated');
setIsAuthenticated(authStatus === 'true'); setIsAuthenticated(authStatus === 'true');
}, [pathname]); // Listen to pathname changes to re-check auth if needed after navigation }, []); // Run only once on mount
useEffect(() => {
if (isMounted) { // Ensure this runs only on the client after mount
const preferredLang = localStorage.getItem('userPreferredLanguage') as 'en' | 'zh-TW' | null;
if (preferredLang && preferredLang !== locale) {
// Check if the current pathname without locale prefix exists for the preferredLang
// This avoids redirect loops if a page doesn't exist in the preferredLang
const currentPathWithoutLocale = pathname.startsWith(`/${locale}`) ? pathname.substring(`/${locale}`.length) : pathname;
// It's hard to verify if `currentPathWithoutLocale` is valid for `preferredLang` without a full route list
// For simplicity, we'll directly change locale. If a 404 occurs, user can switch back.
changeLocale(preferredLang);
}
}
}, [isMounted, locale, pathname, changeLocale]);
const handleLogout = () => { const handleLogout = () => {
localStorage.removeItem('isToyShareAuthenticated'); localStorage.removeItem('isToyShareAuthenticated');
setIsAuthenticated(false); setIsAuthenticated(false);
// No need to router.push here, links will use current locale // No need to router.push here, links will use current locale, or redirect logic will apply
}; };
// Helper to remove locale prefix for path comparison // Helper to remove locale prefix for path comparison
@ -78,7 +96,7 @@ export default function Header() {
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem onClick={handleLogout}> <DropdownMenuItem onClick={handleLogout}>
<LogIn className="mr-2 h-4 w-4" /> {/* Using LogIn icon for logout action for consistency */} <LogIn className="mr-2 h-4 w-4" />
{t('header.logout')} {t('header.logout')}
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
@ -102,7 +120,6 @@ export default function Header() {
</nav> </nav>
<LanguageSwitcher /> <LanguageSwitcher />
<ThemeToggleButton /> <ThemeToggleButton />
{/* Mobile menu trigger could be added here if needed */}
</div> </div>
</div> </div>
</header> </header>

View File

@ -92,8 +92,12 @@ export default {
'dashboard.profile.confirm_new_password_label': 'Confirm New Password', 'dashboard.profile.confirm_new_password_label': 'Confirm New Password',
'dashboard.profile.update_password_button': 'Update Password', 'dashboard.profile.update_password_button': 'Update Password',
'dashboard.profile.updating_password_button': 'Updating Password...', 'dashboard.profile.updating_password_button': 'Updating Password...',
'dashboard.profile.language_settings_title': 'Language Settings',
'dashboard.profile.language_settings_description': 'Choose your preferred display language for the application.',
'dashboard.profile.display_language_label': 'Display Language',
'dashboard.profile.language_updated_toast': 'Language updated successfully.',
'lang.english': 'English', 'lang.english': 'English',
'lang.traditional_chinese': '繁體中文', 'lang.traditional_chinese': 'Traditional Chinese',
'lang.select_language': 'Select Language', 'lang.select_language': 'Select Language',
'general.back_to_my_toys': 'Back to My Toys', 'general.back_to_my_toys': 'Back to My Toys',
'general.toy_not_found': 'Toy Not Found', 'general.toy_not_found': 'Toy Not Found',

View File

@ -92,6 +92,10 @@ export default {
'dashboard.profile.confirm_new_password_label': '確認新密碼', 'dashboard.profile.confirm_new_password_label': '確認新密碼',
'dashboard.profile.update_password_button': '更新密碼', 'dashboard.profile.update_password_button': '更新密碼',
'dashboard.profile.updating_password_button': '更新密碼中...', 'dashboard.profile.updating_password_button': '更新密碼中...',
'dashboard.profile.language_settings_title': '語言設定',
'dashboard.profile.language_settings_description': '選擇您偏好的應用程式顯示語言。',
'dashboard.profile.display_language_label': '顯示語言',
'dashboard.profile.language_updated_toast': '語言已成功更新。',
'lang.english': 'English', 'lang.english': 'English',
'lang.traditional_chinese': '繁體中文', 'lang.traditional_chinese': '繁體中文',
'lang.select_language': '選擇語言', 'lang.select_language': '選擇語言',

View File

@ -3,9 +3,8 @@ import { createI18nMiddleware } from 'next-international/middleware';
const I18nMiddleware = createI18nMiddleware({ const I18nMiddleware = createI18nMiddleware({
locales: ['en', 'zh-TW'], locales: ['en', 'zh-TW'],
defaultLocale: 'en', defaultLocale: 'zh-TW', // Changed default locale
urlMappingStrategy: 'rewrite', // Or 'redirect', depending on preference urlMappingStrategy: 'rewrite',
// prefix: 'as-needed' // default is 'as-needed'
}); });
export function middleware(request: NextRequest) { export function middleware(request: NextRequest) {
@ -13,6 +12,5 @@ export function middleware(request: NextRequest) {
} }
export const config = { export const config = {
// Matcher ignoring `/_next/` and `/api/`
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'], matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
}; };