feat: Complete internationalization and code cleanup

## Translation Files
- Add 11 new language files (es, de, pt, ru, zh, ja, ko, ar, hi, nl, pl)
- Add 100+ missing translation keys across all 15 languages
- New sections: notebook, pagination, ai.batchOrganization, ai.autoLabels
- Update nav section with workspace, quickAccess, myLibrary keys

## Component Updates
- Update 15+ components to use translation keys instead of hardcoded text
- Components: notebook dialogs, sidebar, header, note-input, ghost-tags, etc.
- Replace 80+ hardcoded English/French strings with t() calls
- Ensure consistent UI across all supported languages

## Code Quality
- Remove 77+ console.log statements from codebase
- Clean up API routes, components, hooks, and services
- Keep only essential error handling (no debugging logs)

## UI/UX Improvements
- Update Keep logo to yellow post-it style (from-yellow-400 to-amber-500)
- Change selection colors to #FEF3C6 (notebooks) and #EFB162 (nav items)
- Make "+" button permanently visible in notebooks section
- Fix grammar and syntax errors in multiple components

## Bug Fixes
- Fix JSON syntax errors in it.json, nl.json, pl.json, zh.json
- Fix syntax errors in notebook-suggestion-toast.tsx
- Fix syntax errors in use-auto-tagging.ts
- Fix syntax errors in paragraph-refactor.service.ts
- Fix duplicate "fusion" section in nl.json

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

Ou une version plus courte si vous préférez :

feat(i18n): Add 15 languages, remove logs, update UI components

- Create 11 new translation files (es, de, pt, ru, zh, ja, ko, ar, hi, nl, pl)
- Add 100+ translation keys: notebook, pagination, AI features
- Update 15+ components to use translations (80+ strings)
- Remove 77+ console.log statements from codebase
- Fix JSON syntax errors in 4 translation files
- Fix component syntax errors (toast, hooks, services)
- Update logo to yellow post-it style
- Change selection colors (#FEF3C6, #EFB162)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-11 22:26:13 +01:00
parent fc2c40249e
commit 7fb486c9a4
183 changed files with 48288 additions and 1290 deletions

View File

@@ -0,0 +1,33 @@
'use client'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { ArrowRight, Sparkles } from 'lucide-react'
import Link from 'next/link'
import { useLanguage } from '@/lib/i18n'
export function AISettingsLinkCard() {
const { t } = useLanguage()
return (
<Card className="mt-6">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Sparkles className="h-5 w-5 text-amber-500" />
{t('nav.aiSettings')}
</CardTitle>
<CardDescription>
{t('nav.configureAI')}
</CardDescription>
</CardHeader>
<CardContent>
<Link href="/settings/ai">
<Button variant="outline" className="w-full justify-between group">
<span>{t('nav.manageAISettings')}</span>
<ArrowRight className="h-4 w-4 group-hover:translate-x-1 transition-transform" />
</Button>
</Link>
</CardContent>
</Card>
)
}

View File

@@ -2,10 +2,14 @@ import { auth } from '@/auth'
import { redirect } from 'next/navigation'
import { ProfileForm } from './profile-form'
import prisma from '@/lib/prisma'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Sparkles } from 'lucide-react'
import { ProfilePageHeader } from '@/components/profile-page-header'
import { AISettingsLinkCard } from './ai-settings-link-card'
export default async function ProfilePage() {
const session = await auth()
if (!session?.user?.id) {
redirect('/login')
}
@@ -19,10 +23,19 @@ export default async function ProfilePage() {
redirect('/login')
}
// Get user AI settings for language preference
const userAISettings = await prisma.userAISettings.findUnique({
where: { userId: session.user.id },
select: { preferredLanguage: true }
})
return (
<div className="container max-w-2xl mx-auto py-10 px-4">
<h1 className="text-3xl font-bold mb-8">Account Settings</h1>
<ProfileForm user={user} />
<ProfilePageHeader />
<ProfileForm user={user} userAISettings={userAISettings} />
{/* AI Settings Link */}
<AISettingsLinkCard />
</div>
)
}

View File

