setIsFilterDropdownOpen(false)}
+ />
+ )}
+
+
+ );
+}
diff --git a/components/delete-confirm-modal.tsx b/components/delete-confirm-modal.tsx
index 206c80b..de5ac4d 100644
--- a/components/delete-confirm-modal.tsx
+++ b/components/delete-confirm-modal.tsx
@@ -1,45 +1,48 @@
-"use client"
-
-import { X } from "lucide-react"
-
-interface DeleteConfirmModalProps {
- onClose: () => void
- onConfirm: () => void
- taskTitle: string
-}
-
-export default function DeleteConfirmModal({ onClose, onConfirm, taskTitle }: DeleteConfirmModalProps) {
- return (
-
-
-
-
Delete Task
-
-
-
-
-
- Are you sure you want to delete the task {taskTitle}?
This action cannot be undone.
-
-
-
-
-
-
-
-
-
- )
-}
+"use client"
+
+import { X } from "lucide-react"
+import { useTranslations } from "next-intl"
+
+interface DeleteConfirmModalProps {
+ onClose: () => void
+ onConfirm: () => void
+ taskTitle: string
+}
+
+export default function DeleteConfirmModal({ onClose, onConfirm, taskTitle }: DeleteConfirmModalProps) {
+ const t = useTranslations('DeleteModal')
+
+ return (
+
+
+
+
{t('title')}
+
+
+
+
+
+ {t('confirmMessage')} {taskTitle}?
{t('warningText')}
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/components/info-section.tsx b/components/info-section.tsx
index e1b9f7f..966da9a 100644
--- a/components/info-section.tsx
+++ b/components/info-section.tsx
@@ -1,166 +1,168 @@
-"use client"
-
-import { useState, useEffect } from "react"
-import { Moon, Sun, Clock } from "lucide-react"
-import { useTheme } from "./theme-provider"
-import TimezoneModal from "./timezone-modal"
-
-interface TimezoneInfo {
- id: string
- timezone: string
- label: string
-}
-
-export default function InfoSection() {
- const [currentTime, setCurrentTime] = useState(new Date())
- const [timezones, setTimezones] = useState
([
- {
- id: "1",
- timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
- label: Intl.DateTimeFormat().resolvedOptions().timeZone.split("/")[1] || "Local",
- },
- ])
- const [isTimezoneModalOpen, setIsTimezoneModalOpen] = useState(false)
- const { theme, toggleTheme } = useTheme()
-
- useEffect(() => {
- const timer = setInterval(() => {
- setCurrentTime(new Date())
- }, 1000)
-
- // Load timezones from localStorage
- const storedTimezones = localStorage.getItem("timezones")
- if (storedTimezones) {
- setTimezones(JSON.parse(storedTimezones))
- }
-
- return () => clearInterval(timer)
- }, [])
-
- // Save timezones to localStorage whenever they change
- useEffect(() => {
- localStorage.setItem("timezones", JSON.stringify(timezones))
- }, [timezones])
-
- const formatTime = (date: Date, timezone: string) => {
- return date.toLocaleTimeString([], {
- hour: "2-digit",
- minute: "2-digit",
- timeZone: timezone,
- })
- }
-
- const formatDay = (date: Date, timezone: string) => {
- return date.toLocaleDateString([], {
- weekday: "long",
- timeZone: timezone,
- })
- }
-
- const formatDateOnly = (date: Date, timezone: string) => {
- return date.toLocaleDateString([], {
- month: "short",
- day: "numeric",
- timeZone: timezone,
- })
- }
-
- const updateTimezones = (newTimezones: TimezoneInfo[]) => {
- setTimezones(newTimezones)
- setIsTimezoneModalOpen(false)
- }
-
- return (
- <>
-
-
-
Time
-
-
-
-
-
-
-
- {timezones.slice(0, 5).map((tz, index) => (
-
0 ? "border-t border-gray-100 dark:border-[#2D2D2D]" : ""}`}>
-
-
Time
-
- {formatTime(currentTime, tz.timezone)}
-
-
-
-
Day
-
- {formatDay(currentTime, tz.timezone)}
-
-
-
-
Date
-
- {formatDateOnly(currentTime, tz.timezone)}
-
-
-
-
Timezone
-
{tz.label}
-
-
- ))}
-
- {timezones.length > 5 && (
-
- {timezones.slice(5).map((tz, index) => (
-
-
-
Time
-
- {formatTime(currentTime, tz.timezone)}
-
-
-
-
Day
-
- {formatDay(currentTime, tz.timezone)}
-
-
-
-
Date
-
- {formatDateOnly(currentTime, tz.timezone)}
-
-
-
-
Timezone
-
{tz.label}
-
-
- ))}
-
- )}
-
-
-
- {isTimezoneModalOpen && (
- setIsTimezoneModalOpen(false)}
- onUpdate={updateTimezones}
- currentTimezones={timezones}
- />
- )}
- >
- )
-}
+"use client"
+
+import { useState, useEffect } from "react"
+import { Moon, Sun, Clock } from "lucide-react"
+import { useTheme } from "./theme-provider"
+import TimezoneModal from "./timezone-modal"
+import { useTranslations } from "next-intl"
+
+interface TimezoneInfo {
+ id: string
+ timezone: string
+ label: string
+}
+
+export default function InfoSection() {
+ const t = useTranslations('InfoSection')
+ const [currentTime, setCurrentTime] = useState(new Date())
+ const [timezones, setTimezones] = useState([
+ {
+ id: "1",
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
+ label: Intl.DateTimeFormat().resolvedOptions().timeZone.split("/")[1] || "Local",
+ },
+ ])
+ const [isTimezoneModalOpen, setIsTimezoneModalOpen] = useState(false)
+ const { theme, toggleTheme } = useTheme()
+
+ useEffect(() => {
+ const timer = setInterval(() => {
+ setCurrentTime(new Date())
+ }, 1000)
+
+ // Load timezones from localStorage
+ const storedTimezones = localStorage.getItem("timezones")
+ if (storedTimezones) {
+ setTimezones(JSON.parse(storedTimezones))
+ }
+
+ return () => clearInterval(timer)
+ }, [])
+
+ // Save timezones to localStorage whenever they change
+ useEffect(() => {
+ localStorage.setItem("timezones", JSON.stringify(timezones))
+ }, [timezones])
+
+ const formatTime = (date: Date, timezone: string) => {
+ return date.toLocaleTimeString([], {
+ hour: "2-digit",
+ minute: "2-digit",
+ timeZone: timezone,
+ })
+ }
+
+ const formatDay = (date: Date, timezone: string) => {
+ return date.toLocaleDateString([], {
+ weekday: "long",
+ timeZone: timezone,
+ })
+ }
+
+ const formatDateOnly = (date: Date, timezone: string) => {
+ return date.toLocaleDateString([], {
+ month: "short",
+ day: "numeric",
+ timeZone: timezone,
+ })
+ }
+
+ const updateTimezones = (newTimezones: TimezoneInfo[]) => {
+ setTimezones(newTimezones)
+ setIsTimezoneModalOpen(false)
+ }
+
+ return (
+ <>
+
+
+
{t('timeLabel')}
+
+
+
+
+
+
+
+ {timezones.slice(0, 5).map((tz, index) => (
+
0 ? "border-t border-gray-100 dark:border-[#2D2D2D]" : ""}`}>
+
+
{t('timeLabel')}
+
+ {formatTime(currentTime, tz.timezone)}
+
+
+
+
{t('dayLabel')}
+
+ {formatDay(currentTime, tz.timezone)}
+
+
+
+
{t('dateLabel')}
+
+ {formatDateOnly(currentTime, tz.timezone)}
+
+
+
+
{t('timezoneLabel')}
+
{tz.label}
+
+
+ ))}
+
+ {timezones.length > 5 && (
+
+ {timezones.slice(5).map((tz, index) => (
+
+
+
{t('timeLabel')}
+
+ {formatTime(currentTime, tz.timezone)}
+
+
+
+
{t('dayLabel')}
+
+ {formatDay(currentTime, tz.timezone)}
+
+
+
+
{t('dateLabel')}
+
+ {formatDateOnly(currentTime, tz.timezone)}
+
+
+
+
{t('timezoneLabel')}
+
{tz.label}
+
+
+ ))}
+
+ )}
+
+
+
+ {isTimezoneModalOpen && (
+ setIsTimezoneModalOpen(false)}
+ onUpdate={updateTimezones}
+ currentTimezones={timezones}
+ />
+ )}
+ >
+ )
+}
diff --git a/components/locale-provider.tsx b/components/locale-provider.tsx
new file mode 100644
index 0000000..49e8c93
--- /dev/null
+++ b/components/locale-provider.tsx
@@ -0,0 +1,30 @@
+"use client";
+
+import { NextIntlClientProvider } from "next-intl";
+import { useState, useEffect, ReactNode } from "react";
+import { getUserLocale } from "@/services/locale";
+import { Locale } from "@/i18n/config";
+
+interface LocaleProviderProps {
+ children: ReactNode;
+ messages: Record;
+}
+
+export function LocaleProvider({ children, messages }: LocaleProviderProps) {
+ const [locale, setLocale] = useState("en");
+
+ useEffect(() => {
+ // Set initial locale based on user preference/browser
+ const userLocale = getUserLocale();
+ setLocale(userLocale);
+ }, []);
+
+ return (
+
+ {children}
+
+ );
+}
\ No newline at end of file
diff --git a/components/task-modal.tsx b/components/task-modal.tsx
index 9cfdd6f..a68d839 100644
--- a/components/task-modal.tsx
+++ b/components/task-modal.tsx
@@ -1,114 +1,116 @@
-"use client"
-
-import type React from "react"
-
-import { useState, useEffect } from "react"
-import { X } from "lucide-react"
-import type { Task } from "@/lib/types"
-
-interface TaskModalProps {
- onClose: () => void
- onAdd: (task: Task) => void
- onUpdate: (task: Task) => void
- editingTask?: Task | null
-}
-
-export default function TaskModal({ onClose, onAdd, onUpdate, editingTask }: TaskModalProps) {
- const [title, setTitle] = useState("")
- const [deadline, setDeadline] = useState("")
-
- useEffect(() => {
- if (editingTask) {
- setTitle(editingTask.title)
- // Convert ISO string to datetime-local format
- const date = new Date(editingTask.deadline)
- const localDateTime = new Date(date.getTime() - date.getTimezoneOffset() * 60000).toISOString().slice(0, 16)
- setDeadline(localDateTime)
- }
- }, [editingTask])
-
- const handleSubmit = (e: React.FormEvent) => {
- e.preventDefault()
- if (!title.trim() || !deadline) return
-
- if (editingTask) {
- const updatedTask: Task = {
- ...editingTask,
- title: title.trim(),
- deadline: new Date(deadline).toISOString(),
- }
- onUpdate(updatedTask)
- } else {
- const newTask: Task = {
- id: Date.now().toString(),
- title: title.trim(),
- deadline: new Date(deadline).toISOString(),
- completed: false,
- createdAt: new Date().toISOString(),
- }
- onAdd(newTask)
- }
- }
-
- return (
-
-
-
-
- {editingTask ? "Edit Task" : "Add New Task"}
-
-
-
-
-
-
-
- )
-}
+"use client"
+
+import type React from "react"
+
+import { useState, useEffect } from "react"
+import { X } from "lucide-react"
+import { useTranslations } from "next-intl"
+import type { Task } from "@/lib/types"
+
+interface TaskModalProps {
+ onClose: () => void
+ onAdd: (task: Task) => void
+ onUpdate: (task: Task) => void
+ editingTask?: Task | null
+}
+
+export default function TaskModal({ onClose, onAdd, onUpdate, editingTask }: TaskModalProps) {
+ const t = useTranslations('TaskModal')
+ const [title, setTitle] = useState("")
+ const [deadline, setDeadline] = useState("")
+
+ useEffect(() => {
+ if (editingTask) {
+ setTitle(editingTask.title)
+ // Convert ISO string to datetime-local format
+ const date = new Date(editingTask.deadline)
+ const localDateTime = new Date(date.getTime() - date.getTimezoneOffset() * 60000).toISOString().slice(0, 16)
+ setDeadline(localDateTime)
+ }
+ }, [editingTask])
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault()
+ if (!title.trim() || !deadline) return
+
+ if (editingTask) {
+ const updatedTask: Task = {
+ ...editingTask,
+ title: title.trim(),
+ deadline: new Date(deadline).toISOString(),
+ }
+ onUpdate(updatedTask)
+ } else {
+ const newTask: Task = {
+ id: Date.now().toString(),
+ title: title.trim(),
+ deadline: new Date(deadline).toISOString(),
+ completed: false,
+ createdAt: new Date().toISOString(),
+ }
+ onAdd(newTask)
+ }
+ }
+
+ return (
+
+
+
+
+ {editingTask ? t('editTaskTitle') : t('addTaskTitle')}
+
+
+
+
+
+
+
+ )
+}
diff --git a/components/task-stats.tsx b/components/task-stats.tsx
index 1d0b67b..78b6c27 100644
--- a/components/task-stats.tsx
+++ b/components/task-stats.tsx
@@ -1,68 +1,70 @@
-"use client"
-
-import { useMemo } from "react"
-import type { Task } from "@/lib/types"
-import { isOverdue } from "@/lib/utils"
-
-interface TaskStatsProps {
- tasks: Task[]
-}
-
-export default function TaskStats({ tasks }: TaskStatsProps) {
- const stats = useMemo(() => {
- const totalTasks = tasks.length
- const completedTasks = tasks.filter((task) => task.completed).length
- const notCompletedTasks = tasks.filter((task) => !task.completed).length
- const overdueTasks = tasks.filter((task) => !task.completed && isOverdue(task.deadline)).length
-
- return {
- totalTasks,
- completedTasks,
- notCompletedTasks,
- overdueTasks,
- }
- }, [tasks])
-
- return (
-
-
-
Statistics
-
-
-
- {[
- {
- label: "Overdue",
- value: stats.overdueTasks,
- color: "text-red-500 dark:text-red-400",
- },
- {
- label: "Not Completed",
- value: stats.notCompletedTasks,
- color: "text-orange-500 dark:text-yellow-400",
- },
- {
- label: "Completed",
- value: stats.completedTasks,
- color: "text-green-500 dark:text-green-400",
- },
- {
- label: "Total Tasks",
- value: stats.totalTasks,
- color: "text-gray-800 dark:text-[#DEE2E2]",
- },
- ].map(({ label, value, color }, idx) => (
-
- ))}
-
-
- )
-}
+"use client"
+
+import { useMemo } from "react"
+import { useTranslations } from "next-intl"
+import type { Task } from "@/lib/types"
+import { isOverdue } from "@/lib/utils"
+
+interface TaskStatsProps {
+ tasks: Task[]
+}
+
+export default function TaskStats({ tasks }: TaskStatsProps) {
+ const t = useTranslations('TaskStats')
+ const stats = useMemo(() => {
+ const totalTasks = tasks.length
+ const completedTasks = tasks.filter((task) => task.completed).length
+ const notCompletedTasks = tasks.filter((task) => !task.completed).length
+ const overdueTasks = tasks.filter((task) => !task.completed && isOverdue(task.deadline)).length
+
+ return {
+ totalTasks,
+ completedTasks,
+ notCompletedTasks,
+ overdueTasks,
+ }
+ }, [tasks])
+
+ return (
+
+
+
{t('title')}
+
+
+
+ {[
+ {
+ label: t('overdue'),
+ value: stats.overdueTasks,
+ color: "text-red-500 dark:text-red-400",
+ },
+ {
+ label: t('notCompleted'),
+ value: stats.notCompletedTasks,
+ color: "text-orange-500 dark:text-yellow-400",
+ },
+ {
+ label: t('completed'),
+ value: stats.completedTasks,
+ color: "text-green-500 dark:text-green-400",
+ },
+ {
+ label: t('totalTasks'),
+ value: stats.totalTasks,
+ color: "text-gray-800 dark:text-[#DEE2E2]",
+ },
+ ].map(({ label, value, color }, idx) => (
+
+ ))}
+
+
+ )
+}
diff --git a/components/timezone-modal.tsx b/components/timezone-modal.tsx
index 7e103e7..8f75dcb 100644
--- a/components/timezone-modal.tsx
+++ b/components/timezone-modal.tsx
@@ -1,206 +1,208 @@
-"use client"
-
-import { useState } from "react"
-import { X, Plus, Trash2, ChevronUp, ChevronDown } from "lucide-react"
-
-interface TimezoneInfo {
- id: string
- timezone: string
- label: string
-}
-
-interface TimezoneModalProps {
- onClose: () => void
- onUpdate: (timezones: TimezoneInfo[]) => void
- currentTimezones: TimezoneInfo[]
-}
-
-const COMMON_TIMEZONES = [
- { value: "America/New_York", label: "New York (EST/EDT)" },
- { value: "America/Los_Angeles", label: "Los Angeles (PST/PDT)" },
- { value: "America/Chicago", label: "Chicago (CST/CDT)" },
- { value: "America/Denver", label: "Denver (MST/MDT)" },
- { value: "Europe/London", label: "London (GMT/BST)" },
- { value: "Europe/Paris", label: "Paris (CET/CEST)" },
- { value: "Europe/Berlin", label: "Berlin (CET/CEST)" },
- { value: "Europe/Rome", label: "Rome (CET/CEST)" },
- { value: "Asia/Tokyo", label: "Tokyo (JST)" },
- { value: "Asia/Shanghai", label: "Shanghai (CST)" },
- { value: "Asia/Kolkata", label: "Mumbai (IST)" },
- { value: "Asia/Dubai", label: "Dubai (GST)" },
- { value: "Australia/Sydney", label: "Sydney (AEST/AEDT)" },
- { value: "Pacific/Auckland", label: "Auckland (NZST/NZDT)" },
- { value: "America/Sao_Paulo", label: "São Paulo (BRT)" },
- { value: "Africa/Cairo", label: "Cairo (EET)" },
-]
-
-export default function TimezoneModal({ onClose, onUpdate, currentTimezones }: TimezoneModalProps) {
- const [timezones, setTimezones] = useState(currentTimezones)
- const [selectedTimezone, setSelectedTimezone] = useState("")
-
- const addTimezone = () => {
- if (!selectedTimezone || timezones.length >= 10) return
- if (timezones.some((tz) => tz.timezone === selectedTimezone)) return
-
- const timezoneData = COMMON_TIMEZONES.find((tz) => tz.value === selectedTimezone)
- if (!timezoneData) return
-
- const newTimezone: TimezoneInfo = {
- id: Date.now().toString(),
- timezone: selectedTimezone,
- label: timezoneData.label.split(" (")[0],
- }
-
- setTimezones([...timezones, newTimezone])
- setSelectedTimezone("")
- }
-
- const removeTimezone = (id: string) => {
- if (timezones.length <= 1) return
- setTimezones(timezones.filter((tz) => tz.id !== id))
- }
-
- const moveTimezone = (index: number, direction: "up" | "down") => {
- if ((direction === "up" && index === 0) || (direction === "down" && index === timezones.length - 1)) return
-
- const newTimezones = [...timezones]
- const targetIndex = direction === "up" ? index - 1 : index + 1
- const temp = newTimezones[index]
- newTimezones[index] = newTimezones[targetIndex]
- newTimezones[targetIndex] = temp
-
- setTimezones(newTimezones)
- }
-
- const handleSubmit = () => {
- onUpdate(timezones)
- }
-
- const availableTimezones = COMMON_TIMEZONES.filter(
- (tz) => !timezones.some((existing) => existing.timezone === tz.value),
- )
-
- return (
-
-
-
-
Manage Timezones
-
-
-
-
- {/* Current Timezones */}
-
-
- Current Timezones ({timezones.length}/10)
-
-
- {timezones.map((tz, index) => (
-
-
- {timezones.length > 1 && (
-
-
-
-
- )}
-
-
{tz.label}
-
{tz.timezone}
-
-
- {timezones.length > 1 && (
-
- )}
-
- ))}
-
-
-
- {/* Add New Timezone */}
- {timezones.length < 10 && availableTimezones.length > 0 && (
-
-
Add Timezone
-
-
-
-
-
-
-
-
- )}
-
-
-
-
-
-
-
-
- )
-}
+"use client"
+
+import { useState } from "react"
+import { X, Plus, Trash2, ChevronUp, ChevronDown } from "lucide-react"
+import { useTranslations } from "next-intl"
+
+interface TimezoneInfo {
+ id: string
+ timezone: string
+ label: string
+}
+
+interface TimezoneModalProps {
+ onClose: () => void
+ onUpdate: (timezones: TimezoneInfo[]) => void
+ currentTimezones: TimezoneInfo[]
+}
+
+const COMMON_TIMEZONES = [
+ { value: "America/New_York", label: "New York (EST/EDT)" },
+ { value: "America/Los_Angeles", label: "Los Angeles (PST/PDT)" },
+ { value: "America/Chicago", label: "Chicago (CST/CDT)" },
+ { value: "America/Denver", label: "Denver (MST/MDT)" },
+ { value: "Europe/London", label: "London (GMT/BST)" },
+ { value: "Europe/Paris", label: "Paris (CET/CEST)" },
+ { value: "Europe/Berlin", label: "Berlin (CET/CEST)" },
+ { value: "Europe/Rome", label: "Rome (CET/CEST)" },
+ { value: "Asia/Tokyo", label: "Tokyo (JST)" },
+ { value: "Asia/Shanghai", label: "Shanghai (CST)" },
+ { value: "Asia/Kolkata", label: "Mumbai (IST)" },
+ { value: "Asia/Dubai", label: "Dubai (GST)" },
+ { value: "Australia/Sydney", label: "Sydney (AEST/AEDT)" },
+ { value: "Pacific/Auckland", label: "Auckland (NZST/NZDT)" },
+ { value: "America/Sao_Paulo", label: "São Paulo (BRT)" },
+ { value: "Africa/Cairo", label: "Cairo (EET)" },
+]
+
+export default function TimezoneModal({ onClose, onUpdate, currentTimezones }: TimezoneModalProps) {
+ const t = useTranslations('TimezoneModal')
+ const [timezones, setTimezones] = useState(currentTimezones)
+ const [selectedTimezone, setSelectedTimezone] = useState("")
+
+ const addTimezone = () => {
+ if (!selectedTimezone || timezones.length >= 10) return
+ if (timezones.some((tz) => tz.timezone === selectedTimezone)) return
+
+ const timezoneData = COMMON_TIMEZONES.find((tz) => tz.value === selectedTimezone)
+ if (!timezoneData) return
+
+ const newTimezone: TimezoneInfo = {
+ id: Date.now().toString(),
+ timezone: selectedTimezone,
+ label: timezoneData.label.split(" (")[0],
+ }
+
+ setTimezones([...timezones, newTimezone])
+ setSelectedTimezone("")
+ }
+
+ const removeTimezone = (id: string) => {
+ if (timezones.length <= 1) return
+ setTimezones(timezones.filter((tz) => tz.id !== id))
+ }
+
+ const moveTimezone = (index: number, direction: "up" | "down") => {
+ if ((direction === "up" && index === 0) || (direction === "down" && index === timezones.length - 1)) return
+
+ const newTimezones = [...timezones]
+ const targetIndex = direction === "up" ? index - 1 : index + 1
+ const temp = newTimezones[index]
+ newTimezones[index] = newTimezones[targetIndex]
+ newTimezones[targetIndex] = temp
+
+ setTimezones(newTimezones)
+ }
+
+ const handleSubmit = () => {
+ onUpdate(timezones)
+ }
+
+ const availableTimezones = COMMON_TIMEZONES.filter(
+ (tz) => !timezones.some((existing) => existing.timezone === tz.value),
+ )
+
+ return (
+
+
+
+
{t('title')}
+
+
+
+
+ {/* Current Timezones */}
+
+
+ {t('currentTimezonesLabel')} ({timezones.length}/10)
+
+
+ {timezones.map((tz, index) => (
+
+
+ {timezones.length > 1 && (
+
+
+
+
+ )}
+
+
{tz.label}
+
{tz.timezone}
+
+
+ {timezones.length > 1 && (
+
+ )}
+
+ ))}
+
+
+
+ {/* Add New Timezone */}
+ {timezones.length < 10 && availableTimezones.length > 0 && (
+
+
{t('addTimezoneLabel')}
+
+
+
+
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/components/user-greeting.tsx b/components/user-greeting.tsx
index eb45ef2..9d012f2 100644
--- a/components/user-greeting.tsx
+++ b/components/user-greeting.tsx
@@ -1,34 +1,38 @@
-"use client"
-
-import { useState } from "react"
-
-interface UserGreetingProps {
- userName: string
- isReturningUser: boolean
- onEditClick: () => void
-}
-
-export default function UserGreeting({ userName, isReturningUser, onEditClick }: UserGreetingProps) {
- const [isHovered, setIsHovered] = useState(false)
-
- const displayName = userName || "Human"
- const greeting = isReturningUser ? `Welcome back, ${displayName}` : `Hello, ${displayName}`
-
- return (
-
-
setIsHovered(true)}
- onMouseLeave={() => setIsHovered(false)}
- onClick={onEditClick}
- >
- {greeting}
- {isHovered && (
-
- )}
-
-
- )
-}
+"use client"
+
+import { useState } from "react"
+import { useTranslations } from "next-intl"
+
+interface UserGreetingProps {
+ userName: string
+ isReturningUser: boolean
+ onEditClick: () => void
+}
+
+export default function UserGreeting({ userName, isReturningUser, onEditClick }: UserGreetingProps) {
+ const t = useTranslations('UserGreeting')
+ const [isHovered, setIsHovered] = useState(false)
+
+ const displayName = userName || t('defaultName')
+ const greeting = isReturningUser
+ ? t('welcomeBack', { name: displayName })
+ : t('hello', { name: displayName })
+
+ return (
+
+
setIsHovered(true)}
+ onMouseLeave={() => setIsHovered(false)}
+ onClick={onEditClick}
+ >
+ {greeting}
+ {isHovered && (
+
+ )}
+
+
+ )
+}
diff --git a/components/user-name-modal.tsx b/components/user-name-modal.tsx
index 9aca932..2b49fe3 100644
--- a/components/user-name-modal.tsx
+++ b/components/user-name-modal.tsx
@@ -1,72 +1,74 @@
-"use client"
-
-import type React from "react"
-import { useState, useEffect } from "react"
-import { X } from "lucide-react"
-
-interface UserNameModalProps {
- onClose: () => void
- onUpdate: (name: string) => void
- currentName: string
-}
-
-export default function UserNameModal({ onClose, onUpdate, currentName }: UserNameModalProps) {
- const [name, setName] = useState(currentName)
-
- useEffect(() => {
- setName(currentName)
- }, [currentName])
-
- const handleSubmit = (e: React.FormEvent) => {
- e.preventDefault()
- if (!name.trim()) return
-
- onUpdate(name.trim())
- }
-
- return (
-
-
-
-
Edit Your Name
-
-
-
-
-
-
- )
-}
+"use client"
+
+import type React from "react"
+import { useState, useEffect } from "react"
+import { X } from "lucide-react"
+import { useTranslations } from "next-intl"
+
+interface UserNameModalProps {
+ onClose: () => void
+ onUpdate: (name: string) => void
+ currentName: string
+}
+
+export default function UserNameModal({ onClose, onUpdate, currentName }: UserNameModalProps) {
+ const t = useTranslations('UserNameModal')
+ const [name, setName] = useState(currentName)
+
+ useEffect(() => {
+ setName(currentName)
+ }, [currentName])
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault()
+ if (!name.trim()) return
+
+ onUpdate(name.trim())
+ }
+
+ return (
+
+
+
+
{t('title')}
+
+
+
+
+
+
+ )
+}
diff --git a/i18n/config.ts b/i18n/config.ts
new file mode 100644
index 0000000..5b7dfbc
--- /dev/null
+++ b/i18n/config.ts
@@ -0,0 +1,5 @@
+export type Locale = "en" | "de";
+
+export const locales: readonly Locale[] = ["en", "de"];
+
+export const defaultLocale: Locale = "en";
\ No newline at end of file
diff --git a/i18n/request.ts b/i18n/request.ts
new file mode 100644
index 0000000..07ee7f3
--- /dev/null
+++ b/i18n/request.ts
@@ -0,0 +1,17 @@
+import { getRequestConfig } from "next-intl/server";
+
+export default getRequestConfig(async () => {
+ // Load all messages for client-side switching
+ const [enMessages, deMessages] = await Promise.all([
+ import(`../messages/en.json`).then(m => m.default),
+ import(`../messages/de.json`).then(m => m.default)
+ ]);
+
+ return {
+ locale: "en", // Default locale
+ messages: {
+ en: enMessages,
+ de: deMessages
+ }
+ };
+});
diff --git a/messages/de.json b/messages/de.json
new file mode 100644
index 0000000..7051d84
--- /dev/null
+++ b/messages/de.json
@@ -0,0 +1,76 @@
+{
+ "Tasks": {
+ "title": "Aufgaben",
+ "searchPlaceholder": "Aufgaben suchen...",
+ "searchAriaLabel": "Aufgaben suchen",
+ "clearSearchAriaLabel": "Suche löschen",
+ "filterAriaLabel": "Aufgaben filtern",
+ "addTaskAriaLabel": "Aufgabe hinzufügen",
+ "editTaskAriaLabel": "Aufgabe bearbeiten",
+ "deleteTaskAriaLabel": "Aufgabe löschen",
+ "showCompleted": "Erledigte anzeigen",
+ "showNotCompleted": "Nicht erledigte anzeigen",
+ "showDueTasks": "Fällige Aufgaben anzeigen",
+ "clearFilter": "Filter löschen",
+ "noTasksFound": "Keine Aufgaben entsprechen deinen Kriterien.",
+ "noTasksYet": "Noch keine Aufgaben. Klicke auf das +",
+ "taskCompleted": "Aufgabe erledigt",
+ "taskNotCompleted": "Aufgabe nicht erledigt"
+ },
+ "DeleteModal": {
+ "title": "Aufgabe löschen",
+ "confirmMessage": "Bist du sicher, dass du die Aufgabe löschen möchtest",
+ "warningText": "Diese Aktion kann nicht rückgängig gemacht werden.",
+ "cancelButton": "Abbrechen",
+ "deleteButton": "Löschen"
+ },
+ "InfoSection": {
+ "timeLabel": "Zeit",
+ "dayLabel": "Tag",
+ "dateLabel": "Datum",
+ "timezoneLabel": "Zeitzone",
+ "manageTimezonesAriaLabel": "Zeitzonen verwalten",
+ "toggleThemeAriaLabel": "Thema umschalten"
+ },
+ "TaskModal": {
+ "editTaskTitle": "Aufgabe bearbeiten",
+ "addTaskTitle": "Neue Aufgabe hinzufügen",
+ "taskTitleLabel": "Titel",
+ "taskTitlePlaceholder": "Aufgabentitel eingeben",
+ "deadlineLabel": "Fälligkeitsdatum",
+ "cancelButton": "Abbrechen",
+ "updateTaskButton": "Aufgabe aktualisieren",
+ "addTaskButton": "Aufgabe hinzufügen"
+ },
+ "TaskStats": {
+ "title": "Statistiken",
+ "overdue": "Überfällig",
+ "notCompleted": "Nicht erledigt",
+ "completed": "Erledigt",
+ "totalTasks": "Alle Aufgaben"
+ },
+ "TimezoneModal": {
+ "title": "Zeitzonen verwalten",
+ "currentTimezonesLabel": "Aktuelle Zeitzonen",
+ "addTimezoneLabel": "Zeitzone hinzufügen",
+ "selectTimezonePlaceholder": "Zeitzone auswählen...",
+ "moveUpAriaLabel": "Nach oben verschieben",
+ "moveDownAriaLabel": "Nach unten verschieben",
+ "removeTimezoneAriaLabel": "Zeitzone entfernen",
+ "addTimezoneAriaLabel": "Zeitzone hinzufügen",
+ "cancelButton": "Abbrechen",
+ "saveChangesButton": "Änderungen speichern"
+ },
+ "UserGreeting": {
+ "defaultName": "Mensch",
+ "welcomeBack": "Willkommen zurück, {name}",
+ "hello": "Hallo, {name}"
+ },
+ "UserNameModal": {
+ "title": "Namen bearbeiten",
+ "nameLabel": "Name",
+ "namePlaceholder": "Namen eingeben",
+ "cancelButton": "Abbrechen",
+ "updateNameButton": "Namen aktualisieren"
+ }
+}
diff --git a/messages/en.json b/messages/en.json
new file mode 100644
index 0000000..59a497a
--- /dev/null
+++ b/messages/en.json
@@ -0,0 +1,76 @@
+{
+ "Tasks": {
+ "title": "Tasks",
+ "searchPlaceholder": "Search tasks...",
+ "searchAriaLabel": "Search tasks",
+ "clearSearchAriaLabel": "Clear search",
+ "filterAriaLabel": "Filter tasks",
+ "addTaskAriaLabel": "Add task",
+ "editTaskAriaLabel": "Edit task",
+ "deleteTaskAriaLabel": "Delete task",
+ "showCompleted": "Show completed",
+ "showNotCompleted": "Show not completed",
+ "showDueTasks": "Show due tasks",
+ "clearFilter": "Clear filter",
+ "noTasksFound": "No tasks match your criteria.",
+ "noTasksYet": "No tasks yet. Click + to add one.",
+ "taskCompleted": "Task completed",
+ "taskNotCompleted": "Task not completed"
+ },
+ "DeleteModal": {
+ "title": "Delete Task",
+ "confirmMessage": "Are you sure you want to delete the task",
+ "warningText": "This action cannot be undone.",
+ "cancelButton": "Cancel",
+ "deleteButton": "Delete"
+ },
+ "InfoSection": {
+ "timeLabel": "Time",
+ "dayLabel": "Day",
+ "dateLabel": "Date",
+ "timezoneLabel": "Timezone",
+ "manageTimezonesAriaLabel": "Manage timezones",
+ "toggleThemeAriaLabel": "Toggle theme"
+ },
+ "TaskModal": {
+ "editTaskTitle": "Edit Task",
+ "addTaskTitle": "Add New Task",
+ "taskTitleLabel": "Task Title",
+ "taskTitlePlaceholder": "Enter task title",
+ "deadlineLabel": "Deadline",
+ "cancelButton": "Cancel",
+ "updateTaskButton": "Update Task",
+ "addTaskButton": "Add Task"
+ },
+ "TaskStats": {
+ "title": "Statistics",
+ "overdue": "Overdue",
+ "notCompleted": "Not Completed",
+ "completed": "Completed",
+ "totalTasks": "Total Tasks"
+ },
+ "TimezoneModal": {
+ "title": "Manage Timezones",
+ "currentTimezonesLabel": "Current Timezones",
+ "addTimezoneLabel": "Add Timezone",
+ "selectTimezonePlaceholder": "Select a timezone...",
+ "moveUpAriaLabel": "Move up",
+ "moveDownAriaLabel": "Move down",
+ "removeTimezoneAriaLabel": "Remove timezone",
+ "addTimezoneAriaLabel": "Add timezone",
+ "cancelButton": "Cancel",
+ "saveChangesButton": "Save Changes"
+ },
+ "UserGreeting": {
+ "defaultName": "Human",
+ "welcomeBack": "Welcome back, {name}",
+ "hello": "Hello, {name}"
+ },
+ "UserNameModal": {
+ "title": "Edit Your Name",
+ "nameLabel": "Name",
+ "namePlaceholder": "Enter your name",
+ "cancelButton": "Cancel",
+ "updateNameButton": "Update Name"
+ }
+}
diff --git a/next.config.mjs b/next.config.mjs
index f2148d0..007fc76 100644
--- a/next.config.mjs
+++ b/next.config.mjs
@@ -1,25 +1,28 @@
-import nextPWA from 'next-pwa';
-
-/**
- * Initialize next-pwa with its options.
- * `dest: 'public'` will output your service-worker to public/sw.js
- */
-const withPWA = nextPWA({
- dest: 'public',
- register: true, // auto-register in the browser
- skipWaiting: true, // activate new SW as soon as it's ready
- disable: process.env.NODE_ENV === 'development',
-});
-
-/** @type {import('next').NextConfig} */
-const nextConfig = {
- output: 'export',
- typescript: {
- ignoreBuildErrors: true,
- },
- images: {
- unoptimized: true,
- },
-};
-
-export default withPWA(nextConfig);
+import nextPWA from "next-pwa";
+import createNextIntlPlugin from 'next-intl/plugin';
+
+/**
+ * Initialize next-pwa with its options.
+ * `dest: 'public'` will output your service-worker to public/sw.js
+ */
+const withPWA = nextPWA({
+ dest: "public",
+ register: true, // auto-register in the browser
+ skipWaiting: true, // activate new SW as soon as it's ready
+ disable: process.env.NODE_ENV === "development",
+});
+
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+ output: "export",
+ typescript: {
+ ignoreBuildErrors: true,
+ },
+ images: {
+ unoptimized: true,
+ },
+};
+
+const withNextIntl = createNextIntlPlugin('./i18n/request.ts');
+
+export default withPWA(withNextIntl(nextConfig));
diff --git a/package.json b/package.json
index 42ca3e0..4c8a8c4 100644
--- a/package.json
+++ b/package.json
@@ -46,6 +46,7 @@
"input-otp": "1.4.1",
"lucide-react": "^0.454.0",
"next": "15.2.4",
+ "next-intl": "^4.1.0",
"next-pwa": "^5.6.0",
"next-themes": "^0.4.4",
"react": "^19",
@@ -68,4 +69,4 @@
"tailwindcss": "^3.4.17",
"typescript": "^5"
}
-}
\ No newline at end of file
+}
diff --git a/services/locale.ts b/services/locale.ts
new file mode 100644
index 0000000..83fc1b3
--- /dev/null
+++ b/services/locale.ts
@@ -0,0 +1,31 @@
+"use client";
+
+import { Locale, defaultLocale } from "@/i18n/config";
+
+const STORAGE_KEY = "NEXT_LOCALE";
+
+export function getUserLocale(): Locale {
+ if (typeof window === "undefined") {
+ return defaultLocale;
+ }
+
+ // Try to get from localStorage first
+ const stored = localStorage.getItem(STORAGE_KEY) as Locale;
+ if (stored && ["en", "de"].includes(stored)) {
+ return stored;
+ }
+
+ // Fallback to browser language detection
+ const browserLang = navigator.language.toLowerCase();
+ if (browserLang.startsWith("de")) {
+ return "de";
+ }
+
+ return defaultLocale;
+}
+
+export function setUserLocale(locale: Locale) {
+ if (typeof window !== "undefined") {
+ localStorage.setItem(STORAGE_KEY, locale);
+ }
+}