diff --git a/app/static/js/contacts.js b/app/static/js/contacts.js
index 77848c5..ea09840 100644
--- a/app/static/js/contacts.js
+++ b/app/static/js/contacts.js
@@ -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 = ' Loading...';
+ previewBtn.innerHTML = ` ${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 = `
@@ -689,8 +689,8 @@ function populateCleanupModal(contacts) {
${escapeHtml(contact.name)}
- Type: ${contact.type_label}
- | Last advert: ${lastSeenText}
+ ${tHtml('contacts.add.type_short')} ${contact.type_label}
+ | ${tHtml('contacts.last_advert', { time: lastSeenText })}
@@ -722,7 +722,7 @@ async function handleCleanupConfirm() {
// Disable button during cleanup
const originalHTML = confirmBtn.innerHTML;
confirmBtn.disabled = true;
- confirmBtn.innerHTML = '' +
+ resultDiv.innerHTML = `${tHtml('contacts.add.scanned')} ` +
decodedText.substring(0, 60) + '...';
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 = 'Adding contact...';
+ statusDiv.innerHTML = `${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 });
}
}
diff --git a/app/static/js/datetime-utils.js b/app/static/js/datetime-utils.js
index 59815df..3faa1a6 100644
--- a/app/static/js/datetime-utils.js
+++ b/app/static/js/datetime-utils.js
@@ -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();
}
diff --git a/app/templates/contacts-add.html b/app/templates/contacts-add.html
index 9fe7eb1..1e278a7 100644
--- a/app/templates/contacts-add.html
+++ b/app/templates/contacts-add.html
@@ -19,7 +19,7 @@
diff --git a/app/templates/contacts-existing.html b/app/templates/contacts-existing.html
index b78f234..9300afc 100644
--- a/app/templates/contacts-existing.html
+++ b/app/templates/contacts-existing.html
@@ -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 %}
- Existing Contacts
+ {{ t('contacts.existing.title') }}
0 / 350
@@ -15,10 +15,10 @@
@@ -49,17 +49,17 @@
- Loading...
+ {{ t('common.loading') }}
{{ t('contacts.existing.loading') }}
diff --git a/app/templates/contacts-pending.html b/app/templates/contacts-pending.html
index 9786f4e..5a22a96 100644
--- a/app/templates/contacts-pending.html
+++ b/app/templates/contacts-pending.html
@@ -15,7 +15,7 @@