mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-08-07 09:12:57 +02:00
feat(i18n): translate contacts.js (stage 5b)
~100 keys, finishing stage 5. Catalog now 471 keys, pl at 100%. Covers the cleanup form's validation and progress messages, the protect/block/ ignore/approve button set, bulk approve and ignore with their partial-failure reports, the QR and URI add flows, and every toast in the file. Folded in the fourth and last copy of relative-time formatting. contacts.js had its own formatRelativeTime with more granularity than the shared helper — months and years instead of falling back to a date past a week. Rather than drop that, formatTimeAgo gained a `long` option that keeps counting, so a stale advert still reads "3 months ago" / "3 miesiące temu". dm.js's copy is the only one left and goes with stage 6. Two things worth recording about verification. I first checked these pages only for leaked catalog keys, which came back clean and told me nothing — the page was still full of English. Running the actual en/pl diff turned up the page heading, Back and Refresh, all four sort options, the "Last advert:" prefix, the activity tooltips, "Protect" in its second render site, and the hop counter. The lesson is that a key-leak check proves the keys you used are real, not that you used them. And the diff's raw output on this page is 2000 lines because every contact name and public key is legitimately identical in both languages. Filtering those out made the eight real misses obvious; the per-page exclusion lists exist for exactly this and the contacts entries now carry them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+117
-153
@@ -331,7 +331,7 @@ async function loadContactCounts() {
|
||||
*/
|
||||
async function loadCleanupSettings() {
|
||||
const statusText = document.getElementById('autoCleanupStatusText');
|
||||
if (statusText) statusText.textContent = 'Loading...';
|
||||
if (statusText) statusText.textContent = t('common.loading');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/contacts/cleanup-settings');
|
||||
@@ -344,11 +344,11 @@ async function loadCleanupSettings() {
|
||||
console.log('Loaded cleanup settings:', autoCleanupSettings, 'timezone:', cleanupTimezone);
|
||||
} else {
|
||||
console.error('Failed to load cleanup settings:', data.error);
|
||||
if (statusText) statusText.textContent = 'Error loading settings';
|
||||
if (statusText) statusText.textContent = t('contacts.toast.settings_load_error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading cleanup settings:', error);
|
||||
if (statusText) statusText.textContent = 'Network error';
|
||||
if (statusText) statusText.textContent = t('common.network_error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -411,7 +411,7 @@ function applyCleanupSettingsToUI(settings) {
|
||||
statusText.classList.remove('text-muted');
|
||||
statusText.classList.add('text-success');
|
||||
} else {
|
||||
statusText.textContent = 'Disabled';
|
||||
statusText.textContent = t('common.disabled');
|
||||
statusText.classList.remove('text-success');
|
||||
statusText.classList.add('text-muted');
|
||||
}
|
||||
@@ -433,14 +433,14 @@ async function handleAutoCleanupToggle(event) {
|
||||
|
||||
// Check if days > 0
|
||||
if (criteria.days <= 0) {
|
||||
showToast('Set "Days of Inactivity" > 0 before enabling auto-cleanup', 'warning');
|
||||
showToast(t('contacts.toast.need_days'), 'warning');
|
||||
event.target.checked = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if at least one type is selected
|
||||
if (criteria.types.length === 0) {
|
||||
showToast('Select at least one contact type before enabling auto-cleanup', 'warning');
|
||||
showToast(t('contacts.toast.need_type_enable'), 'warning');
|
||||
event.target.checked = false;
|
||||
return;
|
||||
}
|
||||
@@ -453,7 +453,7 @@ async function handleAutoCleanupToggle(event) {
|
||||
|
||||
// Update status text while saving
|
||||
if (statusText) {
|
||||
statusText.textContent = 'Saving...';
|
||||
statusText.textContent = t('common.saving');
|
||||
statusText.classList.remove('text-success', 'text-muted');
|
||||
}
|
||||
|
||||
@@ -536,7 +536,7 @@ async function saveCleanupSettings(enabled) {
|
||||
statusText.classList.remove('text-muted');
|
||||
statusText.classList.add('text-success');
|
||||
} else {
|
||||
statusText.textContent = 'Disabled';
|
||||
statusText.textContent = t('common.disabled');
|
||||
statusText.classList.remove('text-success');
|
||||
statusText.classList.add('text-muted');
|
||||
}
|
||||
@@ -546,7 +546,7 @@ async function saveCleanupSettings(enabled) {
|
||||
return true;
|
||||
} else {
|
||||
console.error('Failed to save cleanup settings:', data.error);
|
||||
showToast('Failed to save settings: ' + data.error, 'danger');
|
||||
showToast(t('contacts.toast.settings_save_failed', { error: data.error }), 'danger');
|
||||
|
||||
// Restore previous status
|
||||
if (statusText && autoCleanupSettings) {
|
||||
@@ -555,7 +555,7 @@ async function saveCleanupSettings(enabled) {
|
||||
const hourStr = prevHour.toString().padStart(2, '0');
|
||||
statusText.textContent = `Enabled (runs daily at ${hourStr}:00 ${cleanupTimezone})`;
|
||||
} else {
|
||||
statusText.textContent = 'Disabled';
|
||||
statusText.textContent = t('common.disabled');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -563,10 +563,10 @@ async function saveCleanupSettings(enabled) {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving cleanup settings:', error);
|
||||
showToast('Network error saving settings', 'danger');
|
||||
showToast(t('contacts.toast.settings_network_error'), 'danger');
|
||||
|
||||
if (statusText) {
|
||||
statusText.textContent = 'Save failed';
|
||||
statusText.textContent = t('common.save_failed');
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -614,14 +614,14 @@ async function handleCleanupPreview() {
|
||||
|
||||
// Validate: at least one type must be selected
|
||||
if (criteria.types.length === 0) {
|
||||
showToast('Please select at least one contact type', 'warning');
|
||||
showToast(t('contacts.toast.need_type'), 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// Disable button during preview
|
||||
const originalHTML = previewBtn.innerHTML;
|
||||
previewBtn.disabled = true;
|
||||
previewBtn.innerHTML = '<i class="bi bi-hourglass-split"></i> Loading...';
|
||||
previewBtn.innerHTML = `<i class="bi bi-hourglass-split"></i> ${tHtml('common.loading')}`;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/contacts/preview-cleanup', {
|
||||
@@ -638,7 +638,7 @@ async function handleCleanupPreview() {
|
||||
cleanupPreviewContacts = data.contacts || [];
|
||||
|
||||
if (cleanupPreviewContacts.length === 0) {
|
||||
showToast('No contacts match the selected criteria', 'info');
|
||||
showToast(t('contacts.toast.no_match'), 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -649,11 +649,11 @@ async function handleCleanupPreview() {
|
||||
const modal = new bootstrap.Modal(document.getElementById('cleanupConfirmModal'));
|
||||
modal.show();
|
||||
} else {
|
||||
showToast('Preview failed: ' + (data.error || 'Unknown error'), 'danger');
|
||||
showToast(t('contacts.toast.preview_failed', { error: data.error || t('common.unknown_error') }), 'danger');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error during cleanup preview:', error);
|
||||
showToast('Network error during preview', 'danger');
|
||||
showToast(t('contacts.toast.preview_network_error'), 'danger');
|
||||
} finally {
|
||||
// Re-enable button
|
||||
previewBtn.disabled = false;
|
||||
@@ -681,7 +681,7 @@ function populateCleanupModal(contacts) {
|
||||
|
||||
// Format last seen time
|
||||
const lastSeenTimestamp = contact.last_advert || 0;
|
||||
const lastSeenText = lastSeenTimestamp > 0 ? formatRelativeTime(lastSeenTimestamp) : 'Never';
|
||||
const lastSeenText = lastSeenTimestamp > 0 ? formatRelativeTime(lastSeenTimestamp) : t('common.never');
|
||||
|
||||
item.innerHTML = `
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
@@ -689,8 +689,8 @@ function populateCleanupModal(contacts) {
|
||||
<strong>${escapeHtml(contact.name)}</strong>
|
||||
<br>
|
||||
<small class="text-muted">
|
||||
Type: <span class="badge bg-secondary">${contact.type_label}</span>
|
||||
| Last advert: ${lastSeenText}
|
||||
${tHtml('contacts.add.type_short')} <span class="badge bg-secondary">${contact.type_label}</span>
|
||||
| ${tHtml('contacts.last_advert', { time: lastSeenText })}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
@@ -722,7 +722,7 @@ async function handleCleanupConfirm() {
|
||||
// Disable button during cleanup
|
||||
const originalHTML = confirmBtn.innerHTML;
|
||||
confirmBtn.disabled = true;
|
||||
confirmBtn.innerHTML = '<i class="bi bi-hourglass-split"></i> Deleting...';
|
||||
confirmBtn.innerHTML = `<i class="bi bi-hourglass-split"></i> ${tHtml('common.deleting')}`;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/contacts/cleanup', {
|
||||
@@ -759,7 +759,7 @@ async function handleCleanupConfirm() {
|
||||
// Clear preview
|
||||
cleanupPreviewContacts = [];
|
||||
} else {
|
||||
showToast('Cleanup failed: ' + (data.error || 'Unknown error'), 'danger');
|
||||
showToast(t('contacts.toast.cleanup_failed', { error: data.error || t('common.unknown_error') }), 'danger');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error during cleanup:', error);
|
||||
@@ -996,13 +996,13 @@ async function toggleContactProtection(publicKey, buttonEl) {
|
||||
|
||||
showToast(data.message, 'success');
|
||||
} else {
|
||||
showToast('Failed to update protection: ' + data.error, 'danger');
|
||||
showToast(t('contacts.toast.protect_failed', { error: data.error }), 'danger');
|
||||
buttonEl.innerHTML = originalHTML;
|
||||
buttonEl.disabled = false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error toggling protection:', error);
|
||||
showToast('Network error', 'danger');
|
||||
showToast(t('common.network_error'), 'danger');
|
||||
buttonEl.innerHTML = originalHTML;
|
||||
buttonEl.disabled = false;
|
||||
}
|
||||
@@ -1020,11 +1020,11 @@ function updateProtectionUI(publicKey, isProtected, buttonEl) {
|
||||
// Update button appearance
|
||||
buttonEl.disabled = false;
|
||||
if (isProtected) {
|
||||
buttonEl.innerHTML = '<i class="bi bi-lock-fill"></i> <span class="btn-label">Protected</span>';
|
||||
buttonEl.innerHTML = `<i class="bi bi-lock-fill"></i> <span class="btn-label">${tHtml('contacts.btn.protected')}</span>`;
|
||||
buttonEl.classList.remove('btn-outline-warning');
|
||||
buttonEl.classList.add('btn-warning');
|
||||
} else {
|
||||
buttonEl.innerHTML = '<i class="bi bi-shield"></i> <span class="btn-label">Protect</span>';
|
||||
buttonEl.innerHTML = `<i class="bi bi-shield"></i> <span class="btn-label">${tHtml('contacts.btn.protect')}</span>`;
|
||||
buttonEl.classList.remove('btn-warning');
|
||||
buttonEl.classList.add('btn-outline-warning');
|
||||
}
|
||||
@@ -1037,7 +1037,7 @@ function updateProtectionUI(publicKey, isProtected, buttonEl) {
|
||||
if (!lockIcon && nameDiv) {
|
||||
const indicator = document.createElement('span');
|
||||
indicator.className = 'protection-indicator ms-2';
|
||||
indicator.innerHTML = '<i class="bi bi-lock-fill text-warning" title="Protected contact"></i>';
|
||||
indicator.innerHTML = `<i class="bi bi-lock-fill text-warning" title="${tHtml('contacts.protected_title')}"></i>`;
|
||||
nameDiv.appendChild(indicator);
|
||||
}
|
||||
} else {
|
||||
@@ -1050,7 +1050,7 @@ function updateProtectionUI(publicKey, isProtected, buttonEl) {
|
||||
if (!icon) return;
|
||||
if (icon.classList.contains('bi-trash') || icon.classList.contains('bi-eye-slash') || icon.classList.contains('bi-slash-circle')) {
|
||||
btn.disabled = isProtected;
|
||||
btn.title = isProtected ? 'Protected contact' : '';
|
||||
btn.title = isProtected ? t('contacts.protected_title') : '';
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1068,16 +1068,16 @@ async function toggleContactIgnore(publicKey, ignored) {
|
||||
loadExistingContacts();
|
||||
loadContactCounts();
|
||||
} else {
|
||||
showToast('Failed: ' + data.error, 'danger');
|
||||
showToast(t('common.failed_with', { error: data.error }), 'danger');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error toggling ignore:', error);
|
||||
showToast('Network error', 'danger');
|
||||
showToast(t('common.network_error'), 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleContactBlock(publicKey, blocked) {
|
||||
if (blocked && !confirm('Block this contact? Their messages will be hidden from chat.')) return;
|
||||
if (blocked && !confirm(t('contacts.confirm.block'))) return;
|
||||
try {
|
||||
const response = await fetch(`/api/contacts/${encodeURIComponent(publicKey)}/block`, {
|
||||
method: 'POST',
|
||||
@@ -1090,11 +1090,11 @@ async function toggleContactBlock(publicKey, blocked) {
|
||||
loadExistingContacts();
|
||||
loadContactCounts();
|
||||
} else {
|
||||
showToast('Failed: ' + data.error, 'danger');
|
||||
showToast(t('common.failed_with', { error: data.error }), 'danger');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error toggling block:', error);
|
||||
showToast('Network error', 'danger');
|
||||
showToast(t('common.network_error'), 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1245,7 +1245,7 @@ async function loadPendingContacts() {
|
||||
console.error('Failed to load pending contacts:', data.error);
|
||||
if (errorEl) {
|
||||
const errorMsg = document.getElementById('pendingErrorMessage');
|
||||
if (errorMsg) errorMsg.textContent = data.error || 'Failed to load pending contacts';
|
||||
if (errorMsg) errorMsg.textContent = data.error || t('contacts.pending.load_failed');
|
||||
errorEl.style.display = 'block';
|
||||
}
|
||||
}
|
||||
@@ -1254,7 +1254,7 @@ async function loadPendingContacts() {
|
||||
if (loadingEl) loadingEl.style.display = 'none';
|
||||
if (errorEl) {
|
||||
const errorMsg = document.getElementById('pendingErrorMessage');
|
||||
if (errorMsg) errorMsg.textContent = 'Network error: ' + error.message;
|
||||
if (errorMsg) errorMsg.textContent = t('common.network_error_detail', { error: error.message });
|
||||
errorEl.style.display = 'block';
|
||||
}
|
||||
}
|
||||
@@ -1327,7 +1327,7 @@ function createContactCard(contact, index) {
|
||||
const keyDiv = document.createElement('div');
|
||||
keyDiv.className = 'contact-key clickable-key';
|
||||
keyDiv.textContent = contact.public_key_prefix || contact.public_key.substring(0, 12);
|
||||
keyDiv.title = 'Click to copy';
|
||||
keyDiv.title = t('contacts.copy_title');
|
||||
keyDiv.onclick = () => copyToClipboard(keyDiv.textContent, keyDiv);
|
||||
|
||||
// Last advert (optional - show if available)
|
||||
@@ -1346,7 +1346,7 @@ function createContactCard(contact, index) {
|
||||
// Approve button
|
||||
const approveBtn = document.createElement('button');
|
||||
approveBtn.className = 'btn btn-sm btn-success';
|
||||
approveBtn.innerHTML = '<i class="bi bi-check-circle"></i> <span class="btn-label">Approve</span>';
|
||||
approveBtn.innerHTML = `<i class="bi bi-check-circle"></i> <span class="btn-label">${tHtml('contacts.btn.approve')}</span>`;
|
||||
approveBtn.onclick = () => approveContact(contact, index);
|
||||
|
||||
actionsDiv.appendChild(approveBtn);
|
||||
@@ -1355,7 +1355,7 @@ function createContactCard(contact, index) {
|
||||
if (contact.adv_lat && contact.adv_lon && (contact.adv_lat !== 0 || contact.adv_lon !== 0)) {
|
||||
const mapBtn = document.createElement('button');
|
||||
mapBtn.className = 'btn btn-sm btn-outline-primary';
|
||||
mapBtn.innerHTML = '<i class="bi bi-geo-alt"></i> <span class="btn-label">Map</span>';
|
||||
mapBtn.innerHTML = `<i class="bi bi-geo-alt"></i> <span class="btn-label">${tHtml('contacts.map.title')}</span>`;
|
||||
mapBtn.onclick = () => window.showContactOnMap(contact.name, contact.adv_lat, contact.adv_lon);
|
||||
actionsDiv.appendChild(mapBtn);
|
||||
}
|
||||
@@ -1363,7 +1363,7 @@ function createContactCard(contact, index) {
|
||||
// Ignore button
|
||||
const ignoreBtn = document.createElement('button');
|
||||
ignoreBtn.className = 'btn btn-sm btn-outline-secondary';
|
||||
ignoreBtn.innerHTML = '<i class="bi bi-eye-slash"></i> <span class="btn-label">Ignore</span>';
|
||||
ignoreBtn.innerHTML = `<i class="bi bi-eye-slash"></i> <span class="btn-label">${tHtml('contacts.btn.ignore')}</span>`;
|
||||
ignoreBtn.onclick = () => {
|
||||
toggleContactIgnore(contact.public_key, true).then(() => loadPendingContacts());
|
||||
};
|
||||
@@ -1372,7 +1372,7 @@ function createContactCard(contact, index) {
|
||||
// Block button
|
||||
const blockBtn = document.createElement('button');
|
||||
blockBtn.className = 'btn btn-sm btn-outline-danger';
|
||||
blockBtn.innerHTML = '<i class="bi bi-slash-circle"></i> <span class="btn-label">Block</span>';
|
||||
blockBtn.innerHTML = `<i class="bi bi-slash-circle"></i> <span class="btn-label">${tHtml('contacts.btn.block')}</span>`;
|
||||
blockBtn.onclick = () => {
|
||||
toggleContactBlock(contact.public_key, true).then(() => loadPendingContacts());
|
||||
};
|
||||
@@ -1410,7 +1410,7 @@ async function approveContact(contact, index) {
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showToast(`Approved: ${contact.name}`, 'success');
|
||||
showToast(t('contacts.toast.approved', { name: contact.name }), 'success');
|
||||
|
||||
// Remove from list with animation
|
||||
if (cardEl) {
|
||||
@@ -1425,7 +1425,7 @@ async function approveContact(contact, index) {
|
||||
}
|
||||
} else {
|
||||
console.error('Failed to approve contact:', data.error);
|
||||
showToast('Failed to approve: ' + data.error, 'danger');
|
||||
showToast(t('contacts.toast.approve_failed', { error: data.error }), 'danger');
|
||||
|
||||
// Re-enable buttons
|
||||
if (cardEl) {
|
||||
@@ -1435,7 +1435,7 @@ async function approveContact(contact, index) {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error approving contact:', error);
|
||||
showToast('Network error: ' + error.message, 'danger');
|
||||
showToast(t('common.network_error_detail', { error: error.message }), 'danger');
|
||||
|
||||
// Re-enable buttons
|
||||
if (cardEl) {
|
||||
@@ -1449,7 +1449,7 @@ function copyPublicKey(publicKey, buttonEl) {
|
||||
navigator.clipboard.writeText(publicKey).then(() => {
|
||||
// Visual feedback
|
||||
const originalHTML = buttonEl.innerHTML;
|
||||
buttonEl.innerHTML = '<i class="bi bi-check"></i> Copied!';
|
||||
buttonEl.innerHTML = `<i class="bi bi-check"></i> ${tHtml('common.copied')}`;
|
||||
buttonEl.classList.remove('btn-outline-secondary');
|
||||
buttonEl.classList.add('btn-success');
|
||||
|
||||
@@ -1459,10 +1459,10 @@ function copyPublicKey(publicKey, buttonEl) {
|
||||
buttonEl.classList.add('btn-outline-secondary');
|
||||
}, 2000);
|
||||
|
||||
showToast('Public key copied to clipboard', 'info');
|
||||
showToast(t('contacts.toast.pubkey_copied'), 'info');
|
||||
}).catch(err => {
|
||||
console.error('Failed to copy:', err);
|
||||
showToast('Failed to copy to clipboard', 'danger');
|
||||
showToast(t('contacts.toast.copy_failed'), 'danger');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1492,7 +1492,7 @@ function applyPendingFilters() {
|
||||
|
||||
function showBatchApprovalModal() {
|
||||
if (filteredPendingContacts.length === 0) {
|
||||
showToast('No contacts to approve', 'warning');
|
||||
showToast(t('contacts.toast.none_to_approve'), 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1592,12 +1592,12 @@ async function batchApproveContacts() {
|
||||
|
||||
// Show result
|
||||
if (successCount > 0 && failedCount === 0) {
|
||||
showToast(`Successfully approved ${successCount} contact${successCount !== 1 ? 's' : ''}`, 'success');
|
||||
showToast(tn('contacts.toast.bulk_approved', successCount), 'success');
|
||||
} else if (successCount > 0 && failedCount > 0) {
|
||||
showToast(`Approved ${successCount}, failed ${failedCount}. Check console for details.`, 'warning');
|
||||
showToast(t('contacts.toast.bulk_approve_partial', { ok: successCount, failed: failedCount }), 'warning');
|
||||
console.error('Failed approvals:', failures);
|
||||
} else {
|
||||
showToast(`Failed to approve contacts. Check console for details.`, 'danger');
|
||||
showToast(t('contacts.toast.bulk_approve_failed'), 'danger');
|
||||
console.error('Failed approvals:', failures);
|
||||
}
|
||||
|
||||
@@ -1607,13 +1607,13 @@ async function batchApproveContacts() {
|
||||
// Re-enable button
|
||||
if (confirmBtn) {
|
||||
confirmBtn.disabled = false;
|
||||
confirmBtn.innerHTML = '<i class="bi bi-check-circle-fill"></i> Approve All';
|
||||
confirmBtn.innerHTML = `<i class="bi bi-check-circle-fill"></i> ${tHtml('contacts.btn.approve_all')}`;
|
||||
}
|
||||
}
|
||||
|
||||
function showBatchIgnoreModal() {
|
||||
if (filteredPendingContacts.length === 0) {
|
||||
showToast('No contacts to ignore', 'warning');
|
||||
showToast(t('contacts.toast.none_to_ignore'), 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1694,12 +1694,12 @@ async function batchIgnoreContacts() {
|
||||
if (modal) modal.hide();
|
||||
|
||||
if (successCount > 0 && failedCount === 0) {
|
||||
showToast(`Successfully ignored ${successCount} contact${successCount !== 1 ? 's' : ''}`, 'info');
|
||||
showToast(tn('contacts.toast.bulk_ignored', successCount), 'info');
|
||||
} else if (successCount > 0 && failedCount > 0) {
|
||||
showToast(`Ignored ${successCount}, failed ${failedCount}. Check console for details.`, 'warning');
|
||||
showToast(t('contacts.toast.bulk_ignore_partial', { ok: successCount, failed: failedCount }), 'warning');
|
||||
console.error('Failed ignores:', failures);
|
||||
} else {
|
||||
showToast(`Failed to ignore contacts. Check console for details.`, 'danger');
|
||||
showToast(t('contacts.toast.bulk_ignore_failed'), 'danger');
|
||||
console.error('Failed ignores:', failures);
|
||||
}
|
||||
|
||||
@@ -1707,7 +1707,7 @@ async function batchIgnoreContacts() {
|
||||
|
||||
if (confirmBtn) {
|
||||
confirmBtn.disabled = false;
|
||||
confirmBtn.innerHTML = '<i class="bi bi-eye-slash"></i> Ignore All';
|
||||
confirmBtn.innerHTML = `<i class="bi bi-eye-slash"></i> ${tHtml('contacts.btn.ignore_all')}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1787,7 +1787,7 @@ async function loadExistingContacts() {
|
||||
const cacheOnlyContacts = cachedContacts
|
||||
.filter(c => !deviceKeySet.has(c.public_key))
|
||||
.map(c => ({
|
||||
name: c.name || 'Unknown',
|
||||
name: c.name || t('common.unknown'),
|
||||
public_key: c.public_key,
|
||||
public_key_prefix: c.public_key_prefix || c.public_key.substring(0, 12),
|
||||
type_label: c.type_label || '',
|
||||
@@ -1815,7 +1815,7 @@ async function loadExistingContacts() {
|
||||
console.error('Failed to load existing contacts:', deviceData.error);
|
||||
if (errorEl) {
|
||||
const errorMsg = document.getElementById('existingErrorMessage');
|
||||
if (errorMsg) errorMsg.textContent = deviceData.error || 'Failed to load contacts';
|
||||
if (errorMsg) errorMsg.textContent = deviceData.error || t('contacts.existing.load_failed');
|
||||
errorEl.style.display = 'block';
|
||||
}
|
||||
}
|
||||
@@ -1824,7 +1824,7 @@ async function loadExistingContacts() {
|
||||
if (loadingEl) loadingEl.style.display = 'none';
|
||||
if (errorEl) {
|
||||
const errorMsg = document.getElementById('existingErrorMessage');
|
||||
if (errorMsg) errorMsg.textContent = 'Network error: ' + error.message;
|
||||
if (errorMsg) errorMsg.textContent = t('common.network_error_detail', { error: error.message });
|
||||
errorEl.style.display = 'block';
|
||||
}
|
||||
}
|
||||
@@ -1969,7 +1969,7 @@ async function loadBlockedNamesList() {
|
||||
// Add a separator header
|
||||
const header = document.createElement('div');
|
||||
header.className = 'text-muted small fw-bold mt-3 mb-2 px-1';
|
||||
header.innerHTML = '<i class="bi bi-slash-circle"></i> Blocked by name';
|
||||
header.innerHTML = `<i class="bi bi-slash-circle"></i> ${tHtml('contacts.blocked_by_name')}`;
|
||||
listEl.appendChild(header);
|
||||
|
||||
data.blocked_names.forEach(entry => {
|
||||
@@ -1986,7 +1986,7 @@ async function loadBlockedNamesList() {
|
||||
const statusIcon = document.createElement('span');
|
||||
statusIcon.className = 'ms-1';
|
||||
statusIcon.style.fontSize = '0.85rem';
|
||||
statusIcon.innerHTML = '<i class="bi bi-slash-circle text-danger" title="Blocked by name"></i>';
|
||||
statusIcon.innerHTML = `<i class="bi bi-slash-circle text-danger" title="${tHtml('contacts.blocked_by_name')}"></i>`;
|
||||
|
||||
infoRow.appendChild(nameDiv);
|
||||
infoRow.appendChild(statusIcon);
|
||||
@@ -1996,7 +1996,7 @@ async function loadBlockedNamesList() {
|
||||
|
||||
const unblockBtn = document.createElement('button');
|
||||
unblockBtn.className = 'btn btn-sm btn-outline-success';
|
||||
unblockBtn.innerHTML = '<i class="bi bi-slash-circle"></i> <span class="btn-label">Unblock</span>';
|
||||
unblockBtn.innerHTML = `<i class="bi bi-slash-circle"></i> <span class="btn-label">${tHtml('contacts.btn.unblock')}</span>`;
|
||||
unblockBtn.onclick = async () => {
|
||||
try {
|
||||
const resp = await fetch('/api/contacts/block-name', {
|
||||
@@ -2009,10 +2009,10 @@ async function loadBlockedNamesList() {
|
||||
showToast(result.message, 'info');
|
||||
loadExistingContacts();
|
||||
} else {
|
||||
showToast('Failed: ' + result.error, 'danger');
|
||||
showToast(t('common.failed_with', { error: result.error }), 'danger');
|
||||
}
|
||||
} catch (err) {
|
||||
showToast('Network error', 'danger');
|
||||
showToast(t('common.network_error'), 'danger');
|
||||
}
|
||||
};
|
||||
actionsDiv.appendChild(unblockBtn);
|
||||
@@ -2030,45 +2030,9 @@ async function loadBlockedNamesList() {
|
||||
* Format Unix timestamp as relative time ("5 minutes ago", "2 hours ago", etc.)
|
||||
*/
|
||||
function formatRelativeTime(timestamp) {
|
||||
if (!timestamp) return 'Never';
|
||||
|
||||
const now = Math.floor(Date.now() / 1000); // Current time in Unix seconds
|
||||
const diffSeconds = now - timestamp;
|
||||
|
||||
if (diffSeconds < 0) return 'Just now'; // Future timestamp (clock skew)
|
||||
|
||||
// Less than 1 minute
|
||||
if (diffSeconds < 60) {
|
||||
return 'Just now';
|
||||
}
|
||||
|
||||
// Less than 1 hour
|
||||
if (diffSeconds < 3600) {
|
||||
const minutes = Math.floor(diffSeconds / 60);
|
||||
return `${minutes} minute${minutes !== 1 ? 's' : ''} ago`;
|
||||
}
|
||||
|
||||
// Less than 1 day
|
||||
if (diffSeconds < 86400) {
|
||||
const hours = Math.floor(diffSeconds / 3600);
|
||||
return `${hours} hour${hours !== 1 ? 's' : ''} ago`;
|
||||
}
|
||||
|
||||
// Less than 30 days
|
||||
if (diffSeconds < 2592000) {
|
||||
const days = Math.floor(diffSeconds / 86400);
|
||||
return `${days} day${days !== 1 ? 's' : ''} ago`;
|
||||
}
|
||||
|
||||
// Less than 1 year
|
||||
if (diffSeconds < 31536000) {
|
||||
const months = Math.floor(diffSeconds / 2592000);
|
||||
return `${months} month${months !== 1 ? 's' : ''} ago`;
|
||||
}
|
||||
|
||||
// More than 1 year
|
||||
const years = Math.floor(diffSeconds / 31536000);
|
||||
return `${years} year${years !== 1 ? 's' : ''} ago`;
|
||||
// Delegates to datetime-utils.js. The "long" form keeps counting in months and
|
||||
// years past a week, which is what a contact list wants for a stale advert.
|
||||
return formatTimeAgo(timestamp, { long: true });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2080,7 +2044,7 @@ function getActivityStatus(timestamp) {
|
||||
return {
|
||||
icon: '⚫',
|
||||
color: '#6c757d',
|
||||
title: 'Never seen'
|
||||
title: t('contacts.never_seen')
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2092,7 +2056,7 @@ function getActivityStatus(timestamp) {
|
||||
return {
|
||||
icon: '🟢',
|
||||
color: '#28a745',
|
||||
title: 'Active (advert received recently)'
|
||||
title: t('contacts.active_recent')
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2101,7 +2065,7 @@ function getActivityStatus(timestamp) {
|
||||
return {
|
||||
icon: '🟡',
|
||||
color: '#ffc107',
|
||||
title: 'Recent activity'
|
||||
title: t('contacts.recent_activity')
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2109,7 +2073,7 @@ function getActivityStatus(timestamp) {
|
||||
return {
|
||||
icon: '🔴',
|
||||
color: '#dc3545',
|
||||
title: 'Inactive'
|
||||
title: t('contacts.inactive')
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2133,7 +2097,7 @@ function createExistingContactCard(contact, index) {
|
||||
if (isProtected) {
|
||||
const lockIndicator = document.createElement('span');
|
||||
lockIndicator.className = 'protection-indicator ms-2';
|
||||
lockIndicator.innerHTML = '<i class="bi bi-lock-fill text-warning" title="Protected contact"></i>';
|
||||
lockIndicator.innerHTML = `<i class="bi bi-lock-fill text-warning" title="${tHtml('contacts.protected_title')}"></i>`;
|
||||
nameDiv.appendChild(lockIndicator);
|
||||
}
|
||||
|
||||
@@ -2151,9 +2115,9 @@ function createExistingContactCard(contact, index) {
|
||||
default: typeBadge.classList.add('bg-secondary');
|
||||
}
|
||||
} else {
|
||||
typeBadge.textContent = 'Cache';
|
||||
typeBadge.textContent = t('contacts.cache_badge');
|
||||
typeBadge.classList.add('bg-secondary');
|
||||
typeBadge.title = 'Not on device - type unknown';
|
||||
typeBadge.title = t('contacts.cache_badge_title');
|
||||
}
|
||||
|
||||
// Source icon (device vs cache)
|
||||
@@ -2161,9 +2125,9 @@ function createExistingContactCard(contact, index) {
|
||||
sourceIcon.className = 'ms-1';
|
||||
sourceIcon.style.fontSize = '0.85rem';
|
||||
if (contact.on_device !== false) {
|
||||
sourceIcon.innerHTML = '<i class="bi bi-cpu text-success" title="On device"></i>';
|
||||
sourceIcon.innerHTML = `<i class="bi bi-cpu text-success" title="${tHtml('contacts.filter.on_device')}"></i>`;
|
||||
} else {
|
||||
sourceIcon.innerHTML = '<i class="bi bi-cloud text-secondary" title="Cache only"></i>';
|
||||
sourceIcon.innerHTML = `<i class="bi bi-cloud text-secondary" title="${tHtml('contacts.filter.cache_only')}"></i>`;
|
||||
}
|
||||
|
||||
// Status icon (ignored/blocked)
|
||||
@@ -2172,12 +2136,12 @@ function createExistingContactCard(contact, index) {
|
||||
statusIcon = document.createElement('span');
|
||||
statusIcon.className = 'ms-1';
|
||||
statusIcon.style.fontSize = '0.85rem';
|
||||
statusIcon.innerHTML = '<i class="bi bi-slash-circle text-danger" title="Blocked"></i>';
|
||||
statusIcon.innerHTML = `<i class="bi bi-slash-circle text-danger" title="${tHtml('contacts.filter.blocked')}"></i>`;
|
||||
} else if (contact.is_ignored) {
|
||||
statusIcon = document.createElement('span');
|
||||
statusIcon.className = 'ms-1';
|
||||
statusIcon.style.fontSize = '0.85rem';
|
||||
statusIcon.innerHTML = '<i class="bi bi-eye-slash text-secondary" title="Ignored"></i>';
|
||||
statusIcon.innerHTML = `<i class="bi bi-eye-slash text-secondary" title="${tHtml('contacts.filter.ignored')}"></i>`;
|
||||
}
|
||||
|
||||
infoRow.appendChild(nameDiv);
|
||||
@@ -2189,7 +2153,7 @@ function createExistingContactCard(contact, index) {
|
||||
const keyDiv = document.createElement('div');
|
||||
keyDiv.className = 'contact-key clickable-key';
|
||||
keyDiv.textContent = contact.public_key_prefix;
|
||||
keyDiv.title = 'Click to copy';
|
||||
keyDiv.title = t('contacts.copy_title');
|
||||
keyDiv.onclick = () => copyToClipboard(contact.public_key_prefix, keyDiv);
|
||||
|
||||
// Last advert row (with activity status indicator)
|
||||
@@ -2207,7 +2171,7 @@ function createExistingContactCard(contact, index) {
|
||||
statusIcon.title = status.title;
|
||||
|
||||
const timeText = document.createElement('span');
|
||||
timeText.textContent = `Last advert: ${relativeTime}`;
|
||||
timeText.textContent = t('contacts.last_advert', { time: relativeTime });
|
||||
|
||||
lastAdvertDiv.appendChild(statusIcon);
|
||||
lastAdvertDiv.appendChild(timeText);
|
||||
@@ -2218,7 +2182,7 @@ function createExistingContactCard(contact, index) {
|
||||
statusIcon.style.fontSize = '0.9rem';
|
||||
|
||||
const timeText = document.createElement('span');
|
||||
timeText.textContent = 'Last advert: Unknown';
|
||||
timeText.textContent = t('contacts.last_advert_unknown');
|
||||
|
||||
lastAdvertDiv.appendChild(statusIcon);
|
||||
lastAdvertDiv.appendChild(timeText);
|
||||
@@ -2237,7 +2201,7 @@ function createExistingContactCard(contact, index) {
|
||||
} else {
|
||||
// mode is formatted path like "E7→DE→54→54→D8"
|
||||
const hopCount = mode.split('→').length;
|
||||
pathDiv.innerHTML = `<i class="bi bi-signpost-split"></i> ${mode} <span class="text-muted">(${hopCount} hops)</span>`;
|
||||
pathDiv.innerHTML = `<i class="bi bi-signpost-split"></i> ${mode} <span class="text-muted">(${tn('contacts.hops', hopCount)})</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2249,7 +2213,7 @@ function createExistingContactCard(contact, index) {
|
||||
if (contact.adv_lat && contact.adv_lon && (contact.adv_lat !== 0 || contact.adv_lon !== 0)) {
|
||||
const mapBtn = document.createElement('button');
|
||||
mapBtn.className = 'btn btn-sm btn-outline-primary';
|
||||
mapBtn.innerHTML = '<i class="bi bi-geo-alt"></i> <span class="btn-label">Map</span>';
|
||||
mapBtn.innerHTML = `<i class="bi bi-geo-alt"></i> <span class="btn-label">${tHtml('contacts.map.title')}</span>`;
|
||||
mapBtn.onclick = () => window.showContactOnMap(contact.name, contact.adv_lat, contact.adv_lon);
|
||||
actionsDiv.appendChild(mapBtn);
|
||||
}
|
||||
@@ -2260,28 +2224,28 @@ function createExistingContactCard(contact, index) {
|
||||
protectBtn.className = isProtected ? 'btn btn-sm btn-warning' : 'btn btn-sm btn-outline-warning';
|
||||
protectBtn.innerHTML = isProtected
|
||||
? '<i class="bi bi-lock-fill"></i> <span class="btn-label">Protected</span>'
|
||||
: '<i class="bi bi-shield"></i> <span class="btn-label">Protect</span>';
|
||||
: `<i class="bi bi-shield"></i> <span class="btn-label">${tHtml('contacts.btn.protect')}</span>`;
|
||||
protectBtn.onclick = () => toggleContactProtection(contact.public_key, protectBtn);
|
||||
actionsDiv.appendChild(protectBtn);
|
||||
|
||||
const moveToCacheBtn = document.createElement('button');
|
||||
moveToCacheBtn.className = 'btn btn-sm btn-outline-info';
|
||||
moveToCacheBtn.innerHTML = '<i class="bi bi-cloud-arrow-down"></i> <span class="btn-label">To cache</span>';
|
||||
moveToCacheBtn.title = 'Remove from device, keep in cache';
|
||||
moveToCacheBtn.innerHTML = `<i class="bi bi-cloud-arrow-down"></i> <span class="btn-label">${tHtml('contacts.btn.to_cache')}</span>`;
|
||||
moveToCacheBtn.title = t('contacts.btn.to_cache_title');
|
||||
moveToCacheBtn.onclick = () => moveContactToCache(contact);
|
||||
moveToCacheBtn.disabled = isProtected;
|
||||
if (isProtected) {
|
||||
moveToCacheBtn.title = 'Cannot move protected contact';
|
||||
moveToCacheBtn.title = t('contacts.protected_cannot_move');
|
||||
}
|
||||
actionsDiv.appendChild(moveToCacheBtn);
|
||||
|
||||
const deleteBtn = document.createElement('button');
|
||||
deleteBtn.className = 'btn btn-sm btn-outline-danger';
|
||||
deleteBtn.innerHTML = '<i class="bi bi-trash"></i> <span class="btn-label">Delete</span>';
|
||||
deleteBtn.innerHTML = `<i class="bi bi-trash"></i> <span class="btn-label">${tHtml('common.delete')}</span>`;
|
||||
deleteBtn.onclick = () => showDeleteModal(contact);
|
||||
deleteBtn.disabled = isProtected;
|
||||
if (isProtected) {
|
||||
deleteBtn.title = 'Cannot delete protected contact';
|
||||
deleteBtn.title = t('contacts.protected_cannot_delete');
|
||||
}
|
||||
actionsDiv.appendChild(deleteBtn);
|
||||
}
|
||||
@@ -2290,14 +2254,14 @@ function createExistingContactCard(contact, index) {
|
||||
if (contact.on_device === false) {
|
||||
const pushToDeviceBtn = document.createElement('button');
|
||||
pushToDeviceBtn.className = 'btn btn-sm btn-outline-success';
|
||||
pushToDeviceBtn.innerHTML = '<i class="bi bi-cpu"></i> <span class="btn-label">To device</span>';
|
||||
pushToDeviceBtn.title = 'Add this contact to the device';
|
||||
pushToDeviceBtn.innerHTML = `<i class="bi bi-cpu"></i> <span class="btn-label">${tHtml('contacts.btn.to_device')}</span>`;
|
||||
pushToDeviceBtn.title = t('contacts.btn.to_device_title');
|
||||
pushToDeviceBtn.onclick = () => pushContactToDevice(contact);
|
||||
actionsDiv.appendChild(pushToDeviceBtn);
|
||||
|
||||
const deleteCacheBtn = document.createElement('button');
|
||||
deleteCacheBtn.className = 'btn btn-sm btn-outline-danger';
|
||||
deleteCacheBtn.innerHTML = '<i class="bi bi-trash"></i> <span class="btn-label">Delete</span>';
|
||||
deleteCacheBtn.innerHTML = `<i class="bi bi-trash"></i> <span class="btn-label">${tHtml('common.delete')}</span>`;
|
||||
deleteCacheBtn.onclick = () => showDeleteModal(contact);
|
||||
actionsDiv.appendChild(deleteCacheBtn);
|
||||
}
|
||||
@@ -2306,33 +2270,33 @@ function createExistingContactCard(contact, index) {
|
||||
if (contact.is_blocked) {
|
||||
const unblockBtn = document.createElement('button');
|
||||
unblockBtn.className = 'btn btn-sm btn-outline-success';
|
||||
unblockBtn.innerHTML = '<i class="bi bi-slash-circle"></i> <span class="btn-label">Unblock</span>';
|
||||
unblockBtn.innerHTML = `<i class="bi bi-slash-circle"></i> <span class="btn-label">${tHtml('contacts.btn.unblock')}</span>`;
|
||||
unblockBtn.onclick = () => toggleContactBlock(contact.public_key, false);
|
||||
actionsDiv.appendChild(unblockBtn);
|
||||
} else if (contact.is_ignored) {
|
||||
const unignoreBtn = document.createElement('button');
|
||||
unignoreBtn.className = 'btn btn-sm btn-outline-success';
|
||||
unignoreBtn.innerHTML = '<i class="bi bi-eye"></i> <span class="btn-label">Unignore</span>';
|
||||
unignoreBtn.innerHTML = `<i class="bi bi-eye"></i> <span class="btn-label">${tHtml('contacts.btn.unignore')}</span>`;
|
||||
unignoreBtn.onclick = () => toggleContactIgnore(contact.public_key, false);
|
||||
actionsDiv.appendChild(unignoreBtn);
|
||||
} else {
|
||||
const ignoreBtn = document.createElement('button');
|
||||
ignoreBtn.className = 'btn btn-sm btn-outline-secondary';
|
||||
ignoreBtn.innerHTML = '<i class="bi bi-eye-slash"></i> <span class="btn-label">Ignore</span>';
|
||||
ignoreBtn.innerHTML = `<i class="bi bi-eye-slash"></i> <span class="btn-label">${tHtml('contacts.btn.ignore')}</span>`;
|
||||
ignoreBtn.onclick = () => toggleContactIgnore(contact.public_key, true);
|
||||
if (isProtected) {
|
||||
ignoreBtn.disabled = true;
|
||||
ignoreBtn.title = 'Cannot ignore protected contact';
|
||||
ignoreBtn.title = t('contacts.protected_cannot_ignore');
|
||||
}
|
||||
actionsDiv.appendChild(ignoreBtn);
|
||||
|
||||
const blockBtn = document.createElement('button');
|
||||
blockBtn.className = 'btn btn-sm btn-outline-danger';
|
||||
blockBtn.innerHTML = '<i class="bi bi-slash-circle"></i> <span class="btn-label">Block</span>';
|
||||
blockBtn.innerHTML = `<i class="bi bi-slash-circle"></i> <span class="btn-label">${tHtml('contacts.btn.block')}</span>`;
|
||||
blockBtn.onclick = () => toggleContactBlock(contact.public_key, true);
|
||||
if (isProtected) {
|
||||
blockBtn.disabled = true;
|
||||
blockBtn.title = 'Cannot block protected contact';
|
||||
blockBtn.title = t('contacts.protected_cannot_block');
|
||||
}
|
||||
actionsDiv.appendChild(blockBtn);
|
||||
}
|
||||
@@ -2385,7 +2349,7 @@ function legacyCopy(text, element, originalText) {
|
||||
showCopyFeedback(element, originalText);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err);
|
||||
showToast('Failed to copy', 'danger');
|
||||
showToast(t('contacts.toast.copy_failed'), 'danger');
|
||||
}
|
||||
|
||||
document.body.removeChild(textArea);
|
||||
@@ -2395,7 +2359,7 @@ function legacyCopy(text, element, originalText) {
|
||||
* Show visual feedback after successful copy.
|
||||
*/
|
||||
function showCopyFeedback(element, originalText) {
|
||||
element.textContent = 'Copied!';
|
||||
element.textContent = t('common.copied');
|
||||
element.classList.add('copied');
|
||||
|
||||
setTimeout(() => {
|
||||
@@ -2403,7 +2367,7 @@ function showCopyFeedback(element, originalText) {
|
||||
element.classList.remove('copied');
|
||||
}, 1500);
|
||||
|
||||
showToast('Key copied to clipboard', 'info');
|
||||
showToast(t('contacts.toast.key_copied'), 'info');
|
||||
}
|
||||
|
||||
function showDeleteModal(contact) {
|
||||
@@ -2430,7 +2394,7 @@ async function confirmDelete() {
|
||||
// Disable button during deletion
|
||||
if (confirmBtn) {
|
||||
confirmBtn.disabled = true;
|
||||
confirmBtn.innerHTML = '<i class="bi bi-hourglass-split"></i> Deleting...';
|
||||
confirmBtn.innerHTML = `<i class="bi bi-hourglass-split"></i> ${tHtml('common.deleting')}`;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -2452,7 +2416,7 @@ async function confirmDelete() {
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showToast(`Deleted: ${contactToDelete.name}`, 'success');
|
||||
showToast(t('contacts.toast.deleted', { name: contactToDelete.name }), 'success');
|
||||
|
||||
// Hide modal
|
||||
if (modal) modal.hide();
|
||||
@@ -2461,16 +2425,16 @@ async function confirmDelete() {
|
||||
setTimeout(() => loadExistingContacts(), 500);
|
||||
} else {
|
||||
console.error('Failed to delete contact:', data.error);
|
||||
showToast('Failed to delete: ' + data.error, 'danger');
|
||||
showToast(t('contacts.toast.delete_failed', { error: data.error }), 'danger');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting contact:', error);
|
||||
showToast('Network error: ' + error.message, 'danger');
|
||||
showToast(t('common.network_error_detail', { error: error.message }), 'danger');
|
||||
} finally {
|
||||
// Re-enable button
|
||||
if (confirmBtn) {
|
||||
confirmBtn.disabled = false;
|
||||
confirmBtn.innerHTML = '<i class="bi bi-trash"></i> Delete Contact';
|
||||
confirmBtn.innerHTML = `<i class="bi bi-trash"></i> ${tHtml('contacts.delete.confirm')}`;
|
||||
}
|
||||
contactToDelete = null;
|
||||
}
|
||||
@@ -2493,10 +2457,10 @@ async function pushContactToDevice(contact) {
|
||||
showToast(data.message || `${contact.name} pushed to device`, 'success');
|
||||
setTimeout(() => loadExistingContacts(), 500);
|
||||
} else {
|
||||
showToast(data.error || 'Failed to push contact', 'danger');
|
||||
showToast(data.error || t('contacts.toast.push_failed'), 'danger');
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('Network error: ' + error.message, 'danger');
|
||||
showToast(t('common.network_error_detail', { error: error.message }), 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2513,10 +2477,10 @@ async function moveContactToCache(contact) {
|
||||
showToast(data.message || `${contact.name} moved to cache`, 'success');
|
||||
setTimeout(() => loadExistingContacts(), 500);
|
||||
} else {
|
||||
showToast(data.error || 'Failed to move contact', 'danger');
|
||||
showToast(data.error || t('contacts.toast.move_failed'), 'danger');
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('Network error: ' + error.message, 'danger');
|
||||
showToast(t('common.network_error_detail', { error: error.message }), 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2621,7 +2585,7 @@ function startQrCamera() {
|
||||
document.getElementById('startCameraBtn').classList.add('d-none');
|
||||
document.getElementById('stopCameraBtn').classList.remove('d-none');
|
||||
}).catch(err => {
|
||||
showQrError('Camera access denied or not available. Try uploading an image instead.');
|
||||
showQrError(t('contacts.qr.camera_denied'));
|
||||
console.error('QR camera error:', err);
|
||||
});
|
||||
}
|
||||
@@ -2645,7 +2609,7 @@ function handleQrFile(event) {
|
||||
scanner.clear();
|
||||
})
|
||||
.catch(err => {
|
||||
showQrError('Could not read QR code from image. Make sure the image contains a valid QR code.');
|
||||
showQrError(t('contacts.qr.read_failed'));
|
||||
console.error('QR file scan error:', err);
|
||||
});
|
||||
}
|
||||
@@ -2671,7 +2635,7 @@ function onQrCodeSuccess(decodedText) {
|
||||
|
||||
// Hex blob format
|
||||
if (decodedText.startsWith('meshcore://') && decodedText.length > 20) {
|
||||
resultDiv.innerHTML = '<strong>Scanned:</strong> <span class="font-monospace small" style="word-break: break-all;">' +
|
||||
resultDiv.innerHTML = `<strong>${tHtml('contacts.add.scanned')}</strong> <span class="font-monospace small" style="word-break: break-all;">` +
|
||||
decodedText.substring(0, 60) + '...</span>';
|
||||
resultDiv.classList.remove('d-none');
|
||||
addBtn.classList.remove('d-none');
|
||||
@@ -2726,7 +2690,7 @@ async function submitContact(mode) {
|
||||
|
||||
// Show loading
|
||||
statusDiv.className = 'mt-3 alert alert-info';
|
||||
statusDiv.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Adding contact...';
|
||||
statusDiv.innerHTML = `<span class="spinner-border spinner-border-sm me-2"></span>${tHtml('contacts.add.adding')}`;
|
||||
statusDiv.classList.remove('d-none');
|
||||
|
||||
try {
|
||||
@@ -2739,16 +2703,16 @@ async function submitContact(mode) {
|
||||
|
||||
if (data.success) {
|
||||
statusDiv.className = 'mt-3 alert alert-success';
|
||||
statusDiv.textContent = data.message || 'Contact added successfully!';
|
||||
statusDiv.textContent = data.message || t('contacts.add.success');
|
||||
// Reset form
|
||||
resetAddForm(mode);
|
||||
} else {
|
||||
statusDiv.className = 'mt-3 alert alert-danger';
|
||||
statusDiv.textContent = data.error || 'Failed to add contact.';
|
||||
statusDiv.textContent = data.error || t('contacts.add.failed');
|
||||
}
|
||||
} catch (error) {
|
||||
statusDiv.className = 'mt-3 alert alert-danger';
|
||||
statusDiv.textContent = 'Network error: ' + error.message;
|
||||
statusDiv.textContent = t('common.network_error_detail', { error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -52,15 +52,35 @@ function formatTimestamp(timestamp, opts) {
|
||||
|
||||
/**
|
||||
* Format a unix timestamp as relative time ("5 min ago", "2h ago").
|
||||
*
|
||||
* @param {number} timestamp - unix seconds
|
||||
* @param {Object} [opts]
|
||||
* @param {boolean} [opts.long] - spell out the units and keep counting in months and
|
||||
* years past a week, instead of falling back to a date. Contact lists want this: a
|
||||
* stale advert reads better as "3 months ago" than as a date nobody recognises.
|
||||
* @returns {string}
|
||||
*/
|
||||
function formatTimeAgo(timestamp) {
|
||||
function formatTimeAgo(timestamp, opts) {
|
||||
if (!timestamp) return t('common.never');
|
||||
const long = !!(opts && opts.long);
|
||||
const diff = Math.floor(Date.now() / 1000) - timestamp;
|
||||
|
||||
// A future timestamp means clock skew between us and the sender, not a real value.
|
||||
if (diff < 60) return t('common.just_now');
|
||||
if (diff < 3600) return t('common.minutes_ago', { count: Math.floor(diff / 60) });
|
||||
if (diff < 86400) return t('common.hours_ago', { count: Math.floor(diff / 3600) });
|
||||
|
||||
if (diff < 3600) {
|
||||
return long ? tn('common.minutes_ago_long', Math.floor(diff / 60))
|
||||
: t('common.minutes_ago', { count: Math.floor(diff / 60) });
|
||||
}
|
||||
if (diff < 86400) {
|
||||
return long ? tn('common.hours_ago_long', Math.floor(diff / 3600))
|
||||
: t('common.hours_ago', { count: Math.floor(diff / 3600) });
|
||||
}
|
||||
if (long) {
|
||||
if (diff < 2592000) return tn('common.days_ago_long', Math.floor(diff / 86400));
|
||||
if (diff < 31536000) return tn('common.months_ago', Math.floor(diff / 2592000));
|
||||
return tn('common.years_ago', Math.floor(diff / 31536000));
|
||||
}
|
||||
if (diff < 604800) return tn('common.days_ago', Math.floor(diff / 86400));
|
||||
return new Date(timestamp * 1000).toLocaleDateString();
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<!-- Action Buttons -->
|
||||
<div class="d-flex gap-2 mb-3">
|
||||
<button class="btn btn-outline-secondary btn-sm" onclick="navigateTo('/contacts/manage');">
|
||||
<i class="bi bi-arrow-left"></i> Back
|
||||
<i class="bi bi-arrow-left"></i> {{ t('common.back') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{% extends "contacts_base.html" %}
|
||||
|
||||
{% block title %}Existing Contacts - mc-webui{% endblock %}
|
||||
{% block title %}{{ t('contacts.existing.title') }} - mc-webui{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
<div id="existingPageContent" class="p-3">
|
||||
<!-- Page Header -->
|
||||
<div class="mb-3">
|
||||
<h4 class="mb-2">
|
||||
<i class="bi bi-person-lines-fill"></i> Existing Contacts
|
||||
<i class="bi bi-person-lines-fill"></i> {{ t('contacts.existing.title') }}
|
||||
<span class="badge counter-badge counter-ok rounded-pill" id="contactsCounter" style="display: none; font-size: 0.8rem;">0 / 350</span>
|
||||
</h4>
|
||||
</div>
|
||||
@@ -15,10 +15,10 @@
|
||||
<!-- Action Buttons -->
|
||||
<div class="d-flex gap-2 mb-3">
|
||||
<button class="btn btn-outline-secondary btn-sm" onclick="navigateTo('/contacts/manage');">
|
||||
<i class="bi bi-arrow-left"></i> Back
|
||||
<i class="bi bi-arrow-left"></i> {{ t('common.back') }}
|
||||
</button>
|
||||
<button class="btn btn-outline-primary btn-sm" id="refreshExistingBtn">
|
||||
<i class="bi bi-arrow-clockwise"></i> Refresh
|
||||
<i class="bi bi-arrow-clockwise"></i> {{ t('common.refresh') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -49,17 +49,17 @@
|
||||
|
||||
<!-- Sort Dropdown -->
|
||||
<select class="form-select" id="sortSelect">
|
||||
<option value="last_advert_desc">Last advert ↓</option>
|
||||
<option value="last_advert_asc">Last advert ↑</option>
|
||||
<option value="name_asc">Name A→Z</option>
|
||||
<option value="name_desc">Name Z→A</option>
|
||||
<option value="last_advert_desc">{{ t('contacts.sort.advert_desc') }}</option>
|
||||
<option value="last_advert_asc">{{ t('contacts.sort.advert_asc') }}</option>
|
||||
<option value="name_asc">{{ t('contacts.sort.name_asc') }}</option>
|
||||
<option value="name_desc">{{ t('contacts.sort.name_desc') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div id="existingLoading" class="text-center py-5" style="display: none;">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
<span class="visually-hidden">{{ t('common.loading') }}</span>
|
||||
</div>
|
||||
<p class="mt-3 text-muted">{{ t('contacts.existing.loading') }}</p>
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<!-- Action Buttons -->
|
||||
<div class="d-flex gap-2 mb-3">
|
||||
<button class="btn btn-outline-secondary btn-sm" onclick="navigateTo('/contacts/manage');">
|
||||
<i class="bi bi-arrow-left"></i> Back
|
||||
<i class="bi bi-arrow-left"></i> {{ t('common.back') }}
|
||||
</button>
|
||||
<button class="btn btn-outline-primary btn-sm" id="refreshPendingBtn">
|
||||
<i class="bi bi-arrow-clockwise"></i> Refresh
|
||||
|
||||
@@ -1,23 +1,56 @@
|
||||
{
|
||||
"common.add": "Add",
|
||||
"common.back": "Back",
|
||||
"common.cancel": "Cancel",
|
||||
"common.cannot_undo": "This action cannot be undone.",
|
||||
"common.close": "Close",
|
||||
"common.copied": "Copied!",
|
||||
"common.days_ago": {
|
||||
"one": "{count}d ago",
|
||||
"other": "{count}d ago"
|
||||
},
|
||||
"common.days_ago_long": {
|
||||
"one": "{count} day ago",
|
||||
"other": "{count} days ago"
|
||||
},
|
||||
"common.delete": "Delete",
|
||||
"common.deleting": "Deleting...",
|
||||
"common.disabled": "Disabled",
|
||||
"common.failed_with": "Failed: {error}",
|
||||
"common.hours_ago": "{count}h ago",
|
||||
"common.hours_ago_long": {
|
||||
"one": "{count} hour ago",
|
||||
"other": "{count} hours ago"
|
||||
},
|
||||
"common.just_now": "just now",
|
||||
"common.loading": "Loading...",
|
||||
"common.minutes_ago": "{count} min ago",
|
||||
"common.minutes_ago_long": {
|
||||
"one": "{count} minute ago",
|
||||
"other": "{count} minutes ago"
|
||||
},
|
||||
"common.months_ago": {
|
||||
"one": "{count} month ago",
|
||||
"other": "{count} months ago"
|
||||
},
|
||||
"common.move_down": "Move down",
|
||||
"common.move_up": "Move up",
|
||||
"common.network_error": "Network error",
|
||||
"common.network_error_detail": "Network error: {error}",
|
||||
"common.never": "Never",
|
||||
"common.refresh": "Refresh",
|
||||
"common.remove": "Remove",
|
||||
"common.save": "Save",
|
||||
"common.save_failed": "Save failed",
|
||||
"common.saving": "Saving...",
|
||||
"common.try_again": "Try again",
|
||||
"common.unknown": "Unknown",
|
||||
"common.unknown_error": "Unknown error",
|
||||
"common.warning": "Warning:",
|
||||
"common.years_ago": {
|
||||
"one": "{count} year ago",
|
||||
"other": "{count} years ago"
|
||||
},
|
||||
"common.yesterday": "Yesterday",
|
||||
"console.clear_title": "Clear output history",
|
||||
"console.connect_failed": "Failed to connect: {error}",
|
||||
@@ -33,6 +66,9 @@
|
||||
"console.status.connecting": "Connecting...",
|
||||
"console.status.disconnected": "Disconnected",
|
||||
"console.title": "Console",
|
||||
"contacts.active_recent": "Active (advert received recently)",
|
||||
"contacts.add.adding": "Adding contact...",
|
||||
"contacts.add.failed": "Failed to add contact.",
|
||||
"contacts.add.key": "Key:",
|
||||
"contacts.add.name": "Name:",
|
||||
"contacts.add.name_ph": "Contact name",
|
||||
@@ -41,11 +77,28 @@
|
||||
"contacts.add.pubkey_ph": "e.g. a1b2c3d4...",
|
||||
"contacts.add.qr_label": "Or upload a QR code image:",
|
||||
"contacts.add.scanned": "Scanned:",
|
||||
"contacts.add.success": "Contact added successfully!",
|
||||
"contacts.add.type": "Contact Type:",
|
||||
"contacts.add.type_short": "Type:",
|
||||
"contacts.add.uri_hint": "Paste a meshcore:// URI from the MeshCore mobile app",
|
||||
"contacts.add.uri_label": "MeshCore URI:",
|
||||
"contacts.add.uri_ph": "meshcore://contact/add?name=...&public_key=...&type=...",
|
||||
"contacts.blocked_by_name": "Blocked by name",
|
||||
"contacts.btn.approve": "Approve",
|
||||
"contacts.btn.approve_all": "Approve All",
|
||||
"contacts.btn.block": "Block",
|
||||
"contacts.btn.ignore": "Ignore",
|
||||
"contacts.btn.ignore_all": "Ignore All",
|
||||
"contacts.btn.protect": "Protect",
|
||||
"contacts.btn.protected": "Protected",
|
||||
"contacts.btn.to_cache": "To cache",
|
||||
"contacts.btn.to_cache_title": "Remove from device, keep in cache",
|
||||
"contacts.btn.to_device": "To device",
|
||||
"contacts.btn.to_device_title": "Add this contact to the device",
|
||||
"contacts.btn.unblock": "Unblock",
|
||||
"contacts.btn.unignore": "Unignore",
|
||||
"contacts.cache_badge": "Cache",
|
||||
"contacts.cache_badge_title": "Not on device - type unknown",
|
||||
"contacts.card.add_desc": "Add from URI, QR code, or manual entry",
|
||||
"contacts.card.existing_desc": "Manage all stored contacts",
|
||||
"contacts.card.pending_desc": "Contacts awaiting manual approval",
|
||||
@@ -61,6 +114,8 @@
|
||||
"contacts.cleanup.title": "Cleanup Contacts",
|
||||
"contacts.cleanup.title_hint": "Remove contacts based on various filter criteria",
|
||||
"contacts.cleanup.types": "Contact Types:",
|
||||
"contacts.confirm.block": "Block this contact? Their messages will be hidden from chat.",
|
||||
"contacts.copy_title": "Click to copy",
|
||||
"contacts.delete.confirm": "Delete Contact",
|
||||
"contacts.delete.question": "Are you sure you want to delete this contact?",
|
||||
"contacts.delete.title": "Confirm Delete",
|
||||
@@ -69,20 +124,75 @@
|
||||
"contacts.existing.none": "No contacts found",
|
||||
"contacts.existing.none_hint": "Contacts will appear here once added",
|
||||
"contacts.existing.search_ph": "Search by name or public key...",
|
||||
"contacts.existing.title": "Existing Contacts",
|
||||
"contacts.filter.all_sources": "All sources",
|
||||
"contacts.filter.all_types": "All Types",
|
||||
"contacts.filter.blocked": "Blocked",
|
||||
"contacts.filter.cache_only": "Cache only",
|
||||
"contacts.filter.ignored": "Ignored",
|
||||
"contacts.filter.on_device": "On device",
|
||||
"contacts.hops": {
|
||||
"one": "{count} hop",
|
||||
"other": "{count} hops"
|
||||
},
|
||||
"contacts.inactive": "Inactive",
|
||||
"contacts.last_advert": "Last advert: {time}",
|
||||
"contacts.last_advert_unknown": "Last advert: Unknown",
|
||||
"contacts.map.show": "Show:",
|
||||
"contacts.map.title": "Map",
|
||||
"contacts.never_seen": "Never seen",
|
||||
"contacts.pending.filter_hint": "Filter contacts by type or name, then approve or ignore them in bulk.",
|
||||
"contacts.pending.load_failed": "Failed to load pending contacts",
|
||||
"contacts.pending.loading": "Loading pending contacts...",
|
||||
"contacts.pending.none": "No pending requests",
|
||||
"contacts.pending.none_hint": "New contact requests will appear here when manual approval is enabled",
|
||||
"contacts.protected_cannot_block": "Cannot block protected contact",
|
||||
"contacts.protected_cannot_delete": "Cannot delete protected contact",
|
||||
"contacts.protected_cannot_ignore": "Cannot ignore protected contact",
|
||||
"contacts.protected_cannot_move": "Cannot move protected contact",
|
||||
"contacts.protected_title": "Protected contact",
|
||||
"contacts.qr.camera_denied": "Camera access denied or not available. Try uploading an image instead.",
|
||||
"contacts.qr.read_failed": "Could not read QR code from image. Make sure the image contains a valid QR code.",
|
||||
"contacts.recent_activity": "Recent activity",
|
||||
"contacts.sort.advert_asc": "Last advert ↑",
|
||||
"contacts.sort.advert_desc": "Last advert ↓",
|
||||
"contacts.sort.name_asc": "Name A→Z",
|
||||
"contacts.sort.name_desc": "Name Z→A",
|
||||
"contacts.title": "Contact Management",
|
||||
"contacts.toast.approve_failed": "Failed to approve: {error}",
|
||||
"contacts.toast.approved": "Approved: {name}",
|
||||
"contacts.toast.bulk_approve_failed": "Failed to approve contacts. Check console for details.",
|
||||
"contacts.toast.bulk_approve_partial": "Approved {ok}, failed {failed}. Check console for details.",
|
||||
"contacts.toast.bulk_approved": {
|
||||
"one": "Successfully approved {count} contact",
|
||||
"other": "Successfully approved {count} contacts"
|
||||
},
|
||||
"contacts.toast.bulk_ignore_failed": "Failed to ignore contacts. Check console for details.",
|
||||
"contacts.toast.bulk_ignore_partial": "Ignored {ok}, failed {failed}. Check console for details.",
|
||||
"contacts.toast.bulk_ignored": {
|
||||
"one": "Successfully ignored {count} contact",
|
||||
"other": "Successfully ignored {count} contacts"
|
||||
},
|
||||
"contacts.toast.cleanup_failed": "Cleanup failed: {error}",
|
||||
"contacts.toast.copy_failed": "Failed to copy to clipboard",
|
||||
"contacts.toast.delete_failed": "Failed to delete: {error}",
|
||||
"contacts.toast.deleted": "Deleted: {name}",
|
||||
"contacts.toast.key_copied": "Key copied to clipboard",
|
||||
"contacts.toast.move_failed": "Failed to move contact",
|
||||
"contacts.toast.need_days": "Set \"Days of Inactivity\" > 0 before enabling auto-cleanup",
|
||||
"contacts.toast.need_type": "Please select at least one contact type",
|
||||
"contacts.toast.need_type_enable": "Select at least one contact type before enabling auto-cleanup",
|
||||
"contacts.toast.no_match": "No contacts match the selected criteria",
|
||||
"contacts.toast.none_to_approve": "No contacts to approve",
|
||||
"contacts.toast.none_to_ignore": "No contacts to ignore",
|
||||
"contacts.toast.preview_failed": "Preview failed: {error}",
|
||||
"contacts.toast.preview_network_error": "Network error during preview",
|
||||
"contacts.toast.protect_failed": "Failed to update protection: {error}",
|
||||
"contacts.toast.pubkey_copied": "Public key copied to clipboard",
|
||||
"contacts.toast.push_failed": "Failed to push contact",
|
||||
"contacts.toast.settings_load_error": "Error loading settings",
|
||||
"contacts.toast.settings_network_error": "Network error saving settings",
|
||||
"contacts.toast.settings_save_failed": "Failed to save settings: {error}",
|
||||
"logs.clear_title": "Clear display",
|
||||
"logs.entries": {
|
||||
"one": "{count} entry",
|
||||
|
||||
@@ -1,25 +1,68 @@
|
||||
{
|
||||
"common.add": "Dodaj",
|
||||
"common.back": "Wstecz",
|
||||
"common.cancel": "Anuluj",
|
||||
"common.cannot_undo": "Tej operacji nie można cofnąć.",
|
||||
"common.close": "Zamknij",
|
||||
"common.copied": "Skopiowano!",
|
||||
"common.days_ago": {
|
||||
"one": "{count} dzień temu",
|
||||
"few": "{count} dni temu",
|
||||
"many": "{count} dni temu",
|
||||
"other": "{count} dnia temu"
|
||||
},
|
||||
"common.days_ago_long": {
|
||||
"one": "{count} dzień temu",
|
||||
"few": "{count} dni temu",
|
||||
"many": "{count} dni temu",
|
||||
"other": "{count} dnia temu"
|
||||
},
|
||||
"common.delete": "Usuń",
|
||||
"common.deleting": "Usuwanie...",
|
||||
"common.disabled": "Wyłączone",
|
||||
"common.failed_with": "Niepowodzenie: {error}",
|
||||
"common.hours_ago": "{count} godz. temu",
|
||||
"common.hours_ago_long": {
|
||||
"one": "{count} godzinę temu",
|
||||
"few": "{count} godziny temu",
|
||||
"many": "{count} godzin temu",
|
||||
"other": "{count} godziny temu"
|
||||
},
|
||||
"common.just_now": "przed chwilą",
|
||||
"common.loading": "Ładowanie...",
|
||||
"common.minutes_ago": "{count} min temu",
|
||||
"common.minutes_ago_long": {
|
||||
"one": "{count} minutę temu",
|
||||
"few": "{count} minuty temu",
|
||||
"many": "{count} minut temu",
|
||||
"other": "{count} minuty temu"
|
||||
},
|
||||
"common.months_ago": {
|
||||
"one": "{count} miesiąc temu",
|
||||
"few": "{count} miesiące temu",
|
||||
"many": "{count} miesięcy temu",
|
||||
"other": "{count} miesiąca temu"
|
||||
},
|
||||
"common.move_down": "Przesuń w dół",
|
||||
"common.move_up": "Przesuń w górę",
|
||||
"common.network_error": "Błąd sieci",
|
||||
"common.network_error_detail": "Błąd sieci: {error}",
|
||||
"common.never": "Nigdy",
|
||||
"common.refresh": "Odśwież",
|
||||
"common.remove": "Usuń",
|
||||
"common.save": "Zapisz",
|
||||
"common.save_failed": "Zapis nie powiódł się",
|
||||
"common.saving": "Zapisywanie...",
|
||||
"common.try_again": "Spróbuj ponownie",
|
||||
"common.unknown": "Nieznany",
|
||||
"common.unknown_error": "Nieznany błąd",
|
||||
"common.warning": "Uwaga:",
|
||||
"common.years_ago": {
|
||||
"one": "{count} rok temu",
|
||||
"few": "{count} lata temu",
|
||||
"many": "{count} lat temu",
|
||||
"other": "{count} roku temu"
|
||||
},
|
||||
"common.yesterday": "Wczoraj",
|
||||
"console.clear_title": "Wyczyść zapis konsoli",
|
||||
"console.connect_failed": "Nie udało się połączyć: {error}",
|
||||
@@ -35,6 +78,9 @@
|
||||
"console.status.connecting": "Łączenie...",
|
||||
"console.status.disconnected": "Rozłączono",
|
||||
"console.title": "Konsola",
|
||||
"contacts.active_recent": "Aktywny (advert odebrany niedawno)",
|
||||
"contacts.add.adding": "Dodawanie kontaktu...",
|
||||
"contacts.add.failed": "Nie udało się dodać kontaktu.",
|
||||
"contacts.add.key": "Klucz:",
|
||||
"contacts.add.name": "Nazwa:",
|
||||
"contacts.add.name_ph": "Nazwa kontaktu",
|
||||
@@ -43,11 +89,28 @@
|
||||
"contacts.add.pubkey_ph": "np. a1b2c3d4...",
|
||||
"contacts.add.qr_label": "Albo wgraj obraz z kodem QR:",
|
||||
"contacts.add.scanned": "Zeskanowano:",
|
||||
"contacts.add.success": "Kontakt dodany pomyślnie!",
|
||||
"contacts.add.type": "Typ kontaktu:",
|
||||
"contacts.add.type_short": "Typ:",
|
||||
"contacts.add.uri_hint": "Wklej URI meshcore:// z aplikacji mobilnej MeshCore",
|
||||
"contacts.add.uri_label": "MeshCore URI:",
|
||||
"contacts.add.uri_ph": "meshcore://contact/add?name=...&public_key=...&type=...",
|
||||
"contacts.blocked_by_name": "Zablokowany po nazwie",
|
||||
"contacts.btn.approve": "Zatwierdź",
|
||||
"contacts.btn.approve_all": "Zatwierdź wszystkie",
|
||||
"contacts.btn.block": "Zablokuj",
|
||||
"contacts.btn.ignore": "Ignoruj",
|
||||
"contacts.btn.ignore_all": "Ignoruj wszystkie",
|
||||
"contacts.btn.protect": "Chroń",
|
||||
"contacts.btn.protected": "Chroniony",
|
||||
"contacts.btn.to_cache": "Do pamięci",
|
||||
"contacts.btn.to_cache_title": "Usuń z urządzenia, zachowaj w pamięci",
|
||||
"contacts.btn.to_device": "Na urządzenie",
|
||||
"contacts.btn.to_device_title": "Dodaj ten kontakt do urządzenia",
|
||||
"contacts.btn.unblock": "Odblokuj",
|
||||
"contacts.btn.unignore": "Przestań ignorować",
|
||||
"contacts.cache_badge": "Pamięć",
|
||||
"contacts.cache_badge_title": "Brak na urządzeniu — typ nieznany",
|
||||
"contacts.card.add_desc": "Dodaj z URI, kodu QR lub ręcznie",
|
||||
"contacts.card.existing_desc": "Zarządzaj wszystkimi zapisanymi kontaktami",
|
||||
"contacts.card.pending_desc": "Kontakty oczekujące na ręczne zatwierdzenie",
|
||||
@@ -63,6 +126,8 @@
|
||||
"contacts.cleanup.title": "Czyszczenie kontaktów",
|
||||
"contacts.cleanup.title_hint": "Usuń kontakty według różnych kryteriów",
|
||||
"contacts.cleanup.types": "Typy kontaktów:",
|
||||
"contacts.confirm.block": "Zablokować ten kontakt? Jego wiadomości będą ukryte w czacie.",
|
||||
"contacts.copy_title": "Kliknij, aby skopiować",
|
||||
"contacts.delete.confirm": "Usuń kontakt",
|
||||
"contacts.delete.question": "Czy na pewno chcesz usunąć ten kontakt?",
|
||||
"contacts.delete.title": "Potwierdź usunięcie",
|
||||
@@ -71,20 +136,81 @@
|
||||
"contacts.existing.none": "Nie znaleziono kontaktów",
|
||||
"contacts.existing.none_hint": "Kontakty pojawią się tutaj po dodaniu",
|
||||
"contacts.existing.search_ph": "Szukaj po nazwie lub kluczu publicznym...",
|
||||
"contacts.existing.title": "Istniejące kontakty",
|
||||
"contacts.filter.all_sources": "Wszystkie źródła",
|
||||
"contacts.filter.all_types": "Wszystkie typy",
|
||||
"contacts.filter.blocked": "Zablokowane",
|
||||
"contacts.filter.cache_only": "Tylko w pamięci",
|
||||
"contacts.filter.ignored": "Ignorowane",
|
||||
"contacts.filter.on_device": "Na urządzeniu",
|
||||
"contacts.hops": {
|
||||
"one": "{count} hop",
|
||||
"few": "{count} hopy",
|
||||
"many": "{count} hopów",
|
||||
"other": "{count} hopa"
|
||||
},
|
||||
"contacts.inactive": "Nieaktywny",
|
||||
"contacts.last_advert": "Ostatni advert: {time}",
|
||||
"contacts.last_advert_unknown": "Ostatni advert: nieznany",
|
||||
"contacts.map.show": "Pokaż:",
|
||||
"contacts.map.title": "Mapa",
|
||||
"contacts.never_seen": "Nigdy niewidziany",
|
||||
"contacts.pending.filter_hint": "Filtruj kontakty po typie lub nazwie, a następnie zatwierdź lub zignoruj je zbiorczo.",
|
||||
"contacts.pending.load_failed": "Nie udało się wczytać oczekujących kontaktów",
|
||||
"contacts.pending.loading": "Wczytywanie oczekujących kontaktów...",
|
||||
"contacts.pending.none": "Brak oczekujących zgłoszeń",
|
||||
"contacts.pending.none_hint": "Nowe zgłoszenia kontaktów pojawią się tutaj, gdy włączone jest ręczne zatwierdzanie",
|
||||
"contacts.protected_cannot_block": "Nie można zablokować chronionego kontaktu",
|
||||
"contacts.protected_cannot_delete": "Nie można usunąć chronionego kontaktu",
|
||||
"contacts.protected_cannot_ignore": "Nie można zignorować chronionego kontaktu",
|
||||
"contacts.protected_cannot_move": "Nie można przenieść chronionego kontaktu",
|
||||
"contacts.protected_title": "Kontakt chroniony",
|
||||
"contacts.qr.camera_denied": "Brak dostępu do kamery lub kamera niedostępna. Spróbuj wgrać obraz.",
|
||||
"contacts.qr.read_failed": "Nie udało się odczytać kodu QR z obrazu. Upewnij się, że obraz zawiera prawidłowy kod QR.",
|
||||
"contacts.recent_activity": "Ostatnia aktywność",
|
||||
"contacts.sort.advert_asc": "Ostatni advert ↑",
|
||||
"contacts.sort.advert_desc": "Ostatni advert ↓",
|
||||
"contacts.sort.name_asc": "Nazwa A→Z",
|
||||
"contacts.sort.name_desc": "Nazwa Z→A",
|
||||
"contacts.title": "Zarządzanie kontaktami",
|
||||
"contacts.toast.approve_failed": "Nie udało się zatwierdzić: {error}",
|
||||
"contacts.toast.approved": "Zatwierdzono: {name}",
|
||||
"contacts.toast.bulk_approve_failed": "Nie udało się zatwierdzić kontaktów. Szczegóły w konsoli.",
|
||||
"contacts.toast.bulk_approve_partial": "Zatwierdzono {ok}, niepowodzeń: {failed}. Szczegóły w konsoli.",
|
||||
"contacts.toast.bulk_approved": {
|
||||
"one": "Zatwierdzono {count} kontakt",
|
||||
"few": "Zatwierdzono {count} kontakty",
|
||||
"many": "Zatwierdzono {count} kontaktów",
|
||||
"other": "Zatwierdzono {count} kontaktu"
|
||||
},
|
||||
"contacts.toast.bulk_ignore_failed": "Nie udało się zignorować kontaktów. Szczegóły w konsoli.",
|
||||
"contacts.toast.bulk_ignore_partial": "Zignorowano {ok}, niepowodzeń: {failed}. Szczegóły w konsoli.",
|
||||
"contacts.toast.bulk_ignored": {
|
||||
"one": "Zignorowano {count} kontakt",
|
||||
"few": "Zignorowano {count} kontakty",
|
||||
"many": "Zignorowano {count} kontaktów",
|
||||
"other": "Zignorowano {count} kontaktu"
|
||||
},
|
||||
"contacts.toast.cleanup_failed": "Czyszczenie nie powiodło się: {error}",
|
||||
"contacts.toast.copy_failed": "Nie udało się skopiować do schowka",
|
||||
"contacts.toast.delete_failed": "Nie udało się usunąć: {error}",
|
||||
"contacts.toast.deleted": "Usunięto: {name}",
|
||||
"contacts.toast.key_copied": "Klucz skopiowany do schowka",
|
||||
"contacts.toast.move_failed": "Nie udało się przenieść kontaktu",
|
||||
"contacts.toast.need_days": "Ustaw „Dni nieaktywności” > 0 przed włączeniem automatycznego czyszczenia",
|
||||
"contacts.toast.need_type": "Wybierz co najmniej jeden typ kontaktu",
|
||||
"contacts.toast.need_type_enable": "Wybierz co najmniej jeden typ kontaktu przed włączeniem automatycznego czyszczenia",
|
||||
"contacts.toast.no_match": "Żaden kontakt nie pasuje do wybranych kryteriów",
|
||||
"contacts.toast.none_to_approve": "Brak kontaktów do zatwierdzenia",
|
||||
"contacts.toast.none_to_ignore": "Brak kontaktów do zignorowania",
|
||||
"contacts.toast.preview_failed": "Podgląd nie powiódł się: {error}",
|
||||
"contacts.toast.preview_network_error": "Błąd sieci podczas podglądu",
|
||||
"contacts.toast.protect_failed": "Nie udało się zmienić ochrony: {error}",
|
||||
"contacts.toast.pubkey_copied": "Klucz publiczny skopiowany do schowka",
|
||||
"contacts.toast.push_failed": "Nie udało się wysłać kontaktu",
|
||||
"contacts.toast.settings_load_error": "Błąd wczytywania ustawień",
|
||||
"contacts.toast.settings_network_error": "Błąd sieci przy zapisie ustawień",
|
||||
"contacts.toast.settings_save_failed": "Nie udało się zapisać ustawień: {error}",
|
||||
"logs.clear_title": "Wyczyść widok",
|
||||
"logs.entries": {
|
||||
"one": "{count} wpis",
|
||||
|
||||
Reference in New Issue
Block a user