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
This commit is contained in:
paul-nothaft
2025-10-23 20:56:05 +00:00
parent 571245e2d9
commit 32d3126dc9
3 changed files with 36 additions and 30 deletions
+6 -4
View File
@@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Home, Calendar, LayoutGrid, Plus, Settings } from 'lucide-react'; import { Home, Calendar, LayoutGrid, Plus, Settings } from 'lucide-react';
@@ -10,14 +11,15 @@ interface BottomNavigationProps {
} }
export default function BottomNavigation({ onTabChange, onCreateTask, activeTab = 'tasks' }: BottomNavigationProps) { export default function BottomNavigation({ onTabChange, onCreateTask, activeTab = 'tasks' }: BottomNavigationProps) {
const { t } = useTranslation();
const [currentTab, setCurrentTab] = useState(activeTab); const [currentTab, setCurrentTab] = useState(activeTab);
const tabs = [ const tabs = [
{ id: 'tasks', label: 'Tasks', icon: Home }, { id: 'tasks', label: t('navigation.tasks'), icon: Home },
{ id: 'calendar', label: 'Calendar', icon: Calendar }, { id: 'calendar', label: t('navigation.calendar'), icon: Calendar },
{ id: 'create', label: 'Create', icon: Plus, isCreate: true }, { id: 'create', label: 'Create', icon: Plus, isCreate: true },
{ id: 'kanban', label: 'Board', icon: LayoutGrid }, { id: 'kanban', label: t('navigation.kanban'), icon: LayoutGrid },
{ id: 'settings', label: 'Settings', icon: Settings } { id: 'settings', label: t('navigation.settings'), icon: Settings }
]; ];
const handleTabClick = (tabId: string, isCreate?: boolean) => { const handleTabClick = (tabId: string, isCreate?: boolean) => {
+14 -12
View File
@@ -1,4 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
@@ -17,6 +18,7 @@ interface TaskCreationModalProps {
} }
export default function TaskCreationModal({ isOpen, onClose, onSave }: TaskCreationModalProps) { export default function TaskCreationModal({ isOpen, onClose, onSave }: TaskCreationModalProps) {
const { t } = useTranslation();
const [title, setTitle] = useState(''); const [title, setTitle] = useState('');
const [description, setDescription] = useState(''); const [description, setDescription] = useState('');
const [priority, setPriority] = useState<'low' | 'medium' | 'high'>('medium'); const [priority, setPriority] = useState<'low' | 'medium' | 'high'>('medium');
@@ -77,14 +79,14 @@ export default function TaskCreationModal({ isOpen, onClose, onSave }: TaskCreat
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center gap-2"> <DialogTitle className="flex items-center gap-2">
<Plus className="w-4 h-4" /> <Plus className="w-4 h-4" />
New Task {t('taskCreation.title')}
</DialogTitle> </DialogTitle>
</DialogHeader> </DialogHeader>
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<Input <Input
placeholder="What needs to be done?" placeholder={t('taskCreation.titlePlaceholder')}
value={title} value={title}
onChange={(e) => setTitle(e.target.value)} onChange={(e) => setTitle(e.target.value)}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
@@ -96,7 +98,7 @@ export default function TaskCreationModal({ isOpen, onClose, onSave }: TaskCreat
<div> <div>
<Textarea <Textarea
placeholder="Add a description (optional)" placeholder={t('taskCreation.descriptionPlaceholder')}
value={description} value={description}
onChange={(e) => setDescription(e.target.value)} onChange={(e) => setDescription(e.target.value)}
className="resize-none text-sm" className="resize-none text-sm"
@@ -109,12 +111,12 @@ export default function TaskCreationModal({ isOpen, onClose, onSave }: TaskCreat
<div className="flex-1"> <div className="flex-1">
<Select value={priority} onValueChange={(value: 'low' | 'medium' | 'high') => setPriority(value)}> <Select value={priority} onValueChange={(value: 'low' | 'medium' | 'high') => setPriority(value)}>
<SelectTrigger data-testid="select-task-priority"> <SelectTrigger data-testid="select-task-priority">
<SelectValue placeholder="Priority" /> <SelectValue placeholder={t('taskCreation.priority')} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="low">Low</SelectItem> <SelectItem value="low">{t('priority.low')}</SelectItem>
<SelectItem value="medium">Medium</SelectItem> <SelectItem value="medium">{t('priority.medium')}</SelectItem>
<SelectItem value="high">High</SelectItem> <SelectItem value="high">{t('priority.high')}</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
@@ -122,10 +124,10 @@ export default function TaskCreationModal({ isOpen, onClose, onSave }: TaskCreat
<div className="flex-1"> <div className="flex-1">
<Select value={labelId || 'none'} onValueChange={(value) => setLabelId(value === 'none' ? undefined : value)}> <Select value={labelId || 'none'} onValueChange={(value) => setLabelId(value === 'none' ? undefined : value)}>
<SelectTrigger data-testid="select-task-label"> <SelectTrigger data-testid="select-task-label">
<SelectValue placeholder="Label" /> <SelectValue placeholder={t('taskCreation.label')} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="none">No Label</SelectItem> <SelectItem value="none">{t('taskCreation.noLabel')}</SelectItem>
{labels.map((label) => ( {labels.map((label) => (
<SelectItem key={label.id} value={label.id}> <SelectItem key={label.id} value={label.id}>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -149,7 +151,7 @@ export default function TaskCreationModal({ isOpen, onClose, onSave }: TaskCreat
data-testid="button-due-date" data-testid="button-due-date"
> >
<CalendarIcon className="w-4 h-4" /> <CalendarIcon className="w-4 h-4" />
{dueDate ? dueDate.toLocaleDateString() : 'Due date'} {dueDate ? dueDate.toLocaleDateString() : t('taskCreation.dueDate')}
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-auto p-0" align="end"> <PopoverContent className="w-auto p-0" align="end">
@@ -174,7 +176,7 @@ export default function TaskCreationModal({ isOpen, onClose, onSave }: TaskCreat
className="flex-1" className="flex-1"
data-testid="button-cancel" data-testid="button-cancel"
> >
Cancel {t('taskCreation.cancel')}
</Button> </Button>
<Button <Button
onClick={handleSave} onClick={handleSave}
@@ -182,7 +184,7 @@ export default function TaskCreationModal({ isOpen, onClose, onSave }: TaskCreat
className="flex-1" className="flex-1"
data-testid="button-save-task" data-testid="button-save-task"
> >
Create Task {t('taskCreation.create')}
</Button> </Button>
</div> </div>
</div> </div>
+16 -14
View File
@@ -1,4 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
@@ -17,6 +18,7 @@ type SortOption = 'dueDate' | 'priority' | 'title' | 'status';
type FilterOption = 'all' | 'todo' | 'inProgress' | 'done' | 'overdue'; type FilterOption = 'all' | 'todo' | 'inProgress' | 'done' | 'overdue';
export default function TaskList({ tasks, onTaskUpdate, onTaskEdit }: TaskListProps) { export default function TaskList({ tasks, onTaskUpdate, onTaskEdit }: TaskListProps) {
const { t } = useTranslation();
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [sortBy, setSortBy] = useState<SortOption>('dueDate'); const [sortBy, setSortBy] = useState<SortOption>('dueDate');
const [filterBy, setFilterBy] = useState<FilterOption>('all'); const [filterBy, setFilterBy] = useState<FilterOption>('all');
@@ -84,10 +86,10 @@ export default function TaskList({ tasks, onTaskUpdate, onTaskEdit }: TaskListPr
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h2 className="text-lg font-semibold" data-testid="text-task-list-title"> <h2 className="text-lg font-semibold" data-testid="text-task-list-title">
My Tasks {t('taskList.title')}
</h2> </h2>
<Badge variant="secondary" data-testid="badge-task-count"> <Badge variant="secondary" data-testid="badge-task-count">
{filteredAndSortedTasks.length} tasks {t('taskList.taskCount', { count: filteredAndSortedTasks.length })}
</Badge> </Badge>
</div> </div>
@@ -96,7 +98,7 @@ export default function TaskList({ tasks, onTaskUpdate, onTaskEdit }: TaskListPr
<div className="relative"> <div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" /> <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input <Input
placeholder="Search tasks..." placeholder={t('taskList.searchPlaceholder')}
value={searchQuery} value={searchQuery}
onChange={(e) => { onChange={(e) => {
setSearchQuery(e.target.value); setSearchQuery(e.target.value);
@@ -119,11 +121,11 @@ export default function TaskList({ tasks, onTaskUpdate, onTaskEdit }: TaskListPr
</div> </div>
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="all">All ({getFilterCount('all')})</SelectItem> <SelectItem value="all">{t('taskList.filter.all')} ({getFilterCount('all')})</SelectItem>
<SelectItem value="todo">To Do ({getFilterCount('todo')})</SelectItem> <SelectItem value="todo">{t('taskList.filter.todo')} ({getFilterCount('todo')})</SelectItem>
<SelectItem value="inProgress">In Progress ({getFilterCount('inProgress')})</SelectItem> <SelectItem value="inProgress">{t('taskList.filter.inProgress')} ({getFilterCount('inProgress')})</SelectItem>
<SelectItem value="done">Done ({getFilterCount('done')})</SelectItem> <SelectItem value="done">{t('taskList.filter.done')} ({getFilterCount('done')})</SelectItem>
<SelectItem value="overdue">Overdue ({getFilterCount('overdue')})</SelectItem> <SelectItem value="overdue">{t('taskList.filter.overdue')} ({getFilterCount('overdue')})</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
@@ -138,10 +140,10 @@ export default function TaskList({ tasks, onTaskUpdate, onTaskEdit }: TaskListPr
</div> </div>
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="dueDate">Due Date</SelectItem> <SelectItem value="dueDate">{t('taskList.sort.dueDate')}</SelectItem>
<SelectItem value="priority">Priority</SelectItem> <SelectItem value="priority">{t('taskList.sort.priority')}</SelectItem>
<SelectItem value="title">Title</SelectItem> <SelectItem value="title">{t('taskList.sort.title')}</SelectItem>
<SelectItem value="status">Status</SelectItem> <SelectItem value="status">{t('taskList.sort.status')}</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
@@ -153,8 +155,8 @@ export default function TaskList({ tasks, onTaskUpdate, onTaskEdit }: TaskListPr
<div className="text-center py-8"> <div className="text-center py-8">
<p className="text-muted-foreground" data-testid="text-no-tasks"> <p className="text-muted-foreground" data-testid="text-no-tasks">
{searchQuery || filterBy !== 'all' {searchQuery || filterBy !== 'all'
? 'No tasks match your filters' ? t('taskList.noMatchingTasks')
: 'No tasks yet. Create your first task!' : t('taskList.noTasks')
} }
</p> </p>
</div> </div>