mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-08-06 16:53:21 +02:00
refactor(i18n): consolidate date/time formatting (stage 0b)
Prerequisite for string extraction. formatTime() existed twice with subtly
different behaviour, and both hardcoded 'Yesterday ' — a string that would have
survived a naive extraction pass and stayed English forever.
app/static/js/datetime-utils.js now owns the today/yesterday/older logic, with
options for the two variants that actually differed: app.js needs an absolute
date in archive views, dm.js needs the short "5 Aug" form because DM rows are
narrower. Both keep a thin local formatTime() wrapper, so their ~15 call sites
are untouched. formatTimeAgo() moves over as-is (it only ever existed in app.js).
repeater-manage.js's fmtInt() no longer forces toLocaleString('en-US'), so
thousands separators follow the reader's locale. Kept as a function declaration
rather than a const alias so it stays hoisted, like the code it replaced.
Policy, documented in the module header: numeric formatting follows the BROWSER
locale, only the words are translated. 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 whatever language the menus are in.
First five real catalog entries: common.yesterday, just_now, minutes_ago,
hours_ago, days_ago. days_ago exercises the plural machinery — Polish needs
"1 dzień temu" / "3 dni temu" / "5 dni temu" where English has one form.
Verified in the browser in both languages: all four helpers global, both
formatTime wrappers still routing through the shared core, Polish plural
categories correct, and the clock and number formats provably unchanged by the
UI language. Live chat timestamps render identically to before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+4
-35
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
+3
-16
@@ -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' });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -8,3 +8,4 @@
|
||||
(including the 6 iframes) share a single cached fetch. #}
|
||||
<script src="{{ i18n_catalog_url }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/i18n-runtime.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/datetime-utils.js') }}"></script>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user