Improve task management by adding translations for all app features
Implement i18n support by adding new translation keys for Calendar, Kanban Board, and TaskCard components, and update existing translations in de.json and en.json. 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/nn5bWKE
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -6,6 +7,7 @@ import { ChevronLeft, ChevronRight, Calendar } from 'lucide-react';
|
||||
import { Task, Label } from '@shared/schema';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { addDays, format, isSameDay, startOfWeek, subWeeks } from 'date-fns';
|
||||
import { de, enUS } from 'date-fns/locale';
|
||||
|
||||
interface CalendarViewProps {
|
||||
tasks: Task[];
|
||||
@@ -15,9 +17,13 @@ interface CalendarViewProps {
|
||||
}
|
||||
|
||||
export default function CalendarView({ tasks, onTaskDrop, onTaskClick, onDateSelect }: CalendarViewProps) {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [currentDate, setCurrentDate] = useState(new Date());
|
||||
const [draggedTask, setDraggedTask] = useState<string | null>(null);
|
||||
|
||||
// Get date-fns locale based on current language
|
||||
const dateLocale = i18n.language === 'de' ? de : enUS;
|
||||
|
||||
// Fetch labels to get label colors
|
||||
const { data: labels = [] } = useQuery<Label[]>({
|
||||
queryKey: ['/api/labels'],
|
||||
@@ -71,7 +77,7 @@ export default function CalendarView({ tasks, onTaskDrop, onTaskClick, onDateSel
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="w-4 h-4 sm:w-5 sm:h-5 text-primary" />
|
||||
<h2 className="text-base sm:text-lg font-semibold" data-testid="text-calendar-title">
|
||||
Calendar
|
||||
{t('calendar.title')}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
@@ -129,12 +135,12 @@ export default function CalendarView({ tasks, onTaskDrop, onTaskClick, onDateSel
|
||||
<div className={`text-xs font-medium ${
|
||||
isPreviousWeek ? 'text-muted-foreground/60' : 'text-muted-foreground'
|
||||
}`}>
|
||||
{format(date, 'EEE')}
|
||||
{format(date, 'EEE', { locale: dateLocale })}
|
||||
</div>
|
||||
<div className={`text-sm font-semibold ${
|
||||
isToday ? 'text-primary' : isPreviousWeek ? 'text-muted-foreground/60' : ''
|
||||
}`}>
|
||||
{format(date, 'd')}
|
||||
{format(date, 'd', { locale: dateLocale })}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -178,7 +184,7 @@ export default function CalendarView({ tasks, onTaskDrop, onTaskClick, onDateSel
|
||||
|
||||
{dayTasks.length > 3 && (
|
||||
<Badge variant="secondary" className="w-full justify-center text-xs">
|
||||
+{dayTasks.length - 3} more
|
||||
{t('calendar.moreItems', { count: dayTasks.length - 3 })}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
@@ -191,15 +197,15 @@ export default function CalendarView({ tasks, onTaskDrop, onTaskClick, onDateSel
|
||||
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-primary"></div>
|
||||
<span>Today</span>
|
||||
<span>{t('calendar.today')}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-muted"></div>
|
||||
<span>Weekend</span>
|
||||
<span>{t('calendar.weekend')}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-muted opacity-50"></div>
|
||||
<span>Previous Week</span>
|
||||
<span>{t('calendar.previousWeek')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
@@ -22,6 +23,7 @@ import { Task, Label } from '@shared/schema';
|
||||
import TaskCard from './TaskCard';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { format, startOfWeek, endOfWeek, startOfMonth, endOfMonth, addWeeks, addMonths, isWithinInterval } from 'date-fns';
|
||||
import { de, enUS } from 'date-fns/locale';
|
||||
|
||||
interface KanbanBoardProps {
|
||||
tasks: Task[];
|
||||
@@ -36,6 +38,7 @@ interface KanbanBoardProps {
|
||||
type ViewMode = 'traditional' | 'weekly' | 'monthly';
|
||||
|
||||
export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, onTaskClick, onTaskDelete, onStartTimer, onStopTimer }: KanbanBoardProps) {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('traditional');
|
||||
const [currentDate, setCurrentDate] = useState(new Date());
|
||||
const [draggedTask, setDraggedTask] = useState<string | null>(null);
|
||||
@@ -44,6 +47,9 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
|
||||
const [selectedLabels, setSelectedLabels] = useState<string[]>([]);
|
||||
const [selectedPriorities, setSelectedPriorities] = useState<string[]>([]);
|
||||
|
||||
// Get date-fns locale based on current language
|
||||
const dateLocale = i18n.language === 'de' ? de : enUS;
|
||||
|
||||
// Fetch labels to get label colors
|
||||
const { data: labels = [] } = useQuery<Label[]>({
|
||||
queryKey: ['/api/labels'],
|
||||
@@ -52,9 +58,9 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
|
||||
});
|
||||
|
||||
const columns = [
|
||||
{ id: 'todo', title: 'To Do', status: 'todo' as const },
|
||||
{ id: 'inProgress', title: 'In Progress', status: 'inProgress' as const },
|
||||
{ id: 'done', title: 'Done', status: 'done' as const }
|
||||
{ id: 'todo', title: t('kanban.todo'), status: 'todo' as const },
|
||||
{ id: 'inProgress', title: t('kanban.inProgress'), status: 'inProgress' as const },
|
||||
{ id: 'done', title: t('kanban.done'), status: 'done' as const }
|
||||
];
|
||||
|
||||
const getTasksForColumn = (status: Task['status']) => {
|
||||
@@ -124,11 +130,11 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
|
||||
|
||||
const getPeriodTitle = () => {
|
||||
if (viewMode === 'weekly') {
|
||||
return `Week of ${format(startOfWeek(currentDate, { weekStartsOn: 1 }), 'MMM d')}`;
|
||||
return t('kanban.weekOf', { date: format(startOfWeek(currentDate, { weekStartsOn: 1 }), 'MMM d', { locale: dateLocale }) });
|
||||
} else if (viewMode === 'monthly') {
|
||||
return format(currentDate, 'MMMM yyyy');
|
||||
return format(currentDate, 'MMMM yyyy', { locale: dateLocale });
|
||||
}
|
||||
return 'All Tasks';
|
||||
return t('kanban.allTasks');
|
||||
};
|
||||
|
||||
// Filter management functions
|
||||
@@ -174,7 +180,7 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
|
||||
<div className="flex items-center gap-2">
|
||||
<LayoutGrid className="w-4 h-4 sm:w-5 sm:h-5 text-primary" />
|
||||
<h2 className="text-base sm:text-lg font-semibold" data-testid="text-kanban-title">
|
||||
Board
|
||||
{t('kanban.title')}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
@@ -212,7 +218,7 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
|
||||
<div className="flex items-center justify-between flex-wrap gap-3 sm:gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Filter className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">Filters</span>
|
||||
<span className="text-sm font-medium">{t('kanban.filters')}</span>
|
||||
{hasActiveFilters && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{selectedLabels.length + selectedPriorities.length}
|
||||
@@ -232,7 +238,7 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
|
||||
data-testid="button-filter-labels"
|
||||
>
|
||||
<Tag className="w-4 h-4 mr-2" />
|
||||
Labels
|
||||
{t('kanban.labels')}
|
||||
{selectedLabels.length > 0 && (
|
||||
<Badge variant="secondary" className="ml-2 text-xs">
|
||||
{selectedLabels.length}
|
||||
@@ -242,7 +248,7 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-56" align="start">
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-medium">Filter by Labels</h4>
|
||||
<h4 className="text-sm font-medium">{t('kanban.filterByLabels')}</h4>
|
||||
<Separator />
|
||||
<div className="space-y-2 max-h-48 overflow-y-auto">
|
||||
{labels.length > 0 ? (
|
||||
@@ -270,7 +276,7 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">No labels available</p>
|
||||
<p className="text-sm text-muted-foreground">{t('kanban.noLabelsAvailable')}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -289,7 +295,7 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
|
||||
data-testid="button-filter-priorities"
|
||||
>
|
||||
<AlertTriangle className="w-4 h-4 mr-2" />
|
||||
Priorities
|
||||
{t('kanban.priorities')}
|
||||
{selectedPriorities.length > 0 && (
|
||||
<Badge variant="secondary" className="ml-2 text-xs">
|
||||
{selectedPriorities.length}
|
||||
@@ -299,7 +305,7 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-48" align="start">
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-medium">Filter by Priority</h4>
|
||||
<h4 className="text-sm font-medium">{t('kanban.filterByPriority')}</h4>
|
||||
<Separator />
|
||||
<div className="space-y-2">
|
||||
{priorities.map((priority) => (
|
||||
@@ -321,7 +327,7 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
|
||||
variant="secondary"
|
||||
className={`text-xs ${getPriorityColor(priority)}`}
|
||||
>
|
||||
{priority}
|
||||
{t(`priority.${priority}`)}
|
||||
</Badge>
|
||||
</label>
|
||||
</div>
|
||||
@@ -344,7 +350,7 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<X className="w-4 h-4 mr-2" />
|
||||
Clear All
|
||||
{t('kanban.clearAll')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
@@ -355,7 +361,7 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
|
||||
{hasActiveFilters && (
|
||||
<div className="mt-3 pt-3 border-t">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-xs text-muted-foreground">Active filters:</span>
|
||||
<span className="text-xs text-muted-foreground">{t('kanban.activeFilters')}</span>
|
||||
{selectedLabels.map((labelId) => {
|
||||
const label = labels.find(l => l.id === labelId);
|
||||
return label ? (
|
||||
@@ -384,7 +390,7 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
|
||||
className={`text-xs ${getPriorityColor(priority)}`}
|
||||
data-testid={`active-filter-priority-${priority}`}
|
||||
>
|
||||
{priority}
|
||||
{t(`priority.${priority}`)}
|
||||
<X
|
||||
className="w-3 h-3 ml-1 cursor-pointer"
|
||||
onClick={() => togglePriority(priority)}
|
||||
@@ -402,15 +408,15 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="traditional" data-testid="tab-traditional">
|
||||
<LayoutGrid className="w-4 h-4 mr-2" />
|
||||
Traditional
|
||||
{t('kanban.traditional')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="weekly" data-testid="tab-weekly">
|
||||
<CalendarIcon className="w-4 h-4 mr-2" />
|
||||
Weekly
|
||||
{t('kanban.weekly')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="monthly" data-testid="tab-monthly">
|
||||
<CalendarIcon className="w-4 h-4 mr-2" />
|
||||
Monthly
|
||||
{t('kanban.monthly')}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
@@ -469,7 +475,7 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
|
||||
|
||||
{columnTasks.length === 0 && (
|
||||
<div className="text-center text-muted-foreground text-sm py-8">
|
||||
No tasks in {column.title.toLowerCase()}
|
||||
{t('kanban.noTasksInColumn', { column: column.title.toLowerCase() })}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -533,7 +539,7 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
|
||||
|
||||
{columnTasks.length === 0 && (
|
||||
<div className="text-center text-muted-foreground text-sm py-8">
|
||||
No tasks in {column.title.toLowerCase()} for this week
|
||||
{t('kanban.noTasksInColumnForWeek', { column: column.title.toLowerCase() })}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -597,7 +603,7 @@ export default function KanbanBoard({ tasks, onTaskStatusChange, onTaskUpdate, o
|
||||
|
||||
{columnTasks.length === 0 && (
|
||||
<div className="text-center text-muted-foreground text-sm py-8">
|
||||
No tasks in {column.title.toLowerCase()} for this month
|
||||
{t('kanban.noTasksInColumnForMonth', { column: column.title.toLowerCase() })}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -23,6 +24,7 @@ interface TaskCardProps {
|
||||
}
|
||||
|
||||
export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDelete, onStatusChange, isDragging }: TaskCardProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Fetch labels to get the label color
|
||||
const { data: labels = [] } = useQuery<Label[]>({
|
||||
@@ -105,7 +107,7 @@ export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDe
|
||||
className={`text-xs ${getPriorityColor(task.priority)}`}
|
||||
data-testid={`badge-priority-${task.id}`}
|
||||
>
|
||||
{task.priority}
|
||||
{t(`priority.${task.priority}`)}
|
||||
</Badge>
|
||||
|
||||
<Badge
|
||||
@@ -113,7 +115,7 @@ export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDe
|
||||
className={`text-xs ${getStatusColor(task.status)}`}
|
||||
data-testid={`badge-status-${task.id}`}
|
||||
>
|
||||
{task.status.replace(/([A-Z])/g, ' $1').toLowerCase()}
|
||||
{t(`status.${task.status}`)}
|
||||
</Badge>
|
||||
|
||||
{task.dueDate && (
|
||||
@@ -134,7 +136,7 @@ export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDe
|
||||
className={task.isTracking ? 'text-primary font-medium' : 'text-muted-foreground'}
|
||||
>
|
||||
{task.timeTracked > 0 ? formatTime(task.timeTracked) : '0m'}
|
||||
{task.isTracking && ' (running)'}
|
||||
{task.isTracking && ` (${t('taskCard.running')})`}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -163,7 +165,7 @@ export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDe
|
||||
data-testid={`menu-edit-${task.id}`}
|
||||
>
|
||||
<Edit className="w-4 h-4 mr-2" />
|
||||
Edit Task
|
||||
{t('taskCard.editTask')}
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
@@ -174,7 +176,7 @@ export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDe
|
||||
data-testid={`menu-timer-${task.id}`}
|
||||
>
|
||||
<Timer className="w-4 h-4 mr-2" />
|
||||
{task.isTracking ? 'Stop Timer' : 'Start Timer'}
|
||||
{task.isTracking ? t('taskCard.stopTimer') : t('taskCard.startTimer')}
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
@@ -189,7 +191,7 @@ export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDe
|
||||
data-testid={`menu-complete-${task.id}`}
|
||||
>
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
Mark as Done
|
||||
{t('taskCard.markAsDone')}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
@@ -203,7 +205,7 @@ export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDe
|
||||
data-testid={`menu-start-${task.id}`}
|
||||
>
|
||||
<Play className="w-4 h-4 mr-2" />
|
||||
Start Working
|
||||
{t('taskCard.startWorking')}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
@@ -217,7 +219,7 @@ export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDe
|
||||
data-testid={`menu-todo-${task.id}`}
|
||||
>
|
||||
<Clock className="w-4 h-4 mr-2" />
|
||||
Move to Todo
|
||||
{t('taskCard.moveToTodo')}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
@@ -231,7 +233,7 @@ export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDe
|
||||
data-testid={`menu-reopen-${task.id}`}
|
||||
>
|
||||
<Play className="w-4 h-4 mr-2" />
|
||||
Reopen Task
|
||||
{t('taskCard.reopenTask')}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
@@ -248,7 +250,7 @@ export default function TaskCard({ task, onStartTimer, onStopTimer, onEdit, onDe
|
||||
data-testid={`menu-delete-${task.id}`}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
Delete Task
|
||||
{t('taskCard.deleteTask')}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
|
||||
Reference in New Issue
Block a user