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
140 changes: 113 additions & 27 deletions src/relative-time.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* @flow strict */

import {strftime, makeFormatter, isDayFirst, isThisYear, isYearSeparator} from './utils'
import {strftime, makeFormatter, makeRelativeFormatter, isDayFirst, isThisYear, isYearSeparator} from './utils'

export default class RelativeTime {
date: Date
Expand Down Expand Up @@ -62,31 +62,29 @@ export default class RelativeTime {
const month = Math.round(day / 30)
const year = Math.round(month / 12)
if (ms < 0) {
return 'just now'
return formatRelativeTime(0, 'second')
} else if (sec < 10) {
return 'just now'
return formatRelativeTime(0, 'second')
} else if (sec < 45) {
return `${sec} seconds ago`
return formatRelativeTime(-sec, 'second')
} else if (sec < 90) {
return 'a minute ago'
return formatRelativeTime(-min, 'minute')
} else if (min < 45) {
return `${min} minutes ago`
return formatRelativeTime(-min, 'minute')
} else if (min < 90) {
return 'an hour ago'
return formatRelativeTime(-hr, 'hour')
} else if (hr < 24) {
return `${hr} hours ago`
return formatRelativeTime(-hr, 'hour')
} else if (hr < 36) {
return 'a day ago'
return formatRelativeTime(-day, 'day')
} else if (day < 30) {
return `${day} days ago`
return formatRelativeTime(-day, 'day')
} else if (day < 45) {
return 'a month ago'
} else if (month < 12) {
return `${month} months ago`
return formatRelativeTime(-month, 'month')
} else if (month < 18) {
return 'a year ago'
return formatRelativeTime(-year, 'year')
} else {
return `${year} years ago`
return formatRelativeTime(-year, 'year')
}
}

Expand Down Expand Up @@ -124,29 +122,29 @@ export default class RelativeTime {
const month = Math.round(day / 30)
const year = Math.round(month / 12)
if (month >= 18) {
return `${year} years from now`
return formatRelativeTime(year, 'year')
} else if (month >= 12) {
return 'a year from now'
return formatRelativeTime(year, 'year')
} else if (day >= 45) {
return `${month} months from now`
return formatRelativeTime(month, 'month')
} else if (day >= 30) {
return 'a month from now'
return formatRelativeTime(month, 'month')
} else if (hr >= 36) {
return `${day} days from now`
return formatRelativeTime(day, 'day')
} else if (hr >= 24) {
return 'a day from now'
return formatRelativeTime(day, 'day')
} else if (min >= 90) {
return `${hr} hours from now`
return formatRelativeTime(hr, 'hour')
} else if (min >= 45) {
return 'an hour from now'
return formatRelativeTime(hr, 'hour')
} else if (sec >= 90) {
return `${min} minutes from now`
return formatRelativeTime(min, 'minute')
} else if (sec >= 45) {
return 'a minute from now'
return formatRelativeTime(min, 'minute')
} else if (sec >= 10) {
return `${sec} seconds from now`
return formatRelativeTime(sec, 'second')
} else {
return 'just now'
return formatRelativeTime(0, 'second')
}
}

Expand Down Expand Up @@ -189,4 +187,92 @@ export default class RelativeTime {
}
}

function formatRelativeTime(value: number, unit: string): string {
const formatter = relativeFormatter()
if (formatter) {
return formatter.format(value, unit)
} else {
return formatEnRelativeTime(value, unit)
}
}

// Simplified "en" RelativeTimeFormat.format function
//
// Values should roughly match
// new Intl.RelativeTimeFormat('en', {numeric: 'auto'}).format(value, unit)
//
function formatEnRelativeTime(value: number, unit: string): string {
if (value === 0) {
switch (unit) {
case 'year':
case 'quarter':
case 'month':
case 'week':
return `this ${unit}`
case 'day':
return 'today'
case 'hour':
case 'minute':
return `in 0 ${unit}s`
case 'second':
return 'now'
}
} else if (value === 1) {
switch (unit) {
case 'year':
case 'quarter':
case 'month':
case 'week':
return `next ${unit}`
case 'day':
return 'tomorrow'
case 'hour':
case 'minute':
case 'second':
return `in 1 ${unit}`
}
} else if (value === -1) {
switch (unit) {
case 'year':
case 'quarter':
case 'month':
case 'week':
return `last ${unit}`
case 'day':
return 'yesterday'
case 'hour':
case 'minute':
case 'second':
return `1 ${unit} ago`
}
} else if (value > 1) {
switch (unit) {
case 'year':
case 'quarter':
case 'month':
case 'week':
case 'day':
case 'hour':
case 'minute':
case 'second':
return `in ${value} ${unit}s`
}
} else if (value < -1) {
switch (unit) {
case 'year':
case 'quarter':
case 'month':
case 'week':
case 'day':
case 'hour':
case 'minute':
case 'second':
return `${-value} ${unit}s ago`
}
}

throw new RangeError(`Invalid unit argument for format() '${unit}'`)
}

