From 0cbca691f1115c818a5e2a1843e6f421b90cbefd Mon Sep 17 00:00:00 2001 From: MohammedEAbdelAziz Date: Tue, 15 Jul 2025 17:47:05 +0300 Subject: [PATCH] feat: - Added localization using paraglide.js, languages are English and Italian (AI translation). - Added language switcher in settings with relevant CSS file (definitely needs to be adjusted). explanation: - Static content is translated on page load. - Dynamic content is handled by the companion DynamicTextUpdater. - User preferences are persisted across sessions. - Messages are saved in {locale}.json and accessed using m['parent.child']() function. - There is a localization manager (utils/localization.js) which ultimately handles the following: - Locale switching - Text updates during runtime (after switching language) - Updates the HTML `lang` tag - There is also a dynamic text updater (utils/dynamic-text-updater.js) that updates text content when: - Timer state changes - DOM elements are added dynamically --- .vscode/extensions.json | 8 +- changes | 22 + messages/en.json | 224 +++ messages/it.json | 224 +++ package-lock.json | 315 ++++- package.json | 5 +- project.inlang/.gitignore | 1 + project.inlang/project_id | 1 + project.inlang/settings.json | 15 + src/index.html | 2199 ++++++++++++++++++----------- src/main.js | 5 +- src/styles/localization.css | 74 + src/utils/dynamic-text-updater.js | 141 ++ src/utils/localization.js | 352 +++++ 14 files changed, 2722 insertions(+), 864 deletions(-) create mode 100644 changes create mode 100644 messages/en.json create mode 100644 messages/it.json create mode 100644 project.inlang/.gitignore create mode 100644 project.inlang/project_id create mode 100644 project.inlang/settings.json create mode 100644 src/styles/localization.css create mode 100644 src/utils/dynamic-text-updater.js create mode 100644 src/utils/localization.js diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 24d7cc6..72bafb9 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,3 +1,7 @@ { - "recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"] -} + "recommendations": [ + "tauri-apps.tauri-vscode", + "rust-lang.rust-analyzer", + "inlang.vs-code-extension" + ] +} \ No newline at end of file diff --git a/changes b/changes new file mode 100644 index 0000000..53542bf --- /dev/null +++ b/changes @@ -0,0 +1,22 @@ +feat: + +- Added localization using paraglide.js, languages are English and Italian (AI translation). +- Added language switcher in settings with relevant CSS file (definitely needs to be adjusted). + +explanation: + +- Static content is translated on page load. +- Dynamic content is handled by the companion DynamicTextUpdater. +- User preferences are persisted across sessions. + +- Messages are saved in {locale}.json and accessed using m['parent.child']() function. +- There is a localization manager (utils/localization.js) which ultimately handles the following: + + - Locale switching + - Text updates during runtime (after switching language) + - Updates the HTML `lang` tag + +- There is also a dynamic text updater (utils/dynamic-text-updater.js) that updates text content when: + + - Timer state changes + - DOM elements are added dynamically diff --git a/messages/en.json b/messages/en.json new file mode 100644 index 0000000..db30715 --- /dev/null +++ b/messages/en.json @@ -0,0 +1,224 @@ +{ + "$schema": "https://inlang.com/schema/inlang-message-format", + "app": { + "title": "Presto" + }, + "navigation": { + "timer": "Timer", + "calendar": "Calendar", + "team": "Team (Coming Soon)", + "settings": "Settings" + }, + "user": { + "avatar": "User Avatar", + "guest": "Guest", + "guestMode": "Guest Mode", + "signIn": "Sign In", + "signOut": "Sign Out" + }, + "timer": { + "focus": "Focus", + "chooseTag": "Choose tag", + "newTag": "New tag...", + "smartPause": "Smart Pause: Click to toggle automatic pause when inactive", + "autoStart": "Auto-start: Click to toggle automatic session start", + "continuousSessions": "Continuous Sessions: Click to toggle continuous mode", + "subtract5Minutes": "Subtract 5 minutes", + "add5Minutes": "Add 5 minutes" + }, + "calendar": { + "title": "Calendar & Statistics", + "weeklyFocusSummary": "Focus Weekly Summary", + "weeklyFocusTime": "Weekly focus time", + "averageFocusDay": "Average focus/day", + "sessionsThisWeek": "Sessions this week", + "weeklyTotalTime": "Weekly total time", + "thisWeeksSessions": "This Week's Sessions", + "todaysDevelopment": "Today's Development", + "focusSessions": "Focus sessions", + "breakTime": "Break time", + "tagUsageThisWeek": "Tag Usage This Week", + "noDataAvailable": "No data available", + "todaysSessions": "Today's Sessions", + "addSession": "Add Session", + "sessionHistory": "Session History", + "export": "Export", + "exportToExcel": "Export to Excel", + "date": "Date", + "time": "Time", + "duration": "Duration", + "tags": "Tags", + "actions": "Actions", + "days": { + "mon": "Mon", + "tue": "Tue", + "wed": "Wed", + "thu": "Thu", + "fri": "Fri", + "sat": "Sat", + "sun": "Sun" + } + }, + "team": { + "title": "Team Dashboard", + "subtitle": "Monitor your team's focus status in real-time", + "focusing": "Focusing", + "onBreak": "On Break", + "privacyMode": "Privacy Mode", + "offline": "Offline", + "teamsOverview": "Teams Overview" + }, + "settings": { + "title": "Settings", + "categories": { + "general": "General", + "shortcuts": "Shortcuts", + "notifications": "Notifications", + "theme": "Theme", + "automation": "Automation", + "goals": "Goals", + "advanced": "Advanced", + "updates": "Updates" + }, + "general": { + "title": "General Settings", + "description": "Configure timer durations and basic behavior", + "timerDurations": "Timer Durations", + "focusDuration": "Focus Duration (minutes):", + "focusDurationDesc": "How long should focus sessions last", + "shortBreak": "Short Break (minutes):", + "shortBreakDesc": "Duration for short breaks between focus sessions", + "longBreak": "Long Break (minutes):", + "longBreakDesc": "Duration for long breaks after completing a cycle", + "dailySessions": "Daily Sessions:", + "dailySessionsDesc": "Number of focus sessions to complete each day", + "maxSessionTime": "Max Session Time (minutes):", + "maxSessionTimeDesc": "Maximum time per session before auto-pause (default: 2 hours)" + }, + "shortcuts": { + "title": "Global Shortcuts", + "description": "Configure keyboard shortcuts that work even when the app is in the background", + "keyboardShortcuts": "Keyboard Shortcuts", + "startStop": "Start/Stop Timer:", + "startStopDesc": "Toggle timer between start and pause states", + "deleteUndo": "Delete Session / Undo:", + "deleteUndoDesc": "Reset current session or undo during breaks", + "saveSession": "Save Session:", + "saveSessionDesc": "Skip to next phase and save current progress" + }, + "notifications": { + "title": "Notifications", + "description": "Control how and when you receive notifications", + "notificationTypes": "Notification Types", + "desktop": "Desktop Notifications", + "desktopDesc": "Show system notifications when timer completes. Browser permission will be requested when enabled.", + "sound": "Sound Notifications", + "soundDesc": "Play a sound when timer phases complete" + }, + "theme": { + "title": "Theme", + "description": "Customize the appearance and visual style of the application", + "colorTheme": "Color Theme", + "themeMode": "Theme Mode:", + "auto": "Auto", + "light": "Light", + "dark": "Dark", + "autoTooltip": "Auto (Follow System)", + "lightTooltip": "Light Mode", + "darkTooltip": "Dark Mode", + "themeModeDesc": "Choose your preferred color theme. Auto will automatically switch between light and dark mode based on your system preferences.", + "timerColors": "Timer Colors", + "timerTheme": "Timer Theme:", + "timerThemeDesc": "Choose a color theme for your timer sessions. Some themes work only in light or dark mode." + }, + "automation": { + "title": "Automation", + "description": "Configure automatic behaviors and smart features", + "timerAutomation": "Timer Automation", + "autoStartTimer": "Auto-start Timer", + "autoStartTimerDesc": "Automatically start the timer when manually skipping to the next session (using the skip button).", + "allowContinuousSessions": "Allow Continuous Sessions", + "allowContinuousSessionsDesc": "Allow all sessions (focus, breaks, and long breaks) to continue beyond their timer duration. When enabled, you can manually decide when to move to the next phase instead of being automatically transitioned.", + "smartFeatures": "Smart Features", + "smartPause": "Smart Pause (Auto-pause when inactive)", + "smartPauseDesc": "Automatically pause the timer during focus sessions when no mouse or keyboard activity is detected, and resume when activity resumes. Only works during focus periods, not breaks.", + "inactivityTimeout": "Inactivity Timeout:", + "seconds": "seconds", + "inactivityTimeoutDesc": "How long to wait before pausing during inactivity", + "sessionManagement": "Session Management", + "autoSaveSessions": "Auto-save Completed Sessions", + "autoSaveSessionsDesc": "Automatically save session data when timer completes without manual confirmation.", + "preventInterruptions": "Prevent Interruptions", + "preventInterruptionsDesc": "Show confirmation dialog before allowing session resets during active focus periods." + }, + "goals": { + "title": "Weekly Goals", + "description": "Set your weekly focus time targets and productivity goals", + "focusGoals": "Focus Goals", + "weeklyGoal": "Weekly Goal (minutes):", + "weeklyGoalDesc": "Target focus time for Monday through Friday (default: 125 minutes = 5 sessions of 25 minutes). Saturday and Sunday are excluded from calculation." + }, + "advanced": { + "title": "Advanced Settings", + "description": "Danger zone and advanced configuration options", + "systemIntegration": "System Integration", + "startWithSystem": "Start with System", + "startWithSystemDesc": "Automatically start Presto when your computer boots up. The app will start minimized to the system tray.", + "privacyAnalytics": "Privacy & Analytics", + "enableAnonymousStats": "Enable Anonymous Statistics", + "enableAnonymousStatsDesc": "Help improve Presto by sharing anonymous usage statistics. No personal data is collected.", + "developerTools": "🔧 Developer Tools", + "debugMode": "Debug Mode (3-second timers)", + "debugModeDesc": "Enable debug mode where all timers (focus, break, long break) are set to 3 seconds for rapid testing. Perfect for development and feature testing.", + "dangerZone": "⚠️ Danger Zone", + "dangerZoneDesc": "These actions are irreversible and will permanently delete your data.", + "resetAllData": "🗑️ Reset All Data", + "resetAllDataDesc": "This will permanently delete all your Pomodoro sessions, tasks, statistics, and reset all settings to default. This action cannot be undone." + }, + "updates": { + "title": "App Updates", + "description": "Manage application updates and version information", + "currentVersion": "Current Version", + "installedVersion": "Installed Version:", + "checkingForUpdates": "Checking for updates...", + "checkForUpdates": "Check for Updates", + "updateSettings": "Update Settings", + "automaticallyCheckUpdates": "Automatically check for updates", + "automaticallyCheckUpdatesDesc": "Check for new versions automatically when the app starts and every hour. You'll be notified when updates are available.", + "includePrerelease": "Include pre-release versions", + "includePrereleaseDesc": "Also check for beta and pre-release versions. These may contain new features but could be less stable.", + "releaseInformation": "Release Information", + "releaseInfoDesc": "Updates are distributed through GitHub releases.", + "viewAllReleases": "View all releases on GitHub", + "updateSource": "Update Source:", + "githubReleases": "GitHub Releases", + "pleaseWait": "Please wait while we check for the latest version.", + "updateAvailable": "Update Available", + "current": "Current:", + "latest": "Latest:", + "downloadInstall": "Download & Install", + "skipVersion": "Skip This Version" + }, + "settingsAutoSave": "✓ Settings are saved automatically", + "resetToDefaults": "Reset to Defaults" + }, + "modal": { + "addSession": "Add Session", + "durationMinutes": "Duration (minutes)", + "startTime": "Start Time", + "endTime": "End Time", + "cancel": "Cancel", + "delete": "Delete", + "saveSession": "Save Session" + }, + "common": { + "close": "Close", + "save": "Save", + "cancel": "Cancel", + "delete": "Delete", + "export": "Export", + "minutes": "minutes", + "seconds": "seconds", + "hours": "hours" + } +} \ No newline at end of file diff --git a/messages/it.json b/messages/it.json new file mode 100644 index 0000000..a7a450e --- /dev/null +++ b/messages/it.json @@ -0,0 +1,224 @@ +{ + "$schema": "https://inlang.com/schema/inlang-message-format", + "app": { + "title": "Presto" + }, + "navigation": { + "timer": "Timer", + "calendar": "Calendario", + "team": "Team (Prossimamente)", + "settings": "Impostazioni" + }, + "user": { + "avatar": "Avatar Utente", + "guest": "Ospite", + "guestMode": "Modalità Ospite", + "signIn": "Accedi", + "signOut": "Esci" + }, + "timer": { + "focus": "Concentrazione", + "chooseTag": "Scegli tag", + "newTag": "Nuovo tag...", + "smartPause": "Pausa Intelligente: Clicca per attivare/disattivare la pausa automatica quando inattivo", + "autoStart": "Avvio Automatico: Clicca per attivare/disattivare l'avvio automatico della sessione", + "continuousSessions": "Sessioni Continue: Clicca per attivare/disattivare la modalità continua", + "subtract5Minutes": "Sottrai 5 minuti", + "add5Minutes": "Aggiungi 5 minuti" + }, + "calendar": { + "title": "Calendario e Statistiche", + "weeklyFocusSummary": "Riepilogo Settimanale di Concentrazione", + "weeklyFocusTime": "Tempo di concentrazione settimanale", + "averageFocusDay": "Media concentrazione/giorno", + "sessionsThisWeek": "Sessioni questa settimana", + "weeklyTotalTime": "Tempo totale settimanale", + "thisWeeksSessions": "Sessioni di Questa Settimana", + "todaysDevelopment": "Sviluppo di Oggi", + "focusSessions": "Sessioni di concentrazione", + "breakTime": "Tempo di pausa", + "tagUsageThisWeek": "Utilizzo Tag Questa Settimana", + "noDataAvailable": "Nessun dato disponibile", + "todaysSessions": "Sessioni di Oggi", + "addSession": "Aggiungi Sessione", + "sessionHistory": "Cronologia Sessioni", + "export": "Esporta", + "exportToExcel": "Esporta in Excel", + "date": "Data", + "time": "Ora", + "duration": "Durata", + "tags": "Tag", + "actions": "Azioni", + "days": { + "mon": "Lun", + "tue": "Mar", + "wed": "Mer", + "thu": "Gio", + "fri": "Ven", + "sat": "Sab", + "sun": "Dom" + } + }, + "team": { + "title": "Dashboard del Team", + "subtitle": "Monitora lo stato di concentrazione del tuo team in tempo reale", + "focusing": "Concentrandosi", + "onBreak": "In Pausa", + "privacyMode": "Modalità Privacy", + "offline": "Offline", + "teamsOverview": "Panoramica Team" + }, + "settings": { + "title": "Impostazioni", + "categories": { + "general": "Generale", + "shortcuts": "Scorciatoie", + "notifications": "Notifiche", + "theme": "Tema", + "automation": "Automazione", + "goals": "Obiettivi", + "advanced": "Avanzate", + "updates": "Aggiornamenti" + }, + "general": { + "title": "Impostazioni Generali", + "description": "Configura la durata del timer e il comportamento di base", + "timerDurations": "Durata Timer", + "focusDuration": "Durata Concentrazione (minuti):", + "focusDurationDesc": "Quanto dovrebbero durare le sessioni di concentrazione", + "shortBreak": "Pausa Breve (minuti):", + "shortBreakDesc": "Durata per le pause brevi tra le sessioni di concentrazione", + "longBreak": "Pausa Lunga (minuti):", + "longBreakDesc": "Durata per le pause lunghe dopo aver completato un ciclo", + "dailySessions": "Sessioni Giornaliere:", + "dailySessionsDesc": "Numero di sessioni di concentrazione da completare ogni giorno", + "maxSessionTime": "Tempo Massimo Sessione (minuti):", + "maxSessionTimeDesc": "Tempo massimo per sessione prima della pausa automatica (predefinito: 2 ore)" + }, + "shortcuts": { + "title": "Scorciatoie Globali", + "description": "Configura le scorciatoie da tastiera che funzionano anche quando l'app è in background", + "keyboardShortcuts": "Scorciatoie da Tastiera", + "startStop": "Avvia/Ferma Timer:", + "startStopDesc": "Alterna il timer tra stati di avvio e pausa", + "deleteUndo": "Elimina Sessione / Annulla:", + "deleteUndoDesc": "Reimposta la sessione corrente o annulla durante le pause", + "saveSession": "Salva Sessione:", + "saveSessionDesc": "Passa alla fase successiva e salva il progresso corrente" + }, + "notifications": { + "title": "Notifiche", + "description": "Controlla come e quando ricevere le notifiche", + "notificationTypes": "Tipi di Notifiche", + "desktop": "Notifiche Desktop", + "desktopDesc": "Mostra notifiche di sistema quando il timer si completa. Sarà richiesto il permesso del browser quando abilitato.", + "sound": "Notifiche Audio", + "soundDesc": "Riproduci un suono quando le fasi del timer si completano" + }, + "theme": { + "title": "Tema", + "description": "Personalizza l'aspetto e lo stile visivo dell'applicazione", + "colorTheme": "Tema Colore", + "themeMode": "Modalità Tema:", + "auto": "Automatico", + "light": "Chiaro", + "dark": "Scuro", + "autoTooltip": "Automatico (Segui Sistema)", + "lightTooltip": "Modalità Chiara", + "darkTooltip": "Modalità Scura", + "themeModeDesc": "Scegli il tuo tema colore preferito. Automatico cambierà automaticamente tra modalità chiara e scura basandosi sulle preferenze del tuo sistema.", + "timerColors": "Colori Timer", + "timerTheme": "Tema Timer:", + "timerThemeDesc": "Scegli un tema colore per le tue sessioni timer. Alcuni temi funzionano solo in modalità chiara o scura." + }, + "automation": { + "title": "Automazione", + "description": "Configura comportamenti automatici e funzionalità intelligenti", + "timerAutomation": "Automazione Timer", + "autoStartTimer": "Avvio Automatico Timer", + "autoStartTimerDesc": "Avvia automaticamente il timer quando si salta manualmente alla sessione successiva (usando il pulsante salta).", + "allowContinuousSessions": "Permetti Sessioni Continue", + "allowContinuousSessionsDesc": "Permetti a tutte le sessioni (concentrazione, pause e pause lunghe) di continuare oltre la loro durata timer. Quando abilitato, puoi decidere manualmente quando passare alla fase successiva invece di essere automaticamente trasferito.", + "smartFeatures": "Funzionalità Intelligenti", + "smartPause": "Pausa Intelligente (Pausa automatica quando inattivo)", + "smartPauseDesc": "Metti automaticamente in pausa il timer durante le sessioni di concentrazione quando non viene rilevata attività del mouse o della tastiera, e riprendi quando l'attività riprende. Funziona solo durante i periodi di concentrazione, non durante le pause.", + "inactivityTimeout": "Timeout Inattività:", + "seconds": "secondi", + "inactivityTimeoutDesc": "Quanto aspettare prima di mettere in pausa durante l'inattività", + "sessionManagement": "Gestione Sessioni", + "autoSaveSessions": "Salvataggio Automatico Sessioni Completate", + "autoSaveSessionsDesc": "Salva automaticamente i dati della sessione quando il timer si completa senza conferma manuale.", + "preventInterruptions": "Previeni Interruzioni", + "preventInterruptionsDesc": "Mostra dialogo di conferma prima di permettere reset delle sessioni durante periodi di concentrazione attivi." + }, + "goals": { + "title": "Obiettivi Settimanali", + "description": "Imposta i tuoi obiettivi di tempo di concentrazione settimanale e produttività", + "focusGoals": "Obiettivi di Concentrazione", + "weeklyGoal": "Obiettivo Settimanale (minuti):", + "weeklyGoalDesc": "Tempo di concentrazione target da lunedì a venerdì (predefinito: 125 minuti = 5 sessioni di 25 minuti). Sabato e domenica sono esclusi dal calcolo." + }, + "advanced": { + "title": "Impostazioni Avanzate", + "description": "Zona pericolosa e opzioni di configurazione avanzate", + "systemIntegration": "Integrazione Sistema", + "startWithSystem": "Avvia con Sistema", + "startWithSystemDesc": "Avvia automaticamente Presto quando il tuo computer si avvia. L'app si avvierà minimizzata nella barra di sistema.", + "privacyAnalytics": "Privacy e Analisi", + "enableAnonymousStats": "Abilita Statistiche Anonime", + "enableAnonymousStatsDesc": "Aiuta a migliorare Presto condividendo statistiche di utilizzo anonime. Nessun dato personale viene raccolto.", + "developerTools": "🔧 Strumenti Sviluppatore", + "debugMode": "Modalità Debug (timer da 3 secondi)", + "debugModeDesc": "Abilita la modalità debug dove tutti i timer (concentrazione, pausa, pausa lunga) sono impostati a 3 secondi per test rapidi. Perfetto per sviluppo e test di funzionalità.", + "dangerZone": "⚠️ Zona Pericolosa", + "dangerZoneDesc": "Queste azioni sono irreversibili e cancelleranno permanentemente i tuoi dati.", + "resetAllData": "🗑️ Reimposta Tutti i Dati", + "resetAllDataDesc": "Questo cancellerà permanentemente tutte le tue sessioni Pomodoro, attività, statistiche e reimpostará tutte le impostazioni al valore predefinito. Questa azione non può essere annullata." + }, + "updates": { + "title": "Aggiornamenti App", + "description": "Gestisci aggiornamenti dell'applicazione e informazioni sulla versione", + "currentVersion": "Versione Corrente", + "installedVersion": "Versione Installata:", + "checkingForUpdates": "Controllo aggiornamenti...", + "checkForUpdates": "Controlla Aggiornamenti", + "updateSettings": "Impostazioni Aggiornamento", + "automaticallyCheckUpdates": "Controlla automaticamente gli aggiornamenti", + "automaticallyCheckUpdatesDesc": "Controlla nuove versioni automaticamente quando l'app si avvia e ogni ora. Sarai notificato quando gli aggiornamenti sono disponibili.", + "includePrerelease": "Includi versioni pre-release", + "includePrereleaseDesc": "Controlla anche per versioni beta e pre-release. Potrebbero contenere nuove funzionalità ma potrebbero essere meno stabili.", + "releaseInformation": "Informazioni Release", + "releaseInfoDesc": "Gli aggiornamenti sono distribuiti attraverso le release GitHub.", + "viewAllReleases": "Visualizza tutte le release su GitHub", + "updateSource": "Sorgente Aggiornamento:", + "githubReleases": "Release GitHub", + "pleaseWait": "Attendere prego mentre controlliamo l'ultima versione.", + "updateAvailable": "Aggiornamento Disponibile", + "current": "Corrente:", + "latest": "Ultimo:", + "downloadInstall": "Scarica e Installa", + "skipVersion": "Salta Questa Versione" + }, + "settingsAutoSave": "✓ Le impostazioni sono salvate automaticamente", + "resetToDefaults": "Reimposta ai Valori Predefiniti" + }, + "modal": { + "addSession": "Aggiungi Sessione", + "durationMinutes": "Durata (minuti)", + "startTime": "Ora Inizio", + "endTime": "Ora Fine", + "cancel": "Annulla", + "delete": "Elimina", + "saveSession": "Salva Sessione" + }, + "common": { + "close": "Chiudi", + "save": "Salva", + "cancel": "Annulla", + "delete": "Elimina", + "export": "Esporta", + "minutes": "minuti", + "seconds": "secondi", + "hours": "ore" + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 9a3b60a..e807f64 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "presto", - "version": "0.2.17", + "version": "0.3.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "presto", - "version": "0.2.17", + "version": "0.3.10", "dependencies": { "@aptabase/tauri": "^0.4.1", "@tauri-apps/api": "^2.5.0", @@ -18,6 +18,7 @@ "xlsx": "^0.18.5" }, "devDependencies": { + "@inlang/paraglide-js": "2.2.0", "@tauri-apps/cli": "^2.5.0" } }, @@ -44,6 +45,95 @@ "url": "https://opencollective.com/tauri" } }, + "node_modules/@inlang/paraglide-js": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@inlang/paraglide-js/-/paraglide-js-2.2.0.tgz", + "integrity": "sha512-pkpXu1LanvpcAbvpVPf7PgF11Uq7DliSEBngrcUN36l4ZOOpzn3QBTvVr/tJxvks0O67WseQgiMHet8KH7Oz5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inlang/recommend-sherlock": "0.2.1", + "@inlang/sdk": "2.4.9", + "commander": "11.1.0", + "consola": "3.4.0", + "json5": "2.2.3", + "unplugin": "^2.1.2", + "urlpattern-polyfill": "^10.0.0" + }, + "bin": { + "paraglide-js": "bin/run.js" + } + }, + "node_modules/@inlang/recommend-sherlock": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@inlang/recommend-sherlock/-/recommend-sherlock-0.2.1.tgz", + "integrity": "sha512-ckv8HvHy/iTqaVAEKrr+gnl+p3XFNwe5D2+6w6wJk2ORV2XkcRkKOJ/XsTUJbPSiyi4PI+p+T3bqbmNx/rDUlg==", + "dev": true, + "license": "MIT", + "dependencies": { + "comment-json": "^4.2.3" + } + }, + "node_modules/@inlang/sdk": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/@inlang/sdk/-/sdk-2.4.9.tgz", + "integrity": "sha512-cvz/C1rF5WBxzHbEoiBoI6Sz6q6M+TdxfWkEGBYTD77opY8i8WN01prUWXEM87GPF4SZcyIySez9U0Ccm12oFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@lix-js/sdk": "0.4.7", + "@sinclair/typebox": "^0.31.17", + "kysely": "^0.27.4", + "sqlite-wasm-kysely": "0.3.0", + "uuid": "^10.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@lix-js/sdk": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@lix-js/sdk/-/sdk-0.4.7.tgz", + "integrity": "sha512-pRbW+joG12L0ULfMiWYosIW0plmW4AsUdiPCp+Z8rAsElJ+wJ6in58zhD3UwUcd4BNcpldEGjg6PdA7e0RgsDQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@lix-js/server-protocol-schema": "0.1.1", + "dedent": "1.5.1", + "human-id": "^4.1.1", + "js-sha256": "^0.11.0", + "kysely": "^0.27.4", + "sqlite-wasm-kysely": "0.3.0", + "uuid": "^10.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@lix-js/server-protocol-schema": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@lix-js/server-protocol-schema/-/server-protocol-schema-0.1.1.tgz", + "integrity": "sha512-jBeALB6prAbtr5q4vTuxnRZZv1M2rKe8iNqRQhFJ4Tv7150unEa0vKyz0hs8Gl3fUGsWaNJBh3J8++fpbrpRBQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@sinclair/typebox": { + "version": "0.31.28", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.31.28.tgz", + "integrity": "sha512-/s55Jujywdw/Jpan+vsy6JZs1z2ZTGxTmbZTPiuSL2wz9mfzA2gN1zzaqmvfi4pq+uOt7Du85fkiwv5ymW84aQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sqlite.org/sqlite-wasm": { + "version": "3.48.0-build4", + "resolved": "https://registry.npmjs.org/@sqlite.org/sqlite-wasm/-/sqlite-wasm-3.48.0-build4.tgz", + "integrity": "sha512-hI6twvUkzOmyGZhQMza1gpfqErZxXRw6JEsiVjUbo7tFanVD+8Oil0Ih3l2nGzHdxPI41zFmfUQG7GHqhciKZQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "sqlite-wasm": "bin/index.js" + } + }, "node_modules/@tauri-apps/api": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.5.0.tgz", @@ -316,6 +406,19 @@ "@tauri-apps/api": "^2.0.0" } }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/adler-32": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", @@ -325,6 +428,13 @@ "node": ">=0.8" } }, + "node_modules/array-timsort": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", + "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cfb": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", @@ -347,6 +457,50 @@ "node": ">=0.8" } }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/comment-json": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.2.5.tgz", + "integrity": "sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-timsort": "^1.0.3", + "core-util-is": "^1.0.3", + "esprima": "^4.0.1", + "has-own-prop": "^2.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/consola": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.0.tgz", + "integrity": "sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, "node_modules/crc-32": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", @@ -359,6 +513,35 @@ "node": ">=0.8" } }, + "node_modules/dedent": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", + "integrity": "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/frac": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", @@ -368,6 +551,91 @@ "node": ">=0.8" } }, + "node_modules/has-own-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-own-prop/-/has-own-prop-2.0.0.tgz", + "integrity": "sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/human-id": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/human-id/-/human-id-4.1.1.tgz", + "integrity": "sha512-3gKm/gCSUipeLsRYZbbdA1BD83lBoWUkZ7G9VFrhWPAU76KwYo5KR8V28bpoPm/ygy0x5/GCbpRQdY7VLYCoIg==", + "dev": true, + "license": "MIT", + "bin": { + "human-id": "dist/cli.js" + } + }, + "node_modules/js-sha256": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.11.1.tgz", + "integrity": "sha512-o6WSo/LUvY2uC4j7mO50a2ms7E/EAdbP0swigLV+nzHKTTaYnaLIWJ02VdXrsJX0vGedDESQnLsOekr94ryfjg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kysely": { + "version": "0.27.6", + "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.27.6.tgz", + "integrity": "sha512-FIyV/64EkKhJmjgC0g2hygpBv5RNWVPyNCqSAD7eTCv6eFWNIi4PN1UvdSJGicN/o35bnevgis4Y0UDC0qi8jQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/sqlite-wasm-kysely": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/sqlite-wasm-kysely/-/sqlite-wasm-kysely-0.3.0.tgz", + "integrity": "sha512-TzjBNv7KwRw6E3pdKdlRyZiTmUIE0UttT/Sl56MVwVARl/u5gp978KepazCJZewFUnlWHz9i3NQd4kOtP/Afdg==", + "dev": true, + "dependencies": { + "@sqlite.org/sqlite-wasm": "^3.48.0-build2" + }, + "peerDependencies": { + "kysely": "*" + } + }, "node_modules/ssf": { "version": "0.11.2", "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", @@ -380,6 +648,49 @@ "node": ">=0.8" } }, + "node_modules/unplugin": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.5.tgz", + "integrity": "sha512-RyWSb5AHmGtjjNQ6gIlA67sHOsWpsbWpwDokLwTcejVdOjEkJZh7QKu14J00gDDVSh8kGH4KYC/TNBceXFZhtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.14.1", + "picomatch": "^4.0.2", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=18.12.0" + } + }, + "node_modules/urlpattern-polyfill": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.1.0.tgz", + "integrity": "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true, + "license": "MIT" + }, "node_modules/wmf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", diff --git a/package.json b/package.json index d5c1375..e613dac 100644 --- a/package.json +++ b/package.json @@ -6,13 +6,14 @@ "scripts": { "tauri": "tauri", "dev": "tauri dev", - "build": "tauri build", + "build": "paraglide-js compile --project ./project.inlang --outdir ./src\\paraglide && tauri build", "build-themes": "node build-themes.js", "prebuild": "npm run build-themes", "predev": "npm run build-themes" }, "devDependencies": { - "@tauri-apps/cli": "^2.5.0" + "@tauri-apps/cli": "^2.5.0", + "@inlang/paraglide-js": "2.2.0" }, "dependencies": { "@aptabase/tauri": "^0.4.1", diff --git a/project.inlang/.gitignore b/project.inlang/.gitignore new file mode 100644 index 0000000..5e46596 --- /dev/null +++ b/project.inlang/.gitignore @@ -0,0 +1 @@ +cache \ No newline at end of file diff --git a/project.inlang/project_id b/project.inlang/project_id new file mode 100644 index 0000000..5120223 --- /dev/null +++ b/project.inlang/project_id @@ -0,0 +1 @@ +FlJyzS1qEspv6pPKeA \ No newline at end of file diff --git a/project.inlang/settings.json b/project.inlang/settings.json new file mode 100644 index 0000000..2a5e312 --- /dev/null +++ b/project.inlang/settings.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://inlang.com/schema/project-settings", + "baseLocale": "en", + "locales": [ + "en", + "it" + ], + "modules": [ + "https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js", + "https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@2/dist/index.js" + ], + "plugin.inlang.messageFormat": { + "pathPattern": "./messages/{locale}.json" + } +} \ No newline at end of file diff --git a/src/index.html b/src/index.html index 23a8dd2..382b21a 100644 --- a/src/index.html +++ b/src/index.html @@ -1,1005 +1,1490 @@ - + - - - - - - - - - - Presto - - - - - - - - - - - -
- -
- -
-
- -
-
+
+ + + - - - - - - - \ No newline at end of file + + + + diff --git a/src/main.js b/src/main.js index 2a22798..7a31e35 100644 --- a/src/main.js +++ b/src/main.js @@ -6,7 +6,7 @@ import { TeamManager } from './managers/team-manager.js'; // Auth manager will be imported after Supabase is loaded import { PomodoroTimer } from './core/pomodoro-timer.js'; import { NotificationUtils } from './utils/common-utils.js'; -// Removed unused import: updateNotification +import { localizationManager } from './utils/localization.js'; // Global application state let timer = null; @@ -15,6 +15,9 @@ let settingsManager = null; let sessionManager = null; let teamManager = null; +// Initialize localization first +localizationManager.init(); + // Global functions for settings (backwards compatibility) window.saveSettings = async function () { if (window.settingsManager) { diff --git a/src/styles/localization.css b/src/styles/localization.css new file mode 100644 index 0000000..43f057b --- /dev/null +++ b/src/styles/localization.css @@ -0,0 +1,74 @@ +/* Language Switcher Styles */ +.locale-switcher-container { + margin-top: 1rem; + padding: 0.5rem; + border-top: 1px solid var(--border-color); +} + +.locale-switcher-container label { + display: block; + margin-bottom: 0.5rem; + font-size: 0.9rem; + color: var(--text-secondary); +} + +.locale-switcher { + width: 100%; + padding: 0.5rem; + border: 1px solid var(--border-color); + border-radius: 4px; + background: var(--background-color); + color: var(--text-color); + font-size: 0.9rem; + cursor: pointer; + transition: border-color 0.2s ease; +} + +.locale-switcher:hover { + border-color: var(--primary-color); +} + +.locale-switcher:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(var(--primary-color-rgb), 0.2); +} + +/* Settings section language switcher */ +.settings-section .locale-switcher { + max-width: 200px; +} + +/* Language loading indicator */ +.language-loading { + opacity: 0.6; + pointer-events: none; +} + +.language-loading::after { + content: ""; + display: inline-block; + width: 12px; + height: 12px; + margin-left: 8px; + border: 2px solid var(--text-secondary); + border-top: 2px solid var(--primary-color); + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +/* Responsive language switcher */ +@media (max-width: 768px) { + .locale-switcher { + font-size: 16px; /* Prevent zoom on iOS */ + } +} diff --git a/src/utils/dynamic-text-updater.js b/src/utils/dynamic-text-updater.js new file mode 100644 index 0000000..334a700 --- /dev/null +++ b/src/utils/dynamic-text-updater.js @@ -0,0 +1,141 @@ +import { localizationManager } from './localization.js'; +import * as m from '../paraglide/messages.js'; + +/** + * Dynamic Text Updater + * Updates text content when timer state changes or new content is added + */ +export class DynamicTextUpdater { + constructor(localizationManager) { + this.localizationManager = localizationManager; + this.observers = new Map(); + this.setupMutationObserver(); + } + + /** + * Setup mutation observer to detect DOM changes + */ + setupMutationObserver() { + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.type === 'childList') { + // Check if new elements were added that need localization + mutation.addedNodes.forEach((node) => { + if (node.nodeType === Node.ELEMENT_NODE) { + this.localizeNewElement(node); + } + }); + } + }); + }); + + observer.observe(document.body, { + childList: true, + subtree: true + }); + } + + /** + * Localize newly added elements + * @param {Element} element - The newly added element + */ + localizeNewElement(element) { + // Check for elements with data-i18n attributes + const elementsToLocalize = element.querySelectorAll('[data-i18n]'); + elementsToLocalize.forEach(el => { + const key = el.getAttribute('data-i18n'); + const messageFunction = this.getMessageFunction(key); + if (messageFunction) { + el.textContent = messageFunction(); + } + }); + + // Check for specific dynamic elements + if (element.classList.contains('session-item')) { + this.localizeSessionItem(element); + } + + if (element.classList.contains('tag-item')) { + this.localizeTagItem(element); + } + } + + /** + * Get message function by key + * @param {string} key - The message key (e.g., 'timer.focus') + * @returns {Function|null} The message function or null + */ + getMessageFunction(key) { + const parts = key.split('.'); + let current = this.localizationManager.messages; + + for (const part of parts) { + if (current && current[part]) { + current = current[part]; + } else { + return null; + } + } + + return typeof current === 'function' ? current : null; + } + + /** + * Localize session item + * @param {Element} element - Session item element + */ + localizeSessionItem(element) { + // Update session-specific text content + const deleteBtn = element.querySelector('.delete-session-btn'); + if (deleteBtn) { + deleteBtn.textContent = m['common.delete'](); + } + } + + /** + * Localize tag item + * @param {Element} element - Tag item element + */ + localizeTagItem(element) { + // Update tag-specific text content if needed + // This would be implemented based on your tag item structure + } + + /** + * Update timer status text based on current state + * @param {string} status - Timer status ('focus', 'break', 'longBreak') + */ + updateTimerStatus(status) { + const statusElement = document.getElementById('status-text'); + if (statusElement) { + switch (status) { + case 'focus': + statusElement.textContent = m['timer.focus'](); + break; + case 'break': + statusElement.textContent = 'Break'; // Add to messages + break; + case 'longBreak': + statusElement.textContent = 'Long Break'; // Add to messages + break; + } + } + } + + /** + * Update progress indicators + * @param {number} completed - Number of completed sessions + * @param {number} total - Total sessions for the day + */ + updateProgressIndicators(completed, total) { + // Update any progress text that needs localization + const progressElements = document.querySelectorAll('.progress-text'); + progressElements.forEach(el => { + // Update with localized progress text + el.textContent = `${completed}/${total} sessions`; // Add to messages + }); + } +} + +// Export for use in other modules +export const dynamicTextUpdater = new DynamicTextUpdater(localizationManager); diff --git a/src/utils/localization.js b/src/utils/localization.js new file mode 100644 index 0000000..5e0caa5 --- /dev/null +++ b/src/utils/localization.js @@ -0,0 +1,352 @@ +import { setLocale, getLocale, locales } from '../paraglide/runtime.js'; +import * as m from '../paraglide/messages.js'; + +/** + * Localization Manager for Presto App + * Handles language switching and text localization + */ +export class LocalizationManager { + constructor() { + this.currentLocale = getLocale(); + this.init(); + } + + /** + * Initialize localization system + */ + init() { + // Load saved locale from localStorage or use default + const savedLocale = localStorage.getItem('app-locale'); + if (savedLocale && locales.includes(savedLocale)) { + this.setLocale(savedLocale); + } + + // Update all text content on page + this.updateAllText(); + + // Set up locale switcher if exists + this.setupLocaleSwitcher(); + } + + /** + * Set the current locale + * @param {string} locale - The locale to set (e.g., 'en', 'it') + */ + setLocale(locale) { + if (!locales.includes(locale)) { + console.warn(`Locale '${locale}' is not supported. Available locales: ${locales.join(', ')}`); + return; + } + + setLocale(locale); + this.currentLocale = locale; + + // Save to localStorage + localStorage.setItem('app-locale', locale); + + // Update HTML lang attribute + document.documentElement.lang = locale; + + // Update all text content + this.updateAllText(); + } + + /** + * Get current locale + * @returns {string} Current locale + */ + getLocale() { + return this.currentLocale; + } + + /** + * Get available locales + * @returns {string[]} Array of available locales + */ + getAvailableLocales() { + return [...locales]; + } + + /** + * Update all text content on the page + */ + updateAllText() { + // Update page title + document.title = m['app.title'](); + + // Update navigation + this.updateElement('timer-nav', 'title', m['navigation.timer']()); + this.updateElement('calendar-nav', 'title', m['navigation.calendar']()); + this.updateElement('team-nav', 'title', m['navigation.team']()); + this.updateElement('settings-nav', 'title', m['navigation.settings']()); + + // Update user section + this.updateElement('user-avatar-img', 'alt', m['user.avatar']()); + this.updateElement('user-name', 'textContent', m['user.guest']()); + this.updateElement('user-status', 'textContent', m['user.guestMode']()); + this.updateElement('user-sign-in', 'textContent', m['user.signIn']()); + this.updateElement('user-sign-out', 'textContent', m['user.signOut']()); + + // Update timer section + this.updateElement('status-text', 'textContent', m['timer.focus']()); + this.updateElement('smart-indicator', 'data-tooltip', m['timer.smartPause']()); + this.updateElement('auto-start-indicator', 'data-tooltip', m['timer.autoStart']()); + this.updateElement('continuous-session-indicator', 'data-tooltip', m['timer.continuousSessions']()); + this.updateElement('timer-minus-btn', 'title', m['timer.subtract5Minutes']()); + this.updateElement('timer-plus-btn', 'title', m['timer.add5Minutes']()); + + // Update tag dropdown + const tagDropdownHeader = document.querySelector('.tag-dropdown-header span'); + if (tagDropdownHeader) { + tagDropdownHeader.textContent = m['timer.chooseTag'](); + } + + const newTagInput = document.getElementById('new-tag-name'); + if (newTagInput) { + newTagInput.placeholder = m['timer.newTag'](); + } + + // Update calendar section + this.updateCalendarSection(); + + // Update team section + this.updateTeamSection(); + + // Update settings section + this.updateSettingsSection(); + + // Update modal + this.updateModalSection(); + } + + /** + * Update calendar section text + */ + updateCalendarSection() { + const calendarView = document.getElementById('calendar-view'); + if (calendarView) { + const title = calendarView.querySelector('h1'); + if (title) title.textContent = m['calendar.title'](); + + // Update summary labels + this.updateElement('total-focus-week', 'textContent', null, '.metric-label', m['calendar.weeklyFocusTime']()); + this.updateElement('avg-focus-day', 'textContent', null, '.metric-label', m['calendar.averageFocusDay']()); + + // Update chart titles + const weeklyChartTitle = document.querySelector('.weekly-chart-card h3'); + if (weeklyChartTitle) weeklyChartTitle.textContent = m['calendar.thisWeeksSessions'](); + + const dailyChartTitle = document.querySelector('.daily-chart-card h3'); + if (dailyChartTitle) dailyChartTitle.textContent = m['calendar.todaysDevelopment'](); + + // Update legend + const focusLegend = document.querySelector('.legend-item .legend-color.focus'); + if (focusLegend && focusLegend.nextSibling) { + focusLegend.nextSibling.textContent = m['calendar.focusSessions'](); + } + + const breakLegend = document.querySelector('.legend-item .legend-color.break'); + if (breakLegend && breakLegend.nextSibling) { + breakLegend.nextSibling.textContent = m['calendar.breakTime'](); + } + + // Update day labels + const dayLabels = document.querySelectorAll('.week-days-labels span'); + const dayMessages = [ + m['calendar.days.mon'](), + m['calendar.days.tue'](), + m['calendar.days.wed'](), + m['calendar.days.thu'](), + m['calendar.days.fri'](), + m['calendar.days.sat'](), + m['calendar.days.sun']() + ]; + dayLabels.forEach((label, index) => { + if (dayMessages[index]) { + label.textContent = dayMessages[index]; + } + }); + + // Update table headers + const tableHeaders = document.querySelectorAll('#sessions-table th'); + const headerMessages = [ + m['calendar.date'](), + m['calendar.time'](), + m['calendar.duration'](), + m['calendar.tags'](), + m['calendar.actions']() + ]; + tableHeaders.forEach((header, index) => { + if (headerMessages[index]) { + header.textContent = headerMessages[index]; + } + }); + } + } + + /** + * Update team section text + */ + updateTeamSection() { + const teamTitle = document.querySelector('#team-view .page-header'); + if (teamTitle) teamTitle.textContent = m['team.title'](); + + const teamSubtitle = document.querySelector('#team-view .team-subtitle'); + if (teamSubtitle) teamSubtitle.textContent = m['team.subtitle'](); + + // Update team stat labels + const statLabels = document.querySelectorAll('.team-stat-card .stat-label'); + const statMessages = [ + m['team.focusing'](), + m['team.onBreak'](), + m['team.privacyMode'](), + m['team.offline']() + ]; + statLabels.forEach((label, index) => { + if (statMessages[index]) { + label.textContent = statMessages[index]; + } + }); + + const teamsOverview = document.querySelector('.team-members-container h2'); + if (teamsOverview) teamsOverview.textContent = m['team.teamsOverview'](); + } + + /** + * Update settings section text + */ + updateSettingsSection() { + // Update settings navigation + const settingsNav = document.querySelectorAll('.settings-nav-item span'); + const navMessages = [ + m['settings.categories.general'](), + m['settings.categories.shortcuts'](), + m['settings.categories.notifications'](), + m['settings.categories.theme'](), + m['settings.categories.automation'](), + m['settings.categories.goals'](), + m['settings.categories.advanced'](), + m['settings.categories.updates']() + ]; + settingsNav.forEach((nav, index) => { + if (navMessages[index]) { + nav.textContent = navMessages[index]; + } + }); + + // Update settings main title + const settingsTitle = document.querySelector('.settings-sidebar h2'); + if (settingsTitle) settingsTitle.textContent = m['settings.title'](); + + // Update category titles and descriptions + this.updateSettingsCategories(); + } + + /** + * Update settings categories + */ + updateSettingsCategories() { + // General settings + const generalTitle = document.querySelector('#category-general .category-header h1'); + if (generalTitle) generalTitle.textContent = m['settings.general.title'](); + + const generalDesc = document.querySelector('#category-general .category-description'); + if (generalDesc) generalDesc.textContent = m['settings.general.description'](); + + // Update more settings sections as needed... + // This would be quite extensive, so I'll show the pattern + } + + /** + * Update modal section text + */ + updateModalSection() { + this.updateElement('session-modal-title', 'textContent', m['modal.addSession']()); + + const durationLabel = document.querySelector('label[for="session-duration"]'); + if (durationLabel) durationLabel.textContent = m['modal.durationMinutes'](); + + const startTimeLabel = document.querySelector('label[for="session-start-time"]'); + if (startTimeLabel) startTimeLabel.textContent = m['modal.startTime'](); + + const endTimeLabel = document.querySelector('label[for="session-end-time"]'); + if (endTimeLabel) endTimeLabel.textContent = m['modal.endTime'](); + + this.updateElement('cancel-session-btn', 'textContent', m['modal.cancel']()); + this.updateElement('delete-session-btn', 'textContent', m['modal.delete']()); + this.updateElement('save-session-btn', 'textContent', m['modal.saveSession']()); + } + + /** + * Helper method to update element content + * @param {string} id - Element ID + * @param {string} property - Property to update ('textContent', 'title', etc.) + * @param {string} value - New value + * @param {string} selector - Additional selector if needed + * @param {string} altValue - Alternative value if element not found by ID + */ + updateElement(id, property, value, selector = null, altValue = null) { + let element = document.getElementById(id); + + if (!element && selector) { + element = document.querySelector(selector); + } + + if (element && value) { + element[property] = value; + } else if (element && altValue) { + element[property] = altValue; + } + } + + /** + * Set up locale switcher UI + */ + setupLocaleSwitcher() { + // Find existing locale switcher in settings + const switcher = document.getElementById('locale-switcher'); + if (switcher) { + // Add event listeners + switcher.addEventListener('change', (e) => { + this.setLocale(e.target.value); + }); + + // Set current value + switcher.value = this.currentLocale; + } + } + + /** + * Create locale switcher element + * @returns {HTMLElement} Locale switcher element + */ + createLocaleSwitcher() { + const switcher = document.createElement('select'); + switcher.id = 'locale-switcher'; + switcher.className = 'locale-switcher'; + + locales.forEach(locale => { + const option = document.createElement('option'); + option.value = locale; + option.textContent = locale.toUpperCase(); + switcher.appendChild(option); + }); + + // Add to settings or navigation area + const settingsNav = document.querySelector('.settings-nav'); + if (settingsNav) { + const switcherContainer = document.createElement('div'); + switcherContainer.className = 'locale-switcher-container'; + switcherContainer.innerHTML = ` + + `; + switcherContainer.appendChild(switcher); + settingsNav.appendChild(switcherContainer); + } + + return switcher; + } +} + +// Export singleton instance +export const localizationManager = new LocalizationManager();