Use Intl to localise dates and times (#11422)

* Use Intl to generate better internationalised date formats

* Get `Yesterday` and `Today` from Intl also

* Correct capitalisation blunder

* Fix formatTime include weekday

* Iterate

* Fix tests

* use jest setSystemTime

* Discard changes to cypress/e2e/settings/general-user-settings-tab.spec.ts

* Discard changes to res/css/_components.pcss

* Discard changes to res/css/views/elements/_LanguageDropdown.pcss

* Discard changes to src/components/views/elements/LanguageDropdown.tsx

* Add docs & tests for getDaysArray & getMonthsArray

* Discard changes to test/components/structures/__snapshots__/MatrixChat-test.tsx.snap

* Consolidate consts

* Improve testing & documentation

* Update snapshot

* Apply suggestions from code review

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Iterate

* Clarify comments

* Update src/DateUtils.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Specify hourCycle

* Discard changes to test/components/views/settings/devices/DeviceDetails-test.tsx

* Update comments

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
This commit is contained in:
Michael Telatynski 2023-08-21 20:38:59 +01:00 committed by GitHub
parent d4571aef68
commit 3c52ba0c92
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 446 additions and 193 deletions

View file

@ -119,7 +119,7 @@ describe("Editing", () => {
// Assert that the date separator is rendered at the top // Assert that the date separator is rendered at the top
cy.get("li:nth-child(1) .mx_DateSeparator").within(() => { cy.get("li:nth-child(1) .mx_DateSeparator").within(() => {
cy.get("h2").within(() => { cy.get("h2").within(() => {
cy.findByText("Today"); cy.findByText("today").should("have.css", "text-transform", "capitalize");
}); });
}); });
@ -184,7 +184,7 @@ describe("Editing", () => {
// Assert that the date is rendered // Assert that the date is rendered
cy.get("li:nth-child(1) .mx_DateSeparator").within(() => { cy.get("li:nth-child(1) .mx_DateSeparator").within(() => {
cy.get("h2").within(() => { cy.get("h2").within(() => {
cy.findByText("Today"); cy.findByText("today").should("have.css", "text-transform", "capitalize");
}); });
}); });

View file

@ -40,6 +40,7 @@ limitations under the License.
font-size: inherit; font-size: inherit;
font-weight: inherit; font-weight: inherit;
color: inherit; color: inherit;
text-transform: capitalize;
} }
.mx_DateSeparator_jumpToDateMenu { .mx_DateSeparator_jumpToDateMenu {

View file

@ -18,95 +18,121 @@ limitations under the License.
import { Optional } from "matrix-events-sdk"; import { Optional } from "matrix-events-sdk";
import { _t } from "./languageHandler"; import { _t, getUserLanguage } from "./languageHandler";
function getDaysArray(): string[] { export const MINUTE_MS = 60000;
return [_t("Sun"), _t("Mon"), _t("Tue"), _t("Wed"), _t("Thu"), _t("Fri"), _t("Sat")]; export const HOUR_MS = MINUTE_MS * 60;
export const DAY_MS = HOUR_MS * 24;
/**
* Returns array of 7 weekday names, from Sunday to Saturday, internationalised to the user's language.
* @param weekday - format desired "short" | "long" | "narrow"
*/
export function getDaysArray(weekday: Intl.DateTimeFormatOptions["weekday"] = "short"): string[] {
const sunday = 1672574400000; // 2023-01-01 12:00 UTC
const { format } = new Intl.DateTimeFormat(getUserLanguage(), { weekday, timeZone: "UTC" });
return [...Array(7).keys()].map((day) => format(sunday + day * DAY_MS));
} }
function getMonthsArray(): string[] { /**
return [ * Returns array of 12 month names, from January to December, internationalised to the user's language.
_t("Jan"), * @param month - format desired "numeric" | "2-digit" | "long" | "short" | "narrow"
_t("Feb"), */
_t("Mar"), export function getMonthsArray(month: Intl.DateTimeFormatOptions["month"] = "short"): string[] {
_t("Apr"), const { format } = new Intl.DateTimeFormat(getUserLanguage(), { month, timeZone: "UTC" });
_t("May"), return [...Array(12).keys()].map((m) => format(Date.UTC(2021, m)));
_t("Jun"),
_t("Jul"),
_t("Aug"),
_t("Sep"),
_t("Oct"),
_t("Nov"),
_t("Dec"),
];
} }
function pad(n: number): string { // XXX: Ideally we could just specify `hour12: boolean` but it has issues on Chrome in the `en` locale
return (n < 10 ? "0" : "") + n; // https://support.google.com/chrome/thread/29828561?hl=en
function getTwelveHourOptions(showTwelveHour: boolean): Intl.DateTimeFormatOptions {
return {
hourCycle: showTwelveHour ? "h12" : "h23",
};
} }
function twelveHourTime(date: Date, showSeconds = false): string { /**
let hours = date.getHours() % 12; * Formats a given date to a date & time string.
const minutes = pad(date.getMinutes()); *
const ampm = date.getHours() >= 12 ? _t("PM") : _t("AM"); * The output format depends on how far away the given date is from now.
hours = hours ? hours : 12; // convert 0 -> 12 * Will use the browser's default time zone.
if (showSeconds) { * If the date is today it will return a time string excluding seconds. See {@formatTime}.
const seconds = pad(date.getSeconds()); * If the date is within the last 6 days it will return the name of the weekday along with the time string excluding seconds.
return `${hours}:${minutes}:${seconds}${ampm}`; * If the date is within the same year then it will return the weekday, month and day of the month along with the time string excluding seconds.
} * Otherwise, it will return a string representing the full date & time in a human friendly manner. See {@formatFullDate}.
return `${hours}:${minutes}${ampm}`; * @param date - date object to format
} * @param showTwelveHour - whether to use 12-hour rather than 24-hour time. Defaults to `false` (24 hour mode).
* Overrides the default from the locale, whether `true` or `false`.
export function formatDate(date: Date, showTwelveHour = false): string { * @param locale - the locale string to use, in BCP 47 format, defaulting to user's selected application locale
*/
export function formatDate(date: Date, showTwelveHour = false, locale?: string): string {
const _locale = locale ?? getUserLanguage();
const now = new Date(); const now = new Date();
const days = getDaysArray();
const months = getMonthsArray();
if (date.toDateString() === now.toDateString()) { if (date.toDateString() === now.toDateString()) {
return formatTime(date, showTwelveHour); return formatTime(date, showTwelveHour, _locale);
} else if (now.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) { } else if (now.getTime() - date.getTime() < 6 * DAY_MS) {
// TODO: use standard date localize function provided in counterpart // Time is within the last 6 days (or in the future)
return _t("%(weekDayName)s %(time)s", { return new Intl.DateTimeFormat(_locale, {
weekDayName: days[date.getDay()], ...getTwelveHourOptions(showTwelveHour),
time: formatTime(date, showTwelveHour), weekday: "short",
}); hour: "numeric",
minute: "2-digit",
}).format(date);
} else if (now.getFullYear() === date.getFullYear()) { } else if (now.getFullYear() === date.getFullYear()) {
// TODO: use standard date localize function provided in counterpart return new Intl.DateTimeFormat(_locale, {
return _t("%(weekDayName)s, %(monthName)s %(day)s %(time)s", { ...getTwelveHourOptions(showTwelveHour),
weekDayName: days[date.getDay()], weekday: "short",
monthName: months[date.getMonth()], month: "short",
day: date.getDate(), day: "numeric",
time: formatTime(date, showTwelveHour), hour: "numeric",
}); minute: "2-digit",
}).format(date);
} }
return formatFullDate(date, showTwelveHour); return formatFullDate(date, showTwelveHour, false, _locale);
} }
export function formatFullDateNoTime(date: Date): string { /**
const days = getDaysArray(); * Formats a given date to a human-friendly string with short weekday.
const months = getMonthsArray(); * Will use the browser's default time zone.
return _t("%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s", { * @example "Thu, 17 Nov 2022" in en-GB locale
weekDayName: days[date.getDay()], * @param date - date object to format
monthName: months[date.getMonth()], * @param locale - the locale string to use, in BCP 47 format, defaulting to user's selected application locale
day: date.getDate(), */
fullYear: date.getFullYear(), export function formatFullDateNoTime(date: Date, locale?: string): string {
}); return new Intl.DateTimeFormat(locale ?? getUserLanguage(), {
weekday: "short",
month: "short",
day: "numeric",
year: "numeric",
}).format(date);
} }
export function formatFullDate(date: Date, showTwelveHour = false, showSeconds = true): string { /**
const days = getDaysArray(); * Formats a given date to a date & time string, optionally including seconds.
const months = getMonthsArray(); * Will use the browser's default time zone.
return _t("%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s", { * @example "Thu, 17 Nov 2022, 4:58:32 pm" in en-GB locale with showTwelveHour=true and showSeconds=true
weekDayName: days[date.getDay()], * @param date - date object to format
monthName: months[date.getMonth()], * @param showTwelveHour - whether to use 12-hour rather than 24-hour time. Defaults to `false` (24 hour mode).
day: date.getDate(), * Overrides the default from the locale, whether `true` or `false`.
fullYear: date.getFullYear(), * @param showSeconds - whether to include seconds in the time portion of the string
time: showSeconds ? formatFullTime(date, showTwelveHour) : formatTime(date, showTwelveHour), * @param locale - the locale string to use, in BCP 47 format, defaulting to user's selected application locale
}); */
export function formatFullDate(date: Date, showTwelveHour = false, showSeconds = true, locale?: string): string {
return new Intl.DateTimeFormat(locale ?? getUserLanguage(), {
...getTwelveHourOptions(showTwelveHour),
weekday: "short",
month: "short",
day: "numeric",
year: "numeric",
hour: "numeric",
minute: "2-digit",
second: showSeconds ? "2-digit" : undefined,
}).format(date);
} }
/** /**
* Formats dates to be compatible with attributes of a `<input type="date">`. Dates * Formats dates to be compatible with attributes of a `<input type="date">`. Dates
* should be formatted like "2020-06-23" (formatted according to ISO8601) * should be formatted like "2020-06-23" (formatted according to ISO8601).
* *
* @param date The date to format. * @param date The date to format.
* @returns The date string in ISO8601 format ready to be used with an `<input>` * @returns The date string in ISO8601 format ready to be used with an `<input>`
@ -115,22 +141,44 @@ export function formatDateForInput(date: Date): string {
const year = `${date.getFullYear()}`.padStart(4, "0"); const year = `${date.getFullYear()}`.padStart(4, "0");
const month = `${date.getMonth() + 1}`.padStart(2, "0"); const month = `${date.getMonth() + 1}`.padStart(2, "0");
const day = `${date.getDate()}`.padStart(2, "0"); const day = `${date.getDate()}`.padStart(2, "0");
const dateInputValue = `${year}-${month}-${day}`; return `${year}-${month}-${day}`;
return dateInputValue;
} }
export function formatFullTime(date: Date, showTwelveHour = false): string { /**
if (showTwelveHour) { * Formats a given date to a time string including seconds.
return twelveHourTime(date, true); * Will use the browser's default time zone.
} * @example "4:58:32 PM" in en-GB locale with showTwelveHour=true
return pad(date.getHours()) + ":" + pad(date.getMinutes()) + ":" + pad(date.getSeconds()); * @example "16:58:32" in en-GB locale with showTwelveHour=false
* @param date - date object to format
* @param showTwelveHour - whether to use 12-hour rather than 24-hour time. Defaults to `false` (24 hour mode).
* Overrides the default from the locale, whether `true` or `false`.
* @param locale - the locale string to use, in BCP 47 format, defaulting to user's selected application locale
*/
export function formatFullTime(date: Date, showTwelveHour = false, locale?: string): string {
return new Intl.DateTimeFormat(locale ?? getUserLanguage(), {
...getTwelveHourOptions(showTwelveHour),
hour: "numeric",
minute: "2-digit",
second: "2-digit",
}).format(date);
} }
export function formatTime(date: Date, showTwelveHour = false): string { /**
if (showTwelveHour) { * Formats a given date to a time string excluding seconds.
return twelveHourTime(date); * Will use the browser's default time zone.
} * @example "4:58 PM" in en-GB locale with showTwelveHour=true
return pad(date.getHours()) + ":" + pad(date.getMinutes()); * @example "16:58" in en-GB locale with showTwelveHour=false
* @param date - date object to format
* @param showTwelveHour - whether to use 12-hour rather than 24-hour time. Defaults to `false` (24 hour mode).
* Overrides the default from the locale, whether `true` or `false`.
* @param locale - the locale string to use, in BCP 47 format, defaulting to user's selected application locale
*/
export function formatTime(date: Date, showTwelveHour = false, locale?: string): string {
return new Intl.DateTimeFormat(locale ?? getUserLanguage(), {
...getTwelveHourOptions(showTwelveHour),
hour: "numeric",
minute: "2-digit",
}).format(date);
} }
export function formatSeconds(inSeconds: number): string { export function formatSeconds(inSeconds: number): string {
@ -183,9 +231,8 @@ export function formatTimeLeft(inSeconds: number): string {
}); });
} }
const MILLIS_IN_DAY = 86400000;
function withinPast24Hours(prevDate: Date, nextDate: Date): boolean { function withinPast24Hours(prevDate: Date, nextDate: Date): boolean {
return Math.abs(prevDate.getTime() - nextDate.getTime()) <= MILLIS_IN_DAY; return Math.abs(prevDate.getTime() - nextDate.getTime()) <= DAY_MS;
} }
function withinCurrentDay(prevDate: Date, nextDate: Date): boolean { function withinCurrentDay(prevDate: Date, nextDate: Date): boolean {
@ -210,15 +257,15 @@ export function wantsDateSeparator(prevEventDate: Optional<Date>, nextEventDate:
} }
export function formatFullDateNoDay(date: Date): string { export function formatFullDateNoDay(date: Date): string {
const locale = getUserLanguage();
return _t("%(date)s at %(time)s", { return _t("%(date)s at %(time)s", {
date: date.toLocaleDateString().replace(/\//g, "-"), date: date.toLocaleDateString(locale).replace(/\//g, "-"),
time: date.toLocaleTimeString().replace(/:/g, "-"), time: date.toLocaleTimeString(locale).replace(/:/g, "-"),
}); });
} }
/** /**
* Returns an ISO date string without textual description of the date (ie: no "Wednesday" or * Returns an ISO date string without textual description of the date (ie: no "Wednesday" or similar)
* similar)
* @param date The date to format. * @param date The date to format.
* @returns The date string in ISO format. * @returns The date string in ISO format.
*/ */
@ -226,12 +273,23 @@ export function formatFullDateNoDayISO(date: Date): string {
return date.toISOString(); return date.toISOString();
} }
export function formatFullDateNoDayNoTime(date: Date): string { /**
return date.getFullYear() + "/" + pad(date.getMonth() + 1) + "/" + pad(date.getDate()); * Formats a given date to a string.
* Will use the browser's default time zone.
* @example 17/11/2022 in en-GB locale
* @param date - date object to format
* @param locale - the locale string to use, in BCP 47 format, defaulting to user's selected application locale
*/
export function formatFullDateNoDayNoTime(date: Date, locale?: string): string {
return new Intl.DateTimeFormat(locale ?? getUserLanguage(), {
year: "numeric",
month: "numeric",
day: "numeric",
}).format(date);
} }
export function formatRelativeTime(date: Date, showTwelveHour = false): string { export function formatRelativeTime(date: Date, showTwelveHour = false): string {
const now = new Date(Date.now()); const now = new Date();
if (withinCurrentDay(date, now)) { if (withinCurrentDay(date, now)) {
return formatTime(date, showTwelveHour); return formatTime(date, showTwelveHour);
} else { } else {
@ -245,15 +303,11 @@ export function formatRelativeTime(date: Date, showTwelveHour = false): string {
} }
} }
const MINUTE_MS = 60000;
const HOUR_MS = MINUTE_MS * 60;
const DAY_MS = HOUR_MS * 24;
/** /**
* Formats duration in ms to human readable string * Formats duration in ms to human-readable string
* Returns value in biggest possible unit (day, hour, min, second) * Returns value in the biggest possible unit (day, hour, min, second)
* Rounds values up until unit threshold * Rounds values up until unit threshold
* ie. 23:13:57 -> 23h, 24:13:57 -> 1d, 44:56:56 -> 2d * i.e. 23:13:57 -> 23h, 24:13:57 -> 1d, 44:56:56 -> 2d
*/ */
export function formatDuration(durationMs: number): string { export function formatDuration(durationMs: number): string {
if (durationMs >= DAY_MS) { if (durationMs >= DAY_MS) {
@ -269,9 +323,9 @@ export function formatDuration(durationMs: number): string {
} }
/** /**
* Formats duration in ms to human readable string * Formats duration in ms to human-readable string
* Returns precise value down to the nearest second * Returns precise value down to the nearest second
* ie. 23:13:57 -> 23h 13m 57s, 44:56:56 -> 1d 20h 56m 56s * i.e. 23:13:57 -> 23h 13m 57s, 44:56:56 -> 1d 20h 56m 56s
*/ */
export function formatPreciseDuration(durationMs: number): string { export function formatPreciseDuration(durationMs: number): string {
const days = Math.floor(durationMs / DAY_MS); const days = Math.floor(durationMs / DAY_MS);
@ -293,13 +347,13 @@ export function formatPreciseDuration(durationMs: number): string {
/** /**
* Formats a timestamp to a short date * Formats a timestamp to a short date
* (eg 25/12/22 in uk locale) * Similar to {@formatFullDateNoDayNoTime} but with 2-digit on day, month, year.
* localised by system locale * @example 25/12/22 in en-GB locale
* @param timestamp - epoch timestamp * @param timestamp - epoch timestamp
* @param locale - the locale string to use, in BCP 47 format, defaulting to user's selected application locale
* @returns {string} formattedDate * @returns {string} formattedDate
*/ */
export const formatLocalDateShort = (timestamp: number): string => export const formatLocalDateShort = (timestamp: number, locale?: string): string =>
new Intl.DateTimeFormat( new Intl.DateTimeFormat(locale ?? getUserLanguage(), { day: "2-digit", month: "2-digit", year: "2-digit" }).format(
undefined, // locales timestamp,
{ day: "2-digit", month: "2-digit", year: "2-digit" }, );
).format(timestamp);

View file

@ -216,6 +216,7 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
}, },
event_id: "$9999999999999999999999999999999999999999999", event_id: "$9999999999999999999999999999999999999999999",
room_id: event.getRoomId(), room_id: event.getRoomId(),
origin_server_ts: event.getTs(),
}); });
mockEvent.sender = { mockEvent.sender = {
name: profileInfo.displayname || userId, name: profileInfo.displayname || userId,

View file

@ -19,8 +19,8 @@ import React from "react";
import { Direction, ConnectionError, MatrixError, HTTPError } from "matrix-js-sdk/src/matrix"; import { Direction, ConnectionError, MatrixError, HTTPError } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { _t } from "../../../languageHandler"; import { _t, getUserLanguage } from "../../../languageHandler";
import { formatFullDateNoDay, formatFullDateNoTime } from "../../../DateUtils"; import { formatFullDateNoDay, formatFullDateNoTime, getDaysArray } from "../../../DateUtils";
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import dispatcher from "../../../dispatcher/dispatcher"; import dispatcher from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions"; import { Action } from "../../../dispatcher/actions";
@ -40,10 +40,6 @@ import JumpToDatePicker from "./JumpToDatePicker";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { SdkContextClass } from "../../../contexts/SDKContext"; import { SdkContextClass } from "../../../contexts/SDKContext";
function getDaysArray(): string[] {
return [_t("Sunday"), _t("Monday"), _t("Tuesday"), _t("Wednesday"), _t("Thursday"), _t("Friday"), _t("Saturday")];
}
interface IProps { interface IProps {
roomId: string; roomId: string;
ts: number; ts: number;
@ -105,15 +101,16 @@ export default class DateSeparator extends React.Component<IProps, IState> {
const today = new Date(); const today = new Date();
const yesterday = new Date(); const yesterday = new Date();
const days = getDaysArray(); const days = getDaysArray("long");
yesterday.setDate(today.getDate() - 1); yesterday.setDate(today.getDate() - 1);
const relativeTimeFormat = new Intl.RelativeTimeFormat(getUserLanguage(), { style: "long", numeric: "auto" });
if (date.toDateString() === today.toDateString()) { if (date.toDateString() === today.toDateString()) {
return _t("Today"); return relativeTimeFormat.format(0, "day"); // Today
} else if (date.toDateString() === yesterday.toDateString()) { } else if (date.toDateString() === yesterday.toDateString()) {
return _t("Yesterday"); return relativeTimeFormat.format(-1, "day"); // Yesterday
} else if (today.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) { } else if (today.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) {
return days[date.getDay()]; return days[date.getDay()]; // Sunday-Saturday
} else { } else {
return formatFullDateNoTime(date); return formatFullDateNoTime(date);
} }

View file

@ -27,31 +27,6 @@
"Server may be unavailable, overloaded, or you hit a bug.": "Server may be unavailable, overloaded, or you hit a bug.", "Server may be unavailable, overloaded, or you hit a bug.": "Server may be unavailable, overloaded, or you hit a bug.",
"The server does not support the room version specified.": "The server does not support the room version specified.", "The server does not support the room version specified.": "The server does not support the room version specified.",
"Failure to create room": "Failure to create room", "Failure to create room": "Failure to create room",
"Sun": "Sun",
"Mon": "Mon",
"Tue": "Tue",
"Wed": "Wed",
"Thu": "Thu",
"Fri": "Fri",
"Sat": "Sat",
"Jan": "Jan",
"Feb": "Feb",
"Mar": "Mar",
"Apr": "Apr",
"May": "May",
"Jun": "Jun",
"Jul": "Jul",
"Aug": "Aug",
"Sep": "Sep",
"Oct": "Oct",
"Nov": "Nov",
"Dec": "Dec",
"PM": "PM",
"AM": "AM",
"%(weekDayName)s %(time)s": "%(weekDayName)s %(time)s",
"%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(time)s",
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s",
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s",
"%(hours)sh %(minutes)sm %(seconds)ss left": "%(hours)sh %(minutes)sm %(seconds)ss left", "%(hours)sh %(minutes)sm %(seconds)ss left": "%(hours)sh %(minutes)sm %(seconds)ss left",
"%(minutes)sm %(seconds)ss left": "%(minutes)sm %(seconds)ss left", "%(minutes)sm %(seconds)ss left": "%(minutes)sm %(seconds)ss left",
"%(seconds)ss left": "%(seconds)ss left", "%(seconds)ss left": "%(seconds)ss left",
@ -2401,15 +2376,6 @@
}, },
"%(name)s started a video call": "%(name)s started a video call", "%(name)s started a video call": "%(name)s started a video call",
"Video call ended": "Video call ended", "Video call ended": "Video call ended",
"Sunday": "Sunday",
"Monday": "Monday",
"Tuesday": "Tuesday",
"Wednesday": "Wednesday",
"Thursday": "Thursday",
"Friday": "Friday",
"Saturday": "Saturday",
"Today": "Today",
"Yesterday": "Yesterday",
"A network error occurred while trying to find and jump to the given date. Your homeserver might be down or there was just a temporary problem with your internet connection. Please try again. If this continues, please contact your homeserver administrator.": "A network error occurred while trying to find and jump to the given date. Your homeserver might be down or there was just a temporary problem with your internet connection. Please try again. If this continues, please contact your homeserver administrator.", "A network error occurred while trying to find and jump to the given date. Your homeserver might be down or there was just a temporary problem with your internet connection. Please try again. If this continues, please contact your homeserver administrator.": "A network error occurred while trying to find and jump to the given date. Your homeserver might be down or there was just a temporary problem with your internet connection. Please try again. If this continues, please contact your homeserver administrator.",
"We were unable to find an event looking forwards from %(dateString)s. Try choosing an earlier date.": "We were unable to find an event looking forwards from %(dateString)s. Try choosing an earlier date.", "We were unable to find an event looking forwards from %(dateString)s. Try choosing an earlier date.": "We were unable to find an event looking forwards from %(dateString)s. Try choosing an earlier date.",
"Server returned %(statusCode)s with error code %(errorCode)s": "Server returned %(statusCode)s with error code %(errorCode)s", "Server returned %(statusCode)s with error code %(errorCode)s": "Server returned %(statusCode)s with error code %(errorCode)s",

View file

@ -93,7 +93,7 @@ export class UserFriendlyError extends Error {
export function getUserLanguage(): string { export function getUserLanguage(): string {
const language = SettingsStore.getValue("language", null, /*excludeDefault:*/ true); const language = SettingsStore.getValue("language", null, /*excludeDefault:*/ true);
if (language) { if (typeof language === "string" && language !== "") {
return language; return language;
} else { } else {
return normalizeLanguageKey(getLanguageFromBrowser()); return normalizeLanguageKey(getLanguageFromBrowser());

View file

@ -96,6 +96,7 @@ const mockEvents = (room: Room, count = 2): MatrixEvent[] => {
type: EventType.RoomMessage, type: EventType.RoomMessage,
sender: "userId", sender: "userId",
content: createMessageEventContent("`Event${index}`"), content: createMessageEventContent("`Event${index}`"),
origin_server_ts: index,
}), }),
); );
} }
@ -447,7 +448,7 @@ describe("TimelinePanel", () => {
render(<TimelinePanel {...props} />); render(<TimelinePanel {...props} />);
const event = new MatrixEvent({ type: RoomEvent.Timeline }); const event = new MatrixEvent({ type: RoomEvent.Timeline, origin_server_ts: 0 });
const data = { timeline: otherTimeline, liveEvent: true }; const data = { timeline: otherTimeline, liveEvent: true };
client.emit(RoomEvent.Timeline, event, room, false, false, data); client.emit(RoomEvent.Timeline, event, room, false, false, data);
@ -463,7 +464,7 @@ describe("TimelinePanel", () => {
render(<TimelinePanel {...props} />); render(<TimelinePanel {...props} />);
const event = new MatrixEvent({ type: RoomEvent.Timeline }); const event = new MatrixEvent({ type: RoomEvent.Timeline, origin_server_ts: 0 });
const data = { timeline: props.timelineSet.getLiveTimeline(), liveEvent: false }; const data = { timeline: props.timelineSet.getLiveTimeline(), liveEvent: false };
client.emit(RoomEvent.Timeline, event, room, false, false, data); client.emit(RoomEvent.Timeline, event, room, false, false, data);
@ -479,7 +480,7 @@ describe("TimelinePanel", () => {
render(<TimelinePanel {...props} />); render(<TimelinePanel {...props} />);
const event = new MatrixEvent({ type: RoomEvent.Timeline }); const event = new MatrixEvent({ type: RoomEvent.Timeline, origin_server_ts: 0 });
const data = { timeline: props.timelineSet.getLiveTimeline(), liveEvent: false }; const data = { timeline: props.timelineSet.getLiveTimeline(), liveEvent: false };
const toStartOfTimeline = true; const toStartOfTimeline = true;
client.emit(RoomEvent.Timeline, event, room, toStartOfTimeline, false, data); client.emit(RoomEvent.Timeline, event, room, toStartOfTimeline, false, data);
@ -496,7 +497,7 @@ describe("TimelinePanel", () => {
render(<TimelinePanel {...props} />); render(<TimelinePanel {...props} />);
const event = new MatrixEvent({ type: RoomEvent.Timeline }); const event = new MatrixEvent({ type: RoomEvent.Timeline, origin_server_ts: 0 });
const data = { timeline: props.timelineSet.getLiveTimeline(), liveEvent: true }; const data = { timeline: props.timelineSet.getLiveTimeline(), liveEvent: true };
client.emit(RoomEvent.Timeline, event, room, false, false, data); client.emit(RoomEvent.Timeline, event, room, false, false, data);
@ -521,7 +522,7 @@ describe("TimelinePanel", () => {
await flushPromises(); await flushPromises();
const event = new MatrixEvent({ type: RoomEvent.Timeline }); const event = new MatrixEvent({ type: RoomEvent.Timeline, origin_server_ts: 0 });
const data = { timeline: props.timelineSet.getLiveTimeline(), liveEvent: true }; const data = { timeline: props.timelineSet.getLiveTimeline(), liveEvent: true };
client.emit(RoomEvent.Timeline, event, room, false, false, data); client.emit(RoomEvent.Timeline, event, room, false, false, data);
@ -539,11 +540,13 @@ describe("TimelinePanel", () => {
type: "m.call.invite", type: "m.call.invite",
room_id: virtualRoom.roomId, room_id: virtualRoom.roomId,
event_id: `virtualCallEvent1`, event_id: `virtualCallEvent1`,
origin_server_ts: 0,
}); });
const virtualCallMetaEvent = new MatrixEvent({ const virtualCallMetaEvent = new MatrixEvent({
type: "org.matrix.call.sdp_stream_metadata_changed", type: "org.matrix.call.sdp_stream_metadata_changed",
room_id: virtualRoom.roomId, room_id: virtualRoom.roomId,
event_id: `virtualCallEvent2`, event_id: `virtualCallEvent2`,
origin_server_ts: 0,
}); });
const virtualEvents = [virtualCallInvite, ...mockEvents(virtualRoom), virtualCallMetaEvent]; const virtualEvents = [virtualCallInvite, ...mockEvents(virtualRoom), virtualCallMetaEvent];
const { timelineSet: overlayTimelineSet } = getProps(virtualRoom, virtualEvents); const { timelineSet: overlayTimelineSet } = getProps(virtualRoom, virtualEvents);
@ -819,6 +822,7 @@ describe("TimelinePanel", () => {
type: EventType.RoomMessage, type: EventType.RoomMessage,
sender: "userId", sender: "userId",
content: createMessageEventContent("ReplyEvent1"), content: createMessageEventContent("ReplyEvent1"),
origin_server_ts: 0,
}); });
reply2 = new MatrixEvent({ reply2 = new MatrixEvent({
@ -827,6 +831,7 @@ describe("TimelinePanel", () => {
type: EventType.RoomMessage, type: EventType.RoomMessage,
sender: "userId", sender: "userId",
content: createMessageEventContent("ReplyEvent2"), content: createMessageEventContent("ReplyEvent2"),
origin_server_ts: 0,
}); });
root = new MatrixEvent({ root = new MatrixEvent({
@ -835,6 +840,7 @@ describe("TimelinePanel", () => {
type: EventType.RoomMessage, type: EventType.RoomMessage,
sender: "userId", sender: "userId",
content: createMessageEventContent("RootEvent"), content: createMessageEventContent("RootEvent"),
origin_server_ts: 0,
}); });
const eventMap: { [key: string]: MatrixEvent } = { const eventMap: { [key: string]: MatrixEvent } = {

View file

@ -39,7 +39,7 @@ exports[`MessagePanel should handle lots of membership events quickly 1`] = `
data-testid="__testid__" data-testid="__testid__"
> >
<div <div
aria-label="Thu, Jan 1 1970" aria-label="Thu, Jan 1, 1970"
class="mx_DateSeparator" class="mx_DateSeparator"
role="separator" role="separator"
> >
@ -53,7 +53,7 @@ exports[`MessagePanel should handle lots of membership events quickly 1`] = `
aria-hidden="true" aria-hidden="true"
class="mx_DateSeparator_dateHeading" class="mx_DateSeparator_dateHeading"
> >
Thu, Jan 1 1970 Thu, Jan 1, 1970
</h2> </h2>
</div> </div>
<hr <hr

View file

@ -51,7 +51,7 @@ describe("<MessageEditHistory />", () => {
new MatrixEvent({ new MatrixEvent({
type: EventType.RoomMessage, type: EventType.RoomMessage,
room_id: roomId, room_id: roomId,
origin_server_ts: e.ts, origin_server_ts: e.ts ?? 0,
content: { content: {
body: e.msg, body: e.msg,
}, },

View file

@ -45,7 +45,7 @@ exports[`<MessageEditHistory /> should match the snapshot 1`] = `
> >
<li> <li>
<div <div
aria-label="Thu, Jan 1 1970" aria-label="Thu, Jan 1, 1970"
class="mx_DateSeparator" class="mx_DateSeparator"
role="separator" role="separator"
> >
@ -59,7 +59,7 @@ exports[`<MessageEditHistory /> should match the snapshot 1`] = `
aria-hidden="true" aria-hidden="true"
class="mx_DateSeparator_dateHeading" class="mx_DateSeparator_dateHeading"
> >
Thu, Jan 1 1970 Thu, Jan 1, 1970
</h2> </h2>
</div> </div>
<hr <hr
@ -161,7 +161,7 @@ exports[`<MessageEditHistory /> should support events with 1`] = `
> >
<li> <li>
<div <div
aria-label=", NaN NaN" aria-label="Thu, Jan 1, 1970"
class="mx_DateSeparator" class="mx_DateSeparator"
role="separator" role="separator"
> >
@ -175,7 +175,7 @@ exports[`<MessageEditHistory /> should support events with 1`] = `
aria-hidden="true" aria-hidden="true"
class="mx_DateSeparator_dateHeading" class="mx_DateSeparator_dateHeading"
> >
, NaN NaN Thu, Jan 1, 1970
</h2> </h2>
</div> </div>
<hr <hr
@ -193,7 +193,7 @@ exports[`<MessageEditHistory /> should support events with 1`] = `
<span <span
class="mx_MessageTimestamp" class="mx_MessageTimestamp"
> >
NaN:NaN 00:00
</span> </span>
<div <div
class="mx_EventTile_content" class="mx_EventTile_content"
@ -236,7 +236,7 @@ exports[`<MessageEditHistory /> should support events with 1`] = `
<span <span
class="mx_MessageTimestamp" class="mx_MessageTimestamp"
> >
NaN:NaN 00:00
</span> </span>
<div <div
class="mx_EventTile_content" class="mx_EventTile_content"
@ -290,7 +290,7 @@ exports[`<MessageEditHistory /> should support events with 1`] = `
<span <span
class="mx_MessageTimestamp" class="mx_MessageTimestamp"
> >
NaN:NaN 00:00
</span> </span>
<div <div
class="mx_EventTile_content" class="mx_EventTile_content"

View file

@ -60,11 +60,11 @@ describe("DateSeparator", () => {
type TestCase = [string, number, string]; type TestCase = [string, number, string];
const testCases: TestCase[] = [ const testCases: TestCase[] = [
["the exact same moment", nowDate.getTime(), "Today"], ["the exact same moment", nowDate.getTime(), "today"],
["same day as current day", nowDate.getTime() - HOUR_MS, "Today"], ["same day as current day", nowDate.getTime() - HOUR_MS, "today"],
["day before the current day", nowDate.getTime() - HOUR_MS * 12, "Yesterday"], ["day before the current day", nowDate.getTime() - HOUR_MS * 12, "yesterday"],
["2 days ago", nowDate.getTime() - DAY_MS * 2, "Wednesday"], ["2 days ago", nowDate.getTime() - DAY_MS * 2, "Wednesday"],
["144 hours ago", nowDate.getTime() - HOUR_MS * 144, "Sat, Dec 11 2021"], ["144 hours ago", nowDate.getTime() - HOUR_MS * 144, "Sat, Dec 11, 2021"],
[ [
"6 days ago, but less than 144h", "6 days ago, but less than 144h",
new Date("Saturday Dec 11 2021 23:59:00 GMT+0100 (Central European Standard Time)").getTime(), new Date("Saturday Dec 11 2021 23:59:00 GMT+0100 (Central European Standard Time)").getTime(),

View file

@ -3,7 +3,7 @@
exports[`DateSeparator renders the date separator correctly 1`] = ` exports[`DateSeparator renders the date separator correctly 1`] = `
<DocumentFragment> <DocumentFragment>
<div <div
aria-label="Today" aria-label="today"
class="mx_DateSeparator" class="mx_DateSeparator"
role="separator" role="separator"
> >
@ -17,7 +17,7 @@ exports[`DateSeparator renders the date separator correctly 1`] = `
aria-hidden="true" aria-hidden="true"
class="mx_DateSeparator_dateHeading" class="mx_DateSeparator_dateHeading"
> >
Today today
</h2> </h2>
</div> </div>
<hr <hr
@ -30,7 +30,7 @@ exports[`DateSeparator renders the date separator correctly 1`] = `
exports[`DateSeparator when feature_jump_to_date is enabled renders the date separator correctly 1`] = ` exports[`DateSeparator when feature_jump_to_date is enabled renders the date separator correctly 1`] = `
<DocumentFragment> <DocumentFragment>
<div <div
aria-label="Fri, Dec 17 2021" aria-label="Fri, Dec 17, 2021"
class="mx_DateSeparator" class="mx_DateSeparator"
role="separator" role="separator"
> >
@ -50,7 +50,7 @@ exports[`DateSeparator when feature_jump_to_date is enabled renders the date sep
aria-hidden="true" aria-hidden="true"
class="mx_DateSeparator_dateHeading" class="mx_DateSeparator_dateHeading"
> >
Fri, Dec 17 2021 Fri, Dec 17, 2021
</h2> </h2>
<div <div
class="mx_DateSeparator_chevron" class="mx_DateSeparator_chevron"

View file

@ -93,7 +93,7 @@ describe("SearchResultTile", () => {
room_id: ROOM_ID, room_id: ROOM_ID,
content: { body: `Message #${i}` }, content: { body: `Message #${i}` },
event_id: `$${i}:server`, event_id: `$${i}:server`,
origin_server_ts: undefined, origin_server_ts: i,
}), }),
), ),
}); });

View file

@ -51,7 +51,7 @@ exports[`<PinnedEventTile /> should render pinned event 1`] = `
<span <span
class="mx_MessageTimestamp mx_PinnedEventTile_timestamp" class="mx_MessageTimestamp mx_PinnedEventTile_timestamp"
> >
Thu, Jan 1 1970 00:00:00 Thu, Jan 1, 1970, 00:00
</span> </span>
<div <div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link" class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link"

View file

@ -103,6 +103,7 @@ export const makeBeaconEvent = (
room_id: roomId, room_id: roomId,
sender, sender,
content: ContentHelpers.makeBeaconContent(geoUri, timestamp, beaconInfoId, description), content: ContentHelpers.makeBeaconContent(geoUri, timestamp, beaconInfoId, description),
origin_server_ts: 0,
}); });
}; };

View file

@ -27,6 +27,7 @@ export const makeLegacyLocationEvent = (geoUri: string): MatrixEvent => {
msgtype: "m.location", msgtype: "m.location",
geo_uri: geoUri, geo_uri: geoUri,
}, },
origin_server_ts: 0,
}); });
}; };
@ -41,6 +42,7 @@ export const makeLocationEvent = (geoUri: string, assetType?: LocationAssetType)
"Human-readable label", "Human-readable label",
assetType, assetType,
), ),
origin_server_ts: 0,
}); });
}; };

View file

@ -538,6 +538,7 @@ export function mkMessage({
} }
const message = msg ?? "Random->" + Math.random(); const message = msg ?? "Random->" + Math.random();
const event: MakeEventProps = { const event: MakeEventProps = {
ts: 0,
...opts, ...opts,
type: "m.room.message", type: "m.room.message",
content: { content: {

View file

@ -23,8 +23,224 @@ import {
formatTimeLeft, formatTimeLeft,
formatPreciseDuration, formatPreciseDuration,
formatLocalDateShort, formatLocalDateShort,
getDaysArray,
getMonthsArray,
formatFullDateNoDayNoTime,
formatTime,
formatFullTime,
formatFullDate,
formatFullDateNoTime,
formatDate,
HOUR_MS,
MINUTE_MS,
DAY_MS,
} from "../../src/DateUtils"; } from "../../src/DateUtils";
import { REPEATABLE_DATE, mockIntlDateTimeFormat, unmockIntlDateTimeFormat } from "../test-utils"; import { REPEATABLE_DATE, mockIntlDateTimeFormat, unmockIntlDateTimeFormat } from "../test-utils";
import * as languageHandler from "../../src/languageHandler";
describe("getDaysArray", () => {
it("should return Sunday-Saturday in long mode", () => {
expect(getDaysArray("long")).toMatchInlineSnapshot(`
[
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
]
`);
});
it("should return Sun-Sat in short mode", () => {
expect(getDaysArray("short")).toMatchInlineSnapshot(`
[
"Sun",
"Mon",
"Tue",
"Wed",
"Thu",
"Fri",
"Sat",
]
`);
});
it("should return S-S in narrow mode", () => {
expect(getDaysArray("narrow")).toMatchInlineSnapshot(`
[
"S",
"M",
"T",
"W",
"T",
"F",
"S",
]
`);
});
});
describe("getMonthsArray", () => {
it("should return January-December in long mode", () => {
expect(getMonthsArray("long")).toMatchInlineSnapshot(`
[
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
]
`);
});
it("should return Jan-Dec in short mode", () => {
expect(getMonthsArray("short")).toMatchInlineSnapshot(`
[
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
]
`);
});
it("should return J-D in narrow mode", () => {
expect(getMonthsArray("narrow")).toMatchInlineSnapshot(`
[
"J",
"F",
"M",
"A",
"M",
"J",
"J",
"A",
"S",
"O",
"N",
"D",
]
`);
});
it("should return 1-12 in numeric mode", () => {
expect(getMonthsArray("numeric")).toMatchInlineSnapshot(`
[
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"10",
"11",
"12",
]
`);
});
it("should return 01-12 in 2-digit mode", () => {
expect(getMonthsArray("2-digit")).toMatchInlineSnapshot(`
[
"01",
"02",
"03",
"04",
"05",
"06",
"07",
"08",
"09",
"10",
"11",
"12",
]
`);
});
});
describe("formatDate", () => {
beforeAll(() => {
jest.useFakeTimers();
jest.setSystemTime(REPEATABLE_DATE);
});
afterAll(() => {
jest.setSystemTime(jest.getRealSystemTime());
jest.useRealTimers();
});
it("should return time string if date is within same day", () => {
const date = new Date(REPEATABLE_DATE.getTime() + 2 * HOUR_MS + 12 * MINUTE_MS);
expect(formatDate(date, false, "en-GB")).toMatchInlineSnapshot(`"19:10"`);
});
it("should return time string with weekday if date is within last 6 days", () => {
const date = new Date(REPEATABLE_DATE.getTime() - 6 * DAY_MS + 2 * HOUR_MS + 12 * MINUTE_MS);
expect(formatDate(date, false, "en-GB")).toMatchInlineSnapshot(`"Fri 19:10"`);
});
it("should return time & date string without year if it is within the same year", () => {
const date = new Date(REPEATABLE_DATE.getTime() - 66 * DAY_MS + 2 * HOUR_MS + 12 * MINUTE_MS);
expect(formatDate(date, false, "en-GB")).toMatchInlineSnapshot(`"Mon, 12 Sept, 19:10"`);
});
it("should return full time & date string otherwise", () => {
const date = new Date(REPEATABLE_DATE.getTime() - 666 * DAY_MS + 2 * HOUR_MS + 12 * MINUTE_MS);
expect(formatDate(date, false, "en-GB")).toMatchInlineSnapshot(`"Wed, 20 Jan 2021, 19:10"`);
});
});
describe("formatFullDateNoTime", () => {
it("should match given locale en-GB", () => {
expect(formatFullDateNoTime(REPEATABLE_DATE, "en-GB")).toMatchInlineSnapshot(`"Thu, 17 Nov 2022"`);
});
});
describe("formatFullDate", () => {
it("correctly formats with seconds", () => {
expect(formatFullDate(REPEATABLE_DATE, true, true, "en-GB")).toMatchInlineSnapshot(
`"Thu, 17 Nov 2022, 4:58:32 pm"`,
);
});
it("correctly formats without seconds", () => {
expect(formatFullDate(REPEATABLE_DATE, false, false, "en-GB")).toMatchInlineSnapshot(
`"Thu, 17 Nov 2022, 16:58"`,
);
});
});
describe("formatFullTime", () => {
it("correctly formats 12 hour mode", () => {
expect(formatFullTime(REPEATABLE_DATE, true, "en-GB")).toMatchInlineSnapshot(`"4:58:32 pm"`);
});
it("correctly formats 24 hour mode", () => {
expect(formatFullTime(REPEATABLE_DATE, false, "en-GB")).toMatchInlineSnapshot(`"16:58:32"`);
});
});
describe("formatTime", () => {
it("correctly formats 12 hour mode", () => {
expect(formatTime(REPEATABLE_DATE, true, "en-GB")).toMatchInlineSnapshot(`"4:58 pm"`);
});
it("correctly formats 24 hour mode", () => {
expect(formatTime(REPEATABLE_DATE, false, "en-GB")).toMatchInlineSnapshot(`"16:58"`);
});
});
describe("formatSeconds", () => { describe("formatSeconds", () => {
it("correctly formats time with hours", () => { it("correctly formats time with hours", () => {
@ -43,16 +259,15 @@ describe("formatSeconds", () => {
}); });
describe("formatRelativeTime", () => { describe("formatRelativeTime", () => {
let dateSpy: jest.SpyInstance<number, []>;
beforeAll(() => { beforeAll(() => {
dateSpy = jest jest.useFakeTimers();
.spyOn(global.Date, "now") // Tuesday, 2 November 2021 11:18:03 UTC
// Tuesday, 2 November 2021 11:18:03 UTC jest.setSystemTime(1635851883000);
.mockImplementation(() => 1635851883000);
}); });
afterAll(() => { afterAll(() => {
dateSpy.mockRestore(); jest.setSystemTime(jest.getRealSystemTime());
jest.useRealTimers();
}); });
it("returns hour format for events created in the same day", () => { it("returns hour format for events created in the same day", () => {
@ -71,7 +286,7 @@ describe("formatRelativeTime", () => {
const date = new Date(2021, 10, 2, 11, 1, 23, 0); const date = new Date(2021, 10, 2, 11, 1, 23, 0);
expect(formatRelativeTime(date)).toBe("11:01"); expect(formatRelativeTime(date)).toBe("11:01");
expect(formatRelativeTime(date, false)).toBe("11:01"); expect(formatRelativeTime(date, false)).toBe("11:01");
expect(formatRelativeTime(date, true)).toBe("11:01AM"); expect(formatRelativeTime(date, true)).toBe("11:01 AM");
}); });
it("returns month and day for events created in the current year", () => { it("returns month and day for events created in the current year", () => {
@ -134,6 +349,12 @@ describe("formatFullDateNoDayISO", () => {
}); });
}); });
describe("formatFullDateNoDayNoTime", () => {
it("should return a date formatted for en-GB locale", () => {
expect(formatFullDateNoDayNoTime(REPEATABLE_DATE, "en-GB")).toMatchInlineSnapshot(`"17/11/2022"`);
});
});
describe("formatDateForInput", () => { describe("formatDateForInput", () => {
it.each([["1993-11-01"], ["1066-10-14"], ["0571-04-22"], ["0062-02-05"]])( it.each([["1993-11-01"], ["1066-10-14"], ["0571-04-22"], ["0062-02-05"]])(
"should format %s", "should format %s",
@ -162,15 +383,18 @@ describe("formatLocalDateShort()", () => {
}); });
const timestamp = new Date("Fri Dec 17 2021 09:09:00 GMT+0100 (Central European Standard Time)").getTime(); const timestamp = new Date("Fri Dec 17 2021 09:09:00 GMT+0100 (Central European Standard Time)").getTime();
it("formats date correctly by locale", () => { it("formats date correctly by locale", () => {
const locale = jest.spyOn(languageHandler, "getUserLanguage");
mockIntlDateTimeFormat();
// format is DD/MM/YY // format is DD/MM/YY
mockIntlDateTimeFormat("en-UK"); locale.mockReturnValue("en-GB");
expect(formatLocalDateShort(timestamp)).toEqual("17/12/21"); expect(formatLocalDateShort(timestamp)).toEqual("17/12/21");
// US date format is MM/DD/YY // US date format is MM/DD/YY
mockIntlDateTimeFormat("en-US"); locale.mockReturnValue("en-US");
expect(formatLocalDateShort(timestamp)).toEqual("12/17/21"); expect(formatLocalDateShort(timestamp)).toEqual("12/17/21");
mockIntlDateTimeFormat("de-DE"); locale.mockReturnValue("de-DE");
expect(formatLocalDateShort(timestamp)).toEqual("17.12.21"); expect(formatLocalDateShort(timestamp)).toEqual("17.12.21");
}); });
}); });

View file

@ -49,8 +49,8 @@ describe("PlainTextExport", () => {
}); });
it.each([ it.each([
[24, false, "Fri, Apr 16 2021 17:20:00 - @alice:example.com: Hello, world!\n"], [24, false, "Fri, Apr 16, 2021, 17:20:00 - @alice:example.com: Hello, world!\n"],
[12, true, "Fri, Apr 16 2021 5:20:00PM - @alice:example.com: Hello, world!\n"], [12, true, "Fri, Apr 16, 2021, 5:20:00 PM - @alice:example.com: Hello, world!\n"],
])("should return text with %i hr time format", async (hour: number, setting: boolean, expectedMessage: string) => { ])("should return text with %i hr time format", async (hour: number, setting: boolean, expectedMessage: string) => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName: string) => jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName: string) =>
settingName === "showTwelveHourTimestamps" ? setting : undefined, settingName === "showTwelveHourTimestamps" ? setting : undefined,

File diff suppressed because one or more lines are too long