From bcb405c25c3e02ccd77c65ad3e26f84ec79907af Mon Sep 17 00:00:00 2001 From: aaronFortuno Date: Mon, 6 Apr 2026 11:18:43 +0200 Subject: [PATCH 1/8] feat(i18n): add internationalisation package with 6 locales MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add internal/i18n package with embedded JSON locale files for English, Catalan, Spanish, French, German, and Italian. Provides T(key) for Go-side translation with automatic fallback to English, Messages(lang) for serving locale data to the dashboard JS, and DetectLocale() for OS-level language detection (Windows API + LANG env fallback). No existing code is modified — this commit only adds the new package. Co-Authored-By: Claude Opus 4.6 (1M context) --- internal/i18n/detect_other.go | 17 +++ internal/i18n/detect_windows.go | 40 ++++++++ internal/i18n/i18n.go | 126 +++++++++++++++++++++++ internal/i18n/locales/ca.json | 177 ++++++++++++++++++++++++++++++++ internal/i18n/locales/de.json | 177 ++++++++++++++++++++++++++++++++ internal/i18n/locales/en.json | 177 ++++++++++++++++++++++++++++++++ internal/i18n/locales/es.json | 177 ++++++++++++++++++++++++++++++++ internal/i18n/locales/fr.json | 177 ++++++++++++++++++++++++++++++++ internal/i18n/locales/it.json | 177 ++++++++++++++++++++++++++++++++ 9 files changed, 1245 insertions(+) create mode 100644 internal/i18n/detect_other.go create mode 100644 internal/i18n/detect_windows.go create mode 100644 internal/i18n/i18n.go create mode 100644 internal/i18n/locales/ca.json create mode 100644 internal/i18n/locales/de.json create mode 100644 internal/i18n/locales/en.json create mode 100644 internal/i18n/locales/es.json create mode 100644 internal/i18n/locales/fr.json create mode 100644 internal/i18n/locales/it.json diff --git a/internal/i18n/detect_other.go b/internal/i18n/detect_other.go new file mode 100644 index 0000000..7fe254a --- /dev/null +++ b/internal/i18n/detect_other.go @@ -0,0 +1,17 @@ +//go:build !windows + +package i18n + +import "os" + +// DetectLocale returns the best locale tag for the current OS session. +// On Linux/macOS it reads LANG, LC_ALL, or LC_MESSAGES, falling back to "en". +func DetectLocale() string { + for _, key := range []string{"LANG", "LC_ALL", "LC_MESSAGES"} { + if v := os.Getenv(key); v != "" { + // Strip encoding suffix: "ca_ES.UTF-8" -> "ca_ES" -> "ca" + return normaliseLang(v) + } + } + return "en" +} diff --git a/internal/i18n/detect_windows.go b/internal/i18n/detect_windows.go new file mode 100644 index 0000000..7908f33 --- /dev/null +++ b/internal/i18n/detect_windows.go @@ -0,0 +1,40 @@ +//go:build windows + +package i18n + +import ( + "os" + "syscall" + "unsafe" +) + +var ( + kernel32 = syscall.NewLazyDLL("kernel32.dll") + procGetUserDefaultLocale = kernel32.NewProc("GetUserDefaultLocaleName") +) + +// DetectLocale returns the best locale tag for the current OS session. +// On Windows it calls GetUserDefaultLocaleName, falling back to the LANG +// environment variable, and finally to "en". +func DetectLocale() string { + // Try Windows API first. + buf := make([]uint16, 85) // LOCALE_NAME_MAX_LENGTH + r, _, _ := procGetUserDefaultLocale.Call( + uintptr(unsafe.Pointer(&buf[0])), + uintptr(len(buf)), + ) + if r > 0 { + name := syscall.UTF16ToString(buf) + if name != "" { + return normaliseLang(name) + } + } + + // Fallback to environment. + for _, key := range []string{"LANG", "LC_ALL", "LC_MESSAGES"} { + if v := os.Getenv(key); v != "" { + return normaliseLang(v) + } + } + return "en" +} diff --git a/internal/i18n/i18n.go b/internal/i18n/i18n.go new file mode 100644 index 0000000..104e0e8 --- /dev/null +++ b/internal/i18n/i18n.go @@ -0,0 +1,126 @@ +// Package i18n provides lightweight internationalisation for Owlrun. +// Locale strings are stored as flat JSON files embedded at compile time. +// The package exposes a simple T(key) function and supports runtime locale +// switching with automatic fallback to English. +package i18n + +import ( + "embed" + "encoding/json" + "fmt" + "log" + "strings" + "sync" +) + +//go:embed locales/*.json +var localeFS embed.FS + +var ( + mu sync.RWMutex + current = "en" + messages = map[string]map[string]string{} // lang -> key -> value +) + +func init() { + loadAll() +} + +// loadAll reads every JSON file in locales/ into memory. +func loadAll() { + entries, err := localeFS.ReadDir("locales") + if err != nil { + log.Printf("i18n: failed to read embedded locales: %v", err) + return + } + for _, e := range entries { + if e.IsDir() || !strings.HasSuffix(e.Name(), ".json") { + continue + } + lang := strings.TrimSuffix(e.Name(), ".json") + data, err := localeFS.ReadFile("locales/" + e.Name()) + if err != nil { + log.Printf("i18n: skip %s: %v", e.Name(), err) + continue + } + var m map[string]string + if err := json.Unmarshal(data, &m); err != nil { + log.Printf("i18n: invalid JSON in %s: %v", e.Name(), err) + continue + } + messages[lang] = m + } +} + +// SetLocale changes the active locale. Unknown locales fall back to English. +func SetLocale(lang string) { + mu.Lock() + defer mu.Unlock() + lang = normaliseLang(lang) + if _, ok := messages[lang]; !ok { + lang = "en" + } + current = lang +} + +// Locale returns the active locale tag. +func Locale() string { + mu.RLock() + defer mu.RUnlock() + return current +} + +// T returns the localised string for key. If the key is missing in the +// current locale it falls back to English; if still missing it returns the +// key itself. Optional args are passed to fmt.Sprintf when present. +func T(key string, args ...any) string { + mu.RLock() + lang := current + mu.RUnlock() + + if v, ok := messages[lang][key]; ok { + if len(args) > 0 { + return fmt.Sprintf(v, args...) + } + return v + } + // Fallback to English. + if v, ok := messages["en"][key]; ok { + if len(args) > 0 { + return fmt.Sprintf(v, args...) + } + return v + } + return key +} + +// Locales returns the list of available locale tags. +func Locales() []string { + mu.RLock() + defer mu.RUnlock() + out := make([]string, 0, len(messages)) + for k := range messages { + out = append(out, k) + } + return out +} + +// Messages returns the full translation map for a locale (for serving +// to the dashboard JS). Falls back to English for unknown locales. +func Messages(lang string) map[string]string { + mu.RLock() + defer mu.RUnlock() + lang = normaliseLang(lang) + if m, ok := messages[lang]; ok { + return m + } + return messages["en"] +} + +// normaliseLang converts "ca-ES" -> "ca", "pt_BR" -> "pt", etc. +func normaliseLang(lang string) string { + if i := strings.IndexAny(lang, "-_"); i > 0 { + lang = lang[:i] + } + return strings.ToLower(lang) +} diff --git a/internal/i18n/locales/ca.json b/internal/i18n/locales/ca.json new file mode 100644 index 0000000..9fea316 --- /dev/null +++ b/internal/i18n/locales/ca.json @@ -0,0 +1,177 @@ +{ + "_lang": "Catal\u00e0", + + "tray.tooltip": "Owlrun \u2014 guanya amb la GPU ociosa", + "tray.earnings_today": "Guanys d'avui", + "tray.earnings_total": "Guanys totals", + "tray.wallet_warning": "\u26a0 Configura el teu moneder a ~/.owlrun/owlrun.conf", + "tray.toggle_tip": "Pausa o reprendre Owlrun", + "tray.open_dashboard": "Obrir Tauler", + "tray.open_dashboard_tip": "Obrir localhost:19131", + "tray.accept_jobs": "Acceptar Treballs", + "tray.job_never": "Mai", + "tray.job_never_tip": "No acceptar mai treballs", + "tray.job_idle_fmt": "Despr\u00e9s de %dm inactiu", + "tray.job_idle_tip": "Acceptar treballs despr\u00e9s del temps d'inactivitat", + "tray.job_always": "Sempre", + "tray.job_always_tip": "Acceptar treballs sempre", + "tray.quit": "Sortir", + "tray.quit_tip": "Tancar Owlrun", + "tray.state.earning": "\ud83d\udfe2 Guanyant", + "tray.state.idle": "\ud83d\udfe1 Inactiu \u2014 esperant", + "tray.state.ready": "\ud83d\udfe1 Preparant-se", + "tray.state.wallet": "\ud83d\udfe2 Moneder no configurat", + "tray.state.error": "\ud83d\udd34 Error", + "tray.state.paused": "\u26aa En pausa", + "tray.resume": "Reprendre", + "tray.pause": "Pausa", + "tray.legend.green": "\ud83d\udfe2 Verd \u2014 Connectat i guanyant", + "tray.legend.yellow": "\ud83d\udfe1 Groc \u2014 Preparant-se", + "tray.legend.blue": "\ud83d\udfe2 Blau \u2014 Moneder no configurat", + "tray.legend.red": "\ud83d\udd34 Vermell\u2014 Error", + "tray.legend.grey": "\u26aa Gris \u2014 En pausa", + "tray.disk_critical_fmt": "\u26a0 Nom\u00e9s %.1f GB lliures \u2014 desc\u00e0rregues bloquejades", + "tray.disk_warning_fmt": "\u26a0 Poc disc: %.1f GB lliures (%.0f%%)", + "tray.today_fmt": "Avui: $%.2f", + "tray.total_fmt": "Total: $%.2f", + "tray.err.mock_failed": "L'Ollama simulat no s'ha pogut iniciar: %s", + "tray.err.not_installed": "Ollama no est\u00e0 instal\u00b7lat. Descarrega'l a ollama.com/download, instal\u00b7la'l i reinicia Owlrun.", + "tray.err.start_failed": "Ollama no s'ha pogut iniciar. Assegura't que Ollama est\u00e0 instal\u00b7lat (ollama.com/download) i reinicia Owlrun.", + "tray.err.no_models": "No hi ha models d'IA instal\u00b7lats. Obre el tauler a localhost:19131 i descarrega un model, o executa: ollama pull qwen2.5:0.5b", + "tray.wallet_msg": "Configura la teva adre\u00e7a Lightning a la secci\u00f3 Moneder per comen\u00e7ar a guanyar Bitcoin.", + "tray.wallet_configured_fmt": "Moneder configurat a %s", + + "dash.title": "Tauler d'Owlrun", + "dash.notifications": "Notificacions", + "dash.notifications_empty": "Les notificacions del gateway apareixeran aqu\u00ed.", + "dash.broadcasts_empty": "Les notificacions del gateway apareixeran aqu\u00ed.", + "dash.wallet": "Moneder", + "dash.wallet_not_configured": "Moneder no configurat", + "dash.wallet_setup_heading": "Configura el teu moneder per cobrar", + "dash.wallet_step1": "Instal\u00b7la el moneder Minibits (de Bitango Technologies)", + "dash.wallet_step2_pre": "Troba la teva adre\u00e7a Lightning a Minibits (sembla ", + "dash.wallet_step2_post": ")", + "dash.wallet_step3": "Enganxa-la aqu\u00ed sota", + "dash.wallet_placeholder": "elteunom@minibits.cash", + "dash.wallet_save_btn": "Desar i comen\u00e7ar a guanyar", + "dash.wallet_saving": "Desant...", + "dash.wallet_any_wallet": "Funciona amb qualsevol moneder Lightning \u2014 Minibits, Phoenix, Wallet of Satoshi, etc.", + "dash.wallet_ln_address": "Adre\u00e7a Lightning", + "dash.wallet_change": "Canviar adre\u00e7a", + "dash.wallet_save": "Desar", + "dash.wallet_cancel": "Cancel\u00b7lar", + "dash.wallet_valid_error": "Introdueix una adre\u00e7a Lightning v\u00e0lida (p. ex. elteunon@minibits.cash)", + "dash.wallet_valid_error_short": "Introdueix una adre\u00e7a Lightning v\u00e0lida", + "dash.wallet_configured_banner": "\u26a1 Moneder configurat", + "dash.wallet_auto_sent": "Els guanys s'envien autom\u00e0ticament al teu moneder Lightning. No cal fer res.", + "dash.payout_threshold": "Llindar de pagament", + "dash.payout_threshold_hint": "M\u00e9s baix = pagaments m\u00e9s r\u00e0pids, comissions m\u00e9s altes. M\u00e9s alt = pagaments m\u00e9s lents, comissions m\u00e9s baixes.", + "dash.payout_fee_label": "Comissi\u00f3 Lightning est.:", + "dash.payout_unlock_50": "Desbloquejar m\u00ednim de 50 sats", + "dash.payout_low_warn": "\u26a0 ~10% consumit per comissions Lightning a aquest nivell", + "dash.accept_jobs": "Acceptar treballs", + "dash.job_always": "Sempre", + "dash.job_idle": "Quan inactiu", + "dash.job_never": "Mai", + "dash.job_hint_always": "Guanyant sempre que est\u00e0s connectat", + "dash.job_hint_idle": "Guanyant nom\u00e9s quan no hi ets", + "dash.job_hint_never": "No s'accepten treballs", + "dash.karma": "Karma", + "dash.karma_score": "Puntuaci\u00f3 de karma", + "dash.free_tier_jobs": "Treballs gratu\u00efts servits", + "dash.donate_free_tier": "Donar al nivell gratu\u00eft", + "dash.donate_hint": "Comparteix cicles ociosos amb usuaris gratu\u00efts. M\u00e9s donaci\u00f3 = m\u00e9s karma = m\u00e9s tr\u00e0fic de pagament cap a tu.", + "dash.keep_warm": "Mantenir model actiu", + "dash.keep_warm_hint": "Ping cada 4 min per evitar l'expulsi\u00f3 de VRAM", + "dash.context_length": "Longitud de context", + "dash.context_hint": "Valors m\u00e9s alts usen m\u00e9s VRAM. El model es recarrega en canviar.", + "dash.models": "Models", + "dash.downloading": "Descarregant\u2026", + "dash.show_slow_fmt": "Mostra %d models m\u00e9s grans (poden ser lents en aquesta m\u00e0quina)", + "dash.model_active": "ACTIU", + "dash.model_available": "DISPONIBLE", + "dash.model_activate": "Activar", + "dash.model_download": "Descarregar", + "dash.model_remove": "Eliminar model", + "dash.status": "Estat", + "dash.earnings": "Guanys", + "dash.pending_earnings": "Guanys pendents", + "dash.usd_approx": "USD aprox.", + "dash.today_earnings": "Guanys d'avui", + "dash.total_withdrawn": "Total retirat", + "dash.next_payout": "Pr\u00f2xim pagament est.", + "dash.payout_progress_fmt": "%d%% del llindar (%d sats)", + "dash.usd_disclaimer": "USD aproximat al preu BTC en temps real", + "dash.recent_payouts": "Pagaments recents", + "dash.fee_disclaimer": "Les comissions Lightning van a la xarxa Bitcoin, no a Owlrun. La nostra comissi\u00f3 sempre \u00e9s inferior al 10%.", + "dash.ecash_advanced": "Avan\u00e7at: Retirar com ecash (QR)", + "dash.ecash_local": "Ecash local", + "dash.ecash_proofs": "Proves", + "dash.ecash_claim_all": "Reclamar tot", + "dash.ecash_claiming": "Reclamant\u2026", + "dash.ecash_copied": "Copiat!", + "dash.ecash_copy": "Copiar", + "dash.ecash_recent": "Tokens recents:", + "dash.ecash_export_hint": "L'exportaci\u00f3 est\u00e0 disponible despr\u00e9s de reclamar. Usa Reclamar tot primer, despr\u00e9s copia el token.", + "dash.ecash_no_proofs": "No hi ha proves locals per exportar.", + "dash.ecash_claim_failed_fmt": "Reclamaci\u00f3 fallida: %s", + "dash.state.earning": "Connectat i guanyant", + "dash.state.ready": "Preparant-se", + "dash.state.idle": "Inactiu \u2014 esperant", + "dash.state.wallet": "Moneder no configurat", + "dash.state.error": "Error", + "dash.state.paused": "En pausa", + "dash.legend.earning": "Guanyant", + "dash.legend.ready": "Preparat", + "dash.legend.no_wallet": "Sense moneder", + "dash.legend.error": "Error", + "dash.legend.paused": "En pausa", + "dash.btc_price": "Preu del Bitcoin", + "dash.btc_live": "En directe", + "dash.btc_yesterday": "Fix d'ahir", + "dash.btc_24h": "Mitjana 24h", + "dash.btc_7d": "Mitjana 7d", + "dash.btc_status": "Estat", + "dash.btc_inactive": "Els pagaments en Bitcoin encara no estan actius en aquest gateway", + "dash.gw": "Gateway", + "dash.gw_connection": "Connexi\u00f3", + "dash.gw_connected": "Connectat", + "dash.gw_disconnected": "Desconnectat", + "dash.gw_jobs_today": "Treballs avui", + "dash.gw_tokens_today": "Tokens avui", + "dash.gw_queue": "Profunditat de cua", + "dash.gpu": "GPU", + "dash.gpu_none": "No s'ha detectat cap GPU", + "dash.gpu_util": "Utilitzaci\u00f3", + "dash.gpu_vram": "VRAM lliure", + "dash.gpu_temp": "Temperatura", + "dash.gpu_power": "Consum", + "dash.disk": "Disc", + "dash.disk_free": "Lliure", + "dash.disk_total": "Total", + "dash.disk_path": "Ruta", + "dash.updated_fmt": "actualitzat %s", + "dash.connection_lost": "connexi\u00f3 perduda\u2026", + "dash.charts_jobs": "Treballs", + "dash.charts_earnings": "Guanys (USD)", + "dash.charts_per_period": "Per per\u00edode", + "dash.charts_cumulative": "Acumulat", + "dash.confirm_remove_title": "Eliminar %s?", + "dash.confirm_remove_body": "Aix\u00f2 eliminar\u00e0 el model del disc i alliberar\u00e0 espai.\nEl pots tornar a descarregar m\u00e9s tard.", + "dash.confirm_switch_title": "Canviar el model actiu a %s?", + "dash.confirm_switch_body": "Aix\u00f2 recarregar\u00e0 el model a mem\u00f2ria i es tornar\u00e0 a registrar al gateway.", + "dash.confirm_download_title": "Descarregar %s?", + "dash.download_size_fmt": "Mida del model: ~%s GB", + "dash.download_size_est_fmt": "Mida del model: ~%s GB (estimat)", + "dash.download_disk_fmt": "Disc lliure: %s GB / %s GB total", + "dash.download_after_fmt": "Despr\u00e9s de la desc\u00e0rrega: ~%s%% usat", + "dash.download_time_hint": "Aix\u00f2 pot trigar uns minuts depenent de la teva connexi\u00f3.", + "dash.download_no_space_title": "No hi ha prou espai al disc!", + "dash.download_no_space_body": "Desc\u00e0rrega avortada \u2014 l'\u00fas del disc superaria el 90%.\nAllibera espai o elimina models no usats primer.", + "dash.download_starting": "Iniciant desc\u00e0rrega\u2026", + "dash.download_complete": "Desc\u00e0rrega completada!", + "dash.download_checking": "Comprovant\u2026", + "dash.sats_claimed_fmt": "%s sats reclamats", + "dash.model": "Model", + "dash.today": "Avui" +} diff --git a/internal/i18n/locales/de.json b/internal/i18n/locales/de.json new file mode 100644 index 0000000..09f8871 --- /dev/null +++ b/internal/i18n/locales/de.json @@ -0,0 +1,177 @@ +{ + "_lang": "Deutsch", + + "tray.tooltip": "Owlrun \u2014 verdiene mit deiner ruhenden GPU", + "tray.earnings_today": "Heutige Einnahmen", + "tray.earnings_total": "Gesamteinnahmen", + "tray.wallet_warning": "\u26a0 Konfiguriere dein Wallet in ~/.owlrun/owlrun.conf", + "tray.toggle_tip": "Owlrun pausieren oder fortsetzen", + "tray.open_dashboard": "Dashboard \u00f6ffnen", + "tray.open_dashboard_tip": "localhost:19131 \u00f6ffnen", + "tray.accept_jobs": "Auftr\u00e4ge annehmen", + "tray.job_never": "Nie", + "tray.job_never_tip": "Niemals Auftr\u00e4ge annehmen", + "tray.job_idle_fmt": "Nach %dm Leerlauf", + "tray.job_idle_tip": "Auftr\u00e4ge nach Leerlaufzeit annehmen", + "tray.job_always": "Immer", + "tray.job_always_tip": "Immer Auftr\u00e4ge annehmen", + "tray.quit": "Beenden", + "tray.quit_tip": "Owlrun beenden", + "tray.state.earning": "\ud83d\udfe2 Verdient", + "tray.state.idle": "\ud83d\udfe1 Leerlauf \u2014 wartet", + "tray.state.ready": "\ud83d\udfe1 Wird vorbereitet", + "tray.state.wallet": "\ud83d\udfe2 Wallet nicht konfiguriert", + "tray.state.error": "\ud83d\udd34 Fehler", + "tray.state.paused": "\u26aa Pausiert", + "tray.resume": "Fortsetzen", + "tray.pause": "Pausieren", + "tray.legend.green": "\ud83d\udfe2 Gr\u00fcn \u2014 Verbunden und verdient", + "tray.legend.yellow": "\ud83d\udfe1 Gelb \u2014 Wird vorbereitet", + "tray.legend.blue": "\ud83d\udfe2 Blau \u2014 Wallet nicht konfiguriert", + "tray.legend.red": "\ud83d\udd34 Rot \u2014 Fehler", + "tray.legend.grey": "\u26aa Grau \u2014 Pausiert", + "tray.disk_critical_fmt": "\u26a0 Nur %.1f GB frei \u2014 Downloads blockiert", + "tray.disk_warning_fmt": "\u26a0 Wenig Speicher: %.1f GB frei (%.0f%%)", + "tray.today_fmt": "Heute: $%.2f", + "tray.total_fmt": "Gesamt: $%.2f", + "tray.err.mock_failed": "Mock-Ollama konnte nicht gestartet werden: %s", + "tray.err.not_installed": "Ollama ist nicht installiert. Lade es von ollama.com/download herunter, installiere es und starte Owlrun neu.", + "tray.err.start_failed": "Ollama konnte nicht gestartet werden. Stelle sicher, dass Ollama installiert ist (ollama.com/download) und starte Owlrun neu.", + "tray.err.no_models": "Keine KI-Modelle installiert. \u00d6ffne das Dashboard unter localhost:19131 und lade ein Modell herunter, oder f\u00fchre aus: ollama pull qwen2.5:0.5b", + "tray.wallet_msg": "Konfiguriere deine Lightning-Adresse im Wallet-Bereich, um Bitcoin zu verdienen.", + "tray.wallet_configured_fmt": "Wallet konfiguriert bei %s", + + "dash.title": "Owlrun Dashboard", + "dash.notifications": "Benachrichtigungen", + "dash.notifications_empty": "Gateway-Benachrichtigungen werden hier angezeigt.", + "dash.broadcasts_empty": "Gateway-Benachrichtigungen werden hier angezeigt.", + "dash.wallet": "Wallet", + "dash.wallet_not_configured": "Wallet nicht konfiguriert", + "dash.wallet_setup_heading": "Richte dein Wallet ein, um bezahlt zu werden", + "dash.wallet_step1": "Installiere das Minibits-Wallet (von Bitango Technologies)", + "dash.wallet_step2_pre": "Finde deine Lightning-Adresse in Minibits (sieht aus wie ", + "dash.wallet_step2_post": ")", + "dash.wallet_step3": "F\u00fcge sie unten ein", + "dash.wallet_placeholder": "deinname@minibits.cash", + "dash.wallet_save_btn": "Speichern und verdienen", + "dash.wallet_saving": "Wird gespeichert...", + "dash.wallet_any_wallet": "Funktioniert mit jedem Lightning-Wallet \u2014 Minibits, Phoenix, Wallet of Satoshi, etc.", + "dash.wallet_ln_address": "Lightning-Adresse", + "dash.wallet_change": "Adresse \u00e4ndern", + "dash.wallet_save": "Speichern", + "dash.wallet_cancel": "Abbrechen", + "dash.wallet_valid_error": "Gib eine g\u00fcltige Lightning-Adresse ein (z.B. deinname@minibits.cash)", + "dash.wallet_valid_error_short": "Gib eine g\u00fcltige Lightning-Adresse ein", + "dash.wallet_configured_banner": "\u26a1 Wallet konfiguriert", + "dash.wallet_auto_sent": "Einnahmen werden automatisch an dein Lightning-Wallet gesendet. Keine Aktion erforderlich.", + "dash.payout_threshold": "Auszahlungsschwelle", + "dash.payout_threshold_hint": "Niedriger = schnellere Auszahlungen, h\u00f6here Geb\u00fchren. H\u00f6her = langsamere Auszahlungen, niedrigere Geb\u00fchren.", + "dash.payout_fee_label": "Gesch. Lightning-Geb\u00fchr:", + "dash.payout_unlock_50": "Minimum 50 Sats freischalten", + "dash.payout_low_warn": "\u26a0 ~10% durch Lightning-Geb\u00fchren bei diesem Niveau", + "dash.accept_jobs": "Auftr\u00e4ge annehmen", + "dash.job_always": "Immer", + "dash.job_idle": "Im Leerlauf", + "dash.job_never": "Nie", + "dash.job_hint_always": "Verdient sobald verbunden", + "dash.job_hint_idle": "Verdient nur wenn du weg bist", + "dash.job_hint_never": "Keine Auftr\u00e4ge angenommen", + "dash.karma": "Karma", + "dash.karma_score": "Karma-Punktzahl", + "dash.free_tier_jobs": "Kostenlose Auftr\u00e4ge bedient", + "dash.donate_free_tier": "An kostenlosen Tier spenden", + "dash.donate_hint": "Teile Leerlaufzyklen mit kostenlosen Nutzern. Mehr Spende = mehr Karma = mehr bezahlter Traffic zu dir.", + "dash.keep_warm": "Modell warmhalten", + "dash.keep_warm_hint": "Ping alle 4 Min. um VRAM-Auslagerung zu verhindern", + "dash.context_length": "Kontextl\u00e4nge", + "dash.context_hint": "H\u00f6here Werte nutzen mehr VRAM. Modell wird bei \u00c4nderung neu geladen.", + "dash.models": "Modelle", + "dash.downloading": "Herunterladen\u2026", + "dash.show_slow_fmt": "%d gr\u00f6\u00dfere Modelle anzeigen (k\u00f6nnen auf dieser Maschine langsam sein)", + "dash.model_active": "AKTIV", + "dash.model_available": "VERF\u00dcGBAR", + "dash.model_activate": "Aktivieren", + "dash.model_download": "Herunterladen", + "dash.model_remove": "Modell entfernen", + "dash.status": "Status", + "dash.earnings": "Einnahmen", + "dash.pending_earnings": "Ausstehende Einnahmen", + "dash.usd_approx": "USD ca.", + "dash.today_earnings": "Heutige Einnahmen", + "dash.total_withdrawn": "Gesamt abgehoben", + "dash.next_payout": "N\u00e4chste Auszahlung ca.", + "dash.payout_progress_fmt": "%d%% zur Schwelle (%d Sats)", + "dash.usd_disclaimer": "USD zum aktuellen BTC-Kurs gesch\u00e4tzt", + "dash.recent_payouts": "Letzte Auszahlungen", + "dash.fee_disclaimer": "Lightning-Geb\u00fchren gehen an das Bitcoin-Netzwerk, nicht an Owlrun. Unsere Geb\u00fchr ist immer unter 10%.", + "dash.ecash_advanced": "Erweitert: Als Ecash abheben (QR)", + "dash.ecash_local": "Lokales Ecash", + "dash.ecash_proofs": "Beweise", + "dash.ecash_claim_all": "Alles einfordern", + "dash.ecash_claiming": "Wird eingefordert\u2026", + "dash.ecash_copied": "Kopiert!", + "dash.ecash_copy": "Kopieren", + "dash.ecash_recent": "Letzte Token:", + "dash.ecash_export_hint": "Export ist nach dem Einfordern verf\u00fcgbar. Verwende zuerst Alles einfordern, dann kopiere den Token.", + "dash.ecash_no_proofs": "Keine lokalen Beweise zum Exportieren.", + "dash.ecash_claim_failed_fmt": "Einfordern fehlgeschlagen: %s", + "dash.state.earning": "Verbunden und verdient", + "dash.state.ready": "Wird vorbereitet", + "dash.state.idle": "Leerlauf \u2014 wartet", + "dash.state.wallet": "Wallet nicht konfiguriert", + "dash.state.error": "Fehler", + "dash.state.paused": "Pausiert", + "dash.legend.earning": "Verdient", + "dash.legend.ready": "Bereit", + "dash.legend.no_wallet": "Kein Wallet", + "dash.legend.error": "Fehler", + "dash.legend.paused": "Pausiert", + "dash.btc_price": "Bitcoin-Preis", + "dash.btc_live": "Live", + "dash.btc_yesterday": "Gestrige Fixierung", + "dash.btc_24h": "24h-Durchschnitt", + "dash.btc_7d": "7T-Durchschnitt", + "dash.btc_status": "Status", + "dash.btc_inactive": "Bitcoin-Zahlungen sind auf diesem Gateway noch nicht aktiv", + "dash.gw": "Gateway", + "dash.gw_connection": "Verbindung", + "dash.gw_connected": "Verbunden", + "dash.gw_disconnected": "Getrennt", + "dash.gw_jobs_today": "Auftr\u00e4ge heute", + "dash.gw_tokens_today": "Tokens heute", + "dash.gw_queue": "Warteschlangentiefe", + "dash.gpu": "GPU", + "dash.gpu_none": "Keine GPU erkannt", + "dash.gpu_util": "Auslastung", + "dash.gpu_vram": "VRAM frei", + "dash.gpu_temp": "Temperatur", + "dash.gpu_power": "Leistungsaufnahme", + "dash.disk": "Festplatte", + "dash.disk_free": "Frei", + "dash.disk_total": "Gesamt", + "dash.disk_path": "Pfad", + "dash.updated_fmt": "aktualisiert %s", + "dash.connection_lost": "Verbindung verloren\u2026", + "dash.charts_jobs": "Auftr\u00e4ge", + "dash.charts_earnings": "Einnahmen (USD)", + "dash.charts_per_period": "Pro Periode", + "dash.charts_cumulative": "Kumuliert", + "dash.confirm_remove_title": "%s entfernen?", + "dash.confirm_remove_body": "Dies l\u00f6scht das Modell von der Festplatte und gibt Speicher frei.\nDu kannst es sp\u00e4ter erneut herunterladen.", + "dash.confirm_switch_title": "Aktives Modell zu %s wechseln?", + "dash.confirm_switch_body": "Dies l\u00e4dt das Modell neu in den Speicher und registriert sich erneut beim Gateway.", + "dash.confirm_download_title": "%s herunterladen?", + "dash.download_size_fmt": "Modellgr\u00f6\u00dfe: ~%s GB", + "dash.download_size_est_fmt": "Modellgr\u00f6\u00dfe: ~%s GB (gesch\u00e4tzt)", + "dash.download_disk_fmt": "Festplatte frei: %s GB / %s GB gesamt", + "dash.download_after_fmt": "Nach Download: ~%s%% belegt", + "dash.download_time_hint": "Dies kann je nach Verbindung einige Minuten dauern.", + "dash.download_no_space_title": "Nicht gen\u00fcgend Speicherplatz!", + "dash.download_no_space_body": "Download abgebrochen \u2014 Festplattennutzung w\u00fcrde 90% \u00fcberschreiten.\nGib Speicher frei oder entferne ungenutzte Modelle zuerst.", + "dash.download_starting": "Download startet\u2026", + "dash.download_complete": "Download abgeschlossen!", + "dash.download_checking": "Pr\u00fcfe\u2026", + "dash.sats_claimed_fmt": "%s Sats eingefordert", + "dash.model": "Modell", + "dash.today": "Heute" +} diff --git a/internal/i18n/locales/en.json b/internal/i18n/locales/en.json new file mode 100644 index 0000000..5c1d066 --- /dev/null +++ b/internal/i18n/locales/en.json @@ -0,0 +1,177 @@ +{ + "_lang": "English", + + "tray.tooltip": "Owlrun — idle GPU earning", + "tray.earnings_today": "Earnings today", + "tray.earnings_total": "All-time earnings", + "tray.wallet_warning": "\u26a0 Set your payout wallet in ~/.owlrun/owlrun.conf", + "tray.toggle_tip": "Pause or resume Owlrun", + "tray.open_dashboard": "Open Dashboard", + "tray.open_dashboard_tip": "Open localhost:19131", + "tray.accept_jobs": "Accept Jobs", + "tray.job_never": "Never", + "tray.job_never_tip": "Never accept jobs", + "tray.job_idle_fmt": "After idle %dm", + "tray.job_idle_tip": "Accept jobs after idle timeout", + "tray.job_always": "Always", + "tray.job_always_tip": "Always accept jobs", + "tray.quit": "Quit", + "tray.quit_tip": "Exit Owlrun", + "tray.state.earning": "\ud83d\udfe2 Earning", + "tray.state.idle": "\ud83d\udfe1 Idle \u2014 waiting", + "tray.state.ready": "\ud83d\udfe1 Getting ready", + "tray.state.wallet": "\ud83d\udfe2 Wallet not set", + "tray.state.error": "\ud83d\udd34 Error", + "tray.state.paused": "\u26aa Paused", + "tray.resume": "Resume", + "tray.pause": "Pause", + "tray.legend.green": "\ud83d\udfe2 Green \u2014 Connected & earning", + "tray.legend.yellow": "\ud83d\udfe1 Yellow \u2014 Getting ready", + "tray.legend.blue": "\ud83d\udfe2 Blue \u2014 Wallet not set", + "tray.legend.red": "\ud83d\udd34 Red \u2014 Error", + "tray.legend.grey": "\u26aa Grey \u2014 Paused", + "tray.disk_critical_fmt": "\u26a0 Only %.1f GB free \u2014 downloads blocked", + "tray.disk_warning_fmt": "\u26a0 Low disk: %.1f GB free (%.0f%%)", + "tray.today_fmt": "Today: $%.2f", + "tray.total_fmt": "Total: $%.2f", + "tray.err.mock_failed": "Mock Ollama failed to start: %s", + "tray.err.not_installed": "Ollama is not installed. Download it from ollama.com/download, install it, then restart Owlrun.", + "tray.err.start_failed": "Ollama failed to start. Make sure Ollama is installed (ollama.com/download) and try restarting Owlrun.", + "tray.err.no_models": "No AI models installed. Open the dashboard at localhost:19131 and download a model, or run: ollama pull qwen2.5:0.5b", + "tray.wallet_msg": "Set your Lightning address in the Wallet section to start earning Bitcoin.", + "tray.wallet_configured_fmt": "Wallet configured at %s", + + "dash.title": "Owlrun Dashboard", + "dash.notifications": "Notifications", + "dash.notifications_empty": "Gateway notifications will appear here.", + "dash.broadcasts_empty": "Broadcast notifications from the gateway will appear here.", + "dash.wallet": "Wallet", + "dash.wallet_not_configured": "Wallet not configured", + "dash.wallet_setup_heading": "Set up your wallet to get paid", + "dash.wallet_step1": "Install Minibits wallet (by Bitango Technologies)", + "dash.wallet_step2_pre": "Find your Lightning address in Minibits (looks like ", + "dash.wallet_step2_post": ")", + "dash.wallet_step3": "Paste it below", + "dash.wallet_placeholder": "yourname@minibits.cash", + "dash.wallet_save_btn": "Save & Start Earning", + "dash.wallet_saving": "Saving...", + "dash.wallet_any_wallet": "Works with any Lightning wallet \u2014 Minibits, Phoenix, Wallet of Satoshi, etc.", + "dash.wallet_ln_address": "Lightning address", + "dash.wallet_change": "Change address", + "dash.wallet_save": "Save", + "dash.wallet_cancel": "Cancel", + "dash.wallet_valid_error": "Enter a valid Lightning address (e.g. yourname@minibits.cash)", + "dash.wallet_valid_error_short": "Enter a valid Lightning address", + "dash.wallet_configured_banner": "\u26a1 Wallet configured", + "dash.wallet_auto_sent": "Earnings auto-sent to your Lightning wallet. No action needed.", + "dash.payout_threshold": "Payout threshold", + "dash.payout_threshold_hint": "Lower = faster payouts, higher fees. Higher = slower payouts, lower fees.", + "dash.payout_fee_label": "Est. Lightning fee:", + "dash.payout_unlock_50": "Unlock 50 sat minimum", + "dash.payout_low_warn": "\u26a0 ~10% eaten by Lightning fees at this level", + "dash.accept_jobs": "Accept jobs", + "dash.job_always": "Always", + "dash.job_idle": "When idle", + "dash.job_never": "Never", + "dash.job_hint_always": "Earning whenever connected", + "dash.job_hint_idle": "Earning only when you are away", + "dash.job_hint_never": "Not accepting any jobs", + "dash.karma": "Karma", + "dash.karma_score": "Karma score", + "dash.free_tier_jobs": "Free tier jobs served", + "dash.donate_free_tier": "Donate to free tier", + "dash.donate_hint": "Share idle cycles with free-tier users. Higher donation = more karma = more paid traffic routed to you.", + "dash.keep_warm": "Keep model warm", + "dash.keep_warm_hint": "Ping every 4 min to prevent VRAM eviction", + "dash.context_length": "Context length", + "dash.context_hint": "Higher values use more VRAM. Model reloads on change.", + "dash.models": "Models", + "dash.downloading": "Downloading\u2026", + "dash.show_slow_fmt": "Show %d larger models (may be slow on this machine)", + "dash.model_active": "ACTIVE", + "dash.model_available": "AVAILABLE", + "dash.model_activate": "Activate", + "dash.model_download": "Download", + "dash.model_remove": "Remove model", + "dash.status": "Status", + "dash.earnings": "Earnings", + "dash.pending_earnings": "Pending earnings", + "dash.usd_approx": "USD approx", + "dash.today_earnings": "Today's earnings", + "dash.total_withdrawn": "Total withdrawn", + "dash.next_payout": "Next payout est.", + "dash.payout_progress_fmt": "%d%% to threshold (%d sats)", + "dash.usd_disclaimer": "USD approximated at live BTC rate", + "dash.recent_payouts": "Recent Payouts", + "dash.fee_disclaimer": "Lightning fees go to the Bitcoin network, not Owlrun. Our fee is always under 10%.", + "dash.ecash_advanced": "Advanced: Withdraw as ecash (QR)", + "dash.ecash_local": "Local ecash", + "dash.ecash_proofs": "Proofs", + "dash.ecash_claim_all": "Claim All", + "dash.ecash_claiming": "Claiming\u2026", + "dash.ecash_copied": "Copied!", + "dash.ecash_copy": "Copy", + "dash.ecash_recent": "Recent tokens:", + "dash.ecash_export_hint": "Export is available after claiming. Use Claim All first, then copy the token.", + "dash.ecash_no_proofs": "No local proofs to export.", + "dash.ecash_claim_failed_fmt": "Claim failed: %s", + "dash.state.earning": "Connected & earning", + "dash.state.ready": "Getting ready", + "dash.state.idle": "Idle \u2014 waiting", + "dash.state.wallet": "Wallet not set", + "dash.state.error": "Error", + "dash.state.paused": "Paused", + "dash.legend.earning": "Earning", + "dash.legend.ready": "Ready", + "dash.legend.no_wallet": "No wallet", + "dash.legend.error": "Error", + "dash.legend.paused": "Paused", + "dash.btc_price": "Bitcoin Price", + "dash.btc_live": "Live", + "dash.btc_yesterday": "Yesterday's Fix", + "dash.btc_24h": "24h Avg", + "dash.btc_7d": "7d Avg", + "dash.btc_status": "Status", + "dash.btc_inactive": "Bitcoin payments not yet active on this gateway", + "dash.gw": "Gateway", + "dash.gw_connection": "Connection", + "dash.gw_connected": "Connected", + "dash.gw_disconnected": "Disconnected", + "dash.gw_jobs_today": "Jobs today", + "dash.gw_tokens_today": "Tokens today", + "dash.gw_queue": "Queue depth", + "dash.gpu": "GPU", + "dash.gpu_none": "No GPU detected", + "dash.gpu_util": "Utilisation", + "dash.gpu_vram": "VRAM free", + "dash.gpu_temp": "Temperature", + "dash.gpu_power": "Power draw", + "dash.disk": "Disk", + "dash.disk_free": "Free", + "dash.disk_total": "Total", + "dash.disk_path": "Path", + "dash.updated_fmt": "updated %s", + "dash.connection_lost": "connection lost\u2026", + "dash.charts_jobs": "Jobs", + "dash.charts_earnings": "Earnings (USD)", + "dash.charts_per_period": "Per period", + "dash.charts_cumulative": "Cumulative", + "dash.confirm_remove_title": "Remove %s?", + "dash.confirm_remove_body": "This will delete the model from disk and free up space.\nYou can re-download it later.", + "dash.confirm_switch_title": "Switch active model to %s?", + "dash.confirm_switch_body": "This will reload the model into memory and re-register with the gateway.", + "dash.confirm_download_title": "Download %s?", + "dash.download_size_fmt": "Model size: ~%s GB", + "dash.download_size_est_fmt": "Model size: ~%s GB (estimated)", + "dash.download_disk_fmt": "Disk free: %s GB / %s GB total", + "dash.download_after_fmt": "After download: ~%s%% used", + "dash.download_time_hint": "This may take a few minutes depending on your connection.", + "dash.download_no_space_title": "Not enough disk space!", + "dash.download_no_space_body": "Download aborted \u2014 disk usage would exceed 90%.\nFree up space or remove unused models first.", + "dash.download_starting": "Starting download\u2026", + "dash.download_complete": "Download complete!", + "dash.download_checking": "Checking\u2026", + "dash.sats_claimed_fmt": "%s sats claimed", + "dash.model": "Model", + "dash.today": "Today" +} diff --git a/internal/i18n/locales/es.json b/internal/i18n/locales/es.json new file mode 100644 index 0000000..87e7fcf --- /dev/null +++ b/internal/i18n/locales/es.json @@ -0,0 +1,177 @@ +{ + "_lang": "Espa\u00f1ol", + + "tray.tooltip": "Owlrun \u2014 gana con tu GPU ociosa", + "tray.earnings_today": "Ganancias de hoy", + "tray.earnings_total": "Ganancias totales", + "tray.wallet_warning": "\u26a0 Configura tu monedero en ~/.owlrun/owlrun.conf", + "tray.toggle_tip": "Pausar o reanudar Owlrun", + "tray.open_dashboard": "Abrir Panel", + "tray.open_dashboard_tip": "Abrir localhost:19131", + "tray.accept_jobs": "Aceptar Trabajos", + "tray.job_never": "Nunca", + "tray.job_never_tip": "No aceptar trabajos nunca", + "tray.job_idle_fmt": "Tras %dm inactivo", + "tray.job_idle_tip": "Aceptar trabajos tras tiempo de inactividad", + "tray.job_always": "Siempre", + "tray.job_always_tip": "Aceptar trabajos siempre", + "tray.quit": "Salir", + "tray.quit_tip": "Cerrar Owlrun", + "tray.state.earning": "\ud83d\udfe2 Ganando", + "tray.state.idle": "\ud83d\udfe1 Inactivo \u2014 esperando", + "tray.state.ready": "\ud83d\udfe1 Preparando", + "tray.state.wallet": "\ud83d\udfe2 Monedero no configurado", + "tray.state.error": "\ud83d\udd34 Error", + "tray.state.paused": "\u26aa En pausa", + "tray.resume": "Reanudar", + "tray.pause": "Pausar", + "tray.legend.green": "\ud83d\udfe2 Verde \u2014 Conectado y ganando", + "tray.legend.yellow": "\ud83d\udfe1 Amarillo \u2014 Preparando", + "tray.legend.blue": "\ud83d\udfe2 Azul \u2014 Monedero no configurado", + "tray.legend.red": "\ud83d\udd34 Rojo \u2014 Error", + "tray.legend.grey": "\u26aa Gris \u2014 En pausa", + "tray.disk_critical_fmt": "\u26a0 Solo %.1f GB libres \u2014 descargas bloqueadas", + "tray.disk_warning_fmt": "\u26a0 Poco disco: %.1f GB libres (%.0f%%)", + "tray.today_fmt": "Hoy: $%.2f", + "tray.total_fmt": "Total: $%.2f", + "tray.err.mock_failed": "El Ollama simulado no pudo iniciarse: %s", + "tray.err.not_installed": "Ollama no est\u00e1 instalado. Desc\u00e1rgalo en ollama.com/download, inst\u00e1lalo y reinicia Owlrun.", + "tray.err.start_failed": "Ollama no pudo iniciarse. Aseg\u00farate de que Ollama est\u00e1 instalado (ollama.com/download) y reinicia Owlrun.", + "tray.err.no_models": "No hay modelos de IA instalados. Abre el panel en localhost:19131 y descarga un modelo, o ejecuta: ollama pull qwen2.5:0.5b", + "tray.wallet_msg": "Configura tu direcci\u00f3n Lightning en la secci\u00f3n Monedero para empezar a ganar Bitcoin.", + "tray.wallet_configured_fmt": "Monedero configurado en %s", + + "dash.title": "Panel de Owlrun", + "dash.notifications": "Notificaciones", + "dash.notifications_empty": "Las notificaciones del gateway aparecer\u00e1n aqu\u00ed.", + "dash.broadcasts_empty": "Las notificaciones del gateway aparecer\u00e1n aqu\u00ed.", + "dash.wallet": "Monedero", + "dash.wallet_not_configured": "Monedero no configurado", + "dash.wallet_setup_heading": "Configura tu monedero para cobrar", + "dash.wallet_step1": "Instala el monedero Minibits (de Bitango Technologies)", + "dash.wallet_step2_pre": "Encuentra tu direcci\u00f3n Lightning en Minibits (parece ", + "dash.wallet_step2_post": ")", + "dash.wallet_step3": "P\u00e9gala abajo", + "dash.wallet_placeholder": "tunombre@minibits.cash", + "dash.wallet_save_btn": "Guardar y empezar a ganar", + "dash.wallet_saving": "Guardando...", + "dash.wallet_any_wallet": "Funciona con cualquier monedero Lightning \u2014 Minibits, Phoenix, Wallet of Satoshi, etc.", + "dash.wallet_ln_address": "Direcci\u00f3n Lightning", + "dash.wallet_change": "Cambiar direcci\u00f3n", + "dash.wallet_save": "Guardar", + "dash.wallet_cancel": "Cancelar", + "dash.wallet_valid_error": "Introduce una direcci\u00f3n Lightning v\u00e1lida (ej. tunombre@minibits.cash)", + "dash.wallet_valid_error_short": "Introduce una direcci\u00f3n Lightning v\u00e1lida", + "dash.wallet_configured_banner": "\u26a1 Monedero configurado", + "dash.wallet_auto_sent": "Las ganancias se env\u00edan autom\u00e1ticamente a tu monedero Lightning. No es necesario hacer nada.", + "dash.payout_threshold": "Umbral de pago", + "dash.payout_threshold_hint": "M\u00e1s bajo = pagos m\u00e1s r\u00e1pidos, comisiones m\u00e1s altas. M\u00e1s alto = pagos m\u00e1s lentos, comisiones m\u00e1s bajas.", + "dash.payout_fee_label": "Comisi\u00f3n Lightning est.:", + "dash.payout_unlock_50": "Desbloquear m\u00ednimo de 50 sats", + "dash.payout_low_warn": "\u26a0 ~10% consumido por comisiones Lightning a este nivel", + "dash.accept_jobs": "Aceptar trabajos", + "dash.job_always": "Siempre", + "dash.job_idle": "Cuando inactivo", + "dash.job_never": "Nunca", + "dash.job_hint_always": "Ganando siempre que est\u00e9s conectado", + "dash.job_hint_idle": "Ganando solo cuando no est\u00e1s", + "dash.job_hint_never": "No se aceptan trabajos", + "dash.karma": "Karma", + "dash.karma_score": "Puntuaci\u00f3n de karma", + "dash.free_tier_jobs": "Trabajos gratuitos servidos", + "dash.donate_free_tier": "Donar al nivel gratuito", + "dash.donate_hint": "Comparte ciclos ociosos con usuarios gratuitos. M\u00e1s donaci\u00f3n = m\u00e1s karma = m\u00e1s tr\u00e1fico de pago hacia ti.", + "dash.keep_warm": "Mantener modelo activo", + "dash.keep_warm_hint": "Ping cada 4 min para evitar la expulsi\u00f3n de VRAM", + "dash.context_length": "Longitud de contexto", + "dash.context_hint": "Valores m\u00e1s altos usan m\u00e1s VRAM. El modelo se recarga al cambiar.", + "dash.models": "Modelos", + "dash.downloading": "Descargando\u2026", + "dash.show_slow_fmt": "Mostrar %d modelos m\u00e1s grandes (pueden ser lentos en esta m\u00e1quina)", + "dash.model_active": "ACTIVO", + "dash.model_available": "DISPONIBLE", + "dash.model_activate": "Activar", + "dash.model_download": "Descargar", + "dash.model_remove": "Eliminar modelo", + "dash.status": "Estado", + "dash.earnings": "Ganancias", + "dash.pending_earnings": "Ganancias pendientes", + "dash.usd_approx": "USD aprox.", + "dash.today_earnings": "Ganancias de hoy", + "dash.total_withdrawn": "Total retirado", + "dash.next_payout": "Pr\u00f3ximo pago est.", + "dash.payout_progress_fmt": "%d%% del umbral (%d sats)", + "dash.usd_disclaimer": "USD aproximado al precio BTC en tiempo real", + "dash.recent_payouts": "Pagos recientes", + "dash.fee_disclaimer": "Las comisiones Lightning van a la red Bitcoin, no a Owlrun. Nuestra comisi\u00f3n siempre es inferior al 10%.", + "dash.ecash_advanced": "Avanzado: Retirar como ecash (QR)", + "dash.ecash_local": "Ecash local", + "dash.ecash_proofs": "Pruebas", + "dash.ecash_claim_all": "Reclamar todo", + "dash.ecash_claiming": "Reclamando\u2026", + "dash.ecash_copied": "\u00a1Copiado!", + "dash.ecash_copy": "Copiar", + "dash.ecash_recent": "Tokens recientes:", + "dash.ecash_export_hint": "La exportaci\u00f3n est\u00e1 disponible tras reclamar. Usa Reclamar todo primero, luego copia el token.", + "dash.ecash_no_proofs": "No hay pruebas locales para exportar.", + "dash.ecash_claim_failed_fmt": "Reclamaci\u00f3n fallida: %s", + "dash.state.earning": "Conectado y ganando", + "dash.state.ready": "Preparando", + "dash.state.idle": "Inactivo \u2014 esperando", + "dash.state.wallet": "Monedero no configurado", + "dash.state.error": "Error", + "dash.state.paused": "En pausa", + "dash.legend.earning": "Ganando", + "dash.legend.ready": "Preparado", + "dash.legend.no_wallet": "Sin monedero", + "dash.legend.error": "Error", + "dash.legend.paused": "En pausa", + "dash.btc_price": "Precio del Bitcoin", + "dash.btc_live": "En directo", + "dash.btc_yesterday": "Fijaci\u00f3n de ayer", + "dash.btc_24h": "Media 24h", + "dash.btc_7d": "Media 7d", + "dash.btc_status": "Estado", + "dash.btc_inactive": "Los pagos en Bitcoin a\u00fan no est\u00e1n activos en este gateway", + "dash.gw": "Gateway", + "dash.gw_connection": "Conexi\u00f3n", + "dash.gw_connected": "Conectado", + "dash.gw_disconnected": "Desconectado", + "dash.gw_jobs_today": "Trabajos hoy", + "dash.gw_tokens_today": "Tokens hoy", + "dash.gw_queue": "Profundidad de cola", + "dash.gpu": "GPU", + "dash.gpu_none": "No se detect\u00f3 ninguna GPU", + "dash.gpu_util": "Utilizaci\u00f3n", + "dash.gpu_vram": "VRAM libre", + "dash.gpu_temp": "Temperatura", + "dash.gpu_power": "Consumo", + "dash.disk": "Disco", + "dash.disk_free": "Libre", + "dash.disk_total": "Total", + "dash.disk_path": "Ruta", + "dash.updated_fmt": "actualizado %s", + "dash.connection_lost": "conexi\u00f3n perdida\u2026", + "dash.charts_jobs": "Trabajos", + "dash.charts_earnings": "Ganancias (USD)", + "dash.charts_per_period": "Por periodo", + "dash.charts_cumulative": "Acumulado", + "dash.confirm_remove_title": "\u00bfEliminar %s?", + "dash.confirm_remove_body": "Esto eliminar\u00e1 el modelo del disco y liberar\u00e1 espacio.\nPuedes volver a descargarlo m\u00e1s tarde.", + "dash.confirm_switch_title": "\u00bfCambiar el modelo activo a %s?", + "dash.confirm_switch_body": "Esto recargar\u00e1 el modelo en memoria y se volver\u00e1 a registrar en el gateway.", + "dash.confirm_download_title": "\u00bfDescargar %s?", + "dash.download_size_fmt": "Tama\u00f1o del modelo: ~%s GB", + "dash.download_size_est_fmt": "Tama\u00f1o del modelo: ~%s GB (estimado)", + "dash.download_disk_fmt": "Disco libre: %s GB / %s GB total", + "dash.download_after_fmt": "Tras la descarga: ~%s%% usado", + "dash.download_time_hint": "Esto puede tardar unos minutos seg\u00fan tu conexi\u00f3n.", + "dash.download_no_space_title": "\u00a1No hay suficiente espacio en disco!", + "dash.download_no_space_body": "Descarga cancelada \u2014 el uso de disco superar\u00eda el 90%.\nLibera espacio o elimina modelos no usados primero.", + "dash.download_starting": "Iniciando descarga\u2026", + "dash.download_complete": "\u00a1Descarga completada!", + "dash.download_checking": "Comprobando\u2026", + "dash.sats_claimed_fmt": "%s sats reclamados", + "dash.model": "Modelo", + "dash.today": "Hoy" +} diff --git a/internal/i18n/locales/fr.json b/internal/i18n/locales/fr.json new file mode 100644 index 0000000..c2e8a5a --- /dev/null +++ b/internal/i18n/locales/fr.json @@ -0,0 +1,177 @@ +{ + "_lang": "Fran\u00e7ais", + + "tray.tooltip": "Owlrun \u2014 gagnez avec votre GPU au repos", + "tray.earnings_today": "Gains du jour", + "tray.earnings_total": "Gains totaux", + "tray.wallet_warning": "\u26a0 Configurez votre portefeuille dans ~/.owlrun/owlrun.conf", + "tray.toggle_tip": "Mettre en pause ou reprendre Owlrun", + "tray.open_dashboard": "Ouvrir le Tableau de bord", + "tray.open_dashboard_tip": "Ouvrir localhost:19131", + "tray.accept_jobs": "Accepter des T\u00e2ches", + "tray.job_never": "Jamais", + "tray.job_never_tip": "Ne jamais accepter de t\u00e2ches", + "tray.job_idle_fmt": "Apr\u00e8s %dm d'inactivit\u00e9", + "tray.job_idle_tip": "Accepter des t\u00e2ches apr\u00e8s le d\u00e9lai d'inactivit\u00e9", + "tray.job_always": "Toujours", + "tray.job_always_tip": "Toujours accepter des t\u00e2ches", + "tray.quit": "Quitter", + "tray.quit_tip": "Fermer Owlrun", + "tray.state.earning": "\ud83d\udfe2 En gain", + "tray.state.idle": "\ud83d\udfe1 Inactif \u2014 en attente", + "tray.state.ready": "\ud83d\udfe1 Pr\u00e9paration", + "tray.state.wallet": "\ud83d\udfe2 Portefeuille non configur\u00e9", + "tray.state.error": "\ud83d\udd34 Erreur", + "tray.state.paused": "\u26aa En pause", + "tray.resume": "Reprendre", + "tray.pause": "Pause", + "tray.legend.green": "\ud83d\udfe2 Vert \u2014 Connect\u00e9 et en gain", + "tray.legend.yellow": "\ud83d\udfe1 Jaune \u2014 Pr\u00e9paration", + "tray.legend.blue": "\ud83d\udfe2 Bleu \u2014 Portefeuille non configur\u00e9", + "tray.legend.red": "\ud83d\udd34 Rouge \u2014 Erreur", + "tray.legend.grey": "\u26aa Gris \u2014 En pause", + "tray.disk_critical_fmt": "\u26a0 Seulement %.1f Go libres \u2014 t\u00e9l\u00e9chargements bloqu\u00e9s", + "tray.disk_warning_fmt": "\u26a0 Disque faible : %.1f Go libres (%.0f%%)", + "tray.today_fmt": "Auj. : $%.2f", + "tray.total_fmt": "Total : $%.2f", + "tray.err.mock_failed": "Le faux Ollama n'a pas pu d\u00e9marrer : %s", + "tray.err.not_installed": "Ollama n'est pas install\u00e9. T\u00e9l\u00e9chargez-le sur ollama.com/download, installez-le et red\u00e9marrez Owlrun.", + "tray.err.start_failed": "Ollama n'a pas pu d\u00e9marrer. V\u00e9rifiez qu'Ollama est install\u00e9 (ollama.com/download) et red\u00e9marrez Owlrun.", + "tray.err.no_models": "Aucun mod\u00e8le d'IA install\u00e9. Ouvrez le tableau de bord sur localhost:19131 et t\u00e9l\u00e9chargez un mod\u00e8le, ou ex\u00e9cutez : ollama pull qwen2.5:0.5b", + "tray.wallet_msg": "Configurez votre adresse Lightning dans la section Portefeuille pour commencer \u00e0 gagner du Bitcoin.", + "tray.wallet_configured_fmt": "Portefeuille configur\u00e9 \u00e0 %s", + + "dash.title": "Tableau de bord Owlrun", + "dash.notifications": "Notifications", + "dash.notifications_empty": "Les notifications de la passerelle appara\u00eetront ici.", + "dash.broadcasts_empty": "Les notifications de la passerelle appara\u00eetront ici.", + "dash.wallet": "Portefeuille", + "dash.wallet_not_configured": "Portefeuille non configur\u00e9", + "dash.wallet_setup_heading": "Configurez votre portefeuille pour \u00eatre pay\u00e9", + "dash.wallet_step1": "Installez le portefeuille Minibits (par Bitango Technologies)", + "dash.wallet_step2_pre": "Trouvez votre adresse Lightning dans Minibits (ressemble \u00e0 ", + "dash.wallet_step2_post": ")", + "dash.wallet_step3": "Collez-la ci-dessous", + "dash.wallet_placeholder": "votrenom@minibits.cash", + "dash.wallet_save_btn": "Enregistrer et commencer \u00e0 gagner", + "dash.wallet_saving": "Enregistrement...", + "dash.wallet_any_wallet": "Fonctionne avec tout portefeuille Lightning \u2014 Minibits, Phoenix, Wallet of Satoshi, etc.", + "dash.wallet_ln_address": "Adresse Lightning", + "dash.wallet_change": "Changer l'adresse", + "dash.wallet_save": "Enregistrer", + "dash.wallet_cancel": "Annuler", + "dash.wallet_valid_error": "Entrez une adresse Lightning valide (ex. votrenom@minibits.cash)", + "dash.wallet_valid_error_short": "Entrez une adresse Lightning valide", + "dash.wallet_configured_banner": "\u26a1 Portefeuille configur\u00e9", + "dash.wallet_auto_sent": "Les gains sont envoy\u00e9s automatiquement \u00e0 votre portefeuille Lightning. Aucune action n\u00e9cessaire.", + "dash.payout_threshold": "Seuil de paiement", + "dash.payout_threshold_hint": "Plus bas = paiements plus rapides, frais plus \u00e9lev\u00e9s. Plus haut = paiements plus lents, frais plus bas.", + "dash.payout_fee_label": "Frais Lightning est. :", + "dash.payout_unlock_50": "D\u00e9bloquer le minimum de 50 sats", + "dash.payout_low_warn": "\u26a0 ~10% consomm\u00e9 par les frais Lightning \u00e0 ce niveau", + "dash.accept_jobs": "Accepter des t\u00e2ches", + "dash.job_always": "Toujours", + "dash.job_idle": "Au repos", + "dash.job_never": "Jamais", + "dash.job_hint_always": "Gagne d\u00e8s que connect\u00e9", + "dash.job_hint_idle": "Gagne uniquement quand vous \u00eates absent", + "dash.job_hint_never": "Aucune t\u00e2che accept\u00e9e", + "dash.karma": "Karma", + "dash.karma_score": "Score de karma", + "dash.free_tier_jobs": "T\u00e2ches gratuites servies", + "dash.donate_free_tier": "Donner au niveau gratuit", + "dash.donate_hint": "Partagez des cycles inactifs avec les utilisateurs gratuits. Plus de dons = plus de karma = plus de trafic payant vers vous.", + "dash.keep_warm": "Garder le mod\u00e8le actif", + "dash.keep_warm_hint": "Ping toutes les 4 min pour \u00e9viter l'\u00e9viction VRAM", + "dash.context_length": "Longueur de contexte", + "dash.context_hint": "Des valeurs plus \u00e9lev\u00e9es utilisent plus de VRAM. Le mod\u00e8le se recharge au changement.", + "dash.models": "Mod\u00e8les", + "dash.downloading": "T\u00e9l\u00e9chargement\u2026", + "dash.show_slow_fmt": "Afficher %d mod\u00e8les plus grands (peuvent \u00eatre lents sur cette machine)", + "dash.model_active": "ACTIF", + "dash.model_available": "DISPONIBLE", + "dash.model_activate": "Activer", + "dash.model_download": "T\u00e9l\u00e9charger", + "dash.model_remove": "Supprimer le mod\u00e8le", + "dash.status": "\u00c9tat", + "dash.earnings": "Gains", + "dash.pending_earnings": "Gains en attente", + "dash.usd_approx": "USD approx.", + "dash.today_earnings": "Gains du jour", + "dash.total_withdrawn": "Total retir\u00e9", + "dash.next_payout": "Prochain paiement est.", + "dash.payout_progress_fmt": "%d%% du seuil (%d sats)", + "dash.usd_disclaimer": "USD approxim\u00e9 au taux BTC en temps r\u00e9el", + "dash.recent_payouts": "Paiements r\u00e9cents", + "dash.fee_disclaimer": "Les frais Lightning vont au r\u00e9seau Bitcoin, pas \u00e0 Owlrun. Notre commission est toujours inf\u00e9rieure \u00e0 10%.", + "dash.ecash_advanced": "Avanc\u00e9 : Retirer en ecash (QR)", + "dash.ecash_local": "Ecash local", + "dash.ecash_proofs": "Preuves", + "dash.ecash_claim_all": "Tout r\u00e9clamer", + "dash.ecash_claiming": "R\u00e9clamation\u2026", + "dash.ecash_copied": "Copi\u00e9 !", + "dash.ecash_copy": "Copier", + "dash.ecash_recent": "Tokens r\u00e9cents :", + "dash.ecash_export_hint": "L'exportation est disponible apr\u00e8s la r\u00e9clamation. Utilisez Tout r\u00e9clamer d'abord, puis copiez le token.", + "dash.ecash_no_proofs": "Aucune preuve locale \u00e0 exporter.", + "dash.ecash_claim_failed_fmt": "\u00c9chec de la r\u00e9clamation : %s", + "dash.state.earning": "Connect\u00e9 et en gain", + "dash.state.ready": "Pr\u00e9paration", + "dash.state.idle": "Inactif \u2014 en attente", + "dash.state.wallet": "Portefeuille non configur\u00e9", + "dash.state.error": "Erreur", + "dash.state.paused": "En pause", + "dash.legend.earning": "En gain", + "dash.legend.ready": "Pr\u00eat", + "dash.legend.no_wallet": "Sans portefeuille", + "dash.legend.error": "Erreur", + "dash.legend.paused": "En pause", + "dash.btc_price": "Prix du Bitcoin", + "dash.btc_live": "En direct", + "dash.btc_yesterday": "Fix d'hier", + "dash.btc_24h": "Moyenne 24h", + "dash.btc_7d": "Moyenne 7j", + "dash.btc_status": "\u00c9tat", + "dash.btc_inactive": "Les paiements Bitcoin ne sont pas encore actifs sur cette passerelle", + "dash.gw": "Passerelle", + "dash.gw_connection": "Connexion", + "dash.gw_connected": "Connect\u00e9", + "dash.gw_disconnected": "D\u00e9connect\u00e9", + "dash.gw_jobs_today": "T\u00e2ches aujourd'hui", + "dash.gw_tokens_today": "Tokens aujourd'hui", + "dash.gw_queue": "Profondeur de file", + "dash.gpu": "GPU", + "dash.gpu_none": "Aucun GPU d\u00e9tect\u00e9", + "dash.gpu_util": "Utilisation", + "dash.gpu_vram": "VRAM libre", + "dash.gpu_temp": "Temp\u00e9rature", + "dash.gpu_power": "Consommation", + "dash.disk": "Disque", + "dash.disk_free": "Libre", + "dash.disk_total": "Total", + "dash.disk_path": "Chemin", + "dash.updated_fmt": "mis \u00e0 jour %s", + "dash.connection_lost": "connexion perdue\u2026", + "dash.charts_jobs": "T\u00e2ches", + "dash.charts_earnings": "Gains (USD)", + "dash.charts_per_period": "Par p\u00e9riode", + "dash.charts_cumulative": "Cumul\u00e9", + "dash.confirm_remove_title": "Supprimer %s ?", + "dash.confirm_remove_body": "Cela supprimera le mod\u00e8le du disque et lib\u00e9rera de l'espace.\nVous pourrez le ret\u00e9l\u00e9charger plus tard.", + "dash.confirm_switch_title": "Changer le mod\u00e8le actif pour %s ?", + "dash.confirm_switch_body": "Cela rechargera le mod\u00e8le en m\u00e9moire et se r\u00e9enregistrera aupr\u00e8s de la passerelle.", + "dash.confirm_download_title": "T\u00e9l\u00e9charger %s ?", + "dash.download_size_fmt": "Taille du mod\u00e8le : ~%s Go", + "dash.download_size_est_fmt": "Taille du mod\u00e8le : ~%s Go (estim\u00e9)", + "dash.download_disk_fmt": "Disque libre : %s Go / %s Go total", + "dash.download_after_fmt": "Apr\u00e8s t\u00e9l\u00e9chargement : ~%s%% utilis\u00e9", + "dash.download_time_hint": "Cela peut prendre quelques minutes selon votre connexion.", + "dash.download_no_space_title": "Pas assez d'espace disque !", + "dash.download_no_space_body": "T\u00e9l\u00e9chargement annul\u00e9 \u2014 l'utilisation du disque d\u00e9passerait 90%.\nLib\u00e9rez de l'espace ou supprimez des mod\u00e8les inutilis\u00e9s d'abord.", + "dash.download_starting": "D\u00e9marrage du t\u00e9l\u00e9chargement\u2026", + "dash.download_complete": "T\u00e9l\u00e9chargement termin\u00e9 !", + "dash.download_checking": "V\u00e9rification\u2026", + "dash.sats_claimed_fmt": "%s sats r\u00e9clam\u00e9s", + "dash.model": "Mod\u00e8le", + "dash.today": "Aujourd'hui" +} diff --git a/internal/i18n/locales/it.json b/internal/i18n/locales/it.json new file mode 100644 index 0000000..db46c7e --- /dev/null +++ b/internal/i18n/locales/it.json @@ -0,0 +1,177 @@ +{ + "_lang": "Italiano", + + "tray.tooltip": "Owlrun \u2014 guadagna con la GPU inattiva", + "tray.earnings_today": "Guadagni di oggi", + "tray.earnings_total": "Guadagni totali", + "tray.wallet_warning": "\u26a0 Configura il tuo portafoglio in ~/.owlrun/owlrun.conf", + "tray.toggle_tip": "Metti in pausa o riprendi Owlrun", + "tray.open_dashboard": "Apri Dashboard", + "tray.open_dashboard_tip": "Apri localhost:19131", + "tray.accept_jobs": "Accetta Lavori", + "tray.job_never": "Mai", + "tray.job_never_tip": "Non accettare mai lavori", + "tray.job_idle_fmt": "Dopo %dm di inattivit\u00e0", + "tray.job_idle_tip": "Accetta lavori dopo il timeout di inattivit\u00e0", + "tray.job_always": "Sempre", + "tray.job_always_tip": "Accetta sempre lavori", + "tray.quit": "Esci", + "tray.quit_tip": "Chiudi Owlrun", + "tray.state.earning": "\ud83d\udfe2 Guadagnando", + "tray.state.idle": "\ud83d\udfe1 Inattivo \u2014 in attesa", + "tray.state.ready": "\ud83d\udfe1 Preparazione", + "tray.state.wallet": "\ud83d\udfe2 Portafoglio non configurato", + "tray.state.error": "\ud83d\udd34 Errore", + "tray.state.paused": "\u26aa In pausa", + "tray.resume": "Riprendi", + "tray.pause": "Pausa", + "tray.legend.green": "\ud83d\udfe2 Verde \u2014 Connesso e guadagnando", + "tray.legend.yellow": "\ud83d\udfe1 Giallo \u2014 Preparazione", + "tray.legend.blue": "\ud83d\udfe2 Blu \u2014 Portafoglio non configurato", + "tray.legend.red": "\ud83d\udd34 Rosso \u2014 Errore", + "tray.legend.grey": "\u26aa Grigio \u2014 In pausa", + "tray.disk_critical_fmt": "\u26a0 Solo %.1f GB liberi \u2014 download bloccati", + "tray.disk_warning_fmt": "\u26a0 Poco disco: %.1f GB liberi (%.0f%%)", + "tray.today_fmt": "Oggi: $%.2f", + "tray.total_fmt": "Totale: $%.2f", + "tray.err.mock_failed": "L'Ollama simulato non \u00e8 riuscito ad avviarsi: %s", + "tray.err.not_installed": "Ollama non \u00e8 installato. Scaricalo da ollama.com/download, installalo e riavvia Owlrun.", + "tray.err.start_failed": "Ollama non \u00e8 riuscito ad avviarsi. Assicurati che Ollama sia installato (ollama.com/download) e riavvia Owlrun.", + "tray.err.no_models": "Nessun modello IA installato. Apri il dashboard su localhost:19131 e scarica un modello, oppure esegui: ollama pull qwen2.5:0.5b", + "tray.wallet_msg": "Configura il tuo indirizzo Lightning nella sezione Portafoglio per iniziare a guadagnare Bitcoin.", + "tray.wallet_configured_fmt": "Portafoglio configurato a %s", + + "dash.title": "Dashboard Owlrun", + "dash.notifications": "Notifiche", + "dash.notifications_empty": "Le notifiche del gateway appariranno qui.", + "dash.broadcasts_empty": "Le notifiche del gateway appariranno qui.", + "dash.wallet": "Portafoglio", + "dash.wallet_not_configured": "Portafoglio non configurato", + "dash.wallet_setup_heading": "Configura il tuo portafoglio per essere pagato", + "dash.wallet_step1": "Installa il portafoglio Minibits (di Bitango Technologies)", + "dash.wallet_step2_pre": "Trova il tuo indirizzo Lightning in Minibits (sembra ", + "dash.wallet_step2_post": ")", + "dash.wallet_step3": "Incollalo qui sotto", + "dash.wallet_placeholder": "tuonome@minibits.cash", + "dash.wallet_save_btn": "Salva e inizia a guadagnare", + "dash.wallet_saving": "Salvataggio...", + "dash.wallet_any_wallet": "Funziona con qualsiasi portafoglio Lightning \u2014 Minibits, Phoenix, Wallet of Satoshi, ecc.", + "dash.wallet_ln_address": "Indirizzo Lightning", + "dash.wallet_change": "Cambia indirizzo", + "dash.wallet_save": "Salva", + "dash.wallet_cancel": "Annulla", + "dash.wallet_valid_error": "Inserisci un indirizzo Lightning valido (es. tuonome@minibits.cash)", + "dash.wallet_valid_error_short": "Inserisci un indirizzo Lightning valido", + "dash.wallet_configured_banner": "\u26a1 Portafoglio configurato", + "dash.wallet_auto_sent": "I guadagni vengono inviati automaticamente al tuo portafoglio Lightning. Nessuna azione necessaria.", + "dash.payout_threshold": "Soglia di pagamento", + "dash.payout_threshold_hint": "Pi\u00f9 basso = pagamenti pi\u00f9 veloci, commissioni pi\u00f9 alte. Pi\u00f9 alto = pagamenti pi\u00f9 lenti, commissioni pi\u00f9 basse.", + "dash.payout_fee_label": "Commissione Lightning stim.:", + "dash.payout_unlock_50": "Sblocca minimo 50 sats", + "dash.payout_low_warn": "\u26a0 ~10% consumato dalle commissioni Lightning a questo livello", + "dash.accept_jobs": "Accetta lavori", + "dash.job_always": "Sempre", + "dash.job_idle": "Quando inattivo", + "dash.job_never": "Mai", + "dash.job_hint_always": "Guadagna ogni volta che sei connesso", + "dash.job_hint_idle": "Guadagna solo quando sei assente", + "dash.job_hint_never": "Nessun lavoro accettato", + "dash.karma": "Karma", + "dash.karma_score": "Punteggio karma", + "dash.free_tier_jobs": "Lavori gratuiti serviti", + "dash.donate_free_tier": "Dona al livello gratuito", + "dash.donate_hint": "Condividi cicli inattivi con utenti gratuiti. Pi\u00f9 donazione = pi\u00f9 karma = pi\u00f9 traffico a pagamento verso di te.", + "dash.keep_warm": "Mantieni modello attivo", + "dash.keep_warm_hint": "Ping ogni 4 min per evitare l'espulsione dalla VRAM", + "dash.context_length": "Lunghezza contesto", + "dash.context_hint": "Valori pi\u00f9 alti usano pi\u00f9 VRAM. Il modello si ricarica alla modifica.", + "dash.models": "Modelli", + "dash.downloading": "Scaricamento\u2026", + "dash.show_slow_fmt": "Mostra %d modelli pi\u00f9 grandi (potrebbero essere lenti su questa macchina)", + "dash.model_active": "ATTIVO", + "dash.model_available": "DISPONIBILE", + "dash.model_activate": "Attiva", + "dash.model_download": "Scarica", + "dash.model_remove": "Rimuovi modello", + "dash.status": "Stato", + "dash.earnings": "Guadagni", + "dash.pending_earnings": "Guadagni in sospeso", + "dash.usd_approx": "USD stim.", + "dash.today_earnings": "Guadagni di oggi", + "dash.total_withdrawn": "Totale prelevato", + "dash.next_payout": "Prossimo pagamento stim.", + "dash.payout_progress_fmt": "%d%% della soglia (%d sats)", + "dash.usd_disclaimer": "USD stimato al tasso BTC in tempo reale", + "dash.recent_payouts": "Pagamenti recenti", + "dash.fee_disclaimer": "Le commissioni Lightning vanno alla rete Bitcoin, non a Owlrun. La nostra commissione \u00e8 sempre inferiore al 10%.", + "dash.ecash_advanced": "Avanzato: Preleva come ecash (QR)", + "dash.ecash_local": "Ecash locale", + "dash.ecash_proofs": "Prove", + "dash.ecash_claim_all": "Reclama tutto", + "dash.ecash_claiming": "Reclamo in corso\u2026", + "dash.ecash_copied": "Copiato!", + "dash.ecash_copy": "Copia", + "dash.ecash_recent": "Token recenti:", + "dash.ecash_export_hint": "L'esportazione \u00e8 disponibile dopo il reclamo. Usa prima Reclama tutto, poi copia il token.", + "dash.ecash_no_proofs": "Nessuna prova locale da esportare.", + "dash.ecash_claim_failed_fmt": "Reclamo fallito: %s", + "dash.state.earning": "Connesso e guadagnando", + "dash.state.ready": "Preparazione", + "dash.state.idle": "Inattivo \u2014 in attesa", + "dash.state.wallet": "Portafoglio non configurato", + "dash.state.error": "Errore", + "dash.state.paused": "In pausa", + "dash.legend.earning": "Guadagnando", + "dash.legend.ready": "Pronto", + "dash.legend.no_wallet": "Senza portafoglio", + "dash.legend.error": "Errore", + "dash.legend.paused": "In pausa", + "dash.btc_price": "Prezzo Bitcoin", + "dash.btc_live": "In diretta", + "dash.btc_yesterday": "Fissaggio di ieri", + "dash.btc_24h": "Media 24h", + "dash.btc_7d": "Media 7g", + "dash.btc_status": "Stato", + "dash.btc_inactive": "I pagamenti Bitcoin non sono ancora attivi su questo gateway", + "dash.gw": "Gateway", + "dash.gw_connection": "Connessione", + "dash.gw_connected": "Connesso", + "dash.gw_disconnected": "Disconnesso", + "dash.gw_jobs_today": "Lavori oggi", + "dash.gw_tokens_today": "Token oggi", + "dash.gw_queue": "Profondit\u00e0 coda", + "dash.gpu": "GPU", + "dash.gpu_none": "Nessuna GPU rilevata", + "dash.gpu_util": "Utilizzo", + "dash.gpu_vram": "VRAM libera", + "dash.gpu_temp": "Temperatura", + "dash.gpu_power": "Consumo", + "dash.disk": "Disco", + "dash.disk_free": "Libero", + "dash.disk_total": "Totale", + "dash.disk_path": "Percorso", + "dash.updated_fmt": "aggiornato %s", + "dash.connection_lost": "connessione persa\u2026", + "dash.charts_jobs": "Lavori", + "dash.charts_earnings": "Guadagni (USD)", + "dash.charts_per_period": "Per periodo", + "dash.charts_cumulative": "Cumulativo", + "dash.confirm_remove_title": "Rimuovere %s?", + "dash.confirm_remove_body": "Questo eliminer\u00e0 il modello dal disco e liberer\u00e0 spazio.\nPuoi riscaricarlo in seguito.", + "dash.confirm_switch_title": "Cambiare il modello attivo a %s?", + "dash.confirm_switch_body": "Questo ricaricher\u00e0 il modello in memoria e si registrer\u00e0 nuovamente al gateway.", + "dash.confirm_download_title": "Scaricare %s?", + "dash.download_size_fmt": "Dimensione modello: ~%s GB", + "dash.download_size_est_fmt": "Dimensione modello: ~%s GB (stimato)", + "dash.download_disk_fmt": "Disco libero: %s GB / %s GB totali", + "dash.download_after_fmt": "Dopo il download: ~%s%% usato", + "dash.download_time_hint": "Potrebbe richiedere alcuni minuti a seconda della connessione.", + "dash.download_no_space_title": "Spazio su disco insufficiente!", + "dash.download_no_space_body": "Download interrotto \u2014 l'utilizzo del disco supererebbe il 90%.\nLibera spazio o rimuovi modelli non utilizzati prima.", + "dash.download_starting": "Avvio download\u2026", + "dash.download_complete": "Download completato!", + "dash.download_checking": "Verifica\u2026", + "dash.sats_claimed_fmt": "%s sats reclamati", + "dash.model": "Modello", + "dash.today": "Oggi" +} From 116981054c489c2fcf902de01b659abb10afded7 Mon Sep 17 00:00:00 2001 From: aaronFortuno Date: Mon, 6 Apr 2026 11:21:21 +0200 Subject: [PATCH 2/8] feat(i18n): migrate system tray strings to i18n.T() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace all hardcoded user-facing strings in the system tray with i18n.T() calls: state labels, menu items, tooltips, error messages, wallet warnings, and disk alerts. The locale is auto-detected from the OS at tray startup via i18n.DetectLocale(). Behaviour is identical for English users — all keys resolve to the same strings that were previously hardcoded. Co-Authored-By: Claude Opus 4.6 (1M context) --- internal/tray/tray.go | 70 ++++++++++++++++++++++--------------------- 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/internal/tray/tray.go b/internal/tray/tray.go index 79db66a..a563f29 100644 --- a/internal/tray/tray.go +++ b/internal/tray/tray.go @@ -25,6 +25,7 @@ import ( "github.com/fabgoodvibes/owlrun/internal/disk" "github.com/fabgoodvibes/owlrun/internal/earnings" "github.com/fabgoodvibes/owlrun/internal/gpu" + "github.com/fabgoodvibes/owlrun/internal/i18n" "github.com/fabgoodvibes/owlrun/internal/idle" "github.com/fabgoodvibes/owlrun/internal/inference" "github.com/fabgoodvibes/owlrun/internal/marketplace" @@ -154,7 +155,8 @@ func Run(cfg config.Config, dash *dashboard.Server, mockMode bool) { // onReady is called by systray once the tray is initialized. func (a *Agent) onReady() { a.applyIcon() - systray.SetTooltip("Owlrun — idle GPU earning") + i18n.SetLocale(i18n.DetectLocale()) + systray.SetTooltip(i18n.T("tray.tooltip")) // Status line (read-only) a.mStatus = systray.AddMenuItem(a.stateLabel(), "") @@ -163,14 +165,14 @@ func (a *Agent) onReady() { // Earnings (read-only) snap := a.tracker.Get() - a.mToday = systray.AddMenuItem(fmtToday(snap.Today), "Earnings today") + a.mToday = systray.AddMenuItem(fmtToday(snap.Today), i18n.T("tray.earnings_today")) a.mToday.Disable() - a.mTotal = systray.AddMenuItem(fmtTotal(snap.Total), "All-time earnings") + a.mTotal = systray.AddMenuItem(fmtTotal(snap.Total), i18n.T("tray.earnings_total")) a.mTotal.Disable() systray.AddSeparator() // Wallet warning - a.mWalletWarn = systray.AddMenuItem("⚠ Set your payout wallet in ~/.owlrun/owlrun.conf", "") + a.mWalletWarn = systray.AddMenuItem(i18n.T("tray.wallet_warning"), "") a.mWalletWarn.Disable() if !config.NeedsWallet(&a.cfg) { a.mWalletWarn.Hide() @@ -178,14 +180,14 @@ func (a *Agent) onReady() { systray.AddSeparator() // Actions - a.mToggle = systray.AddMenuItem(a.toggleLabel(), "Pause or resume Owlrun") - a.mDashboard = systray.AddMenuItem("Open Dashboard", "Open localhost:19131") + a.mToggle = systray.AddMenuItem(a.toggleLabel(), i18n.T("tray.toggle_tip")) + a.mDashboard = systray.AddMenuItem(i18n.T("tray.open_dashboard"), i18n.T("tray.open_dashboard_tip")) // Job mode submenu - a.mJobMode = systray.AddMenuItem("Accept Jobs", "") - a.mJobNever = a.mJobMode.AddSubMenuItem("Never", "Never accept jobs") - a.mJobIdle = a.mJobMode.AddSubMenuItem(fmt.Sprintf("After idle %dm", a.cfg.Idle.TriggerMinutes), "Accept jobs after idle timeout") - a.mJobAlways = a.mJobMode.AddSubMenuItem("Always", "Always accept jobs") + a.mJobMode = systray.AddMenuItem(i18n.T("tray.accept_jobs"), "") + a.mJobNever = a.mJobMode.AddSubMenuItem(i18n.T("tray.job_never"), i18n.T("tray.job_never_tip")) + a.mJobIdle = a.mJobMode.AddSubMenuItem(i18n.T("tray.job_idle_fmt", a.cfg.Idle.TriggerMinutes), i18n.T("tray.job_idle_tip")) + a.mJobAlways = a.mJobMode.AddSubMenuItem(i18n.T("tray.job_always"), i18n.T("tray.job_always_tip")) a.applyJobModeChecks() systray.AddSeparator() @@ -196,17 +198,17 @@ func (a *Agent) onReady() { // Color legend systray.AddSeparator() for _, l := range []string{ - "🟢 Green — Connected & earning", - "🟡 Yellow — Getting ready", - "🔵 Blue — Wallet not set", - "🔴 Red — Error", - "⚪ Grey — Paused", + i18n.T("tray.legend.green"), + i18n.T("tray.legend.yellow"), + i18n.T("tray.legend.blue"), + i18n.T("tray.legend.red"), + i18n.T("tray.legend.grey"), } { m := systray.AddMenuItem(l, "") m.Disable() } systray.AddSeparator() - a.mQuit = systray.AddMenuItem("Quit", "Exit Owlrun") + a.mQuit = systray.AddMenuItem(i18n.T("tray.quit"), i18n.T("tray.quit_tip")) if a.dash != nil { a.dash.SetProvider(a.statusSnapshot) @@ -466,7 +468,7 @@ func (a *Agent) startEarning() { a.mu.Lock() a.starting = false a.state = StateError - a.errorDetail = "Mock Ollama failed to start: " + err.Error() + a.errorDetail = i18n.T("tray.err.mock_failed", err.Error()) a.refreshMenuLocked() a.mu.Unlock() return @@ -497,7 +499,7 @@ func (a *Agent) startEarning() { a.mu.Lock() a.starting = false a.state = StateError - a.errorDetail = "Ollama is not installed. Download it from ollama.com/download, install it, then restart Owlrun." + a.errorDetail = i18n.T("tray.err.not_installed") a.refreshMenuLocked() a.mu.Unlock() return @@ -507,7 +509,7 @@ func (a *Agent) startEarning() { a.mu.Lock() a.starting = false a.state = StateError - a.errorDetail = "Ollama failed to start. Make sure Ollama is installed (ollama.com/download) and try restarting Owlrun." + a.errorDetail = i18n.T("tray.err.start_failed") a.refreshMenuLocked() a.mu.Unlock() return @@ -529,7 +531,7 @@ func (a *Agent) startEarning() { a.mu.Lock() a.starting = false a.state = StateError - a.errorDetail = "No AI models installed. Open the dashboard at localhost:19131 and download a model, or run: ollama pull qwen2.5:0.5b" + a.errorDetail = i18n.T("tray.err.no_models") a.refreshMenuLocked() a.mu.Unlock() return @@ -626,26 +628,26 @@ func (a *Agent) applyIcon() { func (a *Agent) stateLabel() string { switch a.state { case StateEarning: - return "🟢 Earning" + return i18n.T("tray.state.earning") case StateIdle: - return "🟡 Idle — waiting" + return i18n.T("tray.state.idle") case StateReady: - return "🟡 Getting ready" + return i18n.T("tray.state.ready") case StateMissingWallet: - return "🔵 Wallet not set" + return i18n.T("tray.state.wallet") case StateError: - return "🔴 Error" + return i18n.T("tray.state.error") case StatePaused: - return "⚪ Paused" + return i18n.T("tray.state.paused") } return "" } func (a *Agent) toggleLabel() string { if a.manuallyPaused { - return "Resume" + return i18n.T("tray.resume") } - return "Pause" + return i18n.T("tray.pause") } // earningsRefreshLoop polls the earnings tracker and updates menu items. @@ -684,7 +686,7 @@ func (a *Agent) checkDisk(showDialogIfCritical bool) { switch { case info.FreeGB < minGB: // Critical: not enough space even for the smallest model. - a.mDiskWarn.SetTitle(fmt.Sprintf("⚠ Only %.1f GB free — downloads blocked", info.FreeGB)) + a.mDiskWarn.SetTitle(i18n.T("tray.disk_critical_fmt", info.FreeGB)) a.mDiskWarn.Show() if showDialogIfCritical { disk.CriticalDialog(info, minGB) @@ -692,7 +694,7 @@ func (a *Agent) checkDisk(showDialogIfCritical bool) { case info.FreePct < warnPct: // Warning: below threshold but still workable. - a.mDiskWarn.SetTitle(fmt.Sprintf("⚠ Low disk: %.1f GB free (%.0f%%)", info.FreeGB, info.FreePct)) + a.mDiskWarn.SetTitle(i18n.T("tray.disk_warning_fmt", info.FreeGB, info.FreePct)) a.mDiskWarn.Show() if showDialogIfCritical { disk.WarnDialog(info, a.cfg.Disk.WarnThresholdPct) @@ -747,8 +749,8 @@ func (a *Agent) applyJobModeChecks() { } } -func fmtToday(v float64) string { return fmt.Sprintf("Today: $%.2f", v) } -func fmtTotal(v float64) string { return fmt.Sprintf("Total: $%.2f", v) } +func fmtToday(v float64) string { return i18n.T("tray.today_fmt", v) } +func fmtTotal(v float64) string { return i18n.T("tray.total_fmt", v) } // statusSnapshot assembles a dashboard.Status from live subsystem data. // This is the StatusProvider function wired into the dashboard in onReady. @@ -767,9 +769,9 @@ func (a *Agent) statusSnapshot() dashboard.Status { s.Network = buildinfo.Network s.Wallet.Address = a.cfg.Account.Wallet if config.NeedsWallet(&a.cfg) { - s.Wallet.Warning = "Set your Lightning address in the Wallet section to start earning Bitcoin." + s.Wallet.Warning = i18n.T("tray.wallet_msg") } else if a.cfg.Account.LightningAddress != "" { - s.Wallet.Configured = "Wallet configured at " + a.cfg.Account.LightningAddress + s.Wallet.Configured = i18n.T("tray.wallet_configured_fmt", a.cfg.Account.LightningAddress) } switch state { case StateEarning: From 7add2bfe09d487dc4d61fbeb9a723a6c1a1e8031 Mon Sep 17 00:00:00 2001 From: aaronFortuno Date: Mon, 6 Apr 2026 11:35:01 +0200 Subject: [PATCH 3/8] feat(i18n): add dashboard internationalisation with language selector Migrate all user-facing strings in the web dashboard to i18n: - Add /api/locale?lang=xx endpoint serving the full translation map - Add /api/locales endpoint listing available languages with names - Add JS t(key) function with localStorage-persisted language choice - Add data-i18n attributes to ~60 static HTML elements - Replace ~40 JS-generated strings with t() calls (state labels, wallet messages, model picker, confirm dialogs, chart labels, etc.) - Add language dropdown selector next to the theme toggle The dashboard auto-detects the browser language on first load and falls back to English. Users can override via the dropdown, which persists in localStorage. Co-Authored-By: Claude Opus 4.6 (1M context) --- internal/dashboard/server.go | 368 ++++++++++++++++++++++------------- 1 file changed, 232 insertions(+), 136 deletions(-) diff --git a/internal/dashboard/server.go b/internal/dashboard/server.go index 51dd403..4a545fe 100644 --- a/internal/dashboard/server.go +++ b/internal/dashboard/server.go @@ -11,11 +11,13 @@ import ( "log" "net" "net/http" + "sort" "strings" "sync/atomic" "time" "github.com/fabgoodvibes/owlrun/internal/buildinfo" + "github.com/fabgoodvibes/owlrun/internal/i18n" "github.com/fabgoodvibes/owlrun/internal/cashu" "github.com/fabgoodvibes/owlrun/internal/earnings" ) @@ -300,6 +302,8 @@ func (s *Server) Start() error { mux.HandleFunc("/api/set-context-length", s.handleSetContextLength) mux.HandleFunc("/api/set-free-tier", s.handleSetFreeTier) mux.HandleFunc("/api/set-keep-warm", s.handleSetKeepWarm) + mux.HandleFunc("/api/locale", handleLocale) + mux.HandleFunc("/api/locales", handleLocales) addr := fmt.Sprintf("127.0.0.1:%d", s.port) ln, err := net.Listen("tcp", addr) if err != nil { @@ -850,9 +854,43 @@ func isValidLightningAddress(addr string) bool { return true } +// handleLocale serves the full translation map for a given locale. +// GET /api/locale?lang=ca → JSON map of key→string. +func handleLocale(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Access-Control-Allow-Origin", "*") + lang := r.URL.Query().Get("lang") + if lang == "" { + lang = "en" + } + json.NewEncoder(w).Encode(i18n.Messages(lang)) +} + +// handleLocales returns the list of available locales with display names. +// GET /api/locales → [{"code":"en","name":"English"}, …] +func handleLocales(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Access-Control-Allow-Origin", "*") + type localeInfo struct { + Code string `json:"code"` + Name string `json:"name"` + } + var out []localeInfo + for _, code := range i18n.Locales() { + msgs := i18n.Messages(code) + name := code + if v, ok := msgs["_lang"]; ok { + name = v + } + out = append(out, localeInfo{Code: code, Name: name}) + } + sort.Slice(out, func(i, j int) bool { return out[i].Code < out[j].Code }) + json.NewEncoder(w).Encode(out) +} + func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") - fmt.Fprint(w, dashboardHTML) + io.WriteString(w, dashboardHTML) //nolint:errcheck } const dashboardHTML = ` @@ -860,7 +898,7 @@ const dashboardHTML = ` -Owlrun Dashboard +Owlrun Dashboard @@ -1026,13 +1031,21 @@ const dashboardHTML = `
-
Wallet not configured
-
+
+
+
Wallet not configured
+
+
+ +
- +
-
Notifications
-
+
+ Notifications + +
+
@@ -1078,31 +1091,7 @@ const dashboardHTML = `
- -
-
- Payout threshold - 500 sats -
- -
- 1005001000 -
-
Lower = faster payouts, higher fees. Higher = slower payouts, lower fees.
-
- Est. Lightning fee: - ~1% -
-
- - -
-
- - +
Accept jobs @@ -1116,63 +1105,94 @@ const dashboardHTML = `
- +
-
- Karma - -
-
- Karma score - 0 +
+ + +
-
- Free tier jobs served - 0 -
-
+ + +
- Donate to free tier - 0% + Payout threshold + 500 sats
- +
- 0%50%100% + 1005001000 +
+
Lower = faster payouts, higher fees. Higher = slower payouts, lower fees.
+
+ Est. Lightning fee: + ~1% +
+
+ +
-
Share idle cycles with free-tier users. Higher donation = more karma = more paid traffic routed to you.
-
- -
-
-
- Keep model warm -
Ping every 4 min to prevent VRAM eviction
+ + -
- -
-
- Context length - 8192 -
-
- + + -
Higher values use more VRAM. Model reloads on change.
@@ -1243,14 +1263,18 @@ const dashboardHTML = `
- +
Status
-
+
+ + +
+
Earning @@ -1277,6 +1301,27 @@ const dashboardHTML = `
USD approximated at live BTC rate
+ +
+
Gateway
+
+ Connection + +
+
+ Jobs today + +
+
+ Tokens today + +
+
+ Queue depth + +
+
+
Bitcoin Price
@@ -1301,27 +1346,6 @@ const dashboardHTML = `
- -
-
Gateway
-
- Connection - -
-
- Jobs today - -
-
- Tokens today - -
-
- Queue depth - -
-
-
GPU
@@ -1367,10 +1391,6 @@ const dashboardHTML = `
- - -
- -
+
- -
-
Wallet
- - -
- - -
-
Status
+ +
+
Status
@@ -1285,108 +1076,158 @@ const dashboardHTML = `
-
-
Earnings
+ +
+
Earnings
0 sats
~$0.00
-
-
- Today - 0 sats -
-
- ~$0.00 -
-
-
USD approximated at live BTC rate
-
- - -
-
Gateway
-
- Connection - +
+
Today0 sats
+
~$0.00
-
- Jobs today - +
+
Pending0 sats
+
USD approx$0.00
+
Withdrawn0 sats
+
Next payout
-
- Tokens today - -
-
- Queue depth - + +
USD approximated at live BTC rate
-
-
Bitcoin Price
-
- Live - -
-
- Yesterday's Fix - -
-
- 24h Avg - + +
+
Gateway
+
Connection
+
Jobs today
+
Tokens today
+
Queue depth
+
+ + +
+
Accept jobs
+
+ + +
-
- 7d Avg - +
+
+ + +
+
Wallet
+ -
- Status - +
-
-
GPU
+ +
+
Payout
+
Threshold500 sats
+ +
1005001000
+
Lower = faster payouts, higher fees.
+
Est. fee: ~1%
+
+
+ + +
+
Bitcoin Price
+
Live
+
Yesterday's Fix
+
24h Avg
+
7d Avg
+
Status
+
+ + +
+
Karma
+
+
Karma score0
+
Free tier jobs0
+
Donate0%
+ +
0%50%100%
+
+ + +
+
GPU
-
- Utilisation - - -
-
-
-
- VRAM free - -
-
- Temperature - -
-
- Power draw - -
+
Utilisation
+
VRAM free
+
Temperature
+
Power draw
-
-
Disk
-
- Free - - -
-
+ +
+
Disk
+
Free
+
Total
+
Path
+
+ + +
+
Model
+
+
Keep model warm
Ping every 4 min
+
-
- Total - +
+
Context length8192
+
-
- Path - +
+ + +
+
Ecash
+
Local ecash0 sats
+
Proofs0
+
+ +
@@ -1496,14 +1337,59 @@ function toggleNotifications() { } })(); -function showCfgTab(name) { - document.querySelectorAll('.cfg-panel').forEach(function(p) { p.style.display = 'none'; }); - document.querySelectorAll('.cfg-tab').forEach(function(t) { t.classList.remove('active'); }); - var panel = document.getElementById('cfg-' + name); - var tab = document.querySelector('.cfg-tab[data-cfg="' + name + '"]'); - if (panel) panel.style.display = ''; - if (tab) tab.classList.add('active'); +// ── Drag and drop ────────────────────────────────────────────────── +var _dragCard = null; +document.addEventListener('dragstart', function(e) { + var card = e.target.closest('.card[draggable]'); + if (!card) return; + _dragCard = card; + card.classList.add('dragging'); + e.dataTransfer.effectAllowed = 'move'; +}); +document.addEventListener('dragend', function(e) { + if (_dragCard) _dragCard.classList.remove('dragging'); + document.querySelectorAll('.card.drag-over').forEach(function(c) { c.classList.remove('drag-over'); }); + _dragCard = null; +}); +document.addEventListener('dragover', function(e) { + e.preventDefault(); + var card = e.target.closest('.card[draggable]'); + if (!card || card === _dragCard) return; + e.dataTransfer.dropEffect = 'move'; + document.querySelectorAll('.card.drag-over').forEach(function(c) { c.classList.remove('drag-over'); }); + card.classList.add('drag-over'); +}); +document.addEventListener('drop', function(e) { + e.preventDefault(); + var target = e.target.closest('.card[draggable]'); + if (!target || !_dragCard || target === _dragCard) return; + target.classList.remove('drag-over'); + var grid = document.getElementById('card-grid'); + var cards = Array.from(grid.querySelectorAll('.card[draggable]')); + var fromIdx = cards.indexOf(_dragCard); + var toIdx = cards.indexOf(target); + if (fromIdx < toIdx) { target.parentNode.insertBefore(_dragCard, target.nextSibling); } + else { target.parentNode.insertBefore(_dragCard, target); } + saveCardOrder(); +}); +function saveCardOrder() { + var grid = document.getElementById('card-grid'); + var order = Array.from(grid.querySelectorAll('.card[draggable]')).map(function(c) { return c.id; }); + localStorage.setItem('owlrun-card-order', JSON.stringify(order)); +} +function restoreCardOrder() { + var saved = localStorage.getItem('owlrun-card-order'); + if (!saved) return; + try { + var order = JSON.parse(saved); + var grid = document.getElementById('card-grid'); + order.forEach(function(id) { + var card = document.getElementById(id); + if (card) grid.appendChild(card); + }); + } catch(e) {} } +restoreCardOrder(); function toggleModels() { var ms = document.getElementById('models-section'); @@ -1735,6 +1621,9 @@ function update(d) { if (sats < 1) return sats.toFixed(1) + ' sats'; return Math.round(sats).toLocaleString() + ' sats'; } + // Show/hide cards that require wallet + document.querySelectorAll('.needs-wallet').forEach(function(c) { c.style.display = lnAddr ? '' : 'none'; }); + if (lnAddr) { document.getElementById('wallet-setup').style.display = 'none'; document.getElementById('wallet-active').style.display = ''; @@ -1793,7 +1682,7 @@ function update(d) { } // Earnings stats - document.getElementById('wallet-today-sats').textContent = fmtSats(sw.gateway_sats); + // Today's sats now displayed in the merged Earnings card via today-sats element document.getElementById('wallet-withdrawn').textContent = fmtSats(sw.local_sats); if (sw.gateway_sats > 0 && thr > 0) { var pct = Math.min(100, Math.round(sw.gateway_sats / thr * 100)); From f0f6ff548400059b28694821c928d202cee810cf Mon Sep 17 00:00:00 2001 From: aaronFortuno Date: Mon, 6 Apr 2026 18:56:07 +0200 Subject: [PATCH 6/8] feat(dashboard): collapsible cards + color tints per card Two new personalisation features for the dashboard: - Cards are now collapsible: click the title to collapse/expand any card. A down-arrow indicates the state. Collapsed state is persisted per card in localStorage, so hidden cards stay hidden across reloads. - Card color tints: each card has a small dot button next to the drag handle that opens a 10-color palette (blue, green, amber, red, purple, cyan, pink, lime, orange + none). Selecting a color applies a subtle background tint that adapts to dark/light theme. Colors are persisted in localStorage and restored on load and theme change. All card content is wrapped in a .card-body div for clean collapse toggling. Both features work alongside drag-and-drop reordering. Fix: regenerated grid HTML from scratch to resolve malformed div nesting (12 extra closing tags) that broke the 4-column layout. Co-Authored-By: Claude Opus 4.6 (1M context) --- internal/dashboard/server.go | 370 +++++++++++++++++++++++------------ 1 file changed, 245 insertions(+), 125 deletions(-) diff --git a/internal/dashboard/server.go b/internal/dashboard/server.go index 6a5ad57..b1bdedc 100644 --- a/internal/dashboard/server.go +++ b/internal/dashboard/server.go @@ -952,7 +952,6 @@ const dashboardHTML = ` .card-title { font-size: 14px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.8px; color: var(--text-dim); margin-bottom: 16px; display: flex; justify-content: space-between; align-items: center; } .card-wide { grid-column: 1 / -1; } .card-notify { margin-bottom: 14px; } - .card.needs-wallet { display: none; } @media (max-width: 1100px) { .grid { grid-template-columns: 1fr 1fr 1fr; } } @media (max-width: 800px) { .grid { grid-template-columns: 1fr 1fr; } } @media (max-width: 550px) { .grid { grid-template-columns: 1fr; } } @@ -962,6 +961,19 @@ const dashboardHTML = ` .card[draggable="true"]:active .drag-handle { cursor: grabbing; } .card.dragging { opacity: 0.4; border-style: dashed; } .card.drag-over { border-color: var(--accent); box-shadow: 0 0 0 2px rgba(245,158,11,0.3); } + /* Collapsible cards */ + .card-title { cursor: pointer; user-select: none; } + .card-title .collapse-arrow { font-size: 10px; transition: transform .2s; margin-left: 4px; opacity: 0.5; } + .card.collapsed .card-body { display: none; } + .card.collapsed .collapse-arrow { transform: rotate(-90deg); } + .card-body { /* wrapper for collapsible content */ } + /* Card color tints */ + .color-dot { width: 12px; height: 12px; border-radius: 3px; cursor: pointer; border: 1px solid transparent; flex-shrink: 0; } + .color-dot:hover { border-color: var(--text-dim); } + .color-dot.active { border-color: var(--text); box-shadow: 0 0 0 1px var(--text); } + .color-picker { display: none; position: absolute; top: 100%; right: 0; background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; padding: 6px; gap: 4px; display: none; flex-wrap: wrap; width: 86px; z-index: 10; box-shadow: 0 4px 12px rgba(0,0,0,0.3); } + .color-picker.open { display: flex; } + .card-controls { display: flex; align-items: center; gap: 6px; position: relative; } .stat { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; min-height: 26px; } .stat:last-child { margin-bottom: 0; } .stat-label { color: var(--text); font-size: 16px; } @@ -1056,182 +1068,209 @@ const dashboardHTML = `
-
Status
-
-
-
-
-
- - +
Status
+
+
+
+
+
+
+ + +
+ +
+
+ Earning + Ready + No wallet + Error + Paused
- -
-
- Earning - Ready - No wallet - Error - Paused
- +
-
Earnings
-
0 sats
-
~$0.00
-
-
Today0 sats
-
~$0.00
-
-
-
Pending0 sats
-
USD approx$0.00
-
Withdrawn0 sats
-
Next payout
-
-
-
Gateway
-
Connection
-
Jobs today
-
Tokens today
-
Queue depth
+
Gateway
+
+
Connection
+
Jobs today
+
Tokens today
+
Queue depth
+
-
-
Accept jobs
-
- - - +
-
Wallet
-