-
GPU
-
-
-
Utilisation
-
- β
-
-
-
-
- VRAM free
- β
-
-
- Temperature
- β
-
-
-
Power draw
-
β
+
+
+
+
+
+
Keep model warmPing every 4 min
+
+
+
+
Context length8192
+
+
-
-
Disk
-
-
- Total
- β
-
-
-
Path
-
β
+
+
+
+
+
Local ecash0 sats
+
Proofs0
+
+
+
-
-
-
@@ -1339,11 +1281,11 @@ const dashboardHTML = `
-
Earnings (USD)
+
Earnings (USD)
@@ -1358,12 +1300,289 @@ function toggleTheme() {
var next = html.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
html.setAttribute('data-theme', next);
localStorage.setItem('owlrun-theme', next);
+ if (typeof restoreCardColors === 'function') restoreCardColors();
}
(function() {
var saved = localStorage.getItem('owlrun-theme');
if (saved) document.documentElement.setAttribute('data-theme', saved);
})();
+// ββ i18n ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+var _i18n = {};
+var _i18nLang = localStorage.getItem('owlrun-lang') || navigator.language || 'en';
+
+function t(key) {
+ return _i18n[key] || key;
+}
+
+function applyI18n() {
+ document.querySelectorAll('[data-i18n]').forEach(function(el) {
+ var key = el.getAttribute('data-i18n');
+ var val = t(key);
+ if (val !== key) {
+ if (el.tagName === 'TITLE') { document.title = val; }
+ else if (el.tagName === 'INPUT' && el.placeholder) { el.placeholder = val; }
+ else { el.textContent = val; }
+ }
+ });
+}
+
+function switchLang(lang) {
+ _i18nLang = lang;
+ localStorage.setItem('owlrun-lang', lang);
+ loadLocale(lang);
+}
+
+function loadLocale(lang) {
+ fetch('/api/locale?lang=' + encodeURIComponent(lang))
+ .then(function(r) { return r.json(); })
+ .then(function(msgs) { _i18n = msgs; applyI18n(); })
+ .catch(function() {});
+}
+
+function loadLocaleList() {
+ fetch('/api/locales')
+ .then(function(r) { return r.json(); })
+ .then(function(locales) {
+ var sel = document.getElementById('lang-select');
+ if (!sel) return;
+ sel.innerHTML = '';
+ var shortLang = _i18nLang.split('-')[0].split('_')[0].toLowerCase();
+ locales.forEach(function(loc) {
+ var opt = document.createElement('option');
+ opt.value = loc.code;
+ opt.textContent = loc.name;
+ if (loc.code === shortLang) opt.selected = true;
+ sel.appendChild(opt);
+ });
+ })
+ .catch(function() {});
+}
+
+// Load locale on startup
+loadLocaleList();
+loadLocale(_i18nLang);
+
+function toggleNotifications() {
+ var bc = document.getElementById('broadcasts');
+ bc.style.display = bc.style.display === 'none' ? '' : 'none';
+}
+// Close notifications dropdown when clicking outside
+document.addEventListener('click', function(e) {
+ if (!e.target.closest('#notify-card')) {
+ document.getElementById('broadcasts').style.display = 'none';
+ }
+});
+
+// Notification scrolling ticker
+function updateTicker(msgs) {
+ var textEl = document.getElementById('notify-ticker-text');
+ if (!msgs || msgs.length === 0) {
+ textEl.textContent = t('dash.notifications_empty');
+ textEl.style.animation = 'none';
+ return;
+ }
+ // Join all messages with a separator
+ var sep = ' \u00a0\u00a0\u2022\u00a0\u00a0 ';
+ var full = msgs.map(function(m) {
+ return (m.title ? m.title + ' \u2014 ' : '') + m.message;
+ }).join(sep);
+ // Only scroll if text overflows the container
+ textEl.textContent = full;
+ var containerW = document.getElementById('notify-card').offsetWidth;
+ var textW = textEl.scrollWidth;
+ if (textW > containerW) {
+ // Duplicate for seamless loop
+ textEl.textContent = full + sep + full;
+ var dur = Math.max(15, textEl.scrollWidth / 40); // ~40px/s
+ textEl.style.animation = 'ticker-scroll ' + dur + 's linear infinite';
+ } else {
+ textEl.style.animation = 'none';
+ }
+}
+
+// ββ Card collapse/expand βββββββββββββββββββββββββββββββββββββββββββ
+function toggleCard(titleEl) {
+ var card = titleEl.closest('.card');
+ if (!card) return;
+ card.classList.toggle('collapsed');
+ var states = JSON.parse(localStorage.getItem('owlrun-card-collapsed') || '{}');
+ states[card.id] = card.classList.contains('collapsed');
+ localStorage.setItem('owlrun-card-collapsed', JSON.stringify(states));
+}
+(function() {
+ var states = JSON.parse(localStorage.getItem('owlrun-card-collapsed') || '{}');
+ for (var id in states) { if (states[id]) { var el = document.getElementById(id); if (el) el.classList.add('collapsed'); } }
+})();
+
+// ββ Card color tints ββββββββββββββββββββββββββββββββββββββββββββββ
+var _cardColors = [
+ {name:'none', dark:'transparent', light:'transparent'},
+ {name:'blue', dark:'rgba(59,130,246,0.08)', light:'rgba(59,130,246,0.06)'},
+ {name:'green', dark:'rgba(34,197,94,0.08)', light:'rgba(34,197,94,0.06)'},
+ {name:'amber', dark:'rgba(245,158,11,0.08)', light:'rgba(245,158,11,0.06)'},
+ {name:'red', dark:'rgba(239,68,68,0.07)', light:'rgba(239,68,68,0.05)'},
+ {name:'purple', dark:'rgba(168,85,247,0.08)', light:'rgba(168,85,247,0.06)'},
+ {name:'cyan', dark:'rgba(6,182,212,0.08)', light:'rgba(6,182,212,0.06)'},
+ {name:'pink', dark:'rgba(236,72,153,0.07)', light:'rgba(236,72,153,0.05)'},
+ {name:'lime', dark:'rgba(132,204,22,0.08)', light:'rgba(132,204,22,0.06)'},
+ {name:'orange', dark:'rgba(249,115,22,0.08)', light:'rgba(249,115,22,0.06)'}
+];
+var _dotColors = ['transparent','#3b82f6','#22c55e','#f59e0b','#ef4444','#a855f7','#06b6d4','#ec4899','#84cc16','#f97316'];
+
+function toggleColorPicker(btn) {
+ var picker = btn.nextElementSibling;
+ if (picker.classList.contains('open')) { picker.classList.remove('open'); return; }
+ document.querySelectorAll('.color-picker.open').forEach(function(p) { p.classList.remove('open'); });
+ if (!picker.children.length) {
+ _dotColors.forEach(function(c, i) {
+ var dot = document.createElement('span');
+ dot.className = 'color-dot';
+ dot.style.background = c === 'transparent' ? 'var(--bg-card)' : c;
+ if (c === 'transparent') dot.style.border = '1px solid var(--border)';
+ dot.onclick = function() { applyCardColor(btn, i); };
+ picker.appendChild(dot);
+ });
+ }
+ picker.classList.add('open');
+ var card = btn.closest('.card');
+ var saved = JSON.parse(localStorage.getItem('owlrun-card-colors') || '{}');
+ var idx = saved[card.id] || 0;
+ Array.from(picker.children).forEach(function(d, i) { d.classList.toggle('active', i === idx); });
+}
+
+function applyCardColor(btn, colorIdx) {
+ var card = btn.closest('.card');
+ var isDark = document.documentElement.getAttribute('data-theme') === 'dark';
+ var c = _cardColors[colorIdx];
+ card.style.background = (c && colorIdx > 0) ? (isDark ? c.dark : c.light) : '';
+ var saved = JSON.parse(localStorage.getItem('owlrun-card-colors') || '{}');
+ if (colorIdx === 0) delete saved[card.id]; else saved[card.id] = colorIdx;
+ localStorage.setItem('owlrun-card-colors', JSON.stringify(saved));
+ btn.nextElementSibling.classList.remove('open');
+}
+
+function restoreCardColors() {
+ var saved = JSON.parse(localStorage.getItem('owlrun-card-colors') || '{}');
+ var isDark = document.documentElement.getAttribute('data-theme') === 'dark';
+ for (var id in saved) {
+ var card = document.getElementById(id);
+ var c = _cardColors[saved[id]];
+ if (card && c) card.style.background = isDark ? c.dark : c.light;
+ }
+}
+restoreCardColors();
+
+document.addEventListener('click', function(e) {
+ if (!e.target.closest('.card-controls')) {
+ document.querySelectorAll('.color-picker.open').forEach(function(p) { p.classList.remove('open'); });
+ }
+});
+
+// ββ Drag and drop (with placeholder gap) ββββββββββββββββββββββββββ
+var _dragCard = null;
+var _placeholder = null;
+var _dragNextSibling = null; // original position marker
+
+function createPlaceholder(h) {
+ var ph = document.createElement('div');
+ ph.className = 'drag-placeholder';
+ ph.style.height = h + 'px';
+ return ph;
+}
+function removePlaceholder() {
+ if (_placeholder && _placeholder.parentNode) _placeholder.parentNode.removeChild(_placeholder);
+ _placeholder = null;
+}
+
+document.addEventListener('dragstart', function(e) {
+ var card = e.target.closest('.card[draggable]');
+ if (!card) return;
+ _dragCard = card;
+ _dragNextSibling = card.nextSibling; // remember original position
+ var h = card.offsetHeight;
+ e.dataTransfer.effectAllowed = 'move';
+ setTimeout(function() {
+ card.classList.add('dragging');
+ _placeholder = createPlaceholder(Math.min(h, 60));
+ }, 0);
+});
+
+document.addEventListener('dragend', function(e) {
+ removePlaceholder();
+ if (_dragCard) _dragCard.classList.remove('dragging');
+ _dragCard = null;
+ _dragNextSibling = null;
+});
+
+document.addEventListener('dragover', function(e) {
+ e.preventDefault();
+ if (!_dragCard || !_placeholder) return;
+ e.dataTransfer.dropEffect = 'move';
+ var target = e.target.closest('.card[draggable]');
+ if (!target || target === _dragCard) return;
+ var grid = document.getElementById('card-grid');
+ var rect = target.getBoundingClientRect();
+ var midY = rect.top + rect.height / 2;
+ var insertBefore = e.clientY < midY ? target : target.nextSibling;
+ // Don't show placeholder at the original position
+ if (insertBefore === _dragCard || insertBefore === _dragNextSibling) {
+ removePlaceholder();
+ _placeholder = createPlaceholder(60);
+ return;
+ }
+ grid.insertBefore(_placeholder, insertBefore);
+});
+
+document.addEventListener('drop', function(e) {
+ e.preventDefault();
+ if (!_dragCard || !_placeholder || !_placeholder.parentNode) return;
+ _placeholder.parentNode.insertBefore(_dragCard, _placeholder);
+ removePlaceholder();
+ _dragCard.classList.remove('dragging');
+ _dragCard = null;
+ _dragNextSibling = null;
+ 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');
+ var arrow = document.getElementById('models-arrow');
+ var open = ms.style.display !== 'none';
+ ms.style.display = open ? 'none' : '';
+ arrow.style.transform = open ? '' : 'rotate(180deg)';
+ localStorage.setItem('owlrun-models-open', open ? '' : '1');
+}
+(function() {
+ if (localStorage.getItem('owlrun-models-open') === '1') {
+ document.getElementById('models-section').style.display = '';
+ document.getElementById('models-arrow').style.transform = 'rotate(180deg)';
+ }
+})();
+
+function dismissWalletBanner() {
+ document.getElementById('wallet-warn').style.display = 'none';
+ localStorage.setItem('owlrun-wallet-banner-dismissed', '1');
+}
+
function escapeHtml(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
function fmt2(n) {
if (n < 0.01) return '$' + n.toFixed(6);
@@ -1375,12 +1594,12 @@ function fmtMB(mb) { return mb > 1024 ? fmtGB(mb) : mb + ' MB'; }
function stateDisplay(state) {
switch(state) {
- case 'earning': return ['dot-green', 'Connected & earning'];
- case 'ready': return ['dot-yellow', 'Getting ready'];
- case 'idle': return ['dot-yellow', 'Idle β waiting'];
- case 'wallet': return ['dot-blue', 'Wallet not set'];
- case 'error': return ['dot-red', 'Error'];
- case 'paused': return ['dot-grey', 'Paused'];
+ case 'earning': return ['dot-green', t('dash.state.earning')];
+ case 'ready': return ['dot-yellow', t('dash.state.ready')];
+ case 'idle': return ['dot-yellow', t('dash.state.idle')];
+ case 'wallet': return ['dot-blue', t('dash.state.wallet')];
+ case 'error': return ['dot-red', t('dash.state.error')];
+ case 'paused': return ['dot-grey', t('dash.state.paused')];
default: return ['dot-grey', state];
}
}
@@ -1415,16 +1634,19 @@ function update(d) {
// Wallet warning / configured banner
var ww = document.getElementById('wallet-warn');
var wwTitle = ww.querySelector('.warn-title');
+ var wwClose = document.getElementById('wallet-warn-close');
if (d.wallet && d.wallet.warning) {
- wwTitle.textContent = 'Wallet not configured';
+ wwTitle.textContent = t('dash.wallet_not_configured');
document.getElementById('wallet-warn-body').innerHTML = d.wallet.warning;
ww.classList.remove('configured');
ww.style.display = 'block';
+ wwClose.style.display = 'none';
} else if (d.wallet && d.wallet.configured) {
- wwTitle.textContent = '\u26a1 Wallet configured';
- document.getElementById('wallet-warn-body').textContent = d.wallet.configured;
+ wwTitle.textContent = t('dash.wallet_configured_banner');
+ document.getElementById('wallet-warn-body').textContent = t('tray.wallet_configured_fmt').replace('%s', d.wallet.configured);
ww.classList.add('configured');
- ww.style.display = 'block';
+ wwClose.style.display = '';
+ if (!localStorage.getItem('owlrun-wallet-banner-dismissed')) ww.style.display = 'block';
} else { ww.classList.remove('configured'); ww.style.display = 'none'; }
const [dotClass, label] = stateDisplay(d.state);
@@ -1453,7 +1675,7 @@ function update(d) {
document.getElementById('today-usd').textContent = '~' + fmt2(d.earnings.today_usd);
const g = d.gpu;
- document.getElementById('gpu-name').textContent = g.name || 'No GPU detected';
+ document.getElementById('gpu-name').textContent = g.name || t('dash.gpu_none');
document.getElementById('util-pct').textContent = g.util_pct + '%';
document.getElementById('util-bar').style.width = g.util_pct + '%';
document.getElementById('vram-free').textContent = fmtMB(g.vram_free_mb);
@@ -1465,10 +1687,12 @@ function update(d) {
// Model picker β interactive
var ms = document.getElementById('models-section');
+ var msSummary = document.getElementById('models-summary-text');
var avail = d.available_models || [];
var pulling = d.pulling || false;
if (avail.length === 0) {
- ms.innerHTML = '
Modelβ
';
+ msSummary.textContent = t('dash.model') + ': β';
+ ms.innerHTML = '';
} else {
// Split into fits vs slow, sort: fits first (installed first within each group)
var fitsModels = avail.filter(function(m) { return m.fits; });
@@ -1476,9 +1700,15 @@ function update(d) {
fitsModels.sort(function(a,b) { return (b.installed?1:0) - (a.installed?1:0) || (b.active?1:0) - (a.active?1:0); });
slowModels.sort(function(a,b) { return (b.installed?1:0) - (a.installed?1:0); });
- var diskInfo = d.disk ? d.disk.free_gb.toFixed(1) + ' GB free' : '';
- var html = '
Models' + diskInfo + '
';
- if (pulling) html += '
Downloadingβ¦
';
+ // Models summary (always visible)
+ var activeModel = avail.filter(function(m) { return m.active; });
+ var installedCount = avail.filter(function(m) { return m.installed; }).length;
+ var activeName = activeModel.length > 0 ? activeModel[0].tag : 'β';
+ msSummary.innerHTML = '
' + escapeHtml(activeName) + ' (' + installedCount + ' ' + t('dash.models').toLowerCase() + ')';
+
+ var diskInfo = d.disk ? d.disk.free_gb.toFixed(1) + ' GB' : '';
+ var html = '
' + t('dash.models') + '' + diskInfo + '
';
+ if (pulling) html += '
' + t('dash.downloading') + '
';
var registeredModels = d.models || [];
function renderModelCard(m) {
@@ -1500,20 +1730,20 @@ function update(d) {
h += '
';
// Action buttons + badges
if (isActive) {
- h += '
ACTIVE';
+ h += '
' + t('dash.model_active') + '';
} else if (isRegistered && m.installed && !pulling) {
h += '
';
- h += 'AVAILABLE';
- h += '';
- h += '';
+ h += '' + t('dash.model_available') + '';
+ h += '';
+ h += '';
h += '
';
} else if (m.installed && !pulling) {
h += '
';
- h += '';
- h += '';
+ h += '';
+ h += '';
h += '
';
} else if (!m.installed && !pulling) {
- h += '
';
+ h += '
';
} else {
h += '
';
}
@@ -1529,7 +1759,7 @@ function update(d) {
var slowChecked = showSlow ? showSlow.checked : false;
html += '
';
+ html += t('dash.show_slow_fmt').replace('%d', slowModels.length) + '';
if (slowChecked) {
slowModels.forEach(function(m) { html += renderModelCard(m); });
}
@@ -1540,7 +1770,7 @@ function update(d) {
const gw = d.gateway;
const connEl = document.getElementById('gw-connected');
- connEl.textContent = gw.connected ? 'Connected' : 'Disconnected';
+ connEl.textContent = gw.connected ? t('dash.gw_connected') : t('dash.gw_disconnected');
connEl.className = 'stat-value ' + (gw.connected ? 'connected' : 'disconnected');
document.getElementById('gw-jobs').textContent = gw.jobs_today;
document.getElementById('gw-tokens').textContent = gw.tokens_today.toLocaleString();
@@ -1563,6 +1793,11 @@ 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('[data-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 = '';
@@ -1621,7 +1856,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));
@@ -1654,7 +1889,7 @@ function update(d) {
// Token history
var histEl = document.getElementById('ecash-token-history');
if (sw.token_history && sw.token_history.length > 0) {
- histEl.innerHTML = '
Recent tokens:
' +
+ histEl.innerHTML = '
' + t('dash.ecash_recent') + '
' +
sw.token_history.slice(0, 5).map(function(t) {
return '
' +
fmtSats(t.sats) + ' β ' + new Date(t.claimed_at).toLocaleString() + '
';
@@ -1676,7 +1911,7 @@ function update(d) {
var n = document.createElement('div');
n.id = 'btc-inactive-notice';
n.style.cssText = 'color:var(--text-muted);font-size:14px;font-style:italic;padding:8px 0;text-align:center';
- n.textContent = 'Bitcoin payments not yet active on this gateway';
+ n.textContent = t('dash.btc_inactive');
btcCard.querySelectorAll('.stat').forEach(function(s) { s.style.display = 'none'; });
btcCard.appendChild(n);
}
@@ -1693,19 +1928,25 @@ function update(d) {
// Broadcasts
var bcEl = document.getElementById('broadcasts');
+ var notifyBadge = document.getElementById('notify-badge');
if (d.broadcasts && d.broadcasts.length > 0) {
+ notifyBadge.textContent = d.broadcasts.length;
+ notifyBadge.style.display = '';
var sorted = d.broadcasts.slice().sort(function(a, b) { return b.timestamp.localeCompare(a.timestamp); });
bcEl.innerHTML = sorted.map(function(b) {
- var t = new Date(b.timestamp);
- var ts = isNaN(t) ? b.timestamp : t.toLocaleString();
+ var ts2 = new Date(b.timestamp);
+ var tsStr = isNaN(ts2) ? b.timestamp : ts2.toLocaleString();
var title = b.title ? '
' + escapeHtml(b.title) + ' β ' : '';
- return '
' + title + escapeHtml(b.message) + '' + ts + '
';
+ return '
' + title + escapeHtml(b.message) + '' + tsStr + '
';
}).join('');
+ updateTicker(sorted);
} else {
- bcEl.innerHTML = '
Broadcast notifications from the gateway will appear here.
';
+ bcEl.innerHTML = '
' + escapeHtml(t('dash.broadcasts_empty')) + '
';
+ notifyBadge.style.display = 'none';
+ updateTicker([]);
}
- document.getElementById('updated').textContent = 'updated ' + new Date().toLocaleTimeString();
+ document.getElementById('updated').textContent = t('dash.updated_fmt').replace('%s', new Date().toLocaleTimeString());
}
async function saveLightningAddress() {
@@ -1713,19 +1954,19 @@ async function saveLightningAddress() {
var errEl = document.getElementById('ln-save-error');
errEl.style.display = 'none';
if (!addr || !addr.includes('@')) {
- errEl.textContent = 'Enter a valid Lightning address (e.g. yourname@minibits.cash)';
+ errEl.textContent = t('dash.wallet_valid_error');
errEl.style.display = '';
return;
}
var btn = document.getElementById('btn-save-ln');
- btn.disabled = true; btn.textContent = 'Saving...';
+ btn.disabled = true; btn.textContent = t('dash.wallet_saving');
try {
var r = await fetch('/api/set-lightning-address', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({address:addr})});
var data = await r.json();
if (data.error) { errEl.textContent = data.error; errEl.style.display = ''; return; }
poll(); // refresh immediately
} catch(e) { errEl.textContent = 'Failed: ' + e.message; errEl.style.display = ''; }
- finally { btn.disabled = false; btn.textContent = 'Save & Start Earning'; }
+ finally { btn.disabled = false; btn.textContent = t('dash.wallet_save_btn'); }
}
async function saveLightningAddressEdit() {
@@ -1733,7 +1974,7 @@ async function saveLightningAddressEdit() {
var errEl = document.getElementById('ln-edit-error');
errEl.style.display = 'none';
if (!addr || !addr.includes('@')) {
- errEl.textContent = 'Enter a valid Lightning address';
+ errEl.textContent = t('dash.wallet_valid_error_short');
errEl.style.display = '';
return;
}
@@ -1799,7 +2040,7 @@ async function setJobMode(mode) {
function applyJobModeUI(mode) {
var btns = {always: document.getElementById('jm-always'), idle: document.getElementById('jm-idle'), never: document.getElementById('jm-never')};
- var hints = {always: 'Earning whenever connected', idle: 'Earning only when you are away', never: 'Not accepting any jobs'};
+ var hints = {always: t('dash.job_hint_always'), idle: t('dash.job_hint_idle'), never: t('dash.job_hint_never')};
for (var k in btns) {
if (k === mode) {
btns[k].style.background = '#f7931a';
@@ -1844,23 +2085,23 @@ async function poll() {
const r = await fetch('/api/status');
update(await r.json());
} catch(e) {
- document.getElementById('updated').textContent = 'connection lostβ¦';
+ document.getElementById('updated').textContent = t('dash.connection_lost');
}
fetchHistory();
}
async function claimEcash() {
var btn = document.getElementById('btn-claim');
- btn.disabled = true; btn.textContent = 'Claimingβ¦';
+ btn.disabled = true; btn.textContent = t('dash.ecash_claiming');
try {
var r = await fetch('/api/claim-ecash', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({amount_sats:0})});
var data = await r.json();
- if (data.error) { alert('Claim failed: ' + data.error); return; }
+ if (data.error) { alert(t('dash.ecash_claim_failed_fmt').replace('%s', data.error)); return; }
document.getElementById('claim-token').value = data.token;
document.getElementById('claim-amount').textContent = data.amount_sats ? Math.round(data.amount_sats/1000).toLocaleString() + ' sats claimed' : '';
document.getElementById('claim-result').style.display = '';
- } catch(e) { alert('Claim failed: ' + e.message); }
- finally { btn.disabled = false; btn.textContent = 'Claim All'; }
+ } catch(e) { alert(t('dash.ecash_claim_failed_fmt').replace('%s', e.message)); }
+ finally { btn.disabled = false; btn.textContent = t('dash.ecash_claim_all'); }
}
async function exportToken() {
@@ -1870,9 +2111,9 @@ async function exportToken() {
var r = await fetch('/api/status');
var d = await r.json();
if (d.sats_wallet && d.sats_wallet.local_sats > 0) {
- alert('Export is available after claiming. Use Claim All first, then copy the token.');
+ alert(t('dash.ecash_export_hint'));
} else {
- alert('No local proofs to export.');
+ alert(t('dash.ecash_no_proofs'));
}
} finally { btn.disabled = false; }
}
@@ -1880,8 +2121,8 @@ async function exportToken() {
function copyToken() {
var ta = document.getElementById('claim-token');
ta.select(); document.execCommand('copy');
- var btn = event.target; btn.textContent = 'Copied!';
- setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
+ var btn = event.target; btn.textContent = t('dash.ecash_copied');
+ setTimeout(function() { btn.textContent = t('dash.ecash_copy'); }, 1500);
}
// ββ Charts ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
@@ -1927,16 +2168,16 @@ function initCharts() {
jobsChart = new Chart(document.getElementById('chart-jobs').getContext('2d'), {
type: 'line',
data: { labels: [], datasets: [
- { label: 'Per period', data: [], borderColor: '#eab308', backgroundColor: '#eab30833', borderWidth: 2, pointRadius: 2, tension: 0.3, fill: true, yAxisID: 'y' },
- { label: 'Cumulative', data: [], borderColor: '#3b82f6', backgroundColor: 'transparent', borderWidth: 2, pointRadius: 1, borderDash: [4,3], tension: 0.3, yAxisID: 'y1' }
+ { label: t('dash.charts_per_period'), data: [], borderColor: '#eab308', backgroundColor: '#eab30833', borderWidth: 2, pointRadius: 2, tension: 0.3, fill: true, yAxisID: 'y' },
+ { label: t('dash.charts_cumulative'), data: [], borderColor: '#3b82f6', backgroundColor: 'transparent', borderWidth: 2, pointRadius: 1, borderDash: [4,3], tension: 0.3, yAxisID: 'y1' }
] },
options: makeChartOpts()
});
earningsChart = new Chart(document.getElementById('chart-earnings').getContext('2d'), {
type: 'line',
data: { labels: [], datasets: [
- { label: 'Per period', data: [], borderColor: '#22c55e', backgroundColor: '#22c55e33', borderWidth: 2, pointRadius: 2, tension: 0.3, fill: true, yAxisID: 'y' },
- { label: 'Cumulative', data: [], borderColor: '#f59e0b', backgroundColor: 'transparent', borderWidth: 2, pointRadius: 1, borderDash: [4,3], tension: 0.3, yAxisID: 'y1' }
+ { label: t('dash.charts_per_period'), data: [], borderColor: '#22c55e', backgroundColor: '#22c55e33', borderWidth: 2, pointRadius: 2, tension: 0.3, fill: true, yAxisID: 'y' },
+ { label: t('dash.charts_cumulative'), data: [], borderColor: '#f59e0b', backgroundColor: 'transparent', borderWidth: 2, pointRadius: 1, borderDash: [4,3], tension: 0.3, yAxisID: 'y1' }
] },
options: makeChartOpts(smartUsd)
});
@@ -1978,7 +2219,7 @@ poll();
setInterval(poll, 5000);
async function removeModel(tag) {
- if (!confirm('Remove ' + tag + '?\n\nThis will delete the model from disk and free up space.\nYou can re-download it later.')) return;
+ if (!confirm(t('dash.confirm_remove_title').replace('%s', tag) + '\n\n' + t('dash.confirm_remove_body'))) return;
try {
var resp = await fetch('/api/remove-model', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({model:tag})});
var data = await resp.json();
@@ -1988,7 +2229,7 @@ async function removeModel(tag) {
}
async function switchModel(tag) {
- if (!confirm('Switch active model to ' + tag + '?\n\nThis will reload the model into memory and re-register with the gateway.')) return;
+ if (!confirm(t('dash.confirm_switch_title').replace('%s', tag) + '\n\n' + t('dash.confirm_switch_body'))) return;
try {
var resp = await fetch('/api/switch-model', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({model:tag})});
var data = await resp.json();
@@ -2001,7 +2242,7 @@ async function pullModel(tag, vramGb) {
// Show spinner on download button while checking size
var btnId = 'dl-' + tag.replace(/[:.]/g, '_');
var dlBtn = document.getElementById(btnId);
- if (dlBtn) { dlBtn.disabled = true; dlBtn.innerHTML = '
Checkingβ¦'; }
+ if (dlBtn) { dlBtn.disabled = true; dlBtn.innerHTML = '
' + t('dash.download_checking'); }
var st = await (await fetch('/api/status')).json();
var diskFree = st.disk ? st.disk.free_gb : 0;
@@ -2015,7 +2256,7 @@ async function pullModel(tag, vramGb) {
var sizeData = await sizeResp.json();
if (sizeData.size_mb > 0) { sizeMb = sizeData.size_mb; sizeSource = 'registry'; }
} catch(e) {}
- if (dlBtn) { dlBtn.disabled = false; dlBtn.textContent = 'Download'; }
+ if (dlBtn) { dlBtn.disabled = false; dlBtn.textContent = t('dash.model_download'); }
// Step 2: Fallback estimate if registry failed
if (sizeMb === 0) {
@@ -2029,20 +2270,19 @@ async function pullModel(tag, vramGb) {
// Step 3: Abort if would exceed 90% disk usage
if (usagePct > 90) {
- alert('Not enough disk space!\n\n' +
- 'Model size: ~' + sizeGb + ' GB' + (sizeSource === 'estimate' ? ' (estimated)' : '') + '\n' +
- 'Disk free: ' + diskFree.toFixed(1) + ' GB / ' + diskTotal.toFixed(0) + ' GB total\n' +
- 'After download: ' + usagePct.toFixed(0) + '% used\n\n' +
- 'Download aborted β disk usage would exceed 90%.\n' +
- 'Free up space or remove unused models first.');
+ alert(t('dash.download_no_space_title') + '\n\n' +
+ (sizeSource === 'estimate' ? t('dash.download_size_est_fmt') : t('dash.download_size_fmt')).replace('%s', sizeGb) + '\n' +
+ t('dash.download_disk_fmt').replace('%s', diskFree.toFixed(1)).replace('%s', diskTotal.toFixed(0)) + '\n' +
+ t('dash.download_after_fmt').replace('%s', usagePct.toFixed(0)) + '\n\n' +
+ t('dash.download_no_space_body'));
return;
}
- if (!confirm('Download ' + tag + '?\n\n' +
- 'Model size: ~' + sizeGb + ' GB' + (sizeSource === 'estimate' ? ' (estimated)' : '') + '\n' +
- 'Disk free: ' + diskFree.toFixed(1) + ' GB / ' + diskTotal.toFixed(0) + ' GB total\n' +
- 'After download: ~' + usagePct.toFixed(0) + '% used\n\n' +
- 'This may take a few minutes depending on your connection.')) return;
+ if (!confirm(t('dash.confirm_download_title').replace('%s', tag) + '\n\n' +
+ (sizeSource === 'estimate' ? t('dash.download_size_est_fmt') : t('dash.download_size_fmt')).replace('%s', sizeGb) + '\n' +
+ t('dash.download_disk_fmt').replace('%s', diskFree.toFixed(1)).replace('%s', diskTotal.toFixed(0)) + '\n' +
+ t('dash.download_after_fmt').replace('%s', usagePct.toFixed(0)) + '\n\n' +
+ t('dash.download_time_hint'))) return;
// Show progress in the models section
var progDiv = document.getElementById('pull-progress');
@@ -2053,7 +2293,7 @@ async function pullModel(tag, vramGb) {
var ms = document.getElementById('models-section');
ms.insertBefore(progDiv, ms.children[1]);
}
- progDiv.innerHTML = '
Starting downloadβ¦';
+ progDiv.innerHTML = '
' + t('dash.download_starting');
try {
var resp = await fetch('/api/pull-model', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({model:tag})});
@@ -2077,7 +2317,7 @@ async function pullModel(tag, vramGb) {
try {
var ev = JSON.parse(line);
if (ev.error) { progDiv.textContent = 'Error: ' + ev.error; progDiv.style.borderColor = '#ef4444'; return; }
- if (ev.status === 'done') { progDiv.textContent = 'Download complete!'; progDiv.style.borderColor = '#4ade80'; progDiv.style.color = '#4ade80';
+ if (ev.status === 'done') { progDiv.textContent = t('dash.download_complete'); progDiv.style.borderColor = '#4ade80'; progDiv.style.color = '#4ade80';
setTimeout(function() { poll(); }, 1000); return; }
if (ev.total > 0) {
var pct = Math.round(ev.completed / ev.total * 100);
@@ -2088,7 +2328,7 @@ async function pullModel(tag, vramGb) {
} catch(e) {}
}
}
- progDiv.textContent = 'Download complete!';
+ progDiv.textContent = t('dash.download_complete');
progDiv.style.borderColor = '#4ade80'; progDiv.style.color = '#4ade80';
setTimeout(function() { poll(); }, 1000);
} catch(e) { progDiv.textContent = 'Failed: ' + e.message; progDiv.style.borderColor = '#ef4444'; }
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..0ae6904
--- /dev/null
+++ b/internal/i18n/locales/ca.json
@@ -0,0 +1,178 @@
+{
+ "_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.settings": "Configuraci\u00f3",
+ "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..7c792c9
--- /dev/null
+++ b/internal/i18n/locales/de.json
@@ -0,0 +1,178 @@
+{
+ "_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.settings": "Einstellungen",
+ "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..7cc4649
--- /dev/null
+++ b/internal/i18n/locales/en.json
@@ -0,0 +1,178 @@
+{
+ "_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.settings": "Settings",
+ "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..2d53a0d
--- /dev/null
+++ b/internal/i18n/locales/es.json
@@ -0,0 +1,178 @@
+{
+ "_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.settings": "Configuraci\u00f3n",
+ "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..7287dfb
--- /dev/null
+++ b/internal/i18n/locales/fr.json
@@ -0,0 +1,178 @@
+{
+ "_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.settings": "Param\u00e8tres",
+ "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..229020e
--- /dev/null
+++ b/internal/i18n/locales/it.json
@@ -0,0 +1,178 @@
+{
+ "_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.settings": "Impostazioni",
+ "dash.model": "Modello",
+ "dash.today": "Oggi"
+}
diff --git a/internal/tray/tray.go b/internal/tray/tray.go
index 79db66a..d08ac37 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 = a.cfg.Account.LightningAddress
}
switch state {
case StateEarning: