Add missing translations for task management features
Integrates i18next for internationalization, adding translated strings for task details, search, filters, and calendar views in both English and German. 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/E3NLe8e
This commit is contained in:
@@ -18,6 +18,10 @@ externalPort = 80
|
||||
localPort = 35345
|
||||
externalPort = 3002
|
||||
|
||||
[[ports]]
|
||||
localPort = 38309
|
||||
externalPort = 4200
|
||||
|
||||
[[ports]]
|
||||
localPort = 39063
|
||||
externalPort = 3001
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Separator } from '@/components/ui/separator';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Clock, Timer, FileText, Plus, Edit2, Save, X, Calendar, Tag } from 'lucide-react';
|
||||
import { Task, Label } from '@shared/schema';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface TimeEntry {
|
||||
id: string;
|
||||
@@ -33,6 +34,8 @@ export default function TaskDetailsModal({
|
||||
onSave,
|
||||
labels = []
|
||||
}: TaskDetailsModalProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Edit form state
|
||||
const [editedTitle, setEditedTitle] = useState('');
|
||||
const [editedDescription, setEditedDescription] = useState('');
|
||||
@@ -73,7 +76,7 @@ export default function TaskDetailsModal({
|
||||
id: 'tracked-time',
|
||||
date: new Date(),
|
||||
timeSpent: task.timeTracked,
|
||||
description: 'Previously tracked time'
|
||||
description: t('taskDetails.previouslyTrackedTime')
|
||||
}]);
|
||||
} else {
|
||||
setTimeEntries([]);
|
||||
@@ -237,7 +240,7 @@ export default function TaskDetailsModal({
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<FileText className="w-5 h-5" />
|
||||
Task Details
|
||||
{t('taskDetails.title')}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -257,14 +260,14 @@ export default function TaskDetailsModal({
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Badge variant="outline" className={`text-xs ${getPriorityColor(task.priority)}`}>
|
||||
{task.priority} priority
|
||||
{t(`taskDetails.priorityValue.${task.priority}`)}
|
||||
</Badge>
|
||||
<Badge variant="outline" className={`text-xs ${getStatusColor(task.status)}`}>
|
||||
{task.status}
|
||||
{t(`taskDetails.statusValue.${task.status}`)}
|
||||
</Badge>
|
||||
{getTotalTrackedTime() > 0 && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{formatMinutesToHours(getTotalTrackedTime())} tracked
|
||||
{formatMinutesToHours(getTotalTrackedTime())} {t('taskDetails.tracked')}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
@@ -275,41 +278,41 @@ export default function TaskDetailsModal({
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="edit" data-testid="tab-edit">
|
||||
<Edit2 className="w-4 h-4 mr-2" />
|
||||
Edit
|
||||
{t('taskDetails.editTab')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="notes" data-testid="tab-notes">
|
||||
<FileText className="w-4 h-4 mr-2" />
|
||||
Notes
|
||||
{t('taskDetails.notesTab')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="time" data-testid="tab-time">
|
||||
<Clock className="w-4 h-4 mr-2" />
|
||||
Time Tracking
|
||||
{t('taskDetails.timeTab')}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Edit Tab */}
|
||||
<TabsContent value="edit" className="space-y-4">
|
||||
<Card className="p-4">
|
||||
<h4 className="font-medium mb-4">Edit Task</h4>
|
||||
<h4 className="font-medium mb-4">{t('taskDetails.editTask')}</h4>
|
||||
<div className="space-y-4">
|
||||
{/* Title */}
|
||||
<div>
|
||||
<label className="text-sm font-medium block mb-2">Title</label>
|
||||
<label className="text-sm font-medium block mb-2">{t('taskDetails.titleLabel')}</label>
|
||||
<Input
|
||||
value={editedTitle}
|
||||
onChange={(e) => setEditedTitle(e.target.value)}
|
||||
placeholder="Task title"
|
||||
placeholder={t('taskDetails.titlePlaceholder')}
|
||||
data-testid="input-task-title"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div>
|
||||
<label className="text-sm font-medium block mb-2">Description</label>
|
||||
<label className="text-sm font-medium block mb-2">{t('taskDetails.description')}</label>
|
||||
<Textarea
|
||||
value={editedDescription}
|
||||
onChange={(e) => setEditedDescription(e.target.value)}
|
||||
placeholder="Task description"
|
||||
placeholder={t('taskDetails.descriptionPlaceholder')}
|
||||
rows={3}
|
||||
data-testid="input-task-description"
|
||||
/>
|
||||
@@ -318,29 +321,29 @@ export default function TaskDetailsModal({
|
||||
{/* Status and Priority */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium block mb-2">Status</label>
|
||||
<label className="text-sm font-medium block mb-2">{t('taskDetails.status')}</label>
|
||||
<Select value={editedStatus} onValueChange={(value) => setEditedStatus(value as 'todo' | 'inProgress' | 'done')}>
|
||||
<SelectTrigger data-testid="select-status">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="todo">To Do</SelectItem>
|
||||
<SelectItem value="inProgress">In Progress</SelectItem>
|
||||
<SelectItem value="done">Done</SelectItem>
|
||||
<SelectItem value="todo">{t('taskDetails.statusValue.todo')}</SelectItem>
|
||||
<SelectItem value="inProgress">{t('taskDetails.statusValue.inProgress')}</SelectItem>
|
||||
<SelectItem value="done">{t('taskDetails.statusValue.done')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium block mb-2">Priority</label>
|
||||
<label className="text-sm font-medium block mb-2">{t('taskDetails.priority')}</label>
|
||||
<Select value={editedPriority} onValueChange={(value) => setEditedPriority(value as 'low' | 'medium' | 'high')}>
|
||||
<SelectTrigger data-testid="select-priority">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="low">Low</SelectItem>
|
||||
<SelectItem value="medium">Medium</SelectItem>
|
||||
<SelectItem value="high">High</SelectItem>
|
||||
<SelectItem value="low">{t('taskDetails.priorityValue.low')}</SelectItem>
|
||||
<SelectItem value="medium">{t('taskDetails.priorityValue.medium')}</SelectItem>
|
||||
<SelectItem value="high">{t('taskDetails.priorityValue.high')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -348,13 +351,13 @@ export default function TaskDetailsModal({
|
||||
|
||||
{/* Label */}
|
||||
<div>
|
||||
<label className="text-sm font-medium block mb-2">Label</label>
|
||||
<label className="text-sm font-medium block mb-2">{t('taskDetails.label')}</label>
|
||||
<Select value={editedLabelId || 'none'} onValueChange={(value) => setEditedLabelId(value === 'none' ? null : value)}>
|
||||
<SelectTrigger data-testid="input-label-selector">
|
||||
<SelectValue placeholder="Select a label" />
|
||||
<SelectValue placeholder={t('taskDetails.selectLabel')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">No Label</SelectItem>
|
||||
<SelectItem value="none">{t('taskDetails.noLabel')}</SelectItem>
|
||||
{labels.map((label) => (
|
||||
<SelectItem key={label.id} value={label.id}>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -372,7 +375,7 @@ export default function TaskDetailsModal({
|
||||
|
||||
{/* Due Date */}
|
||||
<div>
|
||||
<label className="text-sm font-medium block mb-2">Due Date</label>
|
||||
<label className="text-sm font-medium block mb-2">{t('taskDetails.dueDate')}</label>
|
||||
<Input
|
||||
type="date"
|
||||
value={editedDueDate}
|
||||
@@ -389,7 +392,7 @@ export default function TaskDetailsModal({
|
||||
data-testid="button-save-task-edit"
|
||||
>
|
||||
<Save className="w-4 h-4 mr-1" />
|
||||
Save Changes
|
||||
{t('taskDetails.save')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -400,7 +403,7 @@ export default function TaskDetailsModal({
|
||||
<TabsContent value="notes" className="space-y-4">
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h4 className="font-medium">Task Notes</h4>
|
||||
<h4 className="font-medium">{t('taskDetails.taskNotes')}</h4>
|
||||
{!isEditingNotes && (
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -409,7 +412,7 @@ export default function TaskDetailsModal({
|
||||
data-testid="button-edit-notes"
|
||||
>
|
||||
<Edit2 className="w-4 h-4 mr-1" />
|
||||
Edit
|
||||
{t('taskDetails.edit')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -419,7 +422,7 @@ export default function TaskDetailsModal({
|
||||
<Textarea
|
||||
value={notes}
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
placeholder="Add your notes for this task..."
|
||||
placeholder={t('taskDetails.notesPlaceholder')}
|
||||
rows={4}
|
||||
data-testid="textarea-task-notes"
|
||||
/>
|
||||
@@ -430,7 +433,7 @@ export default function TaskDetailsModal({
|
||||
data-testid="button-save-notes"
|
||||
>
|
||||
<Save className="w-4 h-4 mr-1" />
|
||||
Save
|
||||
{t('taskDetails.save')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -442,7 +445,7 @@ export default function TaskDetailsModal({
|
||||
data-testid="button-cancel-notes"
|
||||
>
|
||||
<X className="w-4 h-4 mr-1" />
|
||||
Cancel
|
||||
{t('taskDetails.cancel')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -454,7 +457,7 @@ export default function TaskDetailsModal({
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground italic">
|
||||
No notes added yet. Click "Edit" to add notes.
|
||||
{t('taskDetails.noNotesYet')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -468,7 +471,7 @@ export default function TaskDetailsModal({
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="font-medium">Total Time Tracked</h4>
|
||||
<h4 className="font-medium">{t('taskDetails.totalTimeTracked')}</h4>
|
||||
<p className="text-2xl font-mono text-primary" data-testid="text-total-time">
|
||||
{formatMinutesToHours(getTotalTrackedTime())}
|
||||
</p>
|
||||
@@ -479,7 +482,7 @@ export default function TaskDetailsModal({
|
||||
data-testid="button-add-time"
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-1" />
|
||||
Add Time
|
||||
{t('taskDetails.addTime')}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -489,7 +492,7 @@ export default function TaskDetailsModal({
|
||||
<Card className="p-4">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="font-medium">Add Time Entry</h4>
|
||||
<h4 className="font-medium">{t('taskDetails.addTimeEntry')}</h4>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@@ -504,11 +507,11 @@ export default function TaskDetailsModal({
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="clock" data-testid="tab-clock-picker">
|
||||
<Clock className="w-4 h-4 mr-2" />
|
||||
Clock
|
||||
{t('taskDetails.clock')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="manual" data-testid="tab-manual-input">
|
||||
<Timer className="w-4 h-4 mr-2" />
|
||||
Manual
|
||||
{t('taskDetails.manual')}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
|
||||
@@ -219,16 +219,16 @@ export default function TasksWithCalendar({ tasks, onTaskUpdate, onTaskEdit, onT
|
||||
{/* Title Bar */}
|
||||
<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>
|
||||
<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);
|
||||
@@ -251,11 +251,11 @@ export default function TasksWithCalendar({ tasks, onTaskUpdate, onTaskEdit, onT
|
||||
</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>
|
||||
|
||||
@@ -270,10 +270,10 @@ export default function TasksWithCalendar({ tasks, onTaskUpdate, onTaskEdit, onT
|
||||
</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>
|
||||
@@ -286,15 +286,15 @@ export default function TasksWithCalendar({ tasks, onTaskUpdate, onTaskEdit, onT
|
||||
<div className="text-center py-8">
|
||||
<p className="text-muted-foreground" data-testid="text-no-tasks">
|
||||
{searchQuery || filterBy !== 'all'
|
||||
? 'No unscheduled tasks match your filters'
|
||||
: 'No unscheduled tasks. Drag tasks to the calendar below to schedule them!'
|
||||
? t('taskList.noMatchingTasks')
|
||||
: t('taskList.noUnscheduledTasks')
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="text-sm font-medium text-muted-foreground mb-2">
|
||||
Unscheduled Tasks - Drag to calendar below to schedule
|
||||
{t('taskList.unscheduledTasksLabel')}
|
||||
</div>
|
||||
{unscheduledTasks.map((task) => (
|
||||
<div
|
||||
@@ -342,7 +342,7 @@ export default function TasksWithCalendar({ tasks, onTaskUpdate, onTaskEdit, onT
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="w-4 h-4 text-primary" />
|
||||
<h3 className="text-sm font-semibold" data-testid="text-calendar-title">
|
||||
Next {visibleDays} Days
|
||||
{t('taskList.calendarTitle', { count: visibleDays })}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
"searchPlaceholder": "Aufgaben suchen...",
|
||||
"noTasks": "Noch keine Aufgaben. Erstelle deine erste Aufgabe!",
|
||||
"noMatchingTasks": "Keine Aufgaben entsprechen deinen Filtern",
|
||||
"noUnscheduledTasks": "Keine ungeplanten Aufgaben. Ziehe Aufgaben in den Kalender unten, um sie zu planen!",
|
||||
"unscheduledTasksLabel": "Ungeplante Aufgaben - In den Kalender ziehen zum Planen",
|
||||
"calendarTitle": "Nächste {{count}} Tage",
|
||||
"filter": {
|
||||
"all": "Alle",
|
||||
"todo": "Zu erledigen",
|
||||
@@ -68,6 +71,33 @@
|
||||
},
|
||||
"taskDetails": {
|
||||
"title": "Aufgabendetails",
|
||||
"selectLabel": "Label auswählen",
|
||||
"tracked": "erfasst",
|
||||
"priorityValue": {
|
||||
"low": "Niedrige Priorität",
|
||||
"medium": "Mittlere Priorität",
|
||||
"high": "Hohe Priorität"
|
||||
},
|
||||
"statusValue": {
|
||||
"todo": "Zu erledigen",
|
||||
"inProgress": "In Bearbeitung",
|
||||
"done": "Erledigt"
|
||||
},
|
||||
"editTab": "Bearbeiten",
|
||||
"notesTab": "Notizen",
|
||||
"timeTab": "Zeiterfassung",
|
||||
"editTask": "Aufgabe bearbeiten",
|
||||
"titleLabel": "Titel",
|
||||
"titlePlaceholder": "Aufgabentitel",
|
||||
"taskNotes": "Aufgabennotizen",
|
||||
"edit": "Bearbeiten",
|
||||
"noNotesYet": "Noch keine Notizen hinzugefügt. Klicke auf \"Bearbeiten\", um Notizen hinzuzufügen.",
|
||||
"totalTimeTracked": "Gesamt erfasste Zeit",
|
||||
"addTime": "Zeit hinzufügen",
|
||||
"addTimeEntry": "Zeiteintrag hinzufügen",
|
||||
"clock": "Uhr",
|
||||
"manual": "Manuell",
|
||||
"previouslyTrackedTime": "Zuvor erfasste Zeit",
|
||||
"description": "Beschreibung",
|
||||
"descriptionPlaceholder": "Beschreibung hinzufügen...",
|
||||
"priority": "Priorität",
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
"searchPlaceholder": "Search tasks...",
|
||||
"noTasks": "No tasks yet. Create your first task!",
|
||||
"noMatchingTasks": "No tasks match your filters",
|
||||
"noUnscheduledTasks": "No unscheduled tasks. Drag tasks to the calendar below to schedule them!",
|
||||
"unscheduledTasksLabel": "Unscheduled Tasks - Drag to calendar below to schedule",
|
||||
"calendarTitle": "Next {{count}} Days",
|
||||
"filter": {
|
||||
"all": "All",
|
||||
"todo": "To Do",
|
||||
@@ -74,6 +77,7 @@
|
||||
"status": "Status",
|
||||
"label": "Label",
|
||||
"noLabel": "No Label",
|
||||
"selectLabel": "Select a label",
|
||||
"dueDate": "Due Date",
|
||||
"selectDate": "Select date",
|
||||
"notes": "Notes",
|
||||
@@ -81,7 +85,33 @@
|
||||
"timeTracked": "Time Tracked",
|
||||
"delete": "Delete Task",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save Changes"
|
||||
"save": "Save Changes",
|
||||
"tracked": "tracked",
|
||||
"priorityValue": {
|
||||
"low": "Low Priority",
|
||||
"medium": "Medium Priority",
|
||||
"high": "High Priority"
|
||||
},
|
||||
"statusValue": {
|
||||
"todo": "To Do",
|
||||
"inProgress": "In Progress",
|
||||
"done": "Done"
|
||||
},
|
||||
"editTab": "Edit",
|
||||
"notesTab": "Notes",
|
||||
"timeTab": "Time Tracking",
|
||||
"editTask": "Edit Task",
|
||||
"titleLabel": "Title",
|
||||
"titlePlaceholder": "Task title",
|
||||
"taskNotes": "Task Notes",
|
||||
"edit": "Edit",
|
||||
"noNotesYet": "No notes added yet. Click \"Edit\" to add notes.",
|
||||
"totalTimeTracked": "Total Time Tracked",
|
||||
"addTime": "Add Time",
|
||||
"addTimeEntry": "Add Time Entry",
|
||||
"clock": "Clock",
|
||||
"manual": "Manual",
|
||||
"previouslyTrackedTime": "Previously tracked time"
|
||||
},
|
||||
"taskCard": {
|
||||
"play": "Start timer",
|
||||
|
||||
Reference in New Issue
Block a user