diff --git a/app/static/js/app.js b/app/static/js/app.js index 016f957..f1e48d3 100644 --- a/app/static/js/app.js +++ b/app/static/js/app.js @@ -4591,45 +4591,14 @@ function scrollToBottom() { } /** - * Format timestamp + * Format a message timestamp. + * Archive views always show the full date, since "Today" is meaningless there. */ function formatTime(timestamp) { - const date = new Date(timestamp * 1000); - - // When viewing archive, always show full date + time - if (currentArchiveDate) { - return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); - } - - // When viewing live messages, compare calendar dates - const now = new Date(); - const yesterday = new Date(now); - yesterday.setDate(yesterday.getDate() - 1); - - if (date.toDateString() === now.toDateString()) { - // Today - show time only - return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); - } else if (date.toDateString() === yesterday.toDateString()) { - // Yesterday - return 'Yesterday ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); - } else { - // Older - show date and time - return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); - } + return formatTimestamp(timestamp, { absolute: !!currentArchiveDate }); } -/** - * Format a unix timestamp as relative time (e.g., "5 min ago", "2h ago") - */ -function formatTimeAgo(timestamp) { - const now = Math.floor(Date.now() / 1000); - const diff = now - timestamp; - if (diff < 60) return 'just now'; - if (diff < 3600) return `${Math.floor(diff / 60)} min ago`; - if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`; - if (diff < 604800) return `${Math.floor(diff / 86400)}d ago`; - return new Date(timestamp * 1000).toLocaleDateString(); -} +// formatTimeAgo() comes from datetime-utils.js. /** * Update character counter (counts UTF-8 bytes, not characters) diff --git a/app/static/js/datetime-utils.js b/app/static/js/datetime-utils.js new file mode 100644 index 0000000..59815df --- /dev/null +++ b/app/static/js/datetime-utils.js @@ -0,0 +1,81 @@ +/** + * Shared date/time and number formatting. + * + * Policy: numeric formatting follows the BROWSER locale, only the words are translated. + * + * That split is deliberate. Tying the clock to the UI language would flip a Polish + * operator to "09:53 AM" the moment they switched the interface to English, and mesh + * operators want 24-hour time regardless of what language the menus are in. The browser + * locale already reflects how the user wants dates and numbers written, so leave it + * alone and translate only "Yesterday", "just now" and friends. + * + * Loaded via _head_i18n.html on every page, after the translation runtime. + */ + +/** HH:MM in the browser's locale. */ +function formatClock(date) { + return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); +} + +/** + * Format a unix timestamp as today / yesterday / an older date. + * + * @param {number} timestamp - unix seconds + * @param {Object} [opts] + * @param {boolean} [opts.absolute] - always show the full date, never today/yesterday + * @param {'full'|'short'} [opts.dateStyle] - how older dates are written + * @returns {string} + */ +function formatTimestamp(timestamp, opts) { + if (!timestamp) return ''; + const { absolute = false, dateStyle = 'full' } = opts || {}; + + const date = new Date(timestamp * 1000); + const longDate = () => (dateStyle === 'short' + ? date.toLocaleDateString([], { month: 'short', day: 'numeric' }) + : date.toLocaleDateString()); + + if (absolute) return `${longDate()} ${formatClock(date)}`; + + const now = new Date(); + const yesterday = new Date(now); + yesterday.setDate(yesterday.getDate() - 1); + + if (date.toDateString() === now.toDateString()) { + return formatClock(date); + } + if (date.toDateString() === yesterday.toDateString()) { + return `${t('common.yesterday')} ${formatClock(date)}`; + } + return `${longDate()} ${formatClock(date)}`; +} + +/** + * Format a unix timestamp as relative time ("5 min ago", "2h ago"). + * @param {number} timestamp - unix seconds + * @returns {string} + */ +function formatTimeAgo(timestamp) { + const diff = Math.floor(Date.now() / 1000) - timestamp; + + 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 < 604800) return tn('common.days_ago', Math.floor(diff / 86400)); + return new Date(timestamp * 1000).toLocaleDateString(); +} + +/** + * Thousands-separated integer in the browser's locale, em dash for missing values. + * @param {number|null} n + * @returns {string} + */ +function formatInt(n) { + if (n == null || isNaN(n)) return '—'; + return Number(n).toLocaleString(); +} + +window.formatClock = formatClock; +window.formatTimestamp = formatTimestamp; +window.formatTimeAgo = formatTimeAgo; +window.formatInt = formatInt; diff --git a/app/static/js/dm.js b/app/static/js/dm.js index b357f7a..55b6556 100644 --- a/app/static/js/dm.js +++ b/app/static/js/dm.js @@ -1783,24 +1783,11 @@ function updateLastRefresh() { } /** - * Format timestamp to readable time + * Format timestamp to readable time. + * Uses the short date form ("5 Aug") — DM rows are narrower than channel messages. */ function formatTime(timestamp) { - if (!timestamp) return ''; - - const date = new Date(timestamp * 1000); - const now = new Date(); - const yesterday = new Date(now); - yesterday.setDate(yesterday.getDate() - 1); - - if (date.toDateString() === now.toDateString()) { - return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); - } else if (date.toDateString() === yesterday.toDateString()) { - return 'Yesterday ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); - } else { - return date.toLocaleDateString([], { month: 'short', day: 'numeric' }) + - ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); - } + return formatTimestamp(timestamp, { dateStyle: 'short' }); } /** diff --git a/app/static/js/repeater-manage.js b/app/static/js/repeater-manage.js index e75257e..6f84e7a 100644 --- a/app/static/js/repeater-manage.js +++ b/app/static/js/repeater-manage.js @@ -278,9 +278,11 @@ function fmtDuration(seconds) { return parts.join(' '); } +// Delegates to datetime-utils.js; no longer forces en-US thousands separators, so +// numbers follow the reader's locale. Kept as a declaration (not const) so it stays +// hoisted, like the implementation it replaced. function fmtInt(n) { - if (n == null || isNaN(n)) return '—'; - return Number(n).toLocaleString('en-US'); + return formatInt(n); } function batteryPercent(mv) { diff --git a/app/static/js/sw.js b/app/static/js/sw.js index ca6ebad..99a5c50 100644 --- a/app/static/js/sw.js +++ b/app/static/js/sw.js @@ -8,6 +8,7 @@ const ASSETS_TO_CACHE = [ '/static/js/message-utils.js', '/static/js/filter-utils.js', '/static/js/i18n-runtime.js', + '/static/js/datetime-utils.js', '/static/js/console.js', '/static/images/android-chrome-192x192.png', '/static/images/android-chrome-512x512.png', diff --git a/app/templates/_head_i18n.html b/app/templates/_head_i18n.html index 0cfc7cd..6219595 100644 --- a/app/templates/_head_i18n.html +++ b/app/templates/_head_i18n.html @@ -8,3 +8,4 @@ (including the 6 iframes) share a single cached fetch. #} + diff --git a/app/translations/en.json b/app/translations/en.json index f11dbee..fc55f5d 100644 --- a/app/translations/en.json +++ b/app/translations/en.json @@ -1,4 +1,13 @@ { + "common.days_ago": { + "one": "{count}d ago", + "other": "{count}d ago" + }, + "common.hours_ago": "{count}h ago", + "common.just_now": "just now", + "common.minutes_ago": "{count} min ago", + "common.yesterday": "Yesterday", + "meta.language_english_name": "English", "meta.language_name": "English", "meta.translator": "mc-webui" diff --git a/app/translations/pl.json b/app/translations/pl.json index 3e12af8..9d76015 100644 --- a/app/translations/pl.json +++ b/app/translations/pl.json @@ -1,4 +1,15 @@ { + "common.days_ago": { + "one": "{count} dzień temu", + "few": "{count} dni temu", + "many": "{count} dni temu", + "other": "{count} dnia temu" + }, + "common.hours_ago": "{count} godz. temu", + "common.just_now": "przed chwilą", + "common.minutes_ago": "{count} min temu", + "common.yesterday": "Wczoraj", + "meta.language_english_name": "Polish", "meta.language_name": "Polski", "meta.translator": "mc-webui"