diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml index 9c2c8881f..b1f537cc8 100644 --- a/js/js-resources.gresource.xml +++ b/js/js-resources.gresource.xml @@ -14,6 +14,7 @@ misc/config.js misc/extensionUtils.js misc/fileUtils.js + misc/dateUtils.js misc/dbusUtils.js misc/gnomeSession.js misc/history.js diff --git a/js/misc/dateUtils.js b/js/misc/dateUtils.js new file mode 100644 index 000000000..2e4c09013 --- /dev/null +++ b/js/misc/dateUtils.js @@ -0,0 +1,191 @@ +/* exported formatDateWithCFormatString, formatTime, formatTimeSpan, clearCachedLocalTimeZone */ + +const System = imports.system; +const Gettext = imports.gettext; +const GLib = imports.gi.GLib; +const Gio = imports.gi.Gio; +const Shell = imports.gi.Shell; +const Params = imports.misc.params; + +let _desktopSettings = null; +let _localTimeZone = null; + +function formatDateWithCFormatString(date, format) { + if (_localTimeZone === null) + _localTimeZone = GLib.TimeZone.new_local(); + + let dt = GLib.DateTime.new(_localTimeZone, + date.getFullYear(), + date.getMonth() + 1, + date.getDate(), + date.getHours(), + date.getMinutes(), + date.getSeconds()); + return dt?.format(format) ?? ''; +} + +function formatTimeSpan(date) { + let now = GLib.DateTime.new_now_local(); + + const timespan = now.difference(date); + + const minutesAgo = timespan / GLib.TIME_SPAN_MINUTE; + const hoursAgo = timespan / GLib.TIME_SPAN_HOUR; + const daysAgo = timespan / GLib.TIME_SPAN_DAY; + const weeksAgo = daysAgo / 7; + const monthsAgo = daysAgo / 30; + const yearsAgo = weeksAgo / 52; + + if (minutesAgo < 5) + return _('Just now'); + if (hoursAgo < 1) { + return Gettext.ngettext( + '%d minute ago', + '%d minutes ago', + minutesAgo + ).format(minutesAgo); + } + if (daysAgo < 1) { + return Gettext.ngettext( + '%d hour ago', + '%d hours ago', + hoursAgo + ).format(hoursAgo); + } + if (daysAgo < 2) + return _('Yesterday'); + if (daysAgo < 15) { + return Gettext.ngettext( + '%d day ago', + '%d days ago', + daysAgo + ).format(daysAgo); + } + if (weeksAgo < 8) { + return Gettext.ngettext( + '%d week ago', + '%d weeks ago', + weeksAgo + ).format(weeksAgo); + } + if (yearsAgo < 1) { + return Gettext.ngettext( + '%d month ago', + '%d months ago', + monthsAgo + ).format(monthsAgo); + } + return Gettext.ngettext( + '%d year ago', + '%d years ago', + yearsAgo + ).format(yearsAgo); +} + +function formatTime(time, params) { + let date; + // HACK: The built-in Date type sucks at timezones, which we need for the + // world clock; it's often more convenient though, so allow either + // Date or GLib.DateTime as parameter + if (time instanceof Date) + date = GLib.DateTime.new_from_unix_local(time.getTime() / 1000); + else + date = time; + + const now = GLib.DateTime.new_now_local(); + + const daysAgo = now.difference(date) / (24 * 60 * 60 * 1000 * 1000); + + let format; + + if (_desktopSettings == null) + _desktopSettings = new Gio.Settings({schema_id: 'org.gnome.desktop.interface'}); + const clockFormat = _desktopSettings.get_string('clock-format'); + + params = Params.parse(params, { + timeOnly: false, + ampm: true, + }); + + if (clockFormat === '24h') { + // Show only the time if date is on today + if (daysAgo < 1 || params.timeOnly) + /* Translators: Time in 24h format */ + format = N_('%H\u2236%M'); + // Show the word "Yesterday" and time if date is on yesterday + else if (daysAgo < 2) + /* Translators: this is the word "Yesterday" followed by a + time string in 24h format. i.e. "Yesterday, 14:30" */ + // xgettext:no-c-format + format = N_('Yesterday, %H\u2236%M'); + // Show a week day and time if date is in the last week + else if (daysAgo < 7) + /* Translators: this is the week day name followed by a time + string in 24h format. i.e. "Monday, 14:30" */ + // xgettext:no-c-format + format = N_('%A, %H\u2236%M'); + else if (date.get_year() === now.get_year()) + /* Translators: this is the month name and day number + followed by a time string in 24h format. + i.e. "May 25, 14:30" */ + // xgettext:no-c-format + format = N_('%B %-d, %H\u2236%M'); + else + /* Translators: this is the month name, day number, year + number followed by a time string in 24h format. + i.e. "May 25 2012, 14:30" */ + // xgettext:no-c-format + format = N_('%B %-d %Y, %H\u2236%M'); + } else { + // Show only the time if date is on today + if (daysAgo < 1 || params.timeOnly) // eslint-disable-line no-lonely-if + /* Translators: Time in 12h format */ + format = N_('%l\u2236%M %p'); + // Show the word "Yesterday" and time if date is on yesterday + else if (daysAgo < 2) + /* Translators: this is the word "Yesterday" followed by a + time string in 12h format. i.e. "Yesterday, 2:30 pm" */ + // xgettext:no-c-format + format = N_('Yesterday, %l\u2236%M %p'); + // Show a week day and time if date is in the last week + else if (daysAgo < 7) + /* Translators: this is the week day name followed by a time + string in 12h format. i.e. "Monday, 2:30 pm" */ + // xgettext:no-c-format + format = N_('%A, %l\u2236%M %p'); + else if (date.get_year() === now.get_year()) + /* Translators: this is the month name and day number + followed by a time string in 12h format. + i.e. "May 25, 2:30 pm" */ + // xgettext:no-c-format + format = N_('%B %-d, %l\u2236%M %p'); + else + /* Translators: this is the month name, day number, year + number followed by a time string in 12h format. + i.e. "May 25 2012, 2:30 pm"*/ + // xgettext:no-c-format + format = N_('%B %-d %Y, %l\u2236%M %p'); + } + + // Time in short 12h format, without the equivalent of "AM" or "PM"; used + // when it is clear from the context + if (!params.ampm) + format = format.replace(/\s*%p/g, ''); + + let formattedTime = date.format(Shell.util_translate_time_string(format)); + // prepend LTR-mark to colon/ratio to force a text direction on times + return formattedTime.replace(/([:\u2236])/g, '\u200e$1'); +} + +/** + * Update the timezone used by JavaScript Date objects and other + * date utilities + */ +function clearCachedLocalTimeZone() { + // SpiderMonkey caches the time zone so we must explicitly clear it + // before we can update the calendar, see + // https://bugzilla.gnome.org/show_bug.cgi?id=678507 + System.clearDateCaches(); + + _localTimeZone = GLib.TimeZone.new_local(); +} diff --git a/js/misc/util.js b/js/misc/util.js index d1c43d2fe..e4097acc1 100644 --- a/js/misc/util.js +++ b/js/misc/util.js @@ -1,8 +1,7 @@ // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- /* exported findUrls, spawn, spawnCommandLine, spawnApp, trySpawnCommandLine, - formatTime, formatTimeSpan, createTimeLabel, insertSorted, - ensureActorVisibleInScrollView, wiggle, lerp, GNOMEversionCompare, - DBusSenderChecker, Highlighter */ + createTimeLabel, insertSorted, ensureActorVisibleInScrollView, + wiggle, lerp, GNOMEversionCompare, DBusSenderChecker, Highlighter */ const Clutter = imports.gi.Clutter; const Gio = imports.gi.Gio; @@ -10,10 +9,10 @@ const GLib = imports.gi.GLib; const Shell = imports.gi.Shell; const St = imports.gi.St; const GnomeDesktop = imports.gi.GnomeDesktop; -const Gettext = imports.gettext; const Main = imports.ui.main; const Params = imports.misc.params; +const {formatTime} = imports.misc.dateUtils; var SCROLL_TIME = 100; @@ -180,141 +179,6 @@ function _handleSpawnError(command, err) { Main.notifyError(title, err.message); } -function formatTimeSpan(date) { - let now = GLib.DateTime.new_now_local(); - - let timespan = now.difference(date); - - let minutesAgo = timespan / GLib.TIME_SPAN_MINUTE; - let hoursAgo = timespan / GLib.TIME_SPAN_HOUR; - let daysAgo = timespan / GLib.TIME_SPAN_DAY; - let weeksAgo = daysAgo / 7; - let monthsAgo = daysAgo / 30; - let yearsAgo = weeksAgo / 52; - - if (minutesAgo < 5) - return _("Just now"); - if (hoursAgo < 1) { - return Gettext.ngettext("%d minute ago", - "%d minutes ago", minutesAgo).format(minutesAgo); - } - if (daysAgo < 1) { - return Gettext.ngettext("%d hour ago", - "%d hours ago", hoursAgo).format(hoursAgo); - } - if (daysAgo < 2) - return _("Yesterday"); - if (daysAgo < 15) { - return Gettext.ngettext("%d day ago", - "%d days ago", daysAgo).format(daysAgo); - } - if (weeksAgo < 8) { - return Gettext.ngettext("%d week ago", - "%d weeks ago", weeksAgo).format(weeksAgo); - } - if (yearsAgo < 1) { - return Gettext.ngettext("%d month ago", - "%d months ago", monthsAgo).format(monthsAgo); - } - return Gettext.ngettext("%d year ago", - "%d years ago", yearsAgo).format(yearsAgo); -} - -function formatTime(time, params) { - let date; - // HACK: The built-in Date type sucks at timezones, which we need for the - // world clock; it's often more convenient though, so allow either - // Date or GLib.DateTime as parameter - if (time instanceof Date) - date = GLib.DateTime.new_from_unix_local(time.getTime() / 1000); - else - date = time; - - let now = GLib.DateTime.new_now_local(); - - let daysAgo = now.difference(date) / (24 * 60 * 60 * 1000 * 1000); - - let format; - - if (_desktopSettings == null) - _desktopSettings = new Gio.Settings({ schema_id: 'org.gnome.desktop.interface' }); - let clockFormat = _desktopSettings.get_string('clock-format'); - - params = Params.parse(params, { - timeOnly: false, - ampm: true, - }); - - if (clockFormat == '24h') { - // Show only the time if date is on today - if (daysAgo < 1 || params.timeOnly) - /* Translators: Time in 24h format */ - format = N_("%H\u2236%M"); - // Show the word "Yesterday" and time if date is on yesterday - else if (daysAgo < 2) - /* Translators: this is the word "Yesterday" followed by a - time string in 24h format. i.e. "Yesterday, 14:30" */ - // xgettext:no-c-format - format = N_("Yesterday, %H\u2236%M"); - // Show a week day and time if date is in the last week - else if (daysAgo < 7) - /* Translators: this is the week day name followed by a time - string in 24h format. i.e. "Monday, 14:30" */ - // xgettext:no-c-format - format = N_("%A, %H\u2236%M"); - else if (date.get_year() == now.get_year()) - /* Translators: this is the month name and day number - followed by a time string in 24h format. - i.e. "May 25, 14:30" */ - // xgettext:no-c-format - format = N_("%B %-d, %H\u2236%M"); - else - /* Translators: this is the month name, day number, year - number followed by a time string in 24h format. - i.e. "May 25 2012, 14:30" */ - // xgettext:no-c-format - format = N_("%B %-d %Y, %H\u2236%M"); - } else { - // Show only the time if date is on today - if (daysAgo < 1 || params.timeOnly) // eslint-disable-line no-lonely-if - /* Translators: Time in 12h format */ - format = N_("%l\u2236%M %p"); - // Show the word "Yesterday" and time if date is on yesterday - else if (daysAgo < 2) - /* Translators: this is the word "Yesterday" followed by a - time string in 12h format. i.e. "Yesterday, 2:30 pm" */ - // xgettext:no-c-format - format = N_("Yesterday, %l\u2236%M %p"); - // Show a week day and time if date is in the last week - else if (daysAgo < 7) - /* Translators: this is the week day name followed by a time - string in 12h format. i.e. "Monday, 2:30 pm" */ - // xgettext:no-c-format - format = N_("%A, %l\u2236%M %p"); - else if (date.get_year() == now.get_year()) - /* Translators: this is the month name and day number - followed by a time string in 12h format. - i.e. "May 25, 2:30 pm" */ - // xgettext:no-c-format - format = N_("%B %-d, %l\u2236%M %p"); - else - /* Translators: this is the month name, day number, year - number followed by a time string in 12h format. - i.e. "May 25 2012, 2:30 pm"*/ - // xgettext:no-c-format - format = N_("%B %-d %Y, %l\u2236%M %p"); - } - - // Time in short 12h format, without the equivalent of "AM" or "PM"; used - // when it is clear from the context - if (!params.ampm) - format = format.replace(/\s*%p/g, ''); - - let formattedTime = date.format(Shell.util_translate_time_string(format)); - // prepend LTR-mark to colon/ratio to force a text direction on times - return formattedTime.replace(/([:\u2236])/g, '\u200e$1'); -} - function createTimeLabel(date, params) { if (_desktopSettings == null) _desktopSettings = new Gio.Settings({ schema_id: 'org.gnome.desktop.interface' }); diff --git a/js/ui/calendar.js b/js/ui/calendar.js index 673c4c05a..aa597fc42 100644 --- a/js/ui/calendar.js +++ b/js/ui/calendar.js @@ -15,7 +15,8 @@ const Mpris = imports.ui.mpris; const PopupMenu = imports.ui.popupMenu; const Util = imports.misc.util; -const { loadInterfaceXML } = imports.misc.fileUtils; +const {formatDateWithCFormatString, formatTimeSpan} = imports.misc.dateUtils; +const {loadInterfaceXML} = imports.misc.fileUtils; var SHOW_WEEKDATE_KEY = 'show-weekdate'; @@ -520,7 +521,7 @@ var Calendar = GObject.registerClass({ iter.setSeconds(0); // Leap second protection. Hah! iter.setHours(12); for (let i = 0; i < 7; i++) { - // Could use iter.toLocaleFormat('%a') but that normally gives three characters + // Could use formatDateWithCFormatString(iter, '%a') but that normally gives three characters // and we want, ideally, a single character for e.g. S M T W T F S let customDayAbbrev = _getCalendarDayAbbreviation(iter.getDay()); let label = new St.Label({ @@ -528,7 +529,7 @@ var Calendar = GObject.registerClass({ text: customDayAbbrev, can_focus: true, }); - label.accessible_name = iter.toLocaleFormat('%A'); + label.accessible_name = formatDateWithCFormatString(iter, '%A'); let col; if (this.get_text_direction() == Clutter.TextDirection.RTL) col = 6 - (7 + iter.getDay() - this._weekStart) % 7; @@ -656,7 +657,7 @@ var Calendar = GObject.registerClass({ while (row < nRows) { let button = new St.Button({ // xgettext:no-javascript-format - label: iter.toLocaleFormat(C_('date day number format', '%d')), + label: formatDateWithCFormatString(iter, C_('date day number format', '%d')), can_focus: true, }); let rtl = button.get_text_direction() == Clutter.TextDirection.RTL; @@ -711,13 +712,13 @@ var Calendar = GObject.registerClass({ if (this._useWeekdate && iter.getDay() == 4) { const label = new St.Label({ - text: iter.toLocaleFormat('%V'), + text: formatDateWithCFormatString(iter, '%V'), style_class: 'calendar-week-number', can_focus: true, }); let weekFormat = Shell.util_translate_time_string(N_("Week %V")); label.clutter_text.y_align = Clutter.ActorAlign.CENTER; - label.accessible_name = iter.toLocaleFormat(weekFormat); + label.accessible_name = formatDateWithCFormatString(iter, weekFormat); layout.attach(label, rtl ? 7 : 0, row, 1, 1); } @@ -736,9 +737,9 @@ var Calendar = GObject.registerClass({ let now = new Date(); if (sameYear(this._selectedDate, now)) - this._monthLabel.text = this._selectedDate.toLocaleFormat(this._headerFormatWithoutYear); + this._monthLabel.text = formatDateWithCFormatString(this._selectedDate, this._headerFormatWithoutYear); else - this._monthLabel.text = this._selectedDate.toLocaleFormat(this._headerFormat); + this._monthLabel.text = formatDateWithCFormatString(this._selectedDate, this._headerFormat); if (!this._calendarBegin || !sameMonth(this._selectedDate, this._calendarBegin) || !sameDay(now, this._markedAsToday)) this._rebuildCalendar(); @@ -818,7 +819,7 @@ class NotificationTimeLabel extends St.Label { } vfunc_map() { - this.text = Util.formatTimeSpan(this._datetime); + this.text = formatTimeSpan(this._datetime); super.vfunc_map(); } }); diff --git a/js/ui/dateMenu.js b/js/ui/dateMenu.js index a1298e546..af97299fd 100644 --- a/js/ui/dateMenu.js +++ b/js/ui/dateMenu.js @@ -11,13 +11,13 @@ const Pango = imports.gi.Pango; const Shell = imports.gi.Shell; const St = imports.gi.St; -const Util = imports.misc.util; const Main = imports.ui.main; const PanelMenu = imports.ui.panelMenu; const Calendar = imports.ui.calendar; const Weather = imports.misc.weather; -const System = imports.system; +const DateUtils = imports.misc.dateUtils; +const {formatDateWithCFormatString, formatTime} = imports.misc.dateUtils; const {loadInterfaceXML} = imports.misc.fileUtils; const NC_ = (context, str) => `${context}\u0004${str}`; @@ -90,7 +90,7 @@ class TodayButton extends St.Button { } setDate(date) { - this._dayLabel.set_text(date.toLocaleFormat('%A')); + this._dayLabel.set_text(formatDateWithCFormatString(date, '%A')); /* Translators: This is the date format to use when the calendar popup is * shown - it is shown just below the time in the top bar (e.g., @@ -98,14 +98,14 @@ class TodayButton extends St.Button { * "February 17 2015". */ const dateFormat = Shell.util_translate_time_string(N_('%B %-d %Y')); - this._dateLabel.set_text(date.toLocaleFormat(dateFormat)); + this._dateLabel.set_text(formatDateWithCFormatString(date, dateFormat)); /* Translators: This is the accessible name of the date button shown * below the time in the shell; it should combine the weekday and the * date, e.g. "Tuesday February 17 2015". */ const dateAccessibleNameFormat = Shell.util_translate_time_string(N_('%A %B %e %Y')); - this.accessible_name = date.toLocaleFormat(dateAccessibleNameFormat); + this.accessible_name = formatDateWithCFormatString(date, dateAccessibleNameFormat); } }); @@ -185,9 +185,9 @@ class EventsSection extends St.Button { else if (this._startDate > now && this._startDate - now <= timeSpanDay) this._title.text = _('Tomorrow'); else if (this._startDate.getFullYear() === now.getFullYear()) - this._title.text = this._startDate.toLocaleFormat(sameYearFormat); + this._title.text = formatDateWithCFormatString(this._startDate, sameYearFormat); else - this._title.text = this._startDate.toLocaleFormat(otherYearFormat); + this._title.text = formatDateWithCFormatString(this._startDate, otherYearFormat); } _isAtMidnight(eventTime) { @@ -204,8 +204,8 @@ class EventsSection extends St.Button { const startsBeforeToday = eventStart < this._startDate; const endsAfterToday = eventEnd > this._endDate; - const startTimeOnly = Util.formatTime(eventStart, {timeOnly: true}); - const endTimeOnly = Util.formatTime(eventEnd, {timeOnly: true}); + const startTimeOnly = formatTime(eventStart, {timeOnly: true}); + const endTimeOnly = formatTime(eventEnd, {timeOnly: true}); const rtl = Clutter.get_default_text_direction() === Clutter.TextDirection.RTL; @@ -240,8 +240,8 @@ class EventsSection extends St.Button { else format = '%x'; - const startDateOnly = eventStart.toLocaleFormat(format); - const endDateOnly = eventEnd.toLocaleFormat(format); + const startDateOnly = formatDateWithCFormatString(eventStart, format); + const endDateOnly = formatDateWithCFormatString(eventEnd, format); if (startsAtMidnight && endsAtMidnight) title = `${rtl ? endDateOnly : startDateOnly} ${EN_CHAR} ${rtl ? startDateOnly : endDateOnly}`; @@ -503,7 +503,7 @@ class WorldClocksSection extends St.Button { for (let i = 0; i < this._locations.length; i++) { let l = this._locations[i]; const now = GLib.DateTime.new_now(l.location.get_timezone()); - l.timeLabel.text = Util.formatTime(now, {timeOnly: true}); + l.timeLabel.text = formatTime(now, {timeOnly: true}); } } @@ -630,7 +630,7 @@ class WeatherSection extends St.Button { let col = 0; infos.forEach(fc => { const [valid_, timestamp] = fc.get_value_update(); - let timeStr = Util.formatTime(new Date(timestamp * 1000), { + let timeStr = formatTime(new Date(timestamp * 1000), { timeOnly: true, ampm: false, }); @@ -970,10 +970,7 @@ class DateMenuButton extends PanelMenu.Button { } _updateTimeZone() { - // SpiderMonkey caches the time zone so we must explicitly clear it - // before we can update the calendar, see - // https://bugzilla.gnome.org/show_bug.cgi?id=678507 - System.clearDateCaches(); + DateUtils.clearCachedLocalTimeZone(); this._calendar.updateTimeZone(); } diff --git a/js/ui/environment.js b/js/ui/environment.js index 3b8cd8233..8f75871c3 100644 --- a/js/ui/environment.js +++ b/js/ui/environment.js @@ -43,7 +43,6 @@ const Polkit = imports.gi.Polkit; const Shell = imports.gi.Shell; const St = imports.gi.St; const Gettext = imports.gettext; -const System = imports.system; const SignalTracker = imports.misc.signalTracker; Gio._promisify(Gio.DataInputStream.prototype, 'fill_async'); @@ -56,8 +55,6 @@ Gio._promisify(Gio.DBusProxy.prototype, 'call_with_unix_fd_list'); Gio._promisify(Gio.File.prototype, 'query_info_async'); Gio._promisify(Polkit.Permission, 'new'); -let _localTimeZone = null; - // We can't import shell JS modules yet, because they may have // variable initializations, etc, that depend on init() already having // been run. @@ -373,28 +370,6 @@ function init() { } }; - // Override to clear our own timezone cache as well - const origClearDateCaches = System.clearDateCaches; - System.clearDateCaches = function () { - _localTimeZone = null; - origClearDateCaches(); - }; - - // Work around https://bugzilla.mozilla.org/show_bug.cgi?id=508783 - Date.prototype.toLocaleFormat = function (format) { - if (_localTimeZone === null) - _localTimeZone = GLib.TimeZone.new_local(); - - let dt = GLib.DateTime.new(_localTimeZone, - this.getFullYear(), - this.getMonth() + 1, - this.getDate(), - this.getHours(), - this.getMinutes(), - this.getSeconds()); - return dt?.format(format) ?? ''; - }; - let slowdownEnv = GLib.getenv('GNOME_SHELL_SLOWDOWN_FACTOR'); if (slowdownEnv) { let factor = parseFloat(slowdownEnv); diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js index 0cb0a3da2..00d445d9b 100644 --- a/js/ui/unlockDialog.js +++ b/js/ui/unlockDialog.js @@ -17,7 +17,7 @@ const Layout = imports.ui.layout; const Main = imports.ui.main; const MessageTray = imports.ui.messageTray; const SwipeTracker = imports.ui.swipeTracker; - +const {formatDateWithCFormatString} = imports.misc.dateUtils; const AuthPrompt = imports.gdm.authPrompt; // The timeout before going back automatically to the lock screen (in seconds) @@ -367,7 +367,7 @@ class UnlockDialogClock extends St.BoxLayout { /* Translators: This is a time format for a date in long format */ let dateFormat = Shell.util_translate_time_string(N_('%A %B %-d')); - this._date.text = date.toLocaleFormat(dateFormat); + this._date.text = formatDateWithCFormatString(date, dateFormat); } _updateHint() { diff --git a/po/POTFILES.in b/po/POTFILES.in index 431580bb0..098142518 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -12,6 +12,7 @@ js/gdm/loginDialog.js js/gdm/util.js js/misc/systemActions.js js/misc/util.js +js/misc/dateUtils.js js/portalHelper/main.js js/ui/accessDialog.js js/ui/appDisplay.js