Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/API-Reference/utils/LocalizationUtils.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Formats a given date object into a locale-aware date and time string.

<a name="dateTimeFromNow"></a>

## dateTimeFromNow([date], [lang]) ⇒ <code>string</code>
## dateTimeFromNow([date], [lang], [fromDate]) ⇒ <code>string</code>
Returns a relative time string (e.g., "2 days ago", "in 3 hours") based on the difference between the given date and now.

**Kind**: global function
Expand All @@ -44,6 +44,7 @@ Returns a relative time string (e.g., "2 days ago", "in 3 hours") based on the d
| --- | --- | --- |
| [date] | <code>Date</code> | The date to compare with the current date and time. If not given, defaults to now. |
| [lang] | <code>string</code> | Optional language code to use for formatting (e.g., 'en', 'fr'). If not provided, defaults to the application locale or 'en'. |
| [fromDate] | <code>string</code> | Optional date to use instead of now to compute the relative dateTime from. |

<a name="dateTimeFromNowFriendly"></a>

Expand Down
11 changes: 10 additions & 1 deletion src/document/DocumentCommandHandlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -1957,27 +1957,36 @@ define(function (require, exports, module) {
if (entry) {
brackets.app.openPathInFileBrowser(entry.fullPath)
.catch(err=>console.error("Error showing '" + entry.fullPath + "' in OS folder:", err));
} else {
brackets.app.openPathInFileBrowser(ProjectManager.getProjectRoot().fullPath)
.catch(err=>console.error("Error showing '" + ProjectManager.getProjectRoot().fullPath + "' in OS folder:", err));
}
}

function openDefaultTerminal() {
const entry = ProjectManager.getSelectedItem();
if (entry && entry.fullPath) {
NodeUtils.openNativeTerminal(entry.fullPath);
} else {
NodeUtils.openNativeTerminal(ProjectManager.getProjectRoot().fullPath);
}
}

function openPowerShell() {
const entry = ProjectManager.getSelectedItem();
if (entry && entry.fullPath) {
NodeUtils.openNativeTerminal(entry.fullPath, true);
} else {
NodeUtils.openNativeTerminal(ProjectManager.getProjectRoot().fullPath, true);
}
}

function openDefaultApp() {
const entry = ProjectManager.getSelectedItem();
if (entry && entry.fullPath) {
NodeUtils.openInDefaultApp(entry.fullPath, true);
NodeUtils.openInDefaultApp(entry.fullPath);
} else {
NodeUtils.openInDefaultApp(ProjectManager.getProjectRoot().fullPath);
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/styles/brackets_core_ui_variables.less
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@
@bc-panel-bg-hover: rgba(255, 255, 255, 0.6);
@bc-panel-bg-hover-alt: rgba(0, 0, 0, 0.04);
@bc-panel-bg-selected: #d0d5d5;
@bc-panel-bg-selected-table: #b8c9d1;
@bc-panel-bg-selected-table: #cddae0;
@bc-panel-bg-text-highlight: #fff;
@bc-panel-border: rgba(0, 0, 0, 0.09);
@bc-panel-separator: #c3c6c5;
Expand Down
25 changes: 14 additions & 11 deletions src/utils/LocalizationUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,28 +77,31 @@ define(function (require, exports, module) {
* @param {Date} [date] - The date to compare with the current date and time. If not given, defaults to now.
* @param {string} [lang] - Optional language code to use for formatting (e.g., 'en', 'fr').
* If not provided, defaults to the application locale or 'en'.
* @param {Date} [fromDate] - Optional date to use instead of now to compute the relative dateTime from.
* @returns {string} - A human-readable relative time string (e.g., "2 days ago", "in 3 hours").
*/
function dateTimeFromNow(date, lang) {
function dateTimeFromNow(date, lang, fromDate) {
date = date || new Date();
const now = new Date();
const diffInSeconds = Math.floor((date - now) / 1000);
fromDate = fromDate || new Date();
const diffInSeconds = Math.floor((date - fromDate) / 1000);

const rtf = new Intl.RelativeTimeFormat([lang || brackets.getLocale() || "en", "en"],
{ numeric: 'auto' });

if (Math.abs(diffInSeconds) < 60) {
if (Math.abs(diffInSeconds) < 3) {
// we consider diffs less than 3 seconds to be always 'now', for better UX and for unit tests stability.
return rtf.format(0, 'second');
} else if (Math.abs(diffInSeconds) < 60) {
return rtf.format(diffInSeconds, 'second');
} else if (Math.abs(diffInSeconds) < 3600) {
return rtf.format(Math.floor(diffInSeconds / 60), 'minute');
return rtf.format(Math.trunc(diffInSeconds / 60), 'minute');
} else if (Math.abs(diffInSeconds) < 86400) {
return rtf.format(Math.floor(diffInSeconds / 3600), 'hour');
return rtf.format(Math.trunc(diffInSeconds / 3600), 'hour');
} else if (Math.abs(diffInSeconds) < 2592000) {
return rtf.format(Math.floor(diffInSeconds / 86400), 'day');
return rtf.format(Math.trunc(diffInSeconds / 86400), 'day');
} else if (Math.abs(diffInSeconds) < 31536000) {
return rtf.format(Math.floor(diffInSeconds / 2592000), 'month');
return rtf.format(Math.trunc(diffInSeconds / 2592000), 'month');
} else {
return rtf.format(Math.floor(diffInSeconds / 31536000), 'year');
return rtf.format(Math.trunc(diffInSeconds / 31536000), 'year');
}
}

Expand All @@ -115,7 +118,7 @@ define(function (require, exports, module) {
function dateTimeFromNowFriendly(date, lang) {
const now = new Date();
const diffInMilliseconds = date - now;
const diffInDays = Math.floor(diffInMilliseconds / (1000 * 60 * 60 * 24));
const diffInDays = Math.trunc(diffInMilliseconds / (1000 * 60 * 60 * 24));

// If within the last 30 days or the future, use relative time
if (Math.abs(diffInDays) <= 30) {
Expand Down
92 changes: 73 additions & 19 deletions test/spec/LocalizationUtils-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,64 +132,118 @@ define(function (require, exports, module) {
});

describe("dateTimeFromNow", function () {
it("should return 'now' for current time", function () {
const now = new Date();
// we take a reference date for tests stability. due to use of math.trunk, using date.now will
// sometimes lead to valid time jumps in tests. so we just take a reference time fixed to not account for
// any time taken in compute for tests.
const refDate = Date.now();
function referenceDate({
seconds = 0,
minutes = 0,
hours = 0,
days = 0,
months = 0,
years = 0
} = {}) {
const date = new Date(refDate); // Original reference date

// Add time components
date.setSeconds(date.getSeconds() + seconds);
date.setMinutes(date.getMinutes() + minutes);
date.setHours(date.getHours() + hours);
date.setDate(date.getDate() + days);
date.setMonth(date.getMonth() + months);
date.setFullYear(date.getFullYear() + years);

return date;
}

it("should return 'now' for current time without specifying fromTime", function () {
let now = new Date();
let result = LocalizationUtils.dateTimeFromNow(now, "en");
expect(result).toBe("now");
result = LocalizationUtils.dateTimeFromNow(now, "de");
expect(result).toBe("jetzt");
});

it("should return 'now' for current time within 3 seconds of time", function () {
let now = referenceDate();
let result = LocalizationUtils.dateTimeFromNow(now, "en", referenceDate());
expect(result).toBe("now");
now = referenceDate({seconds: 1});
result = LocalizationUtils.dateTimeFromNow(now, "en", referenceDate());
expect(result).toBe("now");
now = referenceDate({seconds: 2});
result = LocalizationUtils.dateTimeFromNow(now, "en", referenceDate());
expect(result).toBe("now");
result = LocalizationUtils.dateTimeFromNow(now, "de", referenceDate());
expect(result).toBe("jetzt");
now = referenceDate({seconds: 1});
result = LocalizationUtils.dateTimeFromNow(now, "de", referenceDate());
expect(result).toBe("jetzt");
now = referenceDate({seconds: 2});
result = LocalizationUtils.dateTimeFromNow(now, "de", referenceDate());
expect(result).toBe("jetzt");
});

it("should handle future dates within seconds", function () {
const futureDate = new Date(Date.now() + 30 * 1000); // 30 seconds in the future
const result = LocalizationUtils.dateTimeFromNow(futureDate, "en");
const futureDate = referenceDate({seconds: 30}); // 30 seconds in the future
const result = LocalizationUtils.dateTimeFromNow(futureDate, "en", referenceDate());
expect(result).toBe("in 30 seconds");
});

it("should handle past dates within minutes", function () {
const pastDate = new Date(Date.now() - 90 * 1000); // 90 seconds in the past
const result = LocalizationUtils.dateTimeFromNow(pastDate, "en");
const pastDate = referenceDate({seconds: -90}); // 90 seconds in the past
const result = LocalizationUtils.dateTimeFromNow(pastDate, "en", referenceDate());
expect(result).toBe("1 minute ago");
});

it("should handle past and future dates similarly", function () {
const pastDate = referenceDate({seconds: -130}); // 2 minutes and 10 secs
let result = LocalizationUtils.dateTimeFromNow(pastDate, "en", referenceDate());
expect(result).toBe("2 minutes ago");
const futureDate = referenceDate({seconds: 130}); // 2 minutes and 10 secs
result = LocalizationUtils.dateTimeFromNow(futureDate, "en", referenceDate());
expect(result).toBe("in 2 minutes");
});

it("should handle future dates within hours", function () {
const futureDate = new Date(Date.now() + 2 * 60 * 60 * 1000); // 2 hours in the future
const result = LocalizationUtils.dateTimeFromNow(futureDate, "en");
const futureDate = referenceDate({hours: 2}); // 2 hours in the future
const result = LocalizationUtils.dateTimeFromNow(futureDate, "en", referenceDate());
expect(result).toBe("in 2 hours");
});

it("should handle past dates within days", function () {
const pastDate = new Date(Date.now() - 3 * 24 * 60 * 60 * 1000); // 3 days ago
const result = LocalizationUtils.dateTimeFromNow(pastDate, "en");
const pastDate = referenceDate({ days: -3 }); // 3 days ago
const result = LocalizationUtils.dateTimeFromNow(pastDate, "en", referenceDate());
expect(result).toBe("3 days ago");
});

it("should handle future dates within months", function () {
const futureDate = new Date(Date.now() + 45 * 24 * 60 * 60 * 1000); // 45 days in the future
const result = LocalizationUtils.dateTimeFromNow(futureDate, "en");
const futureDate = referenceDate({ days: 45 }); // 45 days in the future
const result = LocalizationUtils.dateTimeFromNow(futureDate, "en", referenceDate());
expect(result).toBe("next month");
});

it("should handle past dates within years", function () {
const pastDate = new Date(Date.now() - 2 * 365 * 24 * 60 * 60 * 1000); // 2 years ago
const result = LocalizationUtils.dateTimeFromNow(pastDate, "en");
const pastDate = referenceDate({ years: -2 }); // 2 years ago
const result = LocalizationUtils.dateTimeFromNow(pastDate, "en", referenceDate());
expect(result).toBe("2 years ago");
});

it("should return relative time in French locale", function () {
const pastDate = new Date(Date.now() - 3 * 24 * 60 * 60 * 1000); // 3 days ago
const result = LocalizationUtils.dateTimeFromNow(pastDate, "fr");
const pastDate = referenceDate({ days: -3 }); // 3 days ago
const result = LocalizationUtils.dateTimeFromNow(pastDate, "fr", referenceDate());
expect(result).toBe("il y a 3 jours");
});

it("should fallback to default locale if an invalid locale is specified", function () {
const futureDate = new Date(Date.now() + 2 * 60 * 60 * 1000); // 2 hours in the future
const result = LocalizationUtils.dateTimeFromNow(futureDate, "invalid-locale");
const futureDate = referenceDate({ hours: 2 }); // 2 hours in the future
const result = LocalizationUtils.dateTimeFromNow(futureDate, "invalid-locale", referenceDate());
expect(result).toBe("in 2 hours");
});

it("should handle default date input (now) gracefully", function () {
const result = LocalizationUtils.dateTimeFromNow(undefined, "en");
const result = LocalizationUtils.dateTimeFromNow();
expect(result).toBe("now");
});
});
Expand Down
Loading