TidyQuest/client/src/hooks/useTranslation.ts
root a7cd81fe81 Initial release: TidyQuest v0.1.0
TidyQuest is a self-hosted web application that gamifies household chores
using RPG mechanics. Complete tasks, earn coins, unlock achievements, and
compete with your family on the leaderboard.

Features:
- 🎯 Health-based task tracking with visual decay over time
- 💰 Coin system with customizable rewards
- 🔥 Daily/weekly streak tracking
- 🏆 Family leaderboard (week/month/quarter/year)
- 🎖️ Automatic achievement unlocking
- 📅 Calendar view for upcoming tasks
- 🌍 Multilingual support (EN/FR/DE/ES)
- 📱 Optional Telegram notifications
- 🏖️ Vacation mode (pause task decay)
- 🐳 Docker deployment (single container)

Tech Stack:
- Frontend: React 19 + Vite + TypeScript
- Backend: Node.js + Express + TypeScript
- Database: SQLite3 with WAL mode
- Auth: JWT + bcrypt
- Deployment: Docker Compose

Includes:
- 8 room types with 60+ predefined tasks
- 10 preset rewards
- 12 achievement types
- Complete API documentation
- Security best practices guide
- Comprehensive README with examples

License: GNU Affero General Public License v3.0 (AGPL-3.0)
This ensures TidyQuest remains free and open-source forever, preventing
proprietary SaaS forks. Any modified hosted version must share source code.
2026-02-17 18:40:09 +01:00

97 lines
3.4 KiB
TypeScript

import en from '../i18n/en.json';
import fr from '../i18n/fr.json';
import de from '../i18n/de.json';
import es from '../i18n/es.json';
type TranslationData = typeof en;
const translations: Record<string, TranslationData> = { en, fr, de, es };
export function useTranslation(language: string = 'en') {
const lang = translations[language] || translations.en;
const fallback = translations.en;
function normalize(value: string): string {
return value.trim().toLowerCase().replace(/\s+/g, ' ');
}
/** Translate a task name: if translationKey exists and has a translation, use it; else return raw name */
function taskName(name: string, translationKey: string | null | undefined): string {
if (!translationKey) return name;
// translationKey format: "room.task_key" e.g. "kitchen.wash_dishes"
const [room, ...rest] = translationKey.split('.');
const taskKey = rest.join('.');
const roomTasks = (lang.tasks as any)?.[room];
if (roomTasks && roomTasks[taskKey]) {
return roomTasks[taskKey];
}
// Fallback to English
const fallbackTasks = (fallback.tasks as any)?.[room];
if (fallbackTasks && fallbackTasks[taskKey]) {
return fallbackTasks[taskKey];
}
return name;
}
/** Translate a UI string by dot-notation key, e.g. "profile.title" */
function t(key: string): string {
const parts = key.split('.');
let value: any = lang.ui;
let fallbackValue: any = fallback.ui;
for (const part of parts) {
value = value?.[part];
fallbackValue = fallbackValue?.[part];
}
return (typeof value === 'string' ? value : null)
|| (typeof fallbackValue === 'string' ? fallbackValue : null)
|| key;
}
/** Translate a room type name */
function roomName(roomType: string): string {
return (lang.rooms as any)?.[roomType]
|| (fallback.rooms as any)?.[roomType]
|| roomType;
}
function roomDisplayName(name: string | null | undefined, roomType: string): string {
const raw = (name || '').trim();
if (!raw) return roomName(roomType);
const normalized = normalize(raw);
for (const locale of Object.values(translations)) {
const localized = (locale.rooms as any)?.[roomType];
if (typeof localized === 'string' && normalize(localized) === normalized) {
return roomName(roomType);
}
}
return raw;
}
function timeAgo(dateStr: string | null | undefined): string {
if (!dateStr) return t('time.never');
const diff = Date.now() - new Date(dateStr).getTime();
const mins = Math.floor(diff / 60000);
if (mins < 1) return t('time.justNow');
if (mins < 60) return t('time.minutesAgo').replace('{count}', `${mins}`);
const hours = Math.floor(mins / 60);
if (hours < 24) return t('time.hoursAgo').replace('{count}', `${hours}`);
const days = Math.floor(hours / 24);
if (days === 1) return t('time.yesterday');
if (days < 7) return t('time.daysAgo').replace('{count}', `${days}`);
const weeks = Math.floor(days / 7);
if (weeks === 1) return t('time.oneWeekAgo');
if (weeks < 5) return t('time.weeksAgo').replace('{count}', `${weeks}`);
const months = Math.floor(days / 30);
if (months === 1) return t('time.oneMonthAgo');
return t('time.monthsAgo').replace('{count}', `${months}`);
}
return { taskName, t, roomName, roomDisplayName, timeAgo };
}