const timeFormatter = makeFormatter({hour: 'numeric', minute: '2-digit'})
const relativeFormatter = makeRelativeFormatter({numeric: 'auto'})
20 changes: 20 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,23 @@ export function isThisYear(date: Date) {
const now = new Date()
return now.getUTCFullYear() === date.getUTCFullYear()
}

// eslint-disable-next-line flowtype/no-weak-types
export function makeRelativeFormatter(options: any): () => ?any {
let format
return function() {
if (format) return format
if ('Intl' in window && 'RelativeTimeFormat' in window.Intl) {
try {
// eslint-disable-next-line flowtype/no-flow-fix-me-comments
// $FlowFixMe: missing RelativeTimeFormat type
format = new Intl.RelativeTimeFormat(undefined, options)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we want to pass en as the locale until we add a locale attribute to the custom element so it's configurable by the host app. That will prevent this text from appearing in the default locale for the browser with the rest of the text appearing in English.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'm down to support that, though I was just stealing from our existing makeFormatter.

https://github.com/github/time-elements/blob/4e6f95a34fc897626e794ed78f0f45701043de67/src/utils.js#L102

Will that need to change as well? Should I do this in a separate PR? Starting to wonder how this change is going to ship. It might be a major version bump. So could bundle this and that locale change in one release.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if the runtime's default locale is hardcoded to en in browsers and this is different from the user's preferred language setting in the operating system. Anyway, since undefined is already working for DateTimeFormat, we can use it here too.

A major version release would be appropriate since this changes the text output slightly.

We could couple that with <relative-time locale="…"> if it's not too much work. The only weird thing is that the locale attribute would only apply to browsers with RelativeTimeFormat support. The older code path would still use the hardcoded English text.

return format
} catch (e) {
if (!(e instanceof RangeError)) {
throw e
}
}
}
}
}
40 changes: 20 additions & 20 deletions test/relative-time.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ suite('relative-time', function() {
const now = new Date(Date.now() + 3 * 60 * 60 * 24 * 1000).toISOString()
const time = document.createElement('relative-time')
time.setAttribute('datetime', now)
assert.equal(time.textContent, '3 days from now')
assert.equal(time.textContent, 'in 3 days')
})