@@ -1,59 +1,235 @@
'use client'
import { useState } from 'react'
import { useState, useEffect } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
import { updateProfile, changePassword } from '@/app/actions/profile'
import { Label } from '@/components/ui/label'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { updateProfile, changePassword, updateLanguage, updateFontSize } from '@/app/actions/profile'
import { toast } from 'sonner'
import { useLanguage } from '@/lib/i18n'
const LANGUAGES = [
{ value: 'auto', label: 'Auto-detect', flag: '🌐' },
{ value: 'en', label: 'English', flag: '🇬🇧' },
{ value: 'fr', label: 'Français', flag: '🇫🇷' },
{ value: 'es', label: 'Español', flag: '🇪🇸' },
{ value: 'de', label: 'Deutsch', flag: '🇩🇪' },
{ value: 'it', label: 'Italiano', flag: '🇮🇹' },
{ value: 'pt', label: 'Português', flag: '🇵🇹' },
{ value: 'ru', label: 'Русский', flag: '🇷🇺' },
{ value: 'zh', label: '中文', flag: '🇨🇳' },
{ value: 'ja', label: '日本語', flag: '🇯🇵' },
{ value: 'ko', label: '한국어', flag: '🇰🇷' },
{ value: 'ar', label: 'العربية', flag: '🇸🇦' },
{ value: 'hi', label: 'हिन्दी', flag: '🇮🇳' },
{ value: 'nl', label: 'Nederlands', flag: '🇳🇱' },
{ value: 'pl', label: 'Polski', flag: '🇵🇱' },
{ value: 'fa', label: 'فارسی (Persian)', flag: '🇮🇷' },
]
export function ProfileForm({ user, userAISettings }: { user: any; userAISettings?: any }) {
const [selectedLanguage, setSelectedLanguage] = useState(userAISettings?.preferredLanguage || 'auto')
const [isUpdatingLanguage, setIsUpdatingLanguage] = useState(false)
const [fontSize, setFontSize] = useState(userAISettings?.fontSize || 'medium')
const [isUpdatingFontSize, setIsUpdatingFontSize] = useState(false)
const { t } = useLanguage()
const FONT_SIZES = [
{ value: 'small', label: t('profile.fontSizeSmall'), size: '14px' },
{ value: 'medium', label: t('profile.fontSizeMedium'), size: '16px' },
{ value: 'large', label: t('profile.fontSizeLarge'), size: '18px' },
{ value: 'extra-large', label: t('profile.fontSizeExtraLarge'), size: '20px' },
]
const handleFontSizeChange = async (size: string) => {
setIsUpdatingFontSize(true)
try {
const result = await updateFontSize(size)
if (result?.error) {
toast.error(t('profile.fontSizeUpdateFailed'))
} else {
setFontSize(size)
// Apply font size immediately
applyFontSize(size)
toast.success(t('profile.fontSizeUpdateSuccess'))
}
} catch (error) {
toast.error(t('profile.fontSizeUpdateFailed'))
} finally {
setIsUpdatingFontSize(false)
}
}
const applyFontSize = (size: string) => {
// Base font size in pixels (16px is standard)
const fontSizeMap = {
'small': '14px', // ~87% of 16px
'medium': '16px', // 100% (standard)
'large': '18px', // ~112% of 16px
'extra-large': '20px' // 125% of 16px
}
const fontSizeFactorMap = {
'small': 0.95,
'medium': 1.0,
'large': 1.1,
'extra-large': 1.25
}
const fontSizeValue = fontSizeMap[size as keyof typeof fontSizeMap] || '16px'
const fontSizeFactor = fontSizeFactorMap[size as keyof typeof fontSizeFactorMap] || 1.0
document.documentElement.style.setProperty('--user-font-size', fontSizeValue)
document.documentElement.style.setProperty('--user-font-size-factor', fontSizeFactor.toString())
localStorage.setItem('user-font-size', size)
}
// Apply saved font size on mount
useEffect(() => {
const savedFontSize = localStorage.getItem('user-font-size') || userAISettings?.fontSize || 'medium'
applyFontSize(savedFontSize as string)
}, [])
const handleLanguageChange = async (language: string) => {
setIsUpdatingLanguage(true)
try {
const result = await updateLanguage(language)
if (result?.error) {
toast.error(t('profile.languageUpdateFailed'))
} else {
setSelectedLanguage(language)
// Update localStorage and reload to apply new language
localStorage.setItem('user-language', language)
toast.success(t('profile.languageUpdateSuccess'))
// Reload page to apply new language
setTimeout(() => window.location.reload(), 500)
}
} catch (error) {
toast.error(t('profile.languageUpdateFailed'))
} finally {
setIsUpdatingLanguage(false)
}
}
export function ProfileForm({ user }: { user: any }) {
return (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Profile Information</CardTitle>
<CardDescription>Update your display name and other public information.</CardDescription>
<CardTitle>{t('profile.title')}</CardTitle>
<CardDescription>{t('profile.description')}</CardDescription>
</CardHeader>
<form action={async (formData) => {
const result = await updateProfile({ name: formData.get('name') as string })
if (result?.error) {
toast.error('Failed to update profile')
toast.error(t('profile.updateFailed'))
} else {
toast.success('Profile updated')
toast.success(t('profile.updateSuccess'))
}
}}>
<CardContent className="space-y-4">
<div className="space-y-2">
<label htmlFor="name" className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">Display Name</label>
<label htmlFor="name" className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">{t('profile.displayName')}</label>
<Input id="name" name="name" defaultValue={user.name} />
</div>
<div className="space-y-2">
<label htmlFor="email" className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">Email</label>
<label htmlFor="email" className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">{t('profile.email')}</label>
<Input id="email" value={user.email} disabled className="bg-muted" />
</div>
</CardContent>
<CardFooter>
<Button type="submit">Save Changes</Button>
<Button type="submit">{t('general.save')}</Button>
</CardFooter>
</form>
</Card>
<Card>
<CardHeader>
<CardTitle>Change Password</CardTitle>
<CardDescription>Update your password. You will need your current password.</CardDescription>
<CardTitle>{t('profile.languagePreferences')}</CardTitle>
<CardDescription>{t('profile.languagePreferencesDescription')}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="language">{t('profile.preferredLanguage')}</Label>
<Select
value={selectedLanguage}
onValueChange={handleLanguageChange}
disabled={isUpdatingLanguage}
>
<SelectTrigger id="language">
<SelectValue placeholder={t('profile.selectLanguage')} />
</SelectTrigger>
<SelectContent>
{LANGUAGES.map((lang) => (
<SelectItem key={lang.value} value={lang.value}>
<span className="flex items-center gap-2">
<span>{lang.flag}</span>
<span>{lang.label}</span>
</span>
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
{t('profile.languageDescription')}
</p>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>{t('profile.displaySettings')}</CardTitle>
<CardDescription>{t('profile.displaySettingsDescription')}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="fontSize">{t('profile.fontSize')}</Label>
<Select
value={fontSize}
onValueChange={handleFontSizeChange}
disabled={isUpdatingFontSize}
>
<SelectTrigger id="fontSize">
<SelectValue placeholder={t('profile.selectFontSize')} />
</SelectTrigger>
<SelectContent>
{FONT_SIZES.map((size) => (
<SelectItem key={size.value} value={size.value}>
<span className="flex items-center gap-2">
<span>{size.label}</span>
<span className="text-xs text-muted-foreground">({size.size})</span>
</span>
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
{t('profile.fontSizeDescription')}
</p>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>{t('profile.changePassword')}</CardTitle>
<CardDescription>{t('profile.changePasswordDescription')}</CardDescription>
</CardHeader>
<form action={async (formData) => {
const result = await changePassword(formData)
if (result?.error) {
const msg = '_form' in result.error
? result.error._form[0]
: result.error.currentPassword?.[0] || result.error.newPassword?.[0] || result.error.confirmPassword?.[0] || 'Failed to change password'
: result.error.currentPassword?.[0] || result.error.newPassword?.[0] || result.error.confirmPassword?.[0] || t('profile.passwordChangeFailed')
toast.error(msg)
} else {
toast.success('Password changed successfully')
toast.success(t('profile.passwordChangeSuccess'))
// Reset form manually or redirect
const form = document.querySelector('form#password-form') as HTMLFormElement
form?.reset()
@@ -61,20 +237,20 @@ export function ProfileForm({ user }: { user: any }) {
}} id="password-form">
<CardContent className="space-y-4">
<div className="space-y-2">
<label htmlFor="currentPassword" className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">Current Password</label>
<label htmlFor="currentPassword" className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">{t('profile.currentPassword')}</label>
<Input id="currentPassword" name="currentPassword" type="password" required />
</div>
<div className="space-y-2">
<label htmlFor="newPassword" className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">New Password</label>
<label htmlFor="newPassword" className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">{t('profile.newPassword')}</label>
<Input id="newPassword" name="newPassword" type="password" required minLength={6} />
</div>
<div className="space-y-2">
<label htmlFor="confirmPassword" className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">Confirm Password</label>
<label htmlFor="confirmPassword" className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">{t('profile.confirmPassword')}</label>
<Input id="confirmPassword" name="confirmPassword" type="password" required minLength={6} />
</div>
</CardContent>
<CardFooter>
<Button type="submit">Update Password</Button>
<Button type="submit">{t('profile.updatePassword')}</Button>
</CardFooter>
</form>
</Card>