localization
This commit is contained in:
@@ -5,6 +5,7 @@ import { Label } from '@/components/ui/label';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useForm } from 'hooks/useForm';
|
||||
import { useAuth } from 'hooks/useAuth';
|
||||
import { useTranslation } from '@/lib/i18n';
|
||||
|
||||
type PasswordFormState = {
|
||||
password?: string;
|
||||
@@ -19,6 +20,7 @@ const validate = (state: Partial<PasswordFormState>) => {
|
||||
|
||||
export const ChangePassword = () => {
|
||||
const { changePassword } = useAuth();
|
||||
const { t } = useTranslation();
|
||||
const [isChanging, setIsChanging] = useState(false);
|
||||
const form = useForm<PasswordFormState>({}, validate);
|
||||
|
||||
@@ -33,11 +35,11 @@ export const ChangePassword = () => {
|
||||
newPassword: form.state.newPassword!,
|
||||
confirmPassword: form.state.confirmPassword!,
|
||||
});
|
||||
toast.success('Password changed');
|
||||
toast.success(t('settings.profile.changePassword.passwordChanged'));
|
||||
form.update({ password: '', newPassword: '', confirmPassword: '' });
|
||||
} catch (ex) {
|
||||
const error = ex as { message?: string };
|
||||
toast.error(error.message || 'Failed to change password');
|
||||
toast.error(error.message || t('settings.profile.changePassword.passwordChangeFailed'));
|
||||
form.update({ password: '', newPassword: '', confirmPassword: '' });
|
||||
} finally {
|
||||
setIsChanging(false);
|
||||
@@ -48,34 +50,34 @@ export const ChangePassword = () => {
|
||||
<div>
|
||||
<form ref={form.formRef} onSubmit={handleSubmit} className="grid gap-4">
|
||||
<Label className="grid gap-2">
|
||||
<span className="text-duck-dark/70">Current Password</span>
|
||||
<span className="text-duck-dark/70">{t('settings.profile.changePassword.currentPasswordLabel')}</span>
|
||||
<Input
|
||||
className="h-11 bg-white/60 border-duck-dark/20 text-duck-dark placeholder:text-duck-dark/40"
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder="Current password"
|
||||
placeholder={t('settings.profile.changePassword.currentPasswordPlaceholder')}
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<Label className="grid gap-2">
|
||||
<span className="text-duck-dark/70">New Password</span>
|
||||
<span className="text-duck-dark/70">{t('settings.profile.changePassword.newPasswordLabel')}</span>
|
||||
<Input
|
||||
className="h-11 bg-white/60 border-duck-dark/20 text-duck-dark placeholder:text-duck-dark/40"
|
||||
type="password"
|
||||
name="newPassword"
|
||||
placeholder="New password"
|
||||
placeholder={t('settings.profile.changePassword.newPasswordPlaceholder')}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<Label className="grid gap-2">
|
||||
<span className="text-duck-dark/70">Confirm New Password</span>
|
||||
<span className="text-duck-dark/70">{t('settings.profile.changePassword.confirmPasswordLabel')}</span>
|
||||
<Input
|
||||
className="h-11 bg-white/60 border-duck-dark/20 text-duck-dark placeholder:text-duck-dark/40"
|
||||
type="password"
|
||||
name="confirmPassword"
|
||||
placeholder="Confirm new password"
|
||||
placeholder={t('settings.profile.changePassword.confirmPasswordPlaceholder')}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</Label>
|
||||
@@ -85,7 +87,9 @@ export const ChangePassword = () => {
|
||||
disabled={!form.isValid || isChanging}
|
||||
className="w-full h-11 bg-duck-yellow hover:bg-duck-yellow/90 text-duck-teal font-bold transition-all duration-200 hover:scale-105 cursor-pointer disabled:opacity-50 disabled:hover:scale-100"
|
||||
>
|
||||
{isChanging ? 'Changing...' : 'Change Password'}
|
||||
{isChanging
|
||||
? t('settings.profile.changePassword.changingButton')
|
||||
: t('settings.profile.changePassword.changeButton')}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState } from 'react';
|
||||
import { X } from 'lucide-react';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { useSettings } from '@/state/useSettings';
|
||||
import { useTranslation } from '@/lib/i18n';
|
||||
|
||||
const LANGUAGES = [
|
||||
{ code: 'en', label: 'English' },
|
||||
@@ -38,6 +39,7 @@ const getLabel = (code: string) => LANGUAGES.find((l) => l.code === code)?.label
|
||||
|
||||
export const Languages = () => {
|
||||
const { settings, saveSettings } = useSettings();
|
||||
const { t } = useTranslation();
|
||||
const { spoken, default: defaultLang, translateTo } = settings.languages;
|
||||
const [addingLang, setAddingLang] = useState('');
|
||||
|
||||
@@ -65,7 +67,7 @@ export const Languages = () => {
|
||||
<div className="grid gap-5">
|
||||
{/* Spoken languages */}
|
||||
<div className="grid gap-2">
|
||||
<span className="text-sm font-medium text-duck-dark/70">Languages you speak</span>
|
||||
<span className="text-sm font-medium text-duck-dark/70">{t('settings.profile.languages.spokenLabel')}</span>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{spoken.map((code) => (
|
||||
<span
|
||||
@@ -91,7 +93,7 @@ export const Languages = () => {
|
||||
onChange={(ev) => addSpoken(ev.target.value)}
|
||||
className="text-sm border border-duck-dark/20 rounded-md px-3 py-1.5 bg-white/60 text-duck-dark cursor-pointer"
|
||||
>
|
||||
<option value="">Add a language...</option>
|
||||
<option value="">{t('settings.profile.languages.addLanguage')}</option>
|
||||
{availableToAdd.map((l) => (
|
||||
<option key={l.code} value={l.code}>
|
||||
{l.label}
|
||||
@@ -104,7 +106,7 @@ export const Languages = () => {
|
||||
|
||||
{/* Default language */}
|
||||
<Label className="grid gap-2">
|
||||
<span className="text-duck-dark/70">Default language</span>
|
||||
<span className="text-duck-dark/70">{t('settings.profile.languages.defaultLabel')}</span>
|
||||
<select
|
||||
value={defaultLang}
|
||||
onChange={(ev) => save({ ...settings.languages, default: ev.target.value })}
|
||||
@@ -116,12 +118,12 @@ export const Languages = () => {
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<span className="text-xs text-duck-dark/40">Used for future UI localization.</span>
|
||||
<span className="text-xs text-duck-dark/40">{t('settings.profile.languages.defaultHint')}</span>
|
||||
</Label>
|
||||
|
||||
{/* Translate from */}
|
||||
{/* Translate to */}
|
||||
<Label className="grid gap-2">
|
||||
<span className="text-duck-dark/70">Translate to</span>
|
||||
<span className="text-duck-dark/70">{t('settings.profile.languages.translateToLabel')}</span>
|
||||
<select
|
||||
value={translateTo}
|
||||
onChange={(ev) => save({ ...settings.languages, translateTo: ev.target.value })}
|
||||
@@ -133,7 +135,7 @@ export const Languages = () => {
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<span className="text-xs text-duck-dark/40">Target language when translating content you don't speak.</span>
|
||||
<span className="text-xs text-duck-dark/40">{t('settings.profile.languages.translateToHint')}</span>
|
||||
</Label>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,51 +1,56 @@
|
||||
import { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import { Search, User, Lock, Globe, ListChecks, Palette } from 'lucide-react';
|
||||
import { Search, User, Lock, Globe, ListChecks } from 'lucide-react';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '@/components/ui/accordion';
|
||||
import { Card } from '@/components/Card';
|
||||
import { useTranslation } from '@/lib/i18n';
|
||||
import { UserData } from './UserData';
|
||||
import { ChangePassword } from './ChangePassword';
|
||||
import { Languages } from './Languages';
|
||||
import { TaskDefaults } from './TaskDefaults';
|
||||
|
||||
const sections = [
|
||||
{
|
||||
key: 'profile',
|
||||
icon: User,
|
||||
title: 'Profile',
|
||||
description: 'Update your name and avatar.',
|
||||
content: <UserData />,
|
||||
},
|
||||
{
|
||||
key: 'change-password',
|
||||
icon: Lock,
|
||||
title: 'Change Password',
|
||||
description: 'Update your account password.',
|
||||
content: <ChangePassword />,
|
||||
},
|
||||
{
|
||||
key: 'tasks',
|
||||
icon: ListChecks,
|
||||
title: 'Tasks',
|
||||
description: 'Default model for file browser tasks.',
|
||||
content: <TaskDefaults />,
|
||||
},
|
||||
{
|
||||
key: 'languages',
|
||||
icon: Globe,
|
||||
title: 'Languages',
|
||||
description: 'Set your spoken languages and translation preferences.',
|
||||
content: <Languages />,
|
||||
},
|
||||
];
|
||||
|
||||
const allKeys = sections.map((s) => s.key);
|
||||
|
||||
export const ProfileSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
const [search, setSearch] = useState('');
|
||||
const [expanded, setExpanded] = useState<string[]>([]);
|
||||
const sectionRefs = useRef<Record<string, HTMLDivElement | null>>({});
|
||||
|
||||
const sections = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: 'profile',
|
||||
icon: User,
|
||||
title: t('settings.profile.sections.profileTitle'),
|
||||
description: t('settings.profile.sections.profileDescription'),
|
||||
content: <UserData />,
|
||||
},
|
||||
{
|
||||
key: 'change-password',
|
||||
icon: Lock,
|
||||
title: t('settings.profile.sections.changePasswordTitle'),
|
||||
description: t('settings.profile.sections.changePasswordDescription'),
|
||||
content: <ChangePassword />,
|
||||
},
|
||||
{
|
||||
key: 'tasks',
|
||||
icon: ListChecks,
|
||||
title: t('settings.profile.sections.tasksTitle'),
|
||||
description: t('settings.profile.sections.tasksDescription'),
|
||||
content: <TaskDefaults />,
|
||||
},
|
||||
{
|
||||
key: 'languages',
|
||||
icon: Globe,
|
||||
title: t('settings.profile.sections.languagesTitle'),
|
||||
description: t('settings.profile.sections.languagesDescription'),
|
||||
content: <Languages />,
|
||||
},
|
||||
],
|
||||
[t],
|
||||
);
|
||||
|
||||
const allKeys = useMemo(() => sections.map((s) => s.key), [sections]);
|
||||
|
||||
const matchingKeys = useMemo(() => {
|
||||
if (!search) return allKeys;
|
||||
const query = search.toLowerCase();
|
||||
@@ -55,7 +60,7 @@ export const ProfileSettings = () => {
|
||||
return (el?.textContent?.toLowerCase() ?? '').includes(query);
|
||||
})
|
||||
.map((s) => s.key);
|
||||
}, [search]);
|
||||
}, [search, allKeys, sections]);
|
||||
|
||||
useEffect(() => {
|
||||
if (search) setExpanded(matchingKeys);
|
||||
@@ -67,7 +72,7 @@ export const ProfileSettings = () => {
|
||||
<div className="relative mb-6">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-duck-dark/40" />
|
||||
<Input
|
||||
placeholder="Search settings..."
|
||||
placeholder={t('settings.profile.searchPlaceholder')}
|
||||
value={search}
|
||||
onChange={(ev) => setSearch(ev.target.value)}
|
||||
className="pl-9"
|
||||
|
||||
Reference in New Issue
Block a user