From 65f9de12a7862a5fc4008f10a5d0a579ce3ce80b Mon Sep 17 00:00:00 2001 From: schekina2 Date: Wed, 21 Jan 2026 21:15:27 +0100 Subject: [PATCH 1/2] feat: implement internationalization support with i18next - Added i18next for multi-language support, enabling French and English localization. - Created language switcher component for dynamic language selection. - Updated various components and pages to utilize translations for improved user experience. - Introduced locale files for English and French with common phrases and UI text. - Enhanced layout components to accommodate language changes seamlessly. --- I18N_SETUP.md | 164 +++++++++++++++ apps/web/next-i18next.config.ts | 24 +++ apps/web/package.json | 4 + apps/web/public/locales/en/common.json | 110 ++++++++++ apps/web/public/locales/fr/common.json | 110 ++++++++++ apps/web/src/app/(auth)/layout.tsx | 8 +- .../web/src/app/(auth)/login/login-client.tsx | 41 ++-- apps/web/src/app/(auth)/register/page.tsx | 37 ++-- apps/web/src/app/layout-client.tsx | 30 +++ apps/web/src/app/layout.tsx | 14 +- apps/web/src/app/page.tsx | 189 ++++++++---------- apps/web/src/components/language-switcher.tsx | 47 +++++ apps/web/src/components/layout/navbar.tsx | 3 + apps/web/src/lib/i18n.ts | 69 +++++++ pnpm-lock.yaml | 132 ++++++++++++ 15 files changed, 839 insertions(+), 143 deletions(-) create mode 100644 I18N_SETUP.md create mode 100644 apps/web/next-i18next.config.ts create mode 100644 apps/web/public/locales/en/common.json create mode 100644 apps/web/public/locales/fr/common.json create mode 100644 apps/web/src/app/layout-client.tsx create mode 100644 apps/web/src/components/language-switcher.tsx create mode 100644 apps/web/src/lib/i18n.ts diff --git a/I18N_SETUP.md b/I18N_SETUP.md new file mode 100644 index 0000000..5f221c7 --- /dev/null +++ b/I18N_SETUP.md @@ -0,0 +1,164 @@ +# i18n Multi-Language Support Setup + +## Overview + +This project now supports multi-language (internationalization) using a custom i18n implementation. Users can switch between French (FR) and English (EN) with their preference saved in localStorage. + +## Files Structure + +### Translation Files +- `public/locales/fr/common.json` - French translations +- `public/locales/en/common.json` - English translations + +### Core Implementation +- `src/lib/i18n.ts` - Custom hooks for language management: + - `useLanguage()` - Manages current language and language switching + - `useTranslation()` - Provides translation function `t()` + +- `src/components/language-switcher.tsx` - Language selector component (FR/EN buttons) + +## Features Implemented + +### 1. Landing Page (`src/app/page.tsx`) +- Fully translated with language selector in header +- Supports dynamic content translation +- All buttons, labels, and text are translatable + +### 2. Authentication Pages +- **Login Page** (`src/app/(auth)/login/login-client.tsx`) + - All form labels and error messages translated + - OAuth provider buttons translated + +- **Register Page** (`src/app/(auth)/register/page.tsx`) + - Email, password fields translated + - Success/error messages translated + +- **Auth Layout** (`src/app/(auth)/layout.tsx`) + - Language selector positioned at top-right + +### 3. Root Layout (`src/app/layout.tsx`) +- Dynamic `lang` attribute on `` tag +- Syncs with user's selected language + +### 4. Dashboard & App Pages (`src/components/layout/navbar.tsx`) +- Language selector in the top navigation bar +- Available on all authenticated pages (dashboard, workspaces, boards, etc.) +- Dark theme matching the dashboard design + +## Language Selector Component + +Two variants are available in `src/components/language-switcher.tsx`: + +### 1. LanguageSwitcher (Light theme) +- Used on landing page and authentication pages +- White background with slate border +- Displays: Français / English + +### 2. LanguageSwitcherDark (Dark theme) +- Used in the dashboard navbar +- Dark background matching dashboard theme +- Displays: Français / English + +Both variants automatically sync with localStorage and update the HTML `lang` attribute. + +## Usage + +### For Developers + +#### Adding Translations + +1. Add your translation keys to both `public/locales/fr/common.json` and `public/locales/en/common.json`: + +```json +{ + "your_namespace": { + "your_key": "Your translation here" + } +} +``` + +2. Use in components: + +```tsx +'use client'; + +import { useTranslation } from '@/lib/i18n'; + +export function MyComponent() { + const { t } = useTranslation(); + + return ; +} +``` + +#### Accessing Arrays/Objects + +For complex translations like lists: + +```tsx +const features = t('landing.features') as Array<{ title: string; description: string }>; + +features.map((feature) => ( +
+

