Compare commits

...

5 Commits

Author SHA1 Message Date
paul-nothaft b74a290337 Improve the way users can edit their profile information
continuous-integration/drone/push Build is passing
Update the profile editing component to allow users to modify their username and email address.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: ceced2fc-aa46-458d-ba87-ddd4b7bb1518
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/659922a9-0087-461c-90dd-6d9a58b81d4d/ceced2fc-aa46-458d-ba87-ddd4b7bb1518/un1X8jl
2025-10-23 21:07:57 +00:00
paul-nothaft dcf8606c4c Improve app navigation and settings by adding new translations
Update i18n files for English and German, and refactor BottomNavigation and Settings components to use translated strings for navigation and settings options.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: ceced2fc-aa46-458d-ba87-ddd4b7bb1518
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/659922a9-0087-461c-90dd-6d9a58b81d4d/ceced2fc-aa46-458d-ba87-ddd4b7bb1518/un1X8jl
2025-10-23 21:07:40 +00:00
paul-nothaft 3c16c1385a Add full language translation support with German option
Implement i18next and react-i18next for internationalization, adding English and German translations with a language selector in settings.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: ceced2fc-aa46-458d-ba87-ddd4b7bb1518
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/659922a9-0087-461c-90dd-6d9a58b81d4d/ceced2fc-aa46-458d-ba87-ddd4b7bb1518/un1X8jl
2025-10-23 21:04:01 +00:00
paul-nothaft 32d3126dc9 Add support for multiple languages in the application interface
Integrates `react-i18next` to enable language translations for UI elements across the BottomNavigation, TaskCreationModal, and TaskList components.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: ceced2fc-aa46-458d-ba87-ddd4b7bb1518
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/659922a9-0087-461c-90dd-6d9a58b81d4d/ceced2fc-aa46-458d-ba87-ddd4b7bb1518/un1X8jl
2025-10-23 20:56:05 +00:00
paul-nothaft 571245e2d9 Add full language translation support and settings option
Integrates i18next and react-i18next for internationalization, adds German language support, and implements a language selection feature in the settings page.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: ceced2fc-aa46-458d-ba87-ddd4b7bb1518
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/659922a9-0087-461c-90dd-6d9a58b81d4d/ceced2fc-aa46-458d-ba87-ddd4b7bb1518/un1X8jl
2025-10-23 20:54:00 +00:00
12 changed files with 553 additions and 47 deletions
+10 -4
View File
@@ -1,4 +1,5 @@
import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { queryClient } from "./lib/queryClient";
import { QueryClientProvider } from "@tanstack/react-query";
import { Toaster } from "@/components/ui/toaster";
@@ -15,6 +16,7 @@ import CalendarView from './components/CalendarView';
import KanbanBoard from './components/KanbanBoard';
import ProjectTemplate from './components/ProjectTemplate';
import ThemeToggle from './components/ThemeToggle';
import Settings from './pages/settings';
import { Task, Label } from '@shared/schema';
import { addDays, subDays } from 'date-fns';
import { useTimer } from './hooks/useTimer';
@@ -89,6 +91,7 @@ const initialTasks: Task[] = [
];
function App() {
const { t } = useTranslation();
const [currentTab, setCurrentTab] = useState('tasks');
const [tasks, setTasks] = useState<Task[]>(initialTasks);
const [labels, setLabels] = useState<Label[]>([]);
@@ -266,7 +269,7 @@ function App() {
/>
);
case 'settings':
case 'templates':
return (
<ProjectTemplate
onCreateFromTemplate={handleCreateFromTemplate}
@@ -274,6 +277,9 @@ function App() {
/>
);
case 'settings':
return <Settings />;
default:
return (
<div className="text-center py-8">
@@ -295,19 +301,19 @@ function App() {
<span className="text-primary-foreground font-bold text-sm">T</span>
</div>
<h1 className="text-lg font-semibold" data-testid="text-app-title">
TaskFlow
{t('app.title')}
</h1>
</div>
<div className="flex items-center gap-2">
{currentTab === 'settings' && (
{(currentTab === 'settings' || currentTab === 'templates') && (
<Button
variant="outline"
size="sm"
onClick={() => setCurrentTab('tasks')}
data-testid="button-back-to-tasks"
>
Back to Tasks
{t('app.backToTasks')}
</Button>
)}
<ThemeToggle />
+7 -5
View File
@@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Home, Calendar, LayoutGrid, Plus, Settings } from 'lucide-react';
@@ -10,14 +11,15 @@ interface BottomNavigationProps {
}
export default function BottomNavigation({ onTabChange, onCreateTask, activeTab = 'tasks' }: BottomNavigationProps) {
const { t } = useTranslation();
const [currentTab, setCurrentTab] = useState(activeTab);
const tabs = [
{ id: 'tasks', label: 'Tasks', icon: Home },
{ id: 'calendar', label: 'Calendar', icon: Calendar },
{ id: 'create', label: 'Create', icon: Plus, isCreate: true },
{ id: 'kanban', label: 'Board', icon: LayoutGrid },
{ id: 'settings', label: 'Settings', icon: Settings }
{ id: 'tasks', label: t('navigation.tasks'), icon: Home },
{ id: 'calendar', label: t('navigation.calendar'), icon: Calendar },
{ id: 'create', label: t('navigation.create'), icon: Plus, isCreate: true },
{ id: 'kanban', label: t('navigation.kanban'), icon: LayoutGrid },
{ id: 'settings', label: t('navigation.settings'), icon: Settings }
];
const handleTabClick = (tabId: string, isCreate?: boolean) => {
+14 -12
View File
@@ -1,4 +1,5 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@@ -17,6 +18,7 @@ interface TaskCreationModalProps {
}
export default function TaskCreationModal({ isOpen, onClose, onSave }: TaskCreationModalProps) {
const { t } = useTranslation();
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [priority, setPriority] = useState<'low' | 'medium' | 'high'>('medium');
@@ -77,14 +79,14 @@ export default function TaskCreationModal({ isOpen, onClose, onSave }: TaskCreat
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Plus className="w-4 h-4" />
New Task
{t('taskCreation.title')}
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<Input
placeholder="What needs to be done?"
placeholder={t('taskCreation.titlePlaceholder')}
value={title}
onChange={(e) => setTitle(e.target.value)}
onKeyDown={handleKeyDown}
@@ -96,7 +98,7 @@ export default function TaskCreationModal({ isOpen, onClose, onSave }: TaskCreat
<div>
<Textarea
placeholder="Add a description (optional)"
placeholder={t('taskCreation.descriptionPlaceholder')}
value={description}
onChange={(e) => setDescription(e.target.value)}
className="resize-none text-sm"
@@ -109,12 +111,12 @@ export default function TaskCreationModal({ isOpen, onClose, onSave }: TaskCreat
<div className="flex-1">
<Select value={priority} onValueChange={(value: 'low' | 'medium' | 'high') => setPriority(value)}>
<SelectTrigger data-testid="select-task-priority">
<SelectValue placeholder="Priority" />
<SelectValue placeholder={t('taskCreation.priority')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="low">Low</SelectItem>
<SelectItem value="medium">Medium</SelectItem>
<SelectItem value="high">High</SelectItem>
<SelectItem value="low">{t('priority.low')}</SelectItem>
<SelectItem value="medium">{t('priority.medium')}</SelectItem>
<SelectItem value="high">{t('priority.high')}</SelectItem>
</SelectContent>
</Select>
</div>
@@ -122,10 +124,10 @@ export default function TaskCreationModal({ isOpen, onClose, onSave }: TaskCreat
<div className="flex-1">
<Select value={labelId || 'none'} onValueChange={(value) => setLabelId(value === 'none' ? undefined : value)}>
<SelectTrigger data-testid="select-task-label">
<SelectValue placeholder="Label" />
<SelectValue placeholder={t('taskCreation.label')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">No Label</SelectItem>
<SelectItem value="none">{t('taskCreation.noLabel')}</SelectItem>
{labels.map((label) => (
<SelectItem key={label.id} value={label.id}>
<div className="flex items-center gap-2">
@@ -149,7 +151,7 @@ export default function TaskCreationModal({ isOpen, onClose, onSave }: TaskCreat
data-testid="button-due-date"
>
<CalendarIcon className="w-4 h-4" />
{dueDate ? dueDate.toLocaleDateString() : 'Due date'}
{dueDate ? dueDate.toLocaleDateString() : t('taskCreation.dueDate')}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="end">
@@ -174,7 +176,7 @@ export default function TaskCreationModal({ isOpen, onClose, onSave }: TaskCreat
className="flex-1"
data-testid="button-cancel"
>
Cancel
{t('taskCreation.cancel')}
</Button>
<Button
onClick={handleSave}
@@ -182,7 +184,7 @@ export default function TaskCreationModal({ isOpen, onClose, onSave }: TaskCreat
className="flex-1"
data-testid="button-save-task"
>
Create Task
{t('taskCreation.create')}
</Button>
</div>
</div>
+16 -14
View File
@@ -1,4 +1,5 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
@@ -17,6 +18,7 @@ type SortOption = 'dueDate' | 'priority' | 'title' | 'status';
type FilterOption = 'all' | 'todo' | 'inProgress' | 'done' | 'overdue';
export default function TaskList({ tasks, onTaskUpdate, onTaskEdit }: TaskListProps) {
const { t } = useTranslation();
const [searchQuery, setSearchQuery] = useState('');
const [sortBy, setSortBy] = useState<SortOption>('dueDate');
const [filterBy, setFilterBy] = useState<FilterOption>('all');
@@ -84,10 +86,10 @@ export default function TaskList({ tasks, onTaskUpdate, onTaskEdit }: TaskListPr
{/* Header */}
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold" data-testid="text-task-list-title">
My Tasks
{t('taskList.title')}
</h2>
<Badge variant="secondary" data-testid="badge-task-count">
{filteredAndSortedTasks.length} tasks
{t('taskList.taskCount', { count: filteredAndSortedTasks.length })}
</Badge>
</div>
@@ -96,7 +98,7 @@ export default function TaskList({ tasks, onTaskUpdate, onTaskEdit }: TaskListPr
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
placeholder="Search tasks..."
placeholder={t('taskList.searchPlaceholder')}
value={searchQuery}
onChange={(e) => {
setSearchQuery(e.target.value);
@@ -119,11 +121,11 @@ export default function TaskList({ tasks, onTaskUpdate, onTaskEdit }: TaskListPr
</div>
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All ({getFilterCount('all')})</SelectItem>
<SelectItem value="todo">To Do ({getFilterCount('todo')})</SelectItem>
<SelectItem value="inProgress">In Progress ({getFilterCount('inProgress')})</SelectItem>
<SelectItem value="done">Done ({getFilterCount('done')})</SelectItem>
<SelectItem value="overdue">Overdue ({getFilterCount('overdue')})</SelectItem>
<SelectItem value="all">{t('taskList.filter.all')} ({getFilterCount('all')})</SelectItem>
<SelectItem value="todo">{t('taskList.filter.todo')} ({getFilterCount('todo')})</SelectItem>
<SelectItem value="inProgress">{t('taskList.filter.inProgress')} ({getFilterCount('inProgress')})</SelectItem>
<SelectItem value="done">{t('taskList.filter.done')} ({getFilterCount('done')})</SelectItem>
<SelectItem value="overdue">{t('taskList.filter.overdue')} ({getFilterCount('overdue')})</SelectItem>
</SelectContent>
</Select>
@@ -138,10 +140,10 @@ export default function TaskList({ tasks, onTaskUpdate, onTaskEdit }: TaskListPr
</div>
</SelectTrigger>
<SelectContent>
<SelectItem value="dueDate">Due Date</SelectItem>
<SelectItem value="priority">Priority</SelectItem>
<SelectItem value="title">Title</SelectItem>
<SelectItem value="status">Status</SelectItem>
<SelectItem value="dueDate">{t('taskList.sort.dueDate')}</SelectItem>
<SelectItem value="priority">{t('taskList.sort.priority')}</SelectItem>
<SelectItem value="title">{t('taskList.sort.title')}</SelectItem>
<SelectItem value="status">{t('taskList.sort.status')}</SelectItem>
</SelectContent>
</Select>
</div>
@@ -153,8 +155,8 @@ export default function TaskList({ tasks, onTaskUpdate, onTaskEdit }: TaskListPr
<div className="text-center py-8">
<p className="text-muted-foreground" data-testid="text-no-tasks">
{searchQuery || filterBy !== 'all'
? 'No tasks match your filters'
: 'No tasks yet. Create your first task!'
? t('taskList.noMatchingTasks')
: t('taskList.noTasks')
}
</p>
</div>
+26
View File
@@ -0,0 +1,26 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import en from './locales/en.json';
import de from './locales/de.json';
const savedLanguage = localStorage.getItem('taskflow-language') || 'en';
i18n
.use(initReactI18next)
.init({
resources: {
en: {
translation: en
},
de: {
translation: de
}
},
lng: savedLanguage,
fallbackLng: 'en',
interpolation: {
escapeValue: false
}
});
export default i18n;
+149
View File
@@ -0,0 +1,149 @@
{
"app": {
"title": "TaskFlow",
"backToTasks": "Zurück zu Aufgaben"
},
"navigation": {
"tasks": "Aufgaben",
"calendar": "Kalender",
"create": "Erstellen",
"kanban": "Kanban",
"settings": "Einstellungen"
},
"taskList": {
"title": "Meine Aufgaben",
"taskCount": "{{count}} Aufgaben",
"searchPlaceholder": "Aufgaben suchen...",
"noTasks": "Noch keine Aufgaben. Erstelle deine erste Aufgabe!",
"noMatchingTasks": "Keine Aufgaben entsprechen deinen Filtern",
"filter": {
"all": "Alle",
"todo": "Zu erledigen",
"inProgress": "In Bearbeitung",
"done": "Erledigt",
"overdue": "Überfällig"
},
"sort": {
"dueDate": "Fälligkeitsdatum",
"priority": "Priorität",
"title": "Titel",
"status": "Status"
}
},
"taskCreation": {
"title": "Neue Aufgabe",
"titlePlaceholder": "Was muss erledigt werden?",
"descriptionPlaceholder": "Beschreibung hinzufügen (optional)",
"priority": "Priorität",
"label": "Label",
"noLabel": "Kein Label",
"dueDate": "Fälligkeitsdatum",
"cancel": "Abbrechen",
"create": "Aufgabe erstellen"
},
"taskDetails": {
"title": "Aufgabendetails",
"description": "Beschreibung",
"descriptionPlaceholder": "Beschreibung hinzufügen...",
"priority": "Priorität",
"status": "Status",
"label": "Label",
"noLabel": "Kein Label",
"dueDate": "Fälligkeitsdatum",
"selectDate": "Datum wählen",
"notes": "Notizen",
"notesPlaceholder": "Notizen hinzufügen...",
"timeTracked": "Erfasste Zeit",
"delete": "Aufgabe löschen",
"cancel": "Abbrechen",
"save": "Änderungen speichern"
},
"taskCard": {
"play": "Timer starten",
"pause": "Timer pausieren",
"edit": "Aufgabe bearbeiten",
"overdue": "Überfällig",
"timeTracked": "{{hours}}Std {{minutes}}Min erfasst"
},
"calendar": {
"title": "Kalenderansicht",
"today": "Heute",
"tasksDue": "{{count}} Aufgabe fällig",
"tasksDue_plural": "{{count}} Aufgaben fällig"
},
"kanban": {
"title": "Kanban-Board",
"todo": "Zu erledigen",
"inProgress": "In Bearbeitung",
"done": "Erledigt",
"taskCount": "{{count}} Aufgabe",
"taskCount_plural": "{{count}} Aufgaben"
},
"settings": {
"title": "Einstellungen",
"language": {
"title": "Sprache",
"description": "Wähle deine bevorzugte Sprache",
"english": "Englisch (English)",
"german": "Deutsch"
},
"appearance": {
"title": "Erscheinungsbild",
"description": "Passe das Aussehen an",
"themeToggle": "Thema-Umschalter ist in der Kopfzeile verfügbar"
},
"templates": {
"title": "Projektvorlagen",
"description": "Verwende Vorlagen zum schnellen Erstellen von Projekten",
"accessInfo": "Zugriff auf Vorlagen über das Navigationsmenü"
}
},
"projectTemplate": {
"title": "Projektvorlagen",
"description": "Wähle eine Vorlage, um ein neues Projekt mit vorkonfigurierten Aufgaben zu erstellen",
"websiteRedesign": "Website-Redesign",
"websiteDescription": "Umfassendes Website-Redesign-Projekt mit Design-, Entwicklungs- und Testphasen",
"mobileApp": "Mobile App-Launch",
"mobileDescription": "End-to-End Mobile App-Entwicklung von der Planung bis zur Bereitstellung",
"marketingCampaign": "Marketingkampagne",
"marketingDescription": "Vollständiger Marketingkampagnen-Workflow von der Planung bis zur Analyse",
"projectName": "Projektname",
"projectNamePlaceholder": "Projektnamen eingeben",
"startDate": "Startdatum",
"selectStartDate": "Startdatum wählen",
"createProject": "Projekt erstellen",
"weeks": "{{count}} Wochen",
"tasks": "{{count}} Aufgaben"
},
"timeCompletion": {
"title": "Aufgabe abgeschlossen!",
"congratulations": "Großartige Arbeit! Du hast abgeschlossen",
"timeSpent": "Zeit für diese Aufgabe",
"addTime": "Zusätzliche Zeit hinzufügen (optional)",
"hours": "Stunden",
"minutes": "Minuten",
"notes": "Notizen",
"notesPlaceholder": "Abschlussnotizen hinzufügen...",
"finish": "Fertig"
},
"priority": {
"low": "Niedrig",
"medium": "Mittel",
"high": "Hoch"
},
"status": {
"todo": "Zu erledigen",
"inProgress": "In Bearbeitung",
"done": "Erledigt"
},
"common": {
"cancel": "Abbrechen",
"save": "Speichern",
"delete": "Löschen",
"edit": "Bearbeiten",
"create": "Erstellen",
"close": "Schließen",
"loading": "Laden...",
"error": "Ein Fehler ist aufgetreten"
}
}
+149
View File
@@ -0,0 +1,149 @@
{
"app": {
"title": "TaskFlow",
"backToTasks": "Back to Tasks"
},
"navigation": {
"tasks": "Tasks",
"calendar": "Calendar",
"create": "Create",
"kanban": "Kanban",
"settings": "Settings"
},
"taskList": {
"title": "My Tasks",
"taskCount": "{{count}} tasks",
"searchPlaceholder": "Search tasks...",
"noTasks": "No tasks yet. Create your first task!",
"noMatchingTasks": "No tasks match your filters",
"filter": {
"all": "All",
"todo": "To Do",
"inProgress": "In Progress",
"done": "Done",
"overdue": "Overdue"
},
"sort": {
"dueDate": "Due Date",
"priority": "Priority",
"title": "Title",
"status": "Status"
}
},
"taskCreation": {
"title": "New Task",
"titlePlaceholder": "What needs to be done?",
"descriptionPlaceholder": "Add a description (optional)",
"priority": "Priority",
"label": "Label",
"noLabel": "No Label",
"dueDate": "Due date",
"cancel": "Cancel",
"create": "Create Task"
},
"taskDetails": {
"title": "Task Details",
"description": "Description",
"descriptionPlaceholder": "Add a description...",
"priority": "Priority",
"status": "Status",
"label": "Label",
"noLabel": "No Label",
"dueDate": "Due Date",
"selectDate": "Select date",
"notes": "Notes",
"notesPlaceholder": "Add notes...",
"timeTracked": "Time Tracked",
"delete": "Delete Task",
"cancel": "Cancel",
"save": "Save Changes"
},
"taskCard": {
"play": "Start timer",
"pause": "Pause timer",
"edit": "Edit task",
"overdue": "Overdue",
"timeTracked": "{{hours}}h {{minutes}}m tracked"
},
"calendar": {
"title": "Calendar View",
"today": "Today",
"tasksDue": "{{count}} task due",
"tasksDue_plural": "{{count}} tasks due"
},
"kanban": {
"title": "Kanban Board",
"todo": "To Do",
"inProgress": "In Progress",
"done": "Done",
"taskCount": "{{count}} task",
"taskCount_plural": "{{count}} tasks"
},
"settings": {
"title": "Settings",
"language": {
"title": "Language",
"description": "Choose your preferred language",
"english": "English",
"german": "German (Deutsch)"
},
"appearance": {
"title": "Appearance",
"description": "Customize the look and feel",
"themeToggle": "Theme toggle available in the header"
},
"templates": {
"title": "Project Templates",
"description": "Use templates to quickly create projects",
"accessInfo": "Access templates from the navigation menu"
}
},
"projectTemplate": {
"title": "Project Templates",
"description": "Select a template to create a new project with pre-configured tasks",
"websiteRedesign": "Website Redesign",
"websiteDescription": "Comprehensive website redesign project with design, development, and testing phases",
"mobileApp": "Mobile App Launch",
"mobileDescription": "End-to-end mobile app development from planning to deployment",
"marketingCampaign": "Marketing Campaign",
"marketingDescription": "Complete marketing campaign workflow from planning to analysis",
"projectName": "Project Name",
"projectNamePlaceholder": "Enter project name",
"startDate": "Start Date",
"selectStartDate": "Select start date",
"createProject": "Create Project",
"weeks": "{{count}} weeks",
"tasks": "{{count}} tasks"
},
"timeCompletion": {
"title": "Task Completed!",
"congratulations": "Great job! You've completed",
"timeSpent": "Time spent on this task",
"addTime": "Add additional time (optional)",
"hours": "Hours",
"minutes": "Minutes",
"notes": "Notes",
"notesPlaceholder": "Add completion notes...",
"finish": "Finish"
},
"priority": {
"low": "Low",
"medium": "Medium",
"high": "High"
},
"status": {
"todo": "To Do",
"inProgress": "In Progress",
"done": "Done"
},
"common": {
"cancel": "Cancel",
"save": "Save",
"delete": "Delete",
"edit": "Edit",
"create": "Create",
"close": "Close",
"loading": "Loading...",
"error": "An error occurred"
}
}
+1
View File
@@ -1,5 +1,6 @@
import { createRoot } from "react-dom/client";
import App from "./App";
import "./index.css";
import "./i18n/config";
createRoot(document.getElementById("root")!).render(<App />);
+82
View File
@@ -0,0 +1,82 @@
import { useTranslation } from 'react-i18next';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Globe } from 'lucide-react';
export default function Settings() {
const { t, i18n } = useTranslation();
const changeLanguage = (lng: string) => {
i18n.changeLanguage(lng);
localStorage.setItem('taskflow-language', lng);
console.log('Language changed to:', lng);
};
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold" data-testid="text-settings-title">
{t('settings.title')}
</h1>
</div>
{/* Language Settings */}
<Card data-testid="card-language-settings">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Globe className="w-5 h-5" />
{t('settings.language.title')}
</CardTitle>
<CardDescription>
{t('settings.language.description')}
</CardDescription>
</CardHeader>
<CardContent>
<Select value={i18n.language} onValueChange={changeLanguage}>
<SelectTrigger className="w-full sm:w-64" data-testid="select-language">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="en" data-testid="option-language-en">
{t('settings.language.english')}
</SelectItem>
<SelectItem value="de" data-testid="option-language-de">
{t('settings.language.german')}
</SelectItem>
</SelectContent>
</Select>
</CardContent>
</Card>
{/* Appearance Settings */}
<Card>
<CardHeader>
<CardTitle>{t('settings.appearance.title')}</CardTitle>
<CardDescription>
{t('settings.appearance.description')}
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
{t('settings.appearance.themeToggle')}
</p>
</CardContent>
</Card>
{/* Project Templates */}
<Card>
<CardHeader>
<CardTitle>{t('settings.templates.title')}</CardTitle>
<CardDescription>
{t('settings.templates.description')}
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
{t('settings.templates.accessInfo')}
</p>
</CardContent>
</Card>
</div>
);
}
+82 -12
View File
@@ -52,6 +52,7 @@
"express": "^4.21.2",
"express-session": "^1.18.1",
"framer-motion": "^11.13.1",
"i18next": "^25.6.0",
"input-otp": "^1.4.2",
"lucide-react": "^0.453.0",
"memorystore": "^1.6.7",
@@ -63,6 +64,7 @@
"react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.55.0",
"react-i18next": "^16.1.6",
"react-icons": "^5.4.0",
"react-resizable-panels": "^2.1.7",
"recharts": "^2.15.2",
@@ -352,12 +354,10 @@
}
},
"node_modules/@babel/runtime": {
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz",
"integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
@@ -5621,6 +5621,15 @@
"node": ">= 0.4"
}
},
"node_modules/html-parse-stringify": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
"license": "MIT",
"dependencies": {
"void-elements": "3.1.0"
}
},
"node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
@@ -5637,6 +5646,37 @@
"node": ">= 0.8"
}
},
"node_modules/i18next": {
"version": "25.6.0",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.6.0.tgz",
"integrity": "sha512-tTn8fLrwBYtnclpL5aPXK/tAYBLWVvoHM1zdfXoRNLcI+RvtMsoZRV98ePlaW3khHYKuNh/Q65W/+NVFUeIwVw==",
"funding": [
{
"type": "individual",
"url": "https://locize.com"
},
{
"type": "individual",
"url": "https://locize.com/i18next.html"
},
{
"type": "individual",
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
}
],
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.27.6"
},
"peerDependencies": {
"typescript": "^5"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@@ -7002,6 +7042,32 @@
"react": "^16.8.0 || ^17 || ^18 || ^19"
}
},
"node_modules/react-i18next": {
"version": "16.1.6",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.1.6.tgz",
"integrity": "sha512-62Iy0TO/2hJpTa80XaTIbHM4yjpile1YNieeg70vmdi91N2L3Q3MuxXBT0n6JpsryT88utpkqv0QbnIrDSxbAQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.27.6",
"html-parse-stringify": "^3.0.1"
},
"peerDependencies": {
"i18next": ">= 25.5.2",
"react": ">= 16.8.0",
"typescript": "^5"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
},
"typescript": {
"optional": true
}
}
},
"node_modules/react-icons": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.4.0.tgz",
@@ -7183,11 +7249,6 @@
"decimal.js-light": "^2.4.1"
}
},
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
},
"node_modules/regexparam": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/regexparam/-/regexparam-3.0.0.tgz",
@@ -8270,7 +8331,7 @@
"version": "5.6.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
"dev": true,
"devOptional": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@@ -8936,6 +8997,15 @@
"@esbuild/win32-x64": "0.21.5"
}
},
"node_modules/void-elements": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+2
View File
@@ -54,6 +54,7 @@
"express": "^4.21.2",
"express-session": "^1.18.1",
"framer-motion": "^11.13.1",
"i18next": "^25.6.0",
"input-otp": "^1.4.2",
"lucide-react": "^0.453.0",
"memorystore": "^1.6.7",
@@ -65,6 +66,7 @@
"react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.55.0",
"react-i18next": "^16.1.6",
"react-icons": "^5.4.0",
"react-resizable-panels": "^2.1.7",
"recharts": "^2.15.2",
+15
View File
@@ -8,6 +8,15 @@ TaskFlow is a modern, mobile-first personal task management application designed
Preferred communication style: Simple, everyday language.
## Recent Changes
### Multilingual Support (October 2025)
- Added full internationalization (i18n) support using react-i18next
- Implemented English and German language translations
- Created Settings page with language selector
- Language preference persists in localStorage
- All UI components translated: TaskList, Calendar, Kanban, Task Creation, Navigation, Settings
## System Architecture
### Frontend Architecture
@@ -20,6 +29,8 @@ Preferred communication style: Simple, everyday language.
**Styling Approach**: Implements Tailwind CSS for utility-first styling with custom CSS variables for theming. The design system supports automatic dark/light mode switching and maintains consistent spacing using Tailwind's spacing units.
**Internationalization**: Implements i18next and react-i18next for multilingual support. Currently supports English (default) and German translations. Language preference is stored in localStorage and persists across sessions. Translation files are organized in JSON format at `client/src/i18n/locales/`.
**Mobile-First Responsive Design**: The interface is fully optimized for mobile devices with adaptive layouts that efficiently use screen space across all viewport sizes:
- **Responsive Container**: Main layout uses responsive padding (px-3/py-4 on mobile, px-4/py-6 on desktop) with max-w-screen-2xl constraint to prevent excessive horizontal spread on large screens
- **Calendar Grid Breakpoints**: Adapts from 2 columns on mobile (375px), to 3 columns on small tablets (640px), 5 columns on tablets (768px), and full 7-column week view on desktop (1024px+)
@@ -111,4 +122,8 @@ Preferred communication style: Simple, everyday language.
- **connect-pg-simple**: PostgreSQL session store for Express.js applications
- **Express Session**: Server-side session management middleware
### Internationalization
- **i18next**: Internationalization framework for managing translations
- **react-i18next**: React bindings for i18next, providing hooks and components for translation
The application architecture prioritizes developer experience, type safety, and scalability while maintaining a clean separation between frontend components, server logic, and data persistence layers.