refactor: localized date time
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import {RichText} from '@atproto/api'
|
import {RichText} from '@atproto/api'
|
||||||
|
import {I18n} from '@lingui/core'
|
||||||
|
|
||||||
import {parseEmbedPlayerFromUrl} from 'lib/strings/embed-player'
|
import {parseEmbedPlayerFromUrl} from 'lib/strings/embed-player'
|
||||||
import {cleanError} from '../../src/lib/strings/errors'
|
import {cleanError} from '../../src/lib/strings/errors'
|
||||||
@@ -207,8 +208,10 @@ describe('ago', () => {
|
|||||||
]
|
]
|
||||||
|
|
||||||
it('correctly calculates how much time passed, in a string', () => {
|
it('correctly calculates how much time passed, in a string', () => {
|
||||||
|
const i18n = new I18n({locale: 'en'})
|
||||||
|
|
||||||
for (let i = 0; i < inputs.length; i++) {
|
for (let i = 0; i < inputs.length; i++) {
|
||||||
const result = ago(inputs[i])
|
const result = ago(i18n, inputs[i])
|
||||||
expect(result).toEqual(outputs[i])
|
expect(result).toEqual(outputs[i])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ function EmbedDialogInner({
|
|||||||
timestamp,
|
timestamp,
|
||||||
}: Omit<EmbedDialogProps, 'control'>) {
|
}: Omit<EmbedDialogProps, 'control'>) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_, i18n} = useLingui()
|
||||||
const ref = useRef<TextInput>(null)
|
const ref = useRef<TextInput>(null)
|
||||||
const [copied, setCopied] = useState(false)
|
const [copied, setCopied] = useState(false)
|
||||||
|
|
||||||
@@ -86,9 +86,9 @@ function EmbedDialogInner({
|
|||||||
)} (<a href="${escapeHtml(profileHref)}">@${escapeHtml(
|
)} (<a href="${escapeHtml(profileHref)}">@${escapeHtml(
|
||||||
postAuthor.handle,
|
postAuthor.handle,
|
||||||
)}</a>) <a href="${escapeHtml(href)}">${escapeHtml(
|
)}</a>) <a href="${escapeHtml(href)}">${escapeHtml(
|
||||||
niceDate(timestamp),
|
niceDate(i18n, timestamp),
|
||||||
)}</a></blockquote><script async src="${EMBED_SCRIPT}" charset="utf-8"></script>`
|
)}</a></blockquote><script async src="${EMBED_SCRIPT}" charset="utf-8"></script>`
|
||||||
}, [postUri, postCid, record, timestamp, postAuthor])
|
}, [i18n, postUri, postCid, record, timestamp, postAuthor])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog.Inner label="Embed post" style={[a.gap_md, {maxWidth: 500}]}>
|
<Dialog.Inner label="Embed post" style={[a.gap_md, {maxWidth: 500}]}>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
View,
|
View,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {ChatBskyConvoDefs, RichText as RichTextAPI} from '@atproto/api'
|
import {ChatBskyConvoDefs, RichText as RichTextAPI} from '@atproto/api'
|
||||||
|
import {I18n} from '@lingui/core'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
@@ -130,7 +131,7 @@ let MessageItemMetadata = ({
|
|||||||
style: StyleProp<TextStyle>
|
style: StyleProp<TextStyle>
|
||||||
}): React.ReactNode => {
|
}): React.ReactNode => {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_, i18n} = useLingui()
|
const {_} = useLingui()
|
||||||
const {message} = item
|
const {message} = item
|
||||||
|
|
||||||
const handleRetry = useCallback(
|
const handleRetry = useCallback(
|
||||||
@@ -145,7 +146,7 @@ let MessageItemMetadata = ({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const relativeTimestamp = useCallback(
|
const relativeTimestamp = useCallback(
|
||||||
(timestamp: string) => {
|
(i18n: I18n, timestamp: string) => {
|
||||||
const date = new Date(timestamp)
|
const date = new Date(timestamp)
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
|
|
||||||
@@ -182,7 +183,7 @@ let MessageItemMetadata = ({
|
|||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[_, i18n],
|
[_],
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
+41
-17
@@ -1,10 +1,15 @@
|
|||||||
|
import {I18n} from '@lingui/core'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
|
|
||||||
|
import {MONTH_FALLBACK_LOCALES} from '#/locale/constants'
|
||||||
|
|
||||||
const NOW = 5
|
const NOW = 5
|
||||||
const MINUTE = 60
|
const MINUTE = 60
|
||||||
const HOUR = MINUTE * 60
|
const HOUR = MINUTE * 60
|
||||||
const DAY = HOUR * 24
|
const DAY = HOUR * 24
|
||||||
const MONTH_30 = DAY * 30
|
const MONTH_30 = DAY * 30
|
||||||
const MONTH = DAY * 30.41675 // This results in 365.001 days in a year, which is close enough for nearly all cases
|
const MONTH = DAY * 30.41675 // This results in 365.001 days in a year, which is close enough for nearly all cases
|
||||||
export function ago(date: number | string | Date): string {
|
export function ago(i18n: I18n, date: number | string | Date): string {
|
||||||
let ts: number
|
let ts: number
|
||||||
if (typeof date === 'string') {
|
if (typeof date === 'string') {
|
||||||
ts = Number(new Date(date))
|
ts = Number(new Date(date))
|
||||||
@@ -15,15 +20,31 @@ export function ago(date: number | string | Date): string {
|
|||||||
}
|
}
|
||||||
const diffSeconds = Math.floor((Date.now() - ts) / 1e3)
|
const diffSeconds = Math.floor((Date.now() - ts) / 1e3)
|
||||||
if (diffSeconds < NOW) {
|
if (diffSeconds < NOW) {
|
||||||
return `now`
|
return i18n._(msg`now`)
|
||||||
} else if (diffSeconds < MINUTE) {
|
} else if (diffSeconds < MINUTE) {
|
||||||
return `${diffSeconds}s`
|
return i18n.number(diffSeconds, {
|
||||||
|
style: 'unit',
|
||||||
|
unitDisplay: 'narrow',
|
||||||
|
unit: 'second',
|
||||||
|
})
|
||||||
} else if (diffSeconds < HOUR) {
|
} else if (diffSeconds < HOUR) {
|
||||||
return `${Math.floor(diffSeconds / MINUTE)}m`
|
return i18n.number(Math.floor(diffSeconds / MINUTE), {
|
||||||
|
style: 'unit',
|
||||||
|
unitDisplay: 'narrow',
|
||||||
|
unit: 'minute',
|
||||||
|
})
|
||||||
} else if (diffSeconds < DAY) {
|
} else if (diffSeconds < DAY) {
|
||||||
return `${Math.floor(diffSeconds / HOUR)}h`
|
return i18n.number(Math.floor(diffSeconds / HOUR), {
|
||||||
|
style: 'unit',
|
||||||
|
unitDisplay: 'narrow',
|
||||||
|
unit: 'hour',
|
||||||
|
})
|
||||||
} else if (diffSeconds < MONTH_30) {
|
} else if (diffSeconds < MONTH_30) {
|
||||||
return `${Math.round(diffSeconds / DAY)}d`
|
return i18n.number(Math.round(diffSeconds / DAY), {
|
||||||
|
style: 'unit',
|
||||||
|
unitDisplay: 'narrow',
|
||||||
|
unit: 'day',
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
let months = diffSeconds / MONTH
|
let months = diffSeconds / MONTH
|
||||||
if (months % 1 >= 0.9) {
|
if (months % 1 >= 0.9) {
|
||||||
@@ -33,23 +54,26 @@ export function ago(date: number | string | Date): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (months < 12) {
|
if (months < 12) {
|
||||||
return `${months}mo`
|
if (MONTH_FALLBACK_LOCALES.includes(i18n.locale)) return `${months}mo`
|
||||||
|
|
||||||
|
return i18n.number(months, {
|
||||||
|
style: 'unit',
|
||||||
|
unitDisplay: 'narrow',
|
||||||
|
unit: 'month',
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
return new Date(ts).toLocaleDateString()
|
return i18n.date(new Date(ts))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function niceDate(date: number | string | Date) {
|
export function niceDate(i18n: I18n, date: number | string | Date) {
|
||||||
const d = new Date(date)
|
const d = new Date(date)
|
||||||
return `${d.toLocaleDateString('en-us', {
|
|
||||||
year: 'numeric',
|
return i18n.date(d, {
|
||||||
month: 'short',
|
dateStyle: 'long',
|
||||||
day: 'numeric',
|
timeStyle: 'short',
|
||||||
})} at ${d.toLocaleTimeString(undefined, {
|
})
|
||||||
hour: 'numeric',
|
|
||||||
minute: '2-digit',
|
|
||||||
})}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAge(birthDate: Date): number {
|
export function getAge(birthDate: Date): number {
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
// The narrow unit display for `month` in English is `m`, which conflicts with
|
||||||
|
// the one for `minute` (also `m`), this also goes for any locale that currently
|
||||||
|
// falls back to English for narrow-unit display (missing localization?)
|
||||||
|
export const MONTH_FALLBACK_LOCALES = ['en', 'ja', 'es', 'fr', 'tr']
|
||||||
@@ -249,7 +249,7 @@ let FeedItem = ({
|
|||||||
{({timeElapsed}) => (
|
{({timeElapsed}) => (
|
||||||
<Text
|
<Text
|
||||||
style={[pal.textLight, styles.pointer]}
|
style={[pal.textLight, styles.pointer]}
|
||||||
title={niceDate(item.notification.indexedAt)}>
|
title={niceDate(i18n, item.notification.indexedAt)}>
|
||||||
{' ' + timeElapsed}
|
{' ' + timeElapsed}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -636,7 +636,7 @@ function ExpandedPostDetails({
|
|||||||
translatorUrl: string
|
translatorUrl: string
|
||||||
}) {
|
}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {_} = useLingui()
|
const {_, i18n} = useLingui()
|
||||||
const openLink = useOpenLink()
|
const openLink = useOpenLink()
|
||||||
|
|
||||||
const text = record?.text || ''
|
const text = record?.text || ''
|
||||||
@@ -654,7 +654,7 @@ function ExpandedPostDetails({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[s.flexRow, s.mt2, s.mb10]}>
|
<View style={[s.flexRow, s.mt2, s.mb10]}>
|
||||||
<Text style={pal.textLight}>{niceDate(post.indexedAt)}</Text>
|
<Text style={pal.textLight}>{niceDate(i18n, post.indexedAt)}</Text>
|
||||||
{needsTranslation && (
|
{needsTranslation && (
|
||||||
<>
|
<>
|
||||||
<Text style={pal.textLight}> · </Text>
|
<Text style={pal.textLight}> · </Text>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, {memo, useCallback} from 'react'
|
import React, {memo, useCallback} from 'react'
|
||||||
import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native'
|
import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native'
|
||||||
import {AppBskyActorDefs, ModerationDecision, ModerationUI} from '@atproto/api'
|
import {AppBskyActorDefs, ModerationDecision, ModerationUI} from '@atproto/api'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
import {useQueryClient} from '@tanstack/react-query'
|
import {useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {precacheProfile, usePrefetchProfileQuery} from '#/state/queries/profile'
|
import {precacheProfile, usePrefetchProfileQuery} from '#/state/queries/profile'
|
||||||
@@ -33,6 +34,8 @@ interface PostMetaOpts {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
|
let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
|
||||||
|
const {i18n} = useLingui()
|
||||||
|
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const displayName = opts.author.displayName || opts.author.handle
|
const displayName = opts.author.displayName || opts.author.handle
|
||||||
const handle = opts.author.handle
|
const handle = opts.author.handle
|
||||||
@@ -114,8 +117,8 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
|
|||||||
style={pal.textLight}
|
style={pal.textLight}
|
||||||
lineHeight={1.2}
|
lineHeight={1.2}
|
||||||
text={timeElapsed}
|
text={timeElapsed}
|
||||||
accessibilityLabel={niceDate(opts.timestamp)}
|
accessibilityLabel={niceDate(i18n, opts.timestamp)}
|
||||||
title={niceDate(opts.timestamp)}
|
title={niceDate(i18n, opts.timestamp)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
href={opts.postHref}
|
href={opts.postHref}
|
||||||
onBeforePress={onBeforePressPost}
|
onBeforePress={onBeforePressPost}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import {I18n} from '@lingui/core'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {useTickEveryMinute} from '#/state/shell'
|
import {useTickEveryMinute} from '#/state/shell'
|
||||||
import {ago} from 'lib/strings/time'
|
import {ago} from 'lib/strings/time'
|
||||||
@@ -10,17 +12,19 @@ export function TimeElapsed({
|
|||||||
}: {
|
}: {
|
||||||
timestamp: string
|
timestamp: string
|
||||||
children: ({timeElapsed}: {timeElapsed: string}) => JSX.Element
|
children: ({timeElapsed}: {timeElapsed: string}) => JSX.Element
|
||||||
timeToString?: (timeElapsed: string) => string
|
timeToString?: (i18n: I18n, timeElapsed: string) => string
|
||||||
}) {
|
}) {
|
||||||
|
const {i18n} = useLingui()
|
||||||
|
|
||||||
const tick = useTickEveryMinute()
|
const tick = useTickEveryMinute()
|
||||||
const [timeElapsed, setTimeAgo] = React.useState(() =>
|
const [timeElapsed, setTimeAgo] = React.useState(() =>
|
||||||
timeToString(timestamp),
|
timeToString(i18n, timestamp),
|
||||||
)
|
)
|
||||||
|
|
||||||
const [prevTick, setPrevTick] = React.useState(tick)
|
const [prevTick, setPrevTick] = React.useState(tick)
|
||||||
if (prevTick !== tick) {
|
if (prevTick !== tick) {
|
||||||
setPrevTick(tick)
|
setPrevTick(tick)
|
||||||
setTimeAgo(timeToString(timestamp))
|
setTimeAgo(timeToString(i18n, timestamp))
|
||||||
}
|
}
|
||||||
|
|
||||||
return children({timeElapsed})
|
return children({timeElapsed})
|
||||||
|
|||||||
+13
-12
@@ -1,25 +1,26 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {StyleSheet, TouchableOpacity, View} from 'react-native'
|
import {StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||||
import {useFocusEffect} from '@react-navigation/native'
|
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
|
|
||||||
import {ScrollView} from '../com/util/Views'
|
|
||||||
import {s} from 'lib/styles'
|
|
||||||
import {ViewHeader} from '../com/util/ViewHeader'
|
|
||||||
import {Text} from '../com/util/text/Text'
|
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
|
||||||
import {getEntries} from '#/logger/logDump'
|
|
||||||
import {ago} from 'lib/strings/time'
|
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {useFocusEffect} from '@react-navigation/native'
|
||||||
|
|
||||||
|
import {getEntries} from '#/logger/logDump'
|
||||||
import {useSetMinimalShellMode} from '#/state/shell'
|
import {useSetMinimalShellMode} from '#/state/shell'
|
||||||
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
|
||||||
|
import {ago} from 'lib/strings/time'
|
||||||
|
import {s} from 'lib/styles'
|
||||||
|
import {Text} from '../com/util/text/Text'
|
||||||
|
import {ViewHeader} from '../com/util/ViewHeader'
|
||||||
|
import {ScrollView} from '../com/util/Views'
|
||||||
|
|
||||||
export function LogScreen({}: NativeStackScreenProps<
|
export function LogScreen({}: NativeStackScreenProps<
|
||||||
CommonNavigatorParams,
|
CommonNavigatorParams,
|
||||||
'Log'
|
'Log'
|
||||||
>) {
|
>) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {_} = useLingui()
|
const {_, i18n} = useLingui()
|
||||||
const setMinimalShellMode = useSetMinimalShellMode()
|
const setMinimalShellMode = useSetMinimalShellMode()
|
||||||
const [expanded, setExpanded] = React.useState<string[]>([])
|
const [expanded, setExpanded] = React.useState<string[]>([])
|
||||||
|
|
||||||
@@ -70,7 +71,7 @@ export function LogScreen({}: NativeStackScreenProps<
|
|||||||
/>
|
/>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
<Text type="sm" style={[styles.ts, pal.textLight]}>
|
<Text type="sm" style={[styles.ts, pal.textLight]}>
|
||||||
{ago(entry.timestamp)}
|
{ago(i18n, entry.timestamp)}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
{expanded.includes(entry.id) ? (
|
{expanded.includes(entry.id) ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user