diff --git a/.dockerignore b/.dockerignore index fa629f9..f7c9f5e 100644 --- a/.dockerignore +++ b/.dockerignore @@ -7,3 +7,5 @@ Dockerfile node_modules npm-debug.log data +CLAUDE.md +docs/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 89b9e3d..f4c5760 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## Version 0.2.25 + +### Added + +* 🌍 Added Catalan language support (Català) + +### Fixed + +* Translation files consistency: Added missing UserForm keys to English and Korean translations + ## Version 0.2.24 ### Added diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f195b8b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,91 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +HabitTrove is a gamified habit tracking PWA built with Next.js 15, TypeScript, and Jotai state management. Users earn coins for completing habits and can redeem them for rewards. Features multi-user support with admin capabilities and shared ownership of habits/wishlist items. + +## Essential Commands + +### Development +- `npm run dev` - Start development server with Turbopack +- `npm run setup:dev` - Full setup: installs bun, dependencies, runs typecheck and lint +- `npm install --force` - Install dependencies (force flag required) + +### Quality Assurance (Run these before committing) +- `npm run typecheck` - TypeScript type checking (required) +- `npm run lint` - ESLint code linting (required) +- `npm test` - Run tests with Bun +- `npm run build` - Build production version + +### Docker Deployment +- `npm run docker-build` - Build Docker image locally +- `docker compose up -d` - Run with docker-compose (recommended) +- Requires `AUTH_SECRET` environment variable: `openssl rand -base64 32` + +## Architecture Overview + +### State Management (Jotai) +- **Central atoms**: `habitsAtom`, `coinsAtom`, `wishlistAtom`, `usersAtom` in `lib/atoms.ts` +- **Derived atoms**: Computed values like `dailyHabitsAtom`, `coinsBalanceAtom` +- **Business logic hooks**: `useHabits`, `useCoins`, `useWishlist` in `/hooks` + +### Data Models & Ownership +- **Individual ownership**: `CoinTransaction` has single `userId` +- **Shared ownership**: `Habit` and `WishlistItemType` have `userIds: string[]` array +- **Admin features**: Admin users can view/manage any user's data via dropdown selectors +- **Data persistence**: JSON files in `/data` directory with automatic `/backups` + +### Key Components Structure +- **Feature components**: `HabitList`, `CoinsManager`, `WishlistManager` - main page components +- **Modal components**: `AddEditHabitModal`, `AddEditWishlistItemModal`, `UserSelectModal` +- **UI components**: `/components/ui` - shadcn/ui based components + +### Authentication & Users +- NextAuth.js v5 with multi-user support +- User permissions: regular users vs admin users +- Admin dropdown patterns: Similar implementation across Habits/Wishlist pages (reference CoinsManager for pattern) + +### Internationalization +- `next-intl` with messages in `/messages/*.json` +- Supported languages: English, Spanish, German, French, Russian, Chinese, Japanese + +## Code Patterns + +### Component Structure +```typescript +// Standard component pattern: +export default function ComponentName() { + const [data, setData] = useAtom(dataAtom) + const { businessLogicFunction } = useCustomHook() + // Component logic +} +``` + +### Hook Patterns +- Custom hooks accept options: `useHabits({ selectedUser?: string })` +- Return destructured functions and computed values +- Handle both individual and shared ownership models + +### Shared Ownership Pattern +```typescript +// Filtering for shared ownership: +const userItems = allItems.filter(item => + item.userIds && item.userIds.includes(targetUserId) +) +``` + +### Admin Dropdown Pattern +Reference `CoinsManager.tsx:107-119` for admin user selection implementation. Similar pattern should be applied to Habits and Wishlist pages. + +## Data Safety +- Always backup `/data` before major changes +- Test with existing data files to prevent data loss +- Validate user permissions for all data operations +- Handle migration scripts carefully (see PLAN.md for shared ownership migration) + +## Performance Considerations +- State updates use immutable patterns +- Large dataset filtering happens at hook level +- Derived atoms prevent unnecessary re-renders \ No newline at end of file diff --git a/README.md b/README.md index 9f4958e..7ccb686 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,6 @@ HabitTrove is a gamified habit tracking application that helps you build and maintain positive habits by rewarding you with coins, which you can use to exchange for rewards. -**⚠️ Important:** HabitTrove is currently in beta. Please regularly backup your `data/` directory to prevent any potential data loss. - ## Differences to Upstream I generally try to keep the `main` branch up to date with upstream features, merging tagged versions and mapping them to `.0`. @@ -27,7 +25,7 @@ Differences (as of writing) are: - 💰 Create a wishlist of rewards to redeem with earned coins - 📊 View your habit completion streaks and statistics - 📅 Calendar heatmap to visualize your progress (WIP) -- 🌍 Multi-language support (English, Español, Deutsch, Français, Русский, 简体中文, 한국어, 日本語) +- 🌍 Multi-language support (English, Español, Català, Deutsch, Français, Русский, 简体中文, 한국어, 日本語) - 🌙 Dark mode support - 📲 Progressive Web App (PWA) support - 💾 Automatic daily backups with rotation diff --git a/app/settings/page.tsx b/app/settings/page.tsx index af9bd98..219e51c 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -227,6 +227,7 @@ export default function SettingsPage() { {/* Add more languages as needed */} + diff --git a/docs/translation-guide.md b/docs/translation-guide.md new file mode 100644 index 0000000..c4be7f9 --- /dev/null +++ b/docs/translation-guide.md @@ -0,0 +1,77 @@ +# Language Guide + +## Adding/Updating Translations + +### Adding a New Language + +To add a new language translation to HabitTrove: + +1. **Create translation file**: + - Copy `messages/en.json` as a template + - Save as `messages/{language-code}.json` (e.g., `ko.json` for Korean) + - Translate all values while preserving keys and placeholder variables like `{username}`, `{count}`, etc. + +2. **Validate translation structure**: + ```bash + # Ensure JSON is valid + jq empty messages/{language-code}.json + + # Compare structure with English (should show no differences) + diff <(jq -S . messages/en.json | jq -r 'keys | sort | .[]') <(jq -S . messages/{language-code}.json | jq -r 'keys | sort | .[]') + ``` + +3. **Add language option to UI**: + - Edit `app/settings/page.tsx` + - Add new `` in alphabetical order + +4. **Update documentation**: + - Add language to README.md supported languages list + - Create new changelog entry with version bump + - Update package.json version + +### Example: Adding Korean (한국어) + +```bash +# 1. Copy translation file +cp /path/to/ko.json messages/ko.json + +# 2. Add to settings page +# Add: + +# 3. Update README.md +# Change: 简体中文, 日본語 +# To: 简체中文, 한국어, 日본語 + +# 4. Add changelog entry +# Create new version section with language addition + +# 5. Bump package version +# Update version in package.json +``` + +### Translation Quality Guidelines + +- Use natural, contextually appropriate expressions +- Maintain consistent terminology throughout +- Preserve all placeholder variables exactly: `{username}`, `{count}`, `{target}`, etc. +- Use appropriate formality level for the target language +- Ensure JSON structure matches English file exactly (385 total keys) + +### Validation Commands + +```bash +# Check JSON validity +jq empty messages/{lang}.json + +# Compare key structure +node -e " +const en = require('./messages/en.json'); +const target = require('./messages/{lang}.json'); +// ... deep key comparison script +" + +# Verify placeholder consistency +grep -o '{[^}]*}' messages/en.json | sort | uniq > en_vars.txt +grep -o '{[^}]*}' messages/{lang}.json | sort | uniq > {lang}_vars.txt +diff en_vars.txt {lang}_vars.txt +``` diff --git a/messages/ca.json b/messages/ca.json new file mode 100644 index 0000000..2cf48a5 --- /dev/null +++ b/messages/ca.json @@ -0,0 +1,433 @@ +{ + "Dashboard": { + "title": "Tauler" + }, + "HabitList": { + "myTasks": "Les meves tasques", + "myHabits": "Els meus hàbits", + "addTaskButton": "Afegeix tasca", + "addHabitButton": "Afegeix hàbit", + "searchTasksPlaceholder": "Cerca tasques...", + "searchHabitsPlaceholder": "Cerca hàbits...", + "sortByLabel": "Ordena per:", + "sortByName": "Nom", + "sortByCoinReward": "Recompensa de monedes", + "sortByDueDate": "Data límit", + "sortByFrequency": "Freqüència", + "toggleSortOrderAriaLabel": "Canvia l'ordre de classificació", + "noTasksFoundMessage": "No s'han trobat tasques que coincideixin amb la teva cerca.", + "noHabitsFoundMessage": "No s'han trobat hàbits que coincideixin amb la teva cerca.", + "emptyStateTasksTitle": "Encara no hi ha tasques", + "emptyStateHabitsTitle": "Encara no hi ha hàbits", + "emptyStateTasksDescription": "Crea la teva primera tasca per començar a seguir el teu progrés", + "emptyStateHabitsDescription": "Crea el teu primer hàbit per començar a seguir el teu progrés", + "archivedSectionTitle": "Arxivat", + "deleteTaskDialogTitle": "Elimina tasca", + "deleteHabitDialogTitle": "Elimina hàbit", + "deleteTaskDialogMessage": "Estàs segur que vols eliminar aquesta tasca? Aquesta acció no es pot desfer.", + "deleteHabitDialogMessage": "Estàs segur que vols eliminar aquest hàbit? Aquesta acció no es pot desfer.", + "deleteButton": "Elimina" + }, + "DailyOverview": { + "addTaskButtonLabel": "Afegeix tasca", + "addHabitButtonLabel": "Afegeix hàbit", + "todaysOverviewTitle": "Resum d'avui", + "dailyTasksTitle": "Tasques diàries", + "noTasksDueTodayMessage": "No hi ha tasques per avui. Afegeix-ne algunes per començar!", + "dailyHabitsTitle": "Hàbits diaris", + "noHabitsDueTodayMessage": "No hi ha hàbits per avui. Afegeix-ne alguns per començar!", + "wishlistGoalsTitle": "Objectius de la llista de desitjos", + "redeemableBadgeLabel": "{count}/{total} bescanviable", + "noWishlistItemsMessage": "Encara no hi ha elements a la llista de desitjos. Afegeix algunes metes per treballar-hi!", + "readyToRedeemMessage": "Llest per bescanviar!", + "coinsToGoMessage": "Falten {amount} monedes", + "showLessButton": "Mostra menys", + "showAllButton": "Mostra tot", + "viewButton": "Visualitza", + "deleteTaskDialogTitle": "Elimina tasca", + "deleteHabitDialogTitle": "Elimina hàbit", + "confirmDeleteDialogMessage": "Estàs segur que vols eliminar \"{name}\"? Aquesta acció no es pot desfer.", + "deleteButton": "Elimina", + "overdueTooltip": "Vençut" + }, + "HabitContextMenuItems": { + "startPomodoro": "Inicia Pomodoro", + "moveToToday": "Mou a avui", + "moveToTomorrow": "Mou a demà", + "unpin": "Desfixa", + "pin": "Fixa", + "edit": "Edita", + "archive": "Arxiva", + "unarchive": "Desarxiva", + "delete": "Elimina" + }, + "HabitStreak": { + "dailyCompletionStreakTitle": "Ratxa de finalització diària", + "tooltipHabitsLabel": "hàbits", + "tooltipTasksLabel": "tasques", + "tooltipCompletedLabel": "Completat" + }, + "CoinBalance": { + "coinBalanceTitle": "Saldo de monedes" + }, + "AddEditHabitModal": { + "editTaskTitle": "Edita tasca", + "editHabitTitle": "Edita hàbit", + "addNewTaskTitle": "Afegeix nova tasca", + "addNewHabitTitle": "Afegeix nou hàbit", + "nameLabel": "Nom *", + "descriptionLabel": "Descripció", + "whenLabel": "Quan *", + "completeLabel": "Completa", + "timesSuffix": "vegades", + "rewardLabel": "Recompensa", + "coinsSuffix": "monedes", + "shareLabel": "Comparteix", + "saveChangesButton": "Desa els canvis", + "addTaskButton": "Afegeix tasca", + "addHabitButton": "Afegeix hàbit" + }, + "ConfirmDialog": { + "confirmButton": "Confirma", + "cancelButton": "Cancel·la" + }, + "AddEditWishlistItemModal": { + "editTitle": "Edita recompensa", + "addTitle": "Afegeix nova recompensa", + "nameLabel": "Nom *", + "descriptionLabel": "Descripció", + "costLabel": "Cost", + "coinsSuffix": "monedes", + "redeemableLabel": "Bescanviable", + "timesSuffix": "vegades", + "errorNameRequired": "El nom és obligatori", + "errorCoinCostMin": "El cost en monedes ha de ser com a mínim 1", + "errorTargetCompletionsMin": "El nombre de finalitzacions objectiu ha de ser com a mínim 1", + "errorInvalidUrl": "Si us plau, introdueix una URL vàlida", + "linkLabel": "Enllaç", + "shareLabel": "Comparteix", + "saveButton": "Desa els canvis", + "addButton": "Afegeix recompensa" + }, + "Navigation": { + "dashboard": "Tauler", + "tasks": "Tasques", + "habits": "Hàbits", + "calendar": "Calendari", + "wishlist": "Llista de desitjos", + "coins": "Monedes" + }, + "TodayEarnedCoins": { + "todaySuffix": "avui" + }, + "WishlistItem": { + "usesLeftSingular": "ús restant", + "usesLeftPlural": "usos restants", + "coinsSuffix": "monedes", + "redeem": "Bescanvia", + "redeemedDone": "Fet", + "redeemedExclamation": "Bescanviat!", + "editButton": "Edita", + "archiveButton": "Arxiva", + "unarchiveButton": "Desarxiva", + "deleteButton": "Elimina" + }, + "WishlistManager": { + "title": "La meva llista de desitjos", + "addRewardButton": "Afegeix recompensa", + "emptyStateTitle": "La teva llista de desitjos està buida", + "emptyStateDescription": "Afegeix recompenses que t'agradaria guanyar amb les teves monedes", + "archivedSectionTitle": "Arxivat", + "popupBlockedTitle": "Finestra emergent bloquejada", + "popupBlockedDescription": "Si us plau, permet les finestres emergents per obrir l'enllaç", + "deleteDialogTitle": "Elimina recompensa", + "deleteDialogMessage": "Estàs segur que vols eliminar aquesta recompensa? Aquesta acció no es pot desfer.", + "deleteButton": "Elimina" + }, + "UserSelectModal": { + "addUserButton": "Afegeix usuari", + "createNewUserTitle": "Crea nou usuari", + "selectUserTitle": "Selecciona usuari", + "signInSuccessTitle": "Inici de sessió correcte", + "signInSuccessDescription": "Benvingut de nou, {username}!", + "errorInvalidPassword": "contrasenya no vàlida", + "deleteUserConfirmation": "Estàs segur que vols eliminar l'usuari {username}? Aquesta acció no es pot desfer.", + "confirmDeleteButtonText": "Elimina", + "deletingButtonText": "Eliminant...", + "deleteUserSuccessTitle": "Usuari eliminat", + "deleteUserSuccessDescription": "L'usuari {username} s'ha eliminat correctament.", + "deleteUserErrorTitle": "Error en eliminar", + "genericError": "S'ha produït un error inesperat.", + "networkError": "S'ha produït un error de xarxa. Si us plau, torna-ho a intentar.", + "editUserTooltip": "Edita usuari", + "deleteUserTooltip": "Elimina usuari" + }, + "CoinsManager": { + "title": "Gestió de monedes", + "currentBalanceLabel": "Saldo actual", + "coinsSuffix": "monedes", + "addCoinsButton": "Afegeix monedes", + "removeCoinsButton": "Elimina monedes", + "statisticsTitle": "Estadístiques", + "totalEarnedLabel": "Total guanyat", + "totalSpentLabel": "Total gastat", + "totalTransactionsLabel": "Transaccions totals", + "todaysEarnedLabel": "Guanyat avui", + "todaysSpentLabel": "Gastat avui", + "todaysTransactionsLabel": "Transaccions d'avui", + "transactionHistoryTitle": "Historial de transaccions", + "showLabel": "Mostra:", + "entriesSuffix": "entrades", + "showingEntries": "Mostrant {from} a {to} de {total} entrades", + "noTransactionsTitle": "Encara no hi ha transaccions", + "noTransactionsDescription": "El teu historial de transaccions apareixerà aquí un cop comencis a guanyar o gastar monedes", + "pageLabel": "Pàgina", + "ofLabel": "de", + "transactionTypeHabitCompletion": "Finalització d'hàbit", + "transactionTypeTaskCompletion": "Finalització de tasca", + "transactionTypeHabitUndo": "Desfer hàbit", + "transactionTypeTaskUndo": "Desfer tasca", + "transactionTypeWishRedemption": "Bescanvi de desig", + "transactionTypeManualAdjustment": "Ajust manual", + "transactionTypeCoinReset": "Reinici de monedes", + "transactionTypeInitialBalance": "Saldo inicial" + }, + "NotificationBell": { + "errorUpdateTimestamp": "Error en actualitzar la marca de temps de notificació llegida:" + }, + "PomodoroTimer": { + "focusLabel1": "Mantén-te concentrat", + "focusLabel2": "Tu pots", + "focusLabel3": "Continua endavant", + "focusLabel4": "Fes-ho", + "focusLabel5": "Fes que passi", + "focusLabel6": "Mantén-te fort", + "focusLabel7": "Esforça't", + "focusLabel8": "Un pas cada vegada", + "focusLabel9": "Tu pots fer-ho", + "focusLabel10": "Concentra't i conquereix", + "breakLabel1": "Fes un descans", + "breakLabel2": "Relaxa't i recarrega", + "breakLabel3": "Respira profundament", + "breakLabel4": "Estira't", + "breakLabel5": "Refresca't", + "breakLabel6": "T'ho mereixes", + "breakLabel7": "Recarrega la teva energia", + "breakLabel8": "Allunya't un moment", + "breakLabel9": "Neteja la teva ment", + "breakLabel10": "Descansa i recupera't", + "focusType": "Concentració", + "breakType": "Descans", + "pauseButton": "Pausa", + "startButton": "Inicia", + "resetButton": "Reinicia", + "skipButton": "Omet", + "wakeLockNotSupported": "El navegador no suporta wake lock", + "wakeLockInUse": "Wake lock ja està en ús", + "wakeLockRequestError": "Error en sol·licitar wake lock:", + "wakeLockReleaseError": "Error en alliberar wake lock:" + }, + "HabitCalendar": { + "title": "Calendari d'hàbits", + "calendarCardTitle": "Calendari", + "selectDatePrompt": "Selecciona una data", + "tasksSectionTitle": "Tasques", + "habitsSectionTitle": "Hàbits", + "errorCompletingPastHabit": "Error en completar hàbit passat:" + }, + "NotificationDropdown": { + "notLoggedIn": "No has iniciat sessió.", + "userCompletedItem": "{username} ha completat {itemName}.", + "userRedeemedItem": "{username} ha bescanviat {itemName}.", + "activityRelatedToItem": "Activitat relacionada amb {itemName} per {username}.", + "defaultUsername": "Algú", + "defaultItemName": "un element compartit", + "notificationsTitle": "Notificacions", + "notificationsTooltip": "Mostra finalitzacions o bescanvis d'altres usuaris per hàbits o llista de desitjos que has compartit amb ells (has de ser administrador)", + "noNotificationsYet": "Encara no hi ha notificacions." + }, + "AboutModal": { + "dialogArisLabel": "quant a", + "changelogButton": "Registre de canvis", + "createdByPrefix": "Creat amb ❤️ per", + "starOnGitHubButton": "Dona una estrella a GitHub" + }, + "PermissionSelector": { + "permissionsTitle": "Permisos", + "adminAccessLabel": "Accés d'administrador", + "adminAccessDescription": "Els administradors tenen permís complet sobre totes les dades de tots els usuaris", + "resourceHabitTask": "Hàbit / Tasca", + "resourceWishlist": "Llista de desitjos", + "resourceCoins": "Monedes", + "permissionWrite": "Escriptura", + "permissionInteract": "Interactua" + }, + "UserForm": { + "toastUserUpdatedTitle": "Usuari actualitzat", + "toastUserUpdatedDescription": "Usuari {username} actualitzat amb èxit", + "toastUserCreatedTitle": "Usuari creat", + "toastUserCreatedDescription": "Usuari {username} creat amb èxit", + "actionUpdate": "actualitzar", + "actionCreate": "crear", + "errorFailedUserAction": "Error en {action} usuari", + "toastDemoDeleteDisabled": "L'eliminació està deshabilitada a la instància de demostració", + "toastCannotDeleteSelf": "No pots eliminar el teu propi compte", + "confirmDeleteUser": "Estàs segur que vols eliminar l'usuari {username}?", + "toastUserDeletedTitle": "Usuari eliminat", + "toastUserDeletedDescription": "L'usuari {username} s'ha eliminat correctament", + "toastDeleteUserFailed": "Error en eliminar l'usuari: {error}", + "errorTitle": "Error", + "errorFileSizeLimit": "La mida de l'arxiu ha de ser inferior a 5MB", + "toastAvatarUploadedTitle": "Avatar pujat", + "toastAvatarUploadedDescription": "Avatar pujat amb èxit", + "errorFailedAvatarUpload": "Error en pujar l'avatar", + "changeAvatarButton": "Canvia avatar", + "uploadAvatarButton": "Puja avatar", + "usernameLabel": "Nom d'usuari", + "usernamePlaceholder": "Nom d'usuari", + "newPasswordLabel": "Nova contrasenya", + "passwordLabel": "Contrasenya", + "passwordPlaceholderEdit": "Deixa en blanc per mantenir l'actual", + "passwordPlaceholderCreate": "Introdueix contrasenya", + "demoPasswordDisabledMessage": "La contrasenya està automàticament desactivada a la instància de demostració", + "disablePasswordLabel": "Desactiva contrasenya", + "cancelButton": "Cancel·la", + "saveChangesButton": "Desa els canvis", + "createUserButton": "Crea usuari", + "deleteAccountButton": "Elimina compte", + "deletingButtonText": "Eliminant...", + "areYouSure": "Estàs segur?", + "deleteUserConfirmation": "Estàs segur que vols eliminar l'usuari {username}?", + "cancel": "Cancel·la", + "confirmDeleteButtonText": "Elimina" + }, + "ViewToggle": { + "habitsLabel": "Hàbits", + "tasksLabel": "Tasques" + }, + "HabitItem": { + "overdue": "Vençut", + "whenLabel": "Quan: {frequency}", + "coinsPerCompletion": "{count} monedes per finalització", + "completedStatus": "Completat", + "completedStatusCount": "Completat ({completed}/{target})", + "completedStatusCountMobile": "{completed}/{target}", + "completeButton": "Completa", + "completeButtonCount": "Completa ({completed}/{target})", + "completeButtonCountMobile": "{completed}/{target}", + "undoButton": "Desfés", + "editButton": "Edita" + }, + "TransactionNoteEditor": { + "noteTooLongTitle": "Nota massa llarga", + "noteTooLongDescription": "Les notes han de tenir menys de 200 caràcters", + "errorSavingNoteTitle": "Error en desar la nota", + "errorDeletingNoteTitle": "Error en eliminar la nota", + "pleaseTryAgainDescription": "Si us plau, torna-ho a intentar", + "addNotePlaceholder": "Afegeix una nota...", + "saveNoteTitle": "Desa nota", + "cancelButtonTitle": "Cancel·la", + "deleteNoteTitle": "Elimina nota", + "editNoteAriaLabel": "Edita nota" + }, + "Profile": { + "guestUsername": "Convidat", + "editProfileButton": "Edita perfil", + "signOutSuccessTitle": "Tancament de sessió correcte", + "signOutSuccessDescription": "Has tancat sessió del teu compte", + "signOutErrorTitle": "Error en tancar sessió", + "signOutErrorDescription": "Error en tancar sessió", + "switchUserButton": "Canvia usuari", + "settingsLink": "Configuració", + "aboutButton": "Quant a", + "themeLabel": "Tema", + "editProfileModalTitle": "Edita perfil" + }, + "PasswordEntryForm": { + "notYouButton": "No ets tu?", + "passwordLabel": "Contrasenya", + "passwordPlaceholder": "Introdueix contrasenya", + "loginErrorToastTitle": "Error", + "loginFailedErrorToastDescription": "Error en iniciar sessió", + "cancelButton": "Cancel·la", + "loginButton": "Inicia sessió" + }, + "CompletionCountBadge": { + "countCompleted": "{completedCount}/{totalCount} completat" + }, + "SettingsPage": { + "title": "Configuració", + "uiSettingsTitle": "Configuració d'interfície", + "numberFormattingLabel": "Format numèric", + "numberFormattingDescription": "Formateja nombres grans (ex: 1K, 1M, 1B)", + "numberGroupingLabel": "Agrupació numèrica", + "numberGroupingDescription": "Usa separadors de milers (ex: 1.000 vs 1000)", + "systemSettingsTitle": "Configuració del sistema", + "timezoneLabel": "Zona horària", + "timezoneDescription": "Selecciona la teva zona horària per a un seguiment precís de dates", + "weekStartDayLabel": "Dia d'inici de setmana", + "weekStartDayDescription": "Selecciona el teu dia preferit per iniciar la setmana", + "weekdays": { + "sunday": "Diumenge", + "monday": "Dilluns", + "tuesday": "Dimarts", + "wednesday": "Dimecres", + "thursday": "Dijous", + "friday": "Divendres", + "saturday": "Dissabte" + }, + "autoBackupLabel": "Còpia de seguretat automàtica", + "autoBackupTooltip": "Quan està habilitat, les dades de l'aplicació (hàbits, monedes, configuracions, etc.) es copien automàticament cada dia al voltant de les 2 AM hora del servidor. Les còpies de seguretat s'emmagatzemen com a fitxers ZIP al directori `backups/` a l'arrel del projecte. Només es conserven les últimes 7 còpies de seguretat; les més antigues s'eliminen automàticament.", + "autoBackupDescription": "Realitza còpia de seguretat automàtica diària", + "languageLabel": "Idioma", + "languageDescription": "Tria el teu idioma preferit per mostrar a l'aplicació.", + "languageChangedTitle": "Idioma canviat", + "languageChangedDescription": "Si us plau, actualitza la pàgina per veure els canvis", + "languageDisabledInDemoTooltip": "Canviar l'idioma està deshabilitat a la versió de demostració." + }, + "Common": { + "authenticationRequiredTitle": "Autenticació requerida", + "authenticationRequiredDescription": "Si us plau, inicia sessió per continuar.", + "permissionDeniedTitle": "Permís denegat", + "permissionDeniedDescription": "No tens permís de {action} per a {resource}.", + "undoButton": "Desfés", + "redoButton": "Refés", + "errorTitle": "Error" + }, + "useHabits": { + "alreadyCompletedTitle": "Ja completat", + "alreadyCompletedDescription": "Ja has completat aquest hàbit avui.", + "completedTitle": "Completat!", + "earnedCoinsDescription": "Has guanyat {coinReward} monedes.", + "progressTitle": "Progrés!", + "progressDescription": "Has completat {count}/{target} vegades avui.", + "completionUndoneTitle": "Finalització desfeta", + "completionUndoneDescription": "Tens {count}/{target} finalitzacions avui.", + "noCompletionsToUndoTitle": "No hi ha finalitzacions per desfer", + "noCompletionsToUndoDescription": "Aquest hàbit no s'ha completat avui.", + "alreadyCompletedPastDateTitle": "Ja completat", + "alreadyCompletedPastDateDescription": "Aquest hàbit ja va ser completat el {dateKey}.", + "earnedCoinsPastDateDescription": "Vas guanyar {coinReward} monedes per {dateKey}.", + "progressPastDateDescription": "Has completat {count}/{target} vegades el {dateKey}." + }, + "useWishlist": { + "redemptionLimitReachedTitle": "Límit de bescanvis assolit", + "redemptionLimitReachedDescription": "Has assolit el màxim de bescanvis per \"{itemName}\".", + "rewardRedeemedTitle": "🎉 Recompensa bescanviada!", + "rewardRedeemedDescription": "Has bescanviat \"{itemName}\" per {itemCoinCost} monedes.", + "notEnoughCoinsTitle": "No hi ha prou monedes", + "notEnoughCoinsDescription": "Necessites {coinsNeeded} monedes més per bescanviar aquesta recompensa." + }, + "Warning": { + "areYouSure": "Estàs segur?", + "cancel": "Cancel·la" + }, + "useCoins": { + "addedCoinsDescription": "S'han afegit {amount} monedes", + "invalidAmountTitle": "Quantitat no vàlida", + "invalidAmountDescription": "Si us plau, introdueix un nombre positiu vàlid", + "successTitle": "Èxit", + "transactionNotFoundDescription": "Transacció no trobada", + "maxAmountExceededDescription": "La quantitat no pot excedir {max}." + } +} diff --git a/messages/en.json b/messages/en.json index fc73841..d03a382 100644 --- a/messages/en.json +++ b/messages/en.json @@ -288,7 +288,18 @@ "cancelButton": "Cancel", "saveChangesButton": "Save Changes", "createUserButton": "Create User", - "deleteAccountButton": "Delete Account" + "deleteAccountButton": "Delete Account", + "toastDemoDeleteDisabled": "Deletion is disabled in demo instance", + "toastCannotDeleteSelf": "You cannot delete your own account", + "confirmDeleteUser": "Are you sure you want to delete user {username}?", + "toastUserDeletedTitle": "User deleted", + "toastUserDeletedDescription": "User {username} has been deleted successfully", + "toastDeleteUserFailed": "Failed to delete user: {error}", + "deletingButtonText": "Deleting...", + "areYouSure": "Are you sure?", + "deleteUserConfirmation": "Are you sure you want to delete user {username}?", + "cancel": "Cancel", + "confirmDeleteButtonText": "Delete" }, "ViewToggle": { "habitsLabel": "Habits", diff --git a/messages/ko.json b/messages/ko.json index 987ac5b..7b7e6ce 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -288,7 +288,18 @@ "cancelButton": "취소", "saveChangesButton": "변경 사항 저장", "createUserButton": "사용자 생성", - "deleteAccountButton": "계정 삭제" + "deleteAccountButton": "계정 삭제", + "toastDemoDeleteDisabled": "데모 인스턴스에서는 삭제가 비활성화되어 있습니다", + "toastCannotDeleteSelf": "본인의 계정을 삭제할 수 없습니다", + "confirmDeleteUser": "사용자 {username}을(를) 삭제하시겠습니까?", + "toastUserDeletedTitle": "사용자 삭제됨", + "toastUserDeletedDescription": "사용자 {username}이(가) 성공적으로 삭제되었습니다", + "toastDeleteUserFailed": "사용자 삭제 실패: {error}", + "deletingButtonText": "삭제 중...", + "areYouSure": "정말로 삭제하시겠습니까?", + "deleteUserConfirmation": "사용자 {username}을(를) 삭제하시겠습니까?", + "cancel": "취소", + "confirmDeleteButtonText": "삭제" }, "ViewToggle": { "habitsLabel": "습관", diff --git a/package.json b/package.json index 03ae429..37c4199 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "habittrove", - "version": "0.2.24", + "version": "0.2.25", "private": true, "scripts": { "dev": "next dev --turbopack",