test('rewrites from now past datetime to a day ago', function() {
test('rewrites from now past datetime to yesterday', function() {
const now = new Date(Date.now() - 1 * 60 * 60 * 24 * 1000).toISOString()
const time = document.createElement('relative-time')
time.setAttribute('datetime', now)
assert.equal(time.textContent, 'a day ago')
assert.equal(time.textContent, 'yesterday')
})

test('rewrites from now past datetime to hours ago', function() {
Expand All @@ -31,14 +31,14 @@ suite('relative-time', function() {
const now = new Date(Date.now() + 3 * 60 * 60 * 1000).toISOString()
const time = document.createElement('relative-time')
time.setAttribute('datetime', now)
assert.equal(time.textContent, '3 hours from now')
assert.equal(time.textContent, 'in 3 hours')
})

test('rewrites from now past datetime to an hour ago', function() {
const now = new Date(Date.now() - 1 * 60 * 60 * 1000).toISOString()
const time = document.createElement('relative-time')
time.setAttribute('datetime', now)
assert.equal(time.textContent, 'an hour ago')
assert.equal(time.textContent, '1 hour ago')
})

test('rewrites from now past datetime to minutes ago', function() {
Expand All @@ -52,49 +52,49 @@ suite('relative-time', function() {
const now = new Date(Date.now() + 3 * 60 * 1000).toISOString()
const time = document.createElement('relative-time')
time.setAttribute('datetime', now)
assert.equal(time.textContent, '3 minutes from now')
assert.equal(time.textContent, 'in 3 minutes')
})

test('rewrites from now past datetime to a minute ago', function() {
const now = new Date(Date.now() - 1 * 60 * 1000).toISOString()
const time = document.createElement('relative-time')
time.setAttribute('datetime', now)
assert.equal(time.textContent, 'a minute ago')
assert.equal(time.textContent, '1 minute ago')
})

test('rewrites a few seconds ago to just now', function() {
test('rewrites a few seconds ago to now', function() {
const now = new Date().toISOString()
const time = document.createElement('relative-time')
time.setAttribute('datetime', now)
assert.equal(time.textContent, 'just now')
assert.equal(time.textContent, 'now')
})

test('rewrites a few seconds from now to just now', function() {
test('rewrites a few seconds from now to now', function() {
const now = new Date().toISOString()
const time = document.createElement('relative-time')
time.setAttribute('datetime', now)
assert.equal(time.textContent, 'just now')
assert.equal(time.textContent, 'now')
})

test('displays future times as just now', function() {
test('displays future times as now', function() {
const now = new Date(Date.now() + 3 * 1000).toISOString()
const time = document.createElement('relative-time')
time.setAttribute('datetime', now)
assert.equal(time.textContent, 'just now')
assert.equal(time.textContent, 'now')
})

test('displays a day ago', function() {
test('displays yesterday', function() {
const now = new Date(Date.now() - 60 * 60 * 24 * 1000).toISOString()
const time = document.createElement('relative-time')
time.setAttribute('datetime', now)
assert.equal(time.textContent, 'a day ago')
assert.equal(time.textContent, 'yesterday')
})

test('displays a day from now', function() {
const now = new Date(Date.now() + 60 * 60 * 24 * 1000).toISOString()
const time = document.createElement('relative-time')
time.setAttribute('datetime', now)
assert.equal(time.textContent, 'a day from now')
assert.equal(time.textContent, 'tomorrow')
})

test('displays 2 days ago', function() {
Expand All @@ -108,7 +108,7 @@ suite('relative-time', function() {
const now = new Date(Date.now() + 2 * 60 * 60 * 24 * 1000).toISOString()
const time = document.createElement('relative-time')
time.setAttribute('datetime', now)
assert.equal(time.textContent, '2 days from now')
assert.equal(time.textContent, 'in 2 days')
})

test('switches to dates after 30 past days', function() {
Expand Down Expand Up @@ -144,10 +144,10 @@ suite('relative-time', function() {
const now = new Date().toISOString()

time.setAttribute('datetime', now)
assert.equal(time.textContent, 'just now')
assert.equal(time.textContent, 'now')

time.removeAttribute('datetime')
assert.equal(time.textContent, 'just now')
assert.equal(time.textContent, 'now')
})

test('sets relative contents when parsed element is upgraded', function() {
Expand All @@ -157,6 +157,6 @@ suite('relative-time', function() {
if ('CustomElements' in window) {
window.CustomElements.upgradeSubtree(root)
}
assert.equal(root.children[0].textContent, 'just now')
assert.equal(root.children[0].textContent, 'now')
})
})
10 changes: 5 additions & 5 deletions test/time-ago.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,18 @@ suite('time-ago', function() {
assert.equal(time.textContent, '3 minutes ago')
})

test('rewrites a few seconds ago to just now', function() {
test('rewrites a few seconds ago to now', function() {
const now = new Date().toISOString()
const time = document.createElement('time-ago')
time.setAttribute('datetime', now)
assert.equal(time.textContent, 'just now')
assert.equal(time.textContent, 'now')
})

test('displays future times as just now', function() {
test('displays future times as now', function() {
const now = new Date(Date.now() + 3 * 1000).toISOString()
const time = document.createElement('time-ago')
time.setAttribute('datetime', now)
assert.equal(time.textContent, 'just now')
assert.equal(time.textContent, 'now')
})

test('sets relative contents when parsed element is upgraded', function() {
Expand All @@ -34,7 +34,7 @@ suite('time-ago', function() {
if ('CustomElements' in window) {
window.CustomElements.upgradeSubtree(root)
}
assert.equal(root.children[0].textContent, 'just now')
assert.equal(root.children[0].textContent, 'now')
})

test('micro formats years', function() {
Expand Down
Loading