feat: added FR and EN translation
Build and Deploy / build-and-push (push) Failing after 32s Details

This commit is contained in:
Ludo 2026-05-04 16:32:22 +02:00
parent 9f03e5bc45
commit 2172dbc483
8 changed files with 231 additions and 53 deletions

View File

@ -11,11 +11,36 @@ main {
}
}
.app-header {
display: flex;
align-items: center;
justify-content: space-between;
margin: 0 0 8px 0;
}
.lang-toggle {
background: none;
border: 1px solid #ccc;
border-radius: 4px;
padding: 4px 10px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
color: #555;
letter-spacing: 0.5px;
transition: border-color 0.15s, color 0.15s;
}
.lang-toggle:hover {
border-color: #111;
color: #111;
}
h1 {
font-size: 28px;
font-weight: 700;
color: #111;
margin: 0 0 8px 0;
margin: 0;
letter-spacing: -0.5px;
}
@ -33,14 +58,14 @@ h1 {
margin: 0 0 16px 0;
padding-bottom: 10px;
border-bottom: 1px solid #e8e8e8;
display: flex;
align-items: center;
user-select: none;
gap: 10px;
}
.section-title--clickable {
cursor: pointer;
display: flex;
align-items: center;
gap: 10px;
user-select: none;
}
.section-title--clickable:hover {

View File

@ -5,12 +5,12 @@ import TodaysTasks from './components/TodaysTasks'
import CreateTaskForm from './components/CreateTaskForm'
import type { Task, NewTaskInput } from './types'
import { isTaskDueToday } from './utils/taskUtils'
const APP_NAME = 'TODO'
import { useLanguage } from './i18n/LanguageContext'
type ApiStatus = 'loading' | 'ok' | 'error'
function App() {
const { t, toggleLocale } = useLanguage()
const [apiStatus, setApiStatus] = useState<ApiStatus>('loading')
const [tasks, setTasks] = useState<Task[]>([])
const [loading, setLoading] = useState(true)
@ -52,14 +52,14 @@ function App() {
if (!response.ok) {
const errorData = await response.json()
setError(errorData.error || 'Failed to create task')
setError(errorData.error || t.errorCreateTask)
return
}
await fetchTasks()
} catch (error) {
console.error('Error creating task:', error)
setError('Failed to create task')
setError(t.errorCreateTask)
}
}
@ -68,13 +68,13 @@ function App() {
try {
const response = await fetch(`/api/tasks/${taskId}`, { method: 'DELETE' })
if (!response.ok) {
setError('Failed to delete task')
setError(t.errorDeleteTask)
return
}
await fetchTasks()
} catch (error) {
console.error('Error deleting task:', error)
setError('Failed to delete task')
setError(t.errorDeleteTask)
}
}
@ -86,13 +86,13 @@ function App() {
})
if (!response.ok) {
setError('Failed to complete task')
setError(t.errorCompleteTask)
return
}
await fetchTasks()
} catch (error) {
console.error('Error completing task:', error)
setError('Failed to complete task')
setError(t.errorCompleteTask)
}
}
@ -100,12 +100,15 @@ function App() {
return (
<main>
<h1>{APP_NAME}</h1>
<div className="app-header">
<h1>{t.appName}</h1>
<button className="lang-toggle" onClick={toggleLocale}>{t.switchLang}</button>
</div>
<div className={`status status--${apiStatus}`}>
{apiStatus === 'loading' && 'Connecting to API…'}
{apiStatus === 'ok' && '✓ API & database connected'}
{apiStatus === 'error' && '⚠️ Could not reach API'}
{apiStatus === 'loading' && t.statusLoading}
{apiStatus === 'ok' && t.statusOk}
{apiStatus === 'error' && t.statusError}
</div>
{error && (
@ -116,7 +119,7 @@ function App() {
)}
{loading ? (
<p>Loading tasks...</p>
<p>{t.loadingTasks}</p>
) : (
<>
<TodaysTasks

View File

@ -1,5 +1,6 @@
import { useState } from 'react';
import type { NewTaskInput } from '../types';
import { useLanguage } from '../i18n/LanguageContext';
import './CreateTaskForm.css';
interface CreateTaskFormProps {
@ -7,6 +8,7 @@ interface CreateTaskFormProps {
}
export default function CreateTaskForm({ onCreateTask }: CreateTaskFormProps) {
const { t } = useLanguage();
const [isOpen, setIsOpen] = useState(false);
const [name, setName] = useState('');
const [doesRepeat, setDoesRepeat] = useState(true);
@ -44,19 +46,19 @@ export default function CreateTaskForm({ onCreateTask }: CreateTaskFormProps) {
<div className="create-task-form">
{!isOpen ? (
<button className="open-form-btn" onClick={() => setIsOpen(true)}>
+ Create new task
{t.createNewTask}
</button>
) : (
<form onSubmit={handleSubmit} className="task-form">
<h3>Create New Task</h3>
<h3>{t.createNewTaskTitle}</h3>
<div className="form-group">
<label>Task</label>
<label>{t.taskLabel}</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value.slice(0, 60))}
placeholder="task name"
placeholder={t.taskPlaceholder}
maxLength={60}
autoFocus
/>
@ -64,7 +66,7 @@ export default function CreateTaskForm({ onCreateTask }: CreateTaskFormProps) {
</div>
<div className="form-group">
<label>Repeats?</label>
<label>{t.repeats}</label>
<div className="radio-group">
<label>
<input
@ -72,7 +74,7 @@ export default function CreateTaskForm({ onCreateTask }: CreateTaskFormProps) {
checked={doesRepeat}
onChange={() => setDoesRepeat(true)}
/>
Yes
{t.yes}
</label>
<label>
<input
@ -80,14 +82,14 @@ export default function CreateTaskForm({ onCreateTask }: CreateTaskFormProps) {
checked={!doesRepeat}
onChange={() => setDoesRepeat(false)}
/>
No
{t.no}
</label>
</div>
</div>
{doesRepeat && (
<div className="form-group">
<label>Every</label>
<label>{t.every}</label>
<div className="repeat-input">
<input
type="number"
@ -102,17 +104,17 @@ export default function CreateTaskForm({ onCreateTask }: CreateTaskFormProps) {
onChange={(e) => setRepeatUnit(e.target.value as any)}
className="repeat-unit"
>
<option value="DAYS">day{repeatAmount !== '1' ? 's' : ''}</option>
<option value="WEEKS">week{repeatAmount !== '1' ? 's' : ''}</option>
<option value="MONTHS">month{repeatAmount !== '1' ? 's' : ''}</option>
<option value="YEARS">year{repeatAmount !== '1' ? 's' : ''}</option>
<option value="DAYS">{repeatAmount !== '1' ? t.days : t.day}</option>
<option value="WEEKS">{repeatAmount !== '1' ? t.weeks : t.week}</option>
<option value="MONTHS">{repeatAmount !== '1' ? t.months : t.month}</option>
<option value="YEARS">{repeatAmount !== '1' ? t.years : t.year}</option>
</select>
</div>
</div>
)}
<div className="form-group">
<label>To do right now?</label>
<label>{t.toDoRightNow}</label>
<div className="radio-group">
<label>
<input
@ -120,7 +122,7 @@ export default function CreateTaskForm({ onCreateTask }: CreateTaskFormProps) {
checked={toDoToday}
onChange={() => setToDoToday(true)}
/>
Yes
{t.yes}
</label>
<label>
<input
@ -128,13 +130,13 @@ export default function CreateTaskForm({ onCreateTask }: CreateTaskFormProps) {
checked={!toDoToday}
onChange={() => setToDoToday(false)}
/>
No
{t.no}
</label>
</div>
</div>
<div className="form-group">
<label>Priority</label>
<label>{t.priority}</label>
<div className="radio-group">
<label>
<input
@ -142,7 +144,7 @@ export default function CreateTaskForm({ onCreateTask }: CreateTaskFormProps) {
checked={priority === 'ESSENTIAL'}
onChange={() => setPriority('ESSENTIAL')}
/>
Essential
{t.essential}
</label>
<label>
<input
@ -150,15 +152,15 @@ export default function CreateTaskForm({ onCreateTask }: CreateTaskFormProps) {
checked={priority === 'WHEN_I_HAVE_TIME'}
onChange={() => setPriority('WHEN_I_HAVE_TIME')}
/>
When I have time
{t.whenIHaveTimePriority}
</label>
</div>
</div>
<div className="form-actions">
<button type="submit" className="submit-btn">Create Task</button>
<button type="submit" className="submit-btn">{t.createTask}</button>
<button type="button" className="cancel-btn" onClick={() => setIsOpen(false)}>
Cancel
{t.cancel}
</button>
</div>
</form>

View File

@ -1,5 +1,6 @@
import type { Task } from '../types';
import { calculateNextDueDate } from '../utils/taskUtils';
import { useLanguage } from '../i18n/LanguageContext';
import './Timeline.css';
interface TimelineProps {
@ -10,6 +11,7 @@ interface TimelineProps {
const MAX_VISIBLE = 5;
export default function Timeline({ tasks, onDeleteTask }: TimelineProps) {
const { t, locale } = useLanguage();
const today = new Date();
today.setHours(0, 0, 0, 0);
@ -29,16 +31,16 @@ export default function Timeline({ tasks, onDeleteTask }: TimelineProps) {
const formatDueDate = (date: Date): string => {
const diff = Math.ceil((date.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
if (diff === 1) return 'Tomorrow';
if (diff <= 7) return `In ${diff} days`;
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
if (diff === 1) return t.tomorrow;
if (diff <= 7) return t.inDays(diff);
return date.toLocaleDateString(locale === 'fr' ? 'fr-FR' : 'en-US', { month: 'short', day: 'numeric' });
};
return (
<section className="upcoming-section">
<h2 className="section-title">What the future holds</h2>
<h2 className="section-title">{t.whatTheFutureHolds}</h2>
{upcomingItems.length === 0 ? (
<p className="no-upcoming">No upcoming tasks scheduled</p>
<p className="no-upcoming">{t.noUpcoming}</p>
) : (
<div className={`upcoming-list${hasMore ? ' upcoming-list--scrollable' : ''}`}>
{upcomingItems.map(({ task, dueDate }) => (
@ -57,16 +59,17 @@ interface UpcomingRowProps {
}
function UpcomingRow({ task, dueLabel, onDelete }: UpcomingRowProps) {
const { t } = useLanguage();
return (
<div className="upcoming-row">
<span className="upcoming-name">
<span className="upcoming-name-text">{task.name}</span>
</span>
<span className="upcoming-badge">
{task.priority === 'ESSENTIAL' ? 'Essential' : 'When I have time'}
{task.priority === 'ESSENTIAL' ? t.essential : t.whenIHaveTime}
</span>
<span className="upcoming-due">{dueLabel}</span>
<button className="upcoming-delete" onClick={onDelete} title="Remove task"></button>
<button className="upcoming-delete" onClick={onDelete} title={t.removeTask}></button>
</div>
);
}

View File

@ -1,5 +1,6 @@
import { useState } from 'react';
import type { Task } from '../types';
import { useLanguage } from '../i18n/LanguageContext';
import './TodaysTasks.css';
interface TodaysTasksProps {
@ -8,17 +9,19 @@ interface TodaysTasksProps {
}
export default function TodaysTasks({ tasks, onComplete }: TodaysTasksProps) {
const essentialTasks = tasks.filter(t => t.priority === 'ESSENTIAL');
const whenIHaveTimeTasks = tasks.filter(t => t.priority === 'WHEN_I_HAVE_TIME');
const { t } = useLanguage();
const essentialTasks = tasks.filter(task => task.priority === 'ESSENTIAL');
const whenIHaveTimeTasks = tasks.filter(task => task.priority === 'WHEN_I_HAVE_TIME');
const [showWhenIHaveTime, setShowWhenIHaveTime] = useState(false);
return (
<div className="todays-tasks">
<section className="task-section">
<h2 className="section-title">Need to do</h2>
<h2 className="section-title">{t.needToDo}
<span className="count-badge">{essentialTasks.length}</span></h2>
{essentialTasks.length === 0 ? (
<p className="no-tasks">Nothing essential for today</p>
<p className="no-tasks">{t.nothingEssential}</p>
) : (
<div className="task-grid">
{essentialTasks.map(task => (
@ -37,13 +40,13 @@ export default function TodaysTasks({ tasks, onComplete }: TodaysTasksProps) {
className="section-title section-title--clickable"
onClick={() => setShowWhenIHaveTime(!showWhenIHaveTime)}
>
When I have time
{t.whenIHaveTime}
<span className="count-badge">{whenIHaveTimeTasks.length}</span>
<span className="toggle-icon">{showWhenIHaveTime ? '▾' : '▸'}</span>
</h2>
{showWhenIHaveTime && (
whenIHaveTimeTasks.length === 0 ? (
<p className="no-tasks">Nothing here</p>
<p className="no-tasks">{t.nothingHere}</p>
) : (
<div className="task-grid">
{whenIHaveTimeTasks.map(task => (
@ -67,6 +70,7 @@ interface TaskCardProps {
}
function TaskCard({ task, onComplete }: TaskCardProps) {
const { t } = useLanguage();
const [tooltipVisible, setTooltipVisible] = useState(false);
return (
@ -84,7 +88,7 @@ function TaskCard({ task, onComplete }: TaskCardProps) {
<button
className="action-btn"
onClick={() => onComplete(task.id)}
title="Mark as completed"
title={t.markCompleted}
>
</button>

View File

@ -0,0 +1,32 @@
import { createContext, useContext, useState } from 'react';
import type { ReactNode } from 'react';
import { translations } from './translations';
import type { Locale, Translations } from './translations';
interface LanguageContextType {
locale: Locale;
t: Translations;
toggleLocale: () => void;
}
const LanguageContext = createContext<LanguageContextType>({
locale: 'en',
t: translations.en,
toggleLocale: () => {},
});
export function LanguageProvider({ children }: { children: ReactNode }) {
const [locale, setLocale] = useState<Locale>('en');
const toggleLocale = () => setLocale(l => (l === 'en' ? 'fr' : 'en'));
return (
<LanguageContext.Provider value={{ locale, t: translations[locale], toggleLocale }}>
{children}
</LanguageContext.Provider>
);
}
export function useLanguage() {
return useContext(LanguageContext);
}

View File

@ -0,0 +1,106 @@
export type Locale = 'en' | 'fr';
export const translations = {
en: {
appName: 'TODO',
// API status
statusLoading: 'Connecting to API…',
statusOk: '✓ API & database connected',
statusError: '⚠️ Could not reach API',
loadingTasks: 'Loading tasks…',
// Errors
errorCreateTask: 'Failed to create task',
errorDeleteTask: 'Failed to delete task',
errorCompleteTask: 'Failed to complete task',
// TodaysTasks
needToDo: 'Need to do',
nothingEssential: 'Nothing essential for today',
whenIHaveTime: 'When I have time',
nothingHere: 'Nothing here',
markCompleted: 'Mark as completed',
// Timeline
whatTheFutureHolds: 'What the future holds',
noUpcoming: 'No upcoming tasks scheduled',
tomorrow: 'Tomorrow',
inDays: (n: number) => `In ${n} days`,
removeTask: 'Remove task',
// Priority badges
essential: 'Essential',
// CreateTaskForm
createNewTask: '+ Create new task',
createNewTaskTitle: 'Create New Task',
taskLabel: 'Task',
taskPlaceholder: 'task name',
repeats: 'Repeats?',
yes: 'Yes',
no: 'No',
every: 'Every',
day: 'day',
days: 'days',
week: 'week',
weeks: 'weeks',
month: 'month',
months: 'months',
year: 'year',
years: 'years',
toDoRightNow: 'To do right now?',
priority: 'Priority',
whenIHaveTimePriority: 'When I have time',
createTask: 'Create Task',
cancel: 'Cancel',
// Lang toggle
switchLang: 'FR',
},
fr: {
appName: 'TODO',
// API status
statusLoading: 'Connexion à l\'API…',
statusOk: '✓ API & base de données connectées',
statusError: '⚠️ Impossible de joindre l\'API',
loadingTasks: 'Chargement des tâches…',
// Errors
errorCreateTask: 'Impossible de créer la tâche',
errorDeleteTask: 'Impossible de supprimer la tâche',
errorCompleteTask: 'Impossible de marquer la tâche comme terminée',
// TodaysTasks
needToDo: 'À faire',
nothingEssential: 'Rien d\'essentiel pour aujourd\'hui',
whenIHaveTime: 'Quand j\'ai le temps',
nothingHere: 'Rien ici',
markCompleted: 'Marquer comme fait',
// Timeline
whatTheFutureHolds: 'Ce que l\'avenir réserve',
noUpcoming: 'Aucune tâche à venir',
tomorrow: 'Demain',
inDays: (n: number) => `Dans ${n} jours`,
removeTask: 'Supprimer la tâche',
// Priority badges
essential: 'Essentiel',
// CreateTaskForm
createNewTask: '+ Créer une tâche',
createNewTaskTitle: 'Créer une nouvelle tâche',
taskLabel: 'Tâche',
taskPlaceholder: 'nom de la tâche',
repeats: 'Se répète ?',
yes: 'Oui',
no: 'Non',
every: 'Tous les',
day: 'jour',
days: 'jours',
week: 'semaine',
weeks: 'semaines',
month: 'mois',
months: 'mois',
year: 'an',
years: 'ans',
toDoRightNow: 'À faire maintenant ?',
priority: 'Priorité',
whenIHaveTimePriority: 'Quand j\'ai le temps',
createTask: 'Créer la tâche',
cancel: 'Annuler',
// Lang toggle
switchLang: 'EN',
},
} as const;
export type Translations = typeof translations['en'];

View File

@ -2,9 +2,12 @@ import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
import { LanguageProvider } from './i18n/LanguageContext.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
<LanguageProvider>
<App />
</LanguageProvider>
</StrictMode>,
)