{feature.title}

+

{feature.description}

+
+)) +``` + +#### String Interpolation + +For dynamic values in translations: + +```tsx +t('landing.footer_copyright', { year: new Date().getFullYear() }) +// Translation: "© {{year}} Taskly. All rights reserved." +// Result: "© 2026 Taskly. All rights reserved." +``` + +### For Users + +- Click the **FR** or **EN** button in the header/auth layout to switch languages +- Selection is automatically saved in browser localStorage +- Page language persists across sessions + +## Current Translations + +### Common Keys +- `common.home`, `common.login`, `common.register`, `common.logout`, `common.dashboard` + +### Header +- `header.login_button`, `header.register_button`, `header.dashboard_button`, `header.logout_button` + +### Landing Page (`landing.*`) +- Titles, descriptions, features, steps, CTA text +- Footer links and copyright + +### Authentication (`auth.*`) +- Form labels and placeholders +- Error messages +- OAuth provider buttons + +## Next Steps + +To apply translations to the entire application: + +1. **Update Dashboard Pages** - Apply `useTranslation` hook to dashboard components +2. **Update Settings Pages** - Translate all settings and configuration labels +3. **Update Notification Pages** - Translate notification messages +4. **Add More Languages** - Create additional locale files (e.g., `de`, `es`, `it`) +5. **Backend Translations** - Coordinate with API to send translated content when needed + +## Configuration + +The i18n configuration is in `next-i18next.config.ts`: +- Default language: French (`fr`) +- Supported languages: `['fr', 'en']` +- Namespace: `common` (single namespace for simplicity) + +## Notes + +- The language preference is stored in `localStorage` with key `language` +- The HTML document's `lang` attribute is automatically updated +- Translations are loaded statically from JSON files at build time +- The system uses client-side rendering for language switching (no page reload needed) diff --git a/apps/web/next-i18next.config.ts b/apps/web/next-i18next.config.ts new file mode 100644 index 0000000..f4fbb50 --- /dev/null +++ b/apps/web/next-i18next.config.ts @@ -0,0 +1,24 @@ +import { Config } from 'next-i18next'; +import path from 'path'; + +const config: Config = { + i18n: { + defaultLocale: 'fr', + locales: ['fr', 'en'], + }, + localePath: path.resolve('./public/locales'), + ns: ['common'], + defaultNS: 'common', + backend: { + loadPath: './public/locales/{{lng}}/{{ns}}.json', + }, + detection: { + order: ['cookie', 'localStorage', 'navigator'], + caches: ['localStorage', 'cookie'], + }, + react: { + useSuspense: false, + }, +}; + +export default config; diff --git a/apps/web/package.json b/apps/web/package.json index 50b6536..4514379 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -26,10 +26,14 @@ "@trpc/react-query": "^11.0.0", "@uiw/react-md-editor": "^4.0.11", "firebase": "^10.14.1", + "i18next": "^25.8.0", + "i18next-browser-languagedetector": "^8.2.0", "lucide-react": "^0.417.0", "next": "^14.2.25", + "next-i18next": "^15.4.3", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-i18next": "^16.5.3", "react-markdown": "^9.1.0", "remark-gfm": "^4.0.1", "superjson": "^2.2.2" diff --git a/apps/web/public/locales/en/common.json b/apps/web/public/locales/en/common.json new file mode 100644 index 0000000..6bc3164 --- /dev/null +++ b/apps/web/public/locales/en/common.json @@ -0,0 +1,110 @@ +{ + "common": { + "home": "Home", + "login": "Sign in", + "register": "Sign up", + "logout": "Sign out", + "dashboard": "Dashboard" + }, + "header": { + "login_button": "Sign in", + "register_button": "Sign up", + "dashboard_button": "Go to dashboard", + "logout_button": "Sign out" + }, + "landing": { + "title": "Manage your projects with clarity, speed and peace of mind.", + "description": "Taskly centralizes your tasks, teams and decisions in simple and visual boards. Go from idea to execution without friction.", + "features_title": "Everything you need to deliver better, together.", + "features": [ + { + "title": "Clear and fast boards", + "description": "Organize your tasks in columns and keep an overview." + }, + { + "title": "Real-time collaboration", + "description": "Invite your teammates, assign tasks and track progress." + }, + { + "title": "Useful notifications", + "description": "Get important updates without being overwhelmed." + }, + { + "title": "Multi-workspace management", + "description": "Separate your personal, client or team projects with one click." + } + ], + "steps_title": "A simple 3-step workflow", + "steps_description": "Taskly adapts to your pace. Start small, scale fast.", + "steps": [ + { + "title": "Create a workspace", + "description": "Start a project in seconds." + }, + { + "title": "Invite your team", + "description": "Add members and set roles." + }, + { + "title": "Launch your board", + "description": "Organize, prioritize and deliver faster." + } + ], + "cta_title": "Ready to make your teams more fluid?", + "cta_description": "Get started now and invite your team in minutes.", + "cta_button": "Create account", + "start_free": "Start free", + "have_account": "Already have an account", + "resume_work": "Resume my work", + "view_workspaces": "View my workspaces", + "new_feature": "New: Google and GitHub integrations", + "tagline": "No commitment", + "tagline_security": "Firebase security", + "tagline_collab": "Smooth collaboration", + "footer_copyright": "© {{year}} Taskly. All rights reserved.", + "footer_links": [ + { + "label": "Sign in", + "href": "/login" + }, + { + "label": "Sign up", + "href": "/register" + }, + { + "label": "Dashboard", + "href": "/dashboard" + } + ], + "example_board_title": "Marketing Board", + "example_board_status": "In progress", + "example_tasks_ready": "3 tasks ready to be delivered", + "why_taskly": "Why Taskly" + }, + "auth": { + "login_title": "Sign in", + "login_description": "Sign in with your email and password.", + "register_title": "Create account", + "register_description": "Create an account to access Taskly.", + "email_label": "Email", + "password_label": "Password", + "password_confirm_label": "Confirm password", + "email_placeholder": "you@example.com", + "continue_google": "Continue with Google", + "continue_github": "Continue with GitHub", + "continue_google_loading": "Google sign in…", + "continue_github_loading": "GitHub sign in…", + "sign_in": "Sign in", + "sign_in_loading": "Signing in…", + "no_account": "Don't have an account?", + "create_account": "Create account", + "have_account": "Already have an account?", + "sign_up": "Sign up", + "sign_up_loading": "Signing up…", + "or": "or", + "error_title": "Sign in failed", + "error_default": "An error occurred.", + "login_link": "Sign in", + "register_link": "Sign up" + } +} diff --git a/apps/web/public/locales/fr/common.json b/apps/web/public/locales/fr/common.json new file mode 100644 index 0000000..1acf615 --- /dev/null +++ b/apps/web/public/locales/fr/common.json @@ -0,0 +1,110 @@ +{ + "common": { + "home": "Accueil", + "login": "Connexion", + "register": "Créer un compte", + "logout": "Se déconnecter", + "dashboard": "Dashboard" + }, + "header": { + "login_button": "Se connecter", + "register_button": "Créer un compte", + "dashboard_button": "Aller au dashboard", + "logout_button": "Se déconnecter" + }, + "landing": { + "title": "Gérez vos projets avec clarté, vitesse et sérénité.", + "description": "Taskly centralise vos tâches, vos équipes et vos décisions dans des boards simples et visuels. Passez de l'idée à l'exécution sans friction.", + "features_title": "Tout ce qu'il faut pour livrer mieux, ensemble.", + "features": [ + { + "title": "Tableaux clairs et rapides", + "description": "Organisez vos tâches en colonnes et gardez une vue d'ensemble." + }, + { + "title": "Collaboration en temps réel", + "description": "Invitez vos coéquipiers, assignez des tâches et suivez l'avancement." + }, + { + "title": "Notifications utiles", + "description": "Recevez les mises à jour importantes sans être submergé." + }, + { + "title": "Gestion multi‑workspaces", + "description": "Séparez vos projets perso, clients ou équipes en un clic." + } + ], + "steps_title": "Un flux simple en 3 étapes", + "steps_description": "Taskly s'adapte à votre rythme. Commencez petit, évoluez vite.", + "steps": [ + { + "title": "Créez un workspace", + "description": "Démarrez un projet en quelques secondes." + }, + { + "title": "Invitez votre équipe", + "description": "Ajoutez des membres et définissez les rôles." + }, + { + "title": "Lancez votre board", + "description": "Organisez, priorisez et livrez plus vite." + } + ], + "cta_title": "Prêt à rendre vos équipes plus fluides ?", + "cta_description": "Commencez maintenant et invitez votre équipe en quelques minutes.", + "cta_button": "Créer un compte", + "start_free": "Démarrer gratuitement", + "have_account": "J'ai déjà un compte", + "resume_work": "Reprendre mon travail", + "view_workspaces": "Voir mes workspaces", + "new_feature": "Nouveau: intégrations Google et GitHub", + "tagline": "Aucun engagement", + "tagline_security": "Sécurité Firebase", + "tagline_collab": "Collaboration fluide", + "footer_copyright": "© {{year}} Taskly. Tous droits réservés.", + "footer_links": [ + { + "label": "Se connecter", + "href": "/login" + }, + { + "label": "S'inscrire", + "href": "/register" + }, + { + "label": "Dashboard", + "href": "/dashboard" + } + ], + "example_board_title": "Board Marketing", + "example_board_status": "En cours", + "example_tasks_ready": "3 tâches prêtes à être livrées", + "why_taskly": "Pourquoi Taskly" + }, + "auth": { + "login_title": "Connexion", + "login_description": "Connecte-toi avec ton email et ton mot de passe.", + "register_title": "Créer un compte", + "register_description": "Crée un compte pour accéder à Taskly.", + "email_label": "Email", + "password_label": "Mot de passe", + "password_confirm_label": "Confirmer le mot de passe", + "email_placeholder": "you@example.com", + "continue_google": "Continuer avec Google", + "continue_github": "Continuer avec GitHub", + "continue_google_loading": "Connexion Google…", + "continue_github_loading": "Connexion GitHub…", + "sign_in": "Se connecter", + "sign_in_loading": "Connexion…", + "no_account": "Pas de compte ?", + "create_account": "Créer un compte", + "have_account": "Tu as déjà un compte ?", + "sign_up": "S'inscrire", + "sign_up_loading": "Inscription…", + "or": "ou", + "error_title": "Connexion impossible", + "error_default": "Une erreur est survenue.", + "login_link": "Se connecter", + "register_link": "S'inscrire" + } +} diff --git a/apps/web/src/app/(auth)/layout.tsx b/apps/web/src/app/(auth)/layout.tsx index 06c7c0c..2c76dae 100644 --- a/apps/web/src/app/(auth)/layout.tsx +++ b/apps/web/src/app/(auth)/layout.tsx @@ -1,8 +1,14 @@ +'use client'; + import type { ReactNode } from 'react'; +import { LanguageSwitcher } from '@/components/language-switcher'; export default function AuthLayout({ children }: { children: ReactNode }) { return ( -
+
+
+ +
{children}
); diff --git a/apps/web/src/app/(auth)/login/login-client.tsx b/apps/web/src/app/(auth)/login/login-client.tsx index a8f9aa2..e986adf 100644 --- a/apps/web/src/app/(auth)/login/login-client.tsx +++ b/apps/web/src/app/(auth)/login/login-client.tsx @@ -4,6 +4,7 @@ * Handles user sign in with email and password via Firebase Auth. * Auto-redirects authenticated users to dashboard or return URL. * Shows error toast on login failure. + * Supports multi-language. * * Features: * - Email/password form @@ -11,6 +12,7 @@ * - Error handling with toasts * - Redirect after successful login * - Link to registration page + * - Multi-language support */ 'use client'; @@ -21,6 +23,7 @@ import { useRouter } from 'next/navigation'; import { Github } from 'lucide-react'; import { Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Input, useToast } from '@taskly/ui'; import { useAuth } from '@/auth/auth-provider'; +import { useTranslation } from '@/lib/i18n'; /** * Safely decodes URI component, returning original value on error. @@ -71,6 +74,7 @@ export function LoginClient({ nextPath }: { nextPath?: string }) { const router = useRouter(); const { toast } = useToast(); const { user, loading, signInWithEmailPassword, signInWithGithub, signInWithGoogle } = useAuth(); + const { t } = useTranslation(); // Safely decode the next path or default to dashboard const redirectTo = safeDecodeURIComponent(nextPath ?? '/dashboard'); @@ -80,12 +84,17 @@ export function LoginClient({ nextPath }: { nextPath?: string }) { const [password, setPassword] = useState(''); const [submitting, setSubmitting] = useState(false); const [oauthSubmitting, setOauthSubmitting] = useState<'google' | 'github' | null>(null); + const [mounted, setMounted] = useState(false); // Auto-redirect if already authenticated useEffect(() => { if (!loading && user) router.replace(redirectTo); }, [loading, user, router, redirectTo]); + useEffect(() => { + setMounted(true); + }, []); + /** * Handles form submission - authenticates user and redirects. */ @@ -100,8 +109,8 @@ export function LoginClient({ nextPath }: { nextPath?: string }) { } catch (err) { // Show error toast on authentication failure toast({ - title: 'Connexion impossible', - description: err instanceof Error ? err.message : 'Une erreur est survenue.', + title: t('auth.error_title'), + description: err instanceof Error ? err.message : t('auth.error_default'), variant: 'destructive', }); } finally { @@ -120,8 +129,8 @@ export function LoginClient({ nextPath }: { nextPath?: string }) { router.replace(redirectTo); } catch (err) { toast({ - title: 'Connexion impossible', - description: err instanceof Error ? err.message : 'Une erreur est survenue.', + title: t('auth.error_title'), + description: err instanceof Error ? err.message : t('auth.error_default'), variant: 'destructive', }); } finally { @@ -129,11 +138,13 @@ export function LoginClient({ nextPath }: { nextPath?: string }) { } } + if (!mounted) return null; + return ( - Connexion - Connecte-toi avec ton email et ton mot de passe. + {t('auth.login_title')} + {t('auth.login_description')}
@@ -146,7 +157,7 @@ export function LoginClient({ nextPath }: { nextPath?: string }) { className="w-full justify-center gap-2" > - {oauthSubmitting === 'google' ? 'Connexion Google…' : 'Continuer avec Google'} + {oauthSubmitting === 'google' ? t('auth.continue_google_loading') : t('auth.continue_google')}
- ou + {t('auth.or')}
setEmail(e.target.value)} - placeholder="you@example.com" + placeholder={t('auth.email_placeholder')} className="bg-[#22272b] border-[#9fadbc29] text-[#b6c2cf] placeholder:text-[#9fadbc]" required /> @@ -182,7 +193,7 @@ export function LoginClient({ nextPath }: { nextPath?: string }) {

- Pas de compte ?{' '} + {t('auth.no_account')}{' '} - Créer un compte + {t('auth.create_account')}

diff --git a/apps/web/src/app/(auth)/register/page.tsx b/apps/web/src/app/(auth)/register/page.tsx index 5ab0c50..0653ea9 100644 --- a/apps/web/src/app/(auth)/register/page.tsx +++ b/apps/web/src/app/(auth)/register/page.tsx @@ -5,27 +5,34 @@ import { useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; import { Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Input, useToast } from '@taskly/ui'; import { useAuth } from '@/auth/auth-provider'; +import { useTranslation } from '@/lib/i18n'; export default function RegisterPage() { const router = useRouter(); const { toast } = useToast(); const { user, loading, registerWithEmailPassword } = useAuth(); + const { t } = useTranslation(); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [password2, setPassword2] = useState(''); const [submitting, setSubmitting] = useState(false); + const [mounted, setMounted] = useState(false); useEffect(() => { if (!loading && user) router.replace('/dashboard'); }, [loading, user, router]); + useEffect(() => { + setMounted(true); + }, []); + async function onSubmit(e: React.FormEvent) { e.preventDefault(); if (password !== password2) { toast({ - title: 'Mot de passe', - description: 'Les mots de passe ne correspondent pas.', + title: 'Password', + description: 'Passwords do not match.', variant: 'destructive', }); return; @@ -37,8 +44,8 @@ export default function RegisterPage() { router.replace('/dashboard'); } catch (err) { toast({ - title: 'Inscription impossible', - description: err instanceof Error ? err.message : 'Une erreur est survenue.', + title: 'Registration failed', + description: err instanceof Error ? err.message : 'An error occurred.', variant: 'destructive', }); } finally { @@ -46,17 +53,19 @@ export default function RegisterPage() { } } + if (!mounted) return null; + return ( - Créer un compte - Inscris-toi avec ton email et un mot de passe. + {t('auth.register_title')} + {t('auth.register_description')}
setEmail(e.target.value)} - placeholder="you@example.com" + placeholder={t('auth.email_placeholder')} className="bg-[#22272b] border-[#9fadbc29] text-[#b6c2cf] placeholder:text-[#9fadbc]" required /> @@ -72,7 +81,7 @@ export default function RegisterPage() {

- Déjà un compte ?{' '} + {t('auth.have_account')}{' '} - Se connecter + {t('auth.login_link')}

@@ -116,5 +125,3 @@ export default function RegisterPage() { ); } - - diff --git a/apps/web/src/app/layout-client.tsx b/apps/web/src/app/layout-client.tsx new file mode 100644 index 0000000..1ba90f8 --- /dev/null +++ b/apps/web/src/app/layout-client.tsx @@ -0,0 +1,30 @@ +/** + * Root Layout Client Component + * + * Client-side wrapper that manages language state and HTML attributes. + * Separated from the server layout to enable dynamic language switching. + */ + +'use client'; + +import { useEffect, type ReactNode } from 'react'; +import { useLanguage } from '@/lib/i18n'; + +export function RootLayoutClient({ children }: { children: ReactNode }) { + const { language, mounted } = useLanguage(); + + useEffect(() => { + if (mounted) { + document.documentElement.lang = language; + } + }, [language, mounted]); + + return ( + + + {/* Global providers wrapped by server component */} + {children} + + + ); +} diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 9a9801d..a4ffb41 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -5,12 +5,13 @@ * Wraps the entire app with global providers (tRPC, Auth, Toaster). * Sets up HTML structure and global styles. * - * Language: French (lang="fr") + * Supports multi-language (FR, EN) with i18n. */ -import type { ReactNode } from 'react'; import './globals.css'; import { Providers } from './providers'; +import { RootLayoutClient } from './layout-client'; +import type { ReactNode } from 'react'; /** * Root layout for the entire application. @@ -21,12 +22,9 @@ import { Providers } from './providers'; */ export default function RootLayout({ children }: { children: ReactNode }) { return ( - - - {/* Global providers: tRPC, Auth, Toaster */} - {children} - - + + {children} + ); } diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index f52e117..0e3aeb5 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -2,14 +2,14 @@ * Public Home Page * * Landing page for unauthenticated users. - * Shows Taskly branding and tRPC test. + * Shows Taskly branding and supports multi-language. * Provides navigation to login/register or dashboard. * * Display logic: * - Loading: Show nothing * - Not authenticated: Show Login/Register buttons * - Authenticated: Show Dashboard/Logout buttons - * - Always: Show tRPC test panel for debugging + * - Always: Show language switcher */ 'use client'; @@ -17,43 +17,20 @@ import Link from 'next/link'; import { Button } from '@taskly/ui'; import { useAuth } from '@/auth/auth-provider'; +import { useTranslation } from '@/lib/i18n'; +import { LanguageSwitcher } from '@/components/language-switcher'; -const features = [ - { - title: 'Tableaux clairs et rapides', - description: 'Organisez vos tâches en colonnes et gardez une vue d’ensemble.', - }, - { - title: 'Collaboration en temps réel', - description: 'Invitez vos coéquipiers, assignez des tâches et suivez l’avancement.', - }, - { - title: 'Notifications utiles', - description: 'Recevez les mises à jour importantes sans être submergé.', - }, - { - title: 'Gestion multi‑workspaces', - description: 'Séparez vos projets perso, clients ou équipes en un clic.', - }, -]; - -const steps = [ - { title: 'Créez un workspace', description: 'Démarrez un projet en quelques secondes.' }, - { title: 'Invitez votre équipe', description: 'Ajoutez des membres et définissez les rôles.' }, - { title: 'Lancez votre board', description: 'Organisez, priorisez et livrez plus vite.' }, -]; - -/** - * Home page component. - * Entry point for new users to the application. - * - * @returns Landing page with auth-based navigation - */ export default function HomePage() { const { user, loading, logout } = useAuth(); + const { t } = useTranslation(); const showAuthButtons = !loading && !user; const showDashboardButtons = !loading && user; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const features = t('landing.features') as Array<{ title: string; description: string }>; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const steps = t('landing.steps') as Array<{ title: string; description: string }>; + return (
@@ -64,68 +41,70 @@ export default function HomePage() {
Taskly
-
- {showAuthButtons && ( - <> - - - - )} - {showDashboardButtons && ( - <> - - - - )} +
+ +
+ {showAuthButtons && ( + <> + + + + )} + {showDashboardButtons && ( + <> + + + + )} +
- Nouveau: intégrations Google et GitHub + {t('landing.new_feature')}

- Gérez vos projets avec clarté, vitesse et sérénité. + {t('landing.title')}

- Taskly centralise vos tâches, vos équipes et vos décisions dans des boards simples - et visuels. Passez de l’idée à l’exécution sans friction. + {t('landing.description')}

{showAuthButtons && ( <> )} {showDashboardButtons && ( <> )}
- Aucun engagement - Sécurité Firebase - Collaboration fluide + {t('landing.tagline')} + {t('landing.tagline_security')} + {t('landing.tagline_collab')}
@@ -134,9 +113,9 @@ export default function HomePage() {
- Board Marketing + {t('landing.example_board_title')} - En cours + {t('landing.example_board_status')}
@@ -150,7 +129,7 @@ export default function HomePage() { ))}
- 3 tâches prêtes à être livrées + {t('landing.example_tasks_ready')}
@@ -160,67 +139,71 @@ export default function HomePage() {
- Pourquoi Taskly + {t('landing.why_taskly')}

- Tout ce qu’il faut pour livrer mieux, ensemble. + {t('landing.features_title')}

- {features.map((feature) => ( -
-

{feature.title}

-

{feature.description}

-
- ))} + {Array.isArray(features) && + // eslint-disable-next-line @typescript-eslint/no-explicit-any + features.map((feature: any) => ( +
+

{feature.title}

+

{feature.description}

+
+ ))}
-

Un flux simple en 3 étapes

+

{t('landing.steps_title')}

- Taskly s’adapte à votre rythme. Commencez petit, évoluez vite. + {t('landing.steps_description')}

- {steps.map((step, index) => ( -
-
- {index + 1} -
-
-

{step.title}

-

{step.description}

+ {Array.isArray(steps) && + // eslint-disable-next-line @typescript-eslint/no-explicit-any + steps.map((step: any, index: number) => ( +
+
+ {index + 1} +
+
+

{step.title}

+

{step.description}

+
-
- ))} + ))}
-

Prêt à rendre vos équipes plus fluides ?

+

{t('landing.cta_title')}

- Commencez maintenant et invitez votre équipe en quelques minutes. + {t('landing.cta_description')}

{showAuthButtons && ( )} {showDashboardButtons && ( )}
@@ -228,16 +211,16 @@ export default function HomePage() {
- © {new Date().getFullYear()} Taskly. Tous droits réservés. + {t('landing.footer_copyright', { year: new Date().getFullYear() })}
- Se connecter + {t('auth.login_link')} - S’inscrire + {t('auth.register_link')} - Dashboard + {t('common.dashboard')}
@@ -245,5 +228,3 @@ export default function HomePage() { ); } - - diff --git a/apps/web/src/components/language-switcher.tsx b/apps/web/src/components/language-switcher.tsx new file mode 100644 index 0000000..7823a95 --- /dev/null +++ b/apps/web/src/components/language-switcher.tsx @@ -0,0 +1,47 @@ +'use client'; + +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@taskly/ui'; +import { useLanguage } from '@/lib/i18n'; + +export function LanguageSwitcher() { + const { language, changeLanguage, mounted } = useLanguage(); + + if (!mounted) return null; + + return ( + + ); +} + +// Version pour le dashboard avec style sombre +export function LanguageSwitcherDark() { + const { language, changeLanguage, mounted } = useLanguage(); + + if (!mounted) return null; + + return ( + + ); +} diff --git a/apps/web/src/components/layout/navbar.tsx b/apps/web/src/components/layout/navbar.tsx index a8b62ab..6046a5a 100644 --- a/apps/web/src/components/layout/navbar.tsx +++ b/apps/web/src/components/layout/navbar.tsx @@ -46,6 +46,7 @@ import { getFirebaseAuth } from "@/lib/firebase/firebase-client" import { updateProfile } from "firebase/auth" import { UserAvatar } from "@/components/user/user-avatar" import { getBoardBackgroundStyle } from "@/components/board/board-background" +import { LanguageSwitcherDark } from "@/components/language-switcher" /** * Formats notification timestamp for display. @@ -387,6 +388,8 @@ export function Navbar() { />
+ + @@ -473,11 +475,11 @@ function SortableBoardColumn({
- dndTicketId(t.id))} strategy={verticalListSortingStrategy}> - {colTickets.map((t) => ( + dndTicketId(ticket.id))} strategy={verticalListSortingStrategy}> + {colTickets.map((ticket) => ( - Add a card + {t('board.add_a_card')} - Add card + {t('board.add_card_dialog')}
- onChangeNewTitle(e.target.value)} placeholder="Card title" /> -