refactor: consistent localized formatting
This commit is contained in:
@@ -358,7 +358,7 @@ function Inner({
|
|||||||
hide: () => void
|
hide: () => void
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_, i18n} = useLingui()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const moderation = React.useMemo(
|
const moderation = React.useMemo(
|
||||||
() => moderateProfile(profile, moderationOpts),
|
() => moderateProfile(profile, moderationOpts),
|
||||||
@@ -371,8 +371,8 @@ function Inner({
|
|||||||
logContext: 'ProfileHoverCard',
|
logContext: 'ProfileHoverCard',
|
||||||
})
|
})
|
||||||
const blockHide = profile.viewer?.blocking || profile.viewer?.blockedBy
|
const blockHide = profile.viewer?.blocking || profile.viewer?.blockedBy
|
||||||
const following = formatCount(profile.followsCount || 0)
|
const following = formatCount(i18n, profile.followsCount || 0)
|
||||||
const followers = formatCount(profile.followersCount || 0)
|
const followers = formatCount(i18n, profile.followersCount || 0)
|
||||||
const pluralizedFollowers = plural(profile.followersCount || 0, {
|
const pluralizedFollowers = plural(profile.followersCount || 0, {
|
||||||
one: 'follower',
|
one: 'follower',
|
||||||
other: 'followers',
|
other: 'followers',
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ let MessageItemMetadata = ({
|
|||||||
style: StyleProp<TextStyle>
|
style: StyleProp<TextStyle>
|
||||||
}): React.ReactNode => {
|
}): React.ReactNode => {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_, i18n} = useLingui()
|
||||||
const {message} = item
|
const {message} = item
|
||||||
|
|
||||||
const handleRetry = useCallback(
|
const handleRetry = useCallback(
|
||||||
@@ -149,10 +149,10 @@ let MessageItemMetadata = ({
|
|||||||
const date = new Date(timestamp)
|
const date = new Date(timestamp)
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
|
|
||||||
const time = new Intl.DateTimeFormat(undefined, {
|
const time = i18n.date(date, {
|
||||||
hour: 'numeric',
|
hour: 'numeric',
|
||||||
minute: 'numeric',
|
minute: 'numeric',
|
||||||
}).format(date)
|
})
|
||||||
|
|
||||||
const diff = now.getTime() - date.getTime()
|
const diff = now.getTime() - date.getTime()
|
||||||
|
|
||||||
@@ -174,15 +174,15 @@ let MessageItemMetadata = ({
|
|||||||
return _(msg`Yesterday, ${time}`)
|
return _(msg`Yesterday, ${time}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Intl.DateTimeFormat(undefined, {
|
return i18n.date(date, {
|
||||||
hour: 'numeric',
|
hour: 'numeric',
|
||||||
minute: 'numeric',
|
minute: 'numeric',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
month: 'numeric',
|
month: 'numeric',
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
}).format(date)
|
})
|
||||||
},
|
},
|
||||||
[_],
|
[_, i18n],
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {Pressable, View} from 'react-native'
|
import {Pressable, View} from 'react-native'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {android, atoms as a, useTheme, web} from '#/alf'
|
import {android, atoms as a, useTheme, web} from '#/alf'
|
||||||
import * as TextField from '#/components/forms/TextField'
|
import * as TextField from '#/components/forms/TextField'
|
||||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||||
import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays'
|
import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {localizeDate} from './utils'
|
|
||||||
|
|
||||||
// looks like a TextField.Input, but is just a button. It'll do something different on each platform on press
|
// looks like a TextField.Input, but is just a button. It'll do something different on each platform on press
|
||||||
// iOS: open a dialog with an inline date picker
|
// iOS: open a dialog with an inline date picker
|
||||||
@@ -25,6 +25,7 @@ export function DateFieldButton({
|
|||||||
isInvalid?: boolean
|
isInvalid?: boolean
|
||||||
accessibilityHint?: string
|
accessibilityHint?: string
|
||||||
}) {
|
}) {
|
||||||
|
const {i18n} = useLingui()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -91,7 +92,7 @@ export function DateFieldButton({
|
|||||||
t.atoms.text,
|
t.atoms.text,
|
||||||
{lineHeight: a.text_md.fontSize * 1.1875},
|
{lineHeight: a.text_md.fontSize * 1.1875},
|
||||||
]}>
|
]}>
|
||||||
{localizeDate(value)}
|
{i18n.date(value, {timeZone: 'UTC'})}
|
||||||
</Text>
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,16 +1,5 @@
|
|||||||
import {getLocales} from 'expo-localization'
|
|
||||||
|
|
||||||
const LOCALE = getLocales()[0]
|
|
||||||
|
|
||||||
// we need the date in the form yyyy-MM-dd to pass to the input
|
// we need the date in the form yyyy-MM-dd to pass to the input
|
||||||
export function toSimpleDateString(date: Date | string): string {
|
export function toSimpleDateString(date: Date | string): string {
|
||||||
const _date = typeof date === 'string' ? new Date(date) : date
|
const _date = typeof date === 'string' ? new Date(date) : date
|
||||||
return _date.toISOString().split('T')[0]
|
return _date.toISOString().split('T')[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function localizeDate(date: Date | string): string {
|
|
||||||
const _date = typeof date === 'string' ? new Date(date) : date
|
|
||||||
return new Intl.DateTimeFormat(LOCALE.languageTag, {
|
|
||||||
timeZone: 'UTC',
|
|
||||||
}).format(_date)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ export function ProfileHeaderMetrics({
|
|||||||
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>
|
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_, i18n} = useLingui()
|
||||||
const following = formatCount(profile.followsCount || 0)
|
const following = formatCount(i18n, profile.followsCount || 0)
|
||||||
const followers = formatCount(profile.followersCount || 0)
|
const followers = formatCount(i18n, profile.followersCount || 0)
|
||||||
const pluralizedFollowers = plural(profile.followersCount || 0, {
|
const pluralizedFollowers = plural(profile.followersCount || 0, {
|
||||||
one: 'follower',
|
one: 'follower',
|
||||||
other: 'followers',
|
other: 'followers',
|
||||||
@@ -54,7 +54,7 @@ export function ProfileHeaderMetrics({
|
|||||||
</Text>
|
</Text>
|
||||||
</InlineLinkText>
|
</InlineLinkText>
|
||||||
<Text style={[a.font_bold, t.atoms.text, a.text_md]}>
|
<Text style={[a.font_bold, t.atoms.text, a.text_md]}>
|
||||||
{formatCount(profile.postsCount || 0)}{' '}
|
{formatCount(i18n, profile.postsCount || 0)}{' '}
|
||||||
<Text style={[t.atoms.text_contrast_medium, a.font_normal, a.text_md]}>
|
<Text style={[t.atoms.text_contrast_medium, a.font_normal, a.text_md]}>
|
||||||
{plural(profile.postsCount || 0, {one: 'post', other: 'posts'})}
|
{plural(profile.postsCount || 0, {one: 'post', other: 'posts'})}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ let FeedItem = ({
|
|||||||
}): React.ReactNode => {
|
}): React.ReactNode => {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {_} = useLingui()
|
const {_, i18n} = useLingui()
|
||||||
const [isAuthorsExpanded, setAuthorsExpanded] = useState<boolean>(false)
|
const [isAuthorsExpanded, setAuthorsExpanded] = useState<boolean>(false)
|
||||||
const itemHref = useMemo(() => {
|
const itemHref = useMemo(() => {
|
||||||
if (item.type === 'post-like' || item.type === 'repost') {
|
if (item.type === 'post-like' || item.type === 'repost') {
|
||||||
@@ -175,7 +175,8 @@ let FeedItem = ({
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
let formattedCount = authors.length > 1 ? formatCount(authors.length - 1) : ''
|
let formattedCount =
|
||||||
|
authors.length > 1 ? formatCount(i18n, authors.length - 1) : ''
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
testID={`feedItem-by-${item.notification.author.handle}`}
|
testID={`feedItem-by-${item.notification.author.handle}`}
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ let PostThreadItemLoaded = ({
|
|||||||
onPostReply: () => void
|
onPostReply: () => void
|
||||||
}): React.ReactNode => {
|
}): React.ReactNode => {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {_} = useLingui()
|
const {_, i18n} = useLingui()
|
||||||
const langPrefs = useLanguagePrefs()
|
const langPrefs = useLanguagePrefs()
|
||||||
const {openComposer} = useComposerControls()
|
const {openComposer} = useComposerControls()
|
||||||
const [limitLines, setLimitLines] = React.useState(
|
const [limitLines, setLimitLines] = React.useState(
|
||||||
@@ -339,7 +339,7 @@ let PostThreadItemLoaded = ({
|
|||||||
type="lg"
|
type="lg"
|
||||||
style={pal.textLight}>
|
style={pal.textLight}>
|
||||||
<Text type="xl-bold" style={pal.text}>
|
<Text type="xl-bold" style={pal.text}>
|
||||||
{formatCount(post.repostCount)}
|
{formatCount(i18n, post.repostCount)}
|
||||||
</Text>{' '}
|
</Text>{' '}
|
||||||
<Plural
|
<Plural
|
||||||
value={post.repostCount}
|
value={post.repostCount}
|
||||||
@@ -359,7 +359,7 @@ let PostThreadItemLoaded = ({
|
|||||||
type="lg"
|
type="lg"
|
||||||
style={pal.textLight}>
|
style={pal.textLight}>
|
||||||
<Text type="xl-bold" style={pal.text}>
|
<Text type="xl-bold" style={pal.text}>
|
||||||
{formatCount(post.likeCount)}
|
{formatCount(i18n, post.likeCount)}
|
||||||
</Text>{' '}
|
</Text>{' '}
|
||||||
<Plural value={post.likeCount} one="like" other="likes" />
|
<Plural value={post.likeCount} one="like" other="likes" />
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
import React, {useState, useCallback} from 'react'
|
import React, {useCallback, useState} from 'react'
|
||||||
import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native'
|
import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native'
|
||||||
|
import DatePicker from 'react-native-date-picker'
|
||||||
import {
|
import {
|
||||||
FontAwesomeIcon,
|
FontAwesomeIcon,
|
||||||
FontAwesomeIconStyle,
|
FontAwesomeIconStyle,
|
||||||
} from '@fortawesome/react-native-fontawesome'
|
} from '@fortawesome/react-native-fontawesome'
|
||||||
import {isIOS, isAndroid} from 'platform/detection'
|
import {useLingui} from '@lingui/react'
|
||||||
import {Button, ButtonType} from './Button'
|
|
||||||
import {Text} from '../text/Text'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {TypographyVariant} from 'lib/ThemeContext'
|
import {TypographyVariant} from 'lib/ThemeContext'
|
||||||
import {useTheme} from 'lib/ThemeContext'
|
import {useTheme} from 'lib/ThemeContext'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {isAndroid, isIOS} from 'platform/detection'
|
||||||
import {getLocales} from 'expo-localization'
|
import {Text} from '../text/Text'
|
||||||
import DatePicker from 'react-native-date-picker'
|
import {Button, ButtonType} from './Button'
|
||||||
|
|
||||||
const LOCALE = getLocales()[0]
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
testID?: string
|
testID?: string
|
||||||
@@ -30,16 +29,11 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function DateInput(props: Props) {
|
export function DateInput(props: Props) {
|
||||||
|
const {i18n} = useLingui()
|
||||||
const [show, setShow] = useState(false)
|
const [show, setShow] = useState(false)
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
|
||||||
const formatter = React.useMemo(() => {
|
|
||||||
return new Intl.DateTimeFormat(LOCALE.languageTag, {
|
|
||||||
timeZone: props.handleAsUTC ? 'UTC' : undefined,
|
|
||||||
})
|
|
||||||
}, [props.handleAsUTC])
|
|
||||||
|
|
||||||
const onChangeInternal = useCallback(
|
const onChangeInternal = useCallback(
|
||||||
(date: Date) => {
|
(date: Date) => {
|
||||||
setShow(false)
|
setShow(false)
|
||||||
@@ -74,7 +68,9 @@ export function DateInput(props: Props) {
|
|||||||
<Text
|
<Text
|
||||||
type={props.buttonLabelType}
|
type={props.buttonLabelType}
|
||||||
style={[pal.text, props.buttonLabelStyle]}>
|
style={[pal.text, props.buttonLabelStyle]}>
|
||||||
{formatter.format(props.value)}
|
{i18n.date(props.value, {
|
||||||
|
timeZone: props.handleAsUTC ? 'UTC' : undefined,
|
||||||
|
})}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,19 +1,12 @@
|
|||||||
export const formatCount = (num: number) =>
|
import type {I18n} from '@lingui/core'
|
||||||
Intl.NumberFormat('en-US', {
|
|
||||||
|
export const formatCount = (i18n: I18n, num: number) => {
|
||||||
|
return i18n.number(num, {
|
||||||
notation: 'compact',
|
notation: 'compact',
|
||||||
maximumFractionDigits: 1,
|
maximumFractionDigits: 1,
|
||||||
// `1,953` shouldn't be rounded up to 2k, it should be truncated.
|
// `1,953` shouldn't be rounded up to 2k, it should be truncated.
|
||||||
// @ts-expect-error: `roundingMode` doesn't seem to be in the typings yet
|
// @ts-expect-error: `roundingMode` doesn't seem to be in the typings yet
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#roundingmode
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#roundingmode
|
||||||
roundingMode: 'trunc',
|
roundingMode: 'trunc',
|
||||||
}).format(num)
|
})
|
||||||
|
|
||||||
export function formatCountShortOnly(num: number): string {
|
|
||||||
if (num >= 1000000) {
|
|
||||||
return (num / 1000000).toFixed(1) + 'M'
|
|
||||||
}
|
|
||||||
if (num >= 1000) {
|
|
||||||
return (num / 1000).toFixed(1) + 'K'
|
|
||||||
}
|
|
||||||
return String(num)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
|||||||
import {CommonNavigatorParams} from '#/lib/routes/types'
|
import {CommonNavigatorParams} from '#/lib/routes/types'
|
||||||
import {cleanError} from '#/lib/strings/errors'
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
import {useLanguagePrefs} from '#/state/preferences'
|
|
||||||
import {
|
import {
|
||||||
useAppPasswordDeleteMutation,
|
useAppPasswordDeleteMutation,
|
||||||
useAppPasswordsQuery,
|
useAppPasswordsQuery,
|
||||||
@@ -218,9 +217,8 @@ function AppPassword({
|
|||||||
privileged?: boolean
|
privileged?: boolean
|
||||||
}) {
|
}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {_} = useLingui()
|
const {_, i18n} = useLingui()
|
||||||
const control = useDialogControl()
|
const control = useDialogControl()
|
||||||
const {contentLanguages} = useLanguagePrefs()
|
|
||||||
const deleteMutation = useAppPasswordDeleteMutation()
|
const deleteMutation = useAppPasswordDeleteMutation()
|
||||||
|
|
||||||
const onDelete = React.useCallback(async () => {
|
const onDelete = React.useCallback(async () => {
|
||||||
@@ -232,9 +230,6 @@ function AppPassword({
|
|||||||
control.open()
|
control.open()
|
||||||
}, [control])
|
}, [control])
|
||||||
|
|
||||||
const primaryLocale =
|
|
||||||
contentLanguages.length > 0 ? contentLanguages[0] : 'en-US'
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID={testID}
|
testID={testID}
|
||||||
@@ -250,14 +245,14 @@ function AppPassword({
|
|||||||
<Text type="md" style={[pal.text, styles.pr10]} numberOfLines={1}>
|
<Text type="md" style={[pal.text, styles.pr10]} numberOfLines={1}>
|
||||||
<Trans>
|
<Trans>
|
||||||
Created{' '}
|
Created{' '}
|
||||||
{Intl.DateTimeFormat(primaryLocale, {
|
{i18n.date(createdAt, {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: 'numeric',
|
month: 'numeric',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
second: '2-digit',
|
second: '2-digit',
|
||||||
}).format(new Date(createdAt))}
|
})}
|
||||||
</Trans>
|
</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
{privileged && (
|
{privileged && (
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import {colors, s} from 'lib/styles'
|
|||||||
import {useTheme} from 'lib/ThemeContext'
|
import {useTheme} from 'lib/ThemeContext'
|
||||||
import {isWeb} from 'platform/detection'
|
import {isWeb} from 'platform/detection'
|
||||||
import {NavSignupCard} from '#/view/shell/NavSignupCard'
|
import {NavSignupCard} from '#/view/shell/NavSignupCard'
|
||||||
import {formatCountShortOnly} from 'view/com/util/numeric/format'
|
import {formatCount} from 'view/com/util/numeric/format'
|
||||||
import {Text} from 'view/com/util/text/Text'
|
import {Text} from 'view/com/util/text/Text'
|
||||||
import {UserAvatar} from 'view/com/util/UserAvatar'
|
import {UserAvatar} from 'view/com/util/UserAvatar'
|
||||||
import {useTheme as useAlfTheme} from '#/alf'
|
import {useTheme as useAlfTheme} from '#/alf'
|
||||||
@@ -68,7 +68,7 @@ let DrawerProfileCard = ({
|
|||||||
account: SessionAccount
|
account: SessionAccount
|
||||||
onPressProfile: () => void
|
onPressProfile: () => void
|
||||||
}): React.ReactNode => {
|
}): React.ReactNode => {
|
||||||
const {_} = useLingui()
|
const {_, i18n} = useLingui()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {data: profile} = useProfileQuery({did: account.did})
|
const {data: profile} = useProfileQuery({did: account.did})
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@ let DrawerProfileCard = ({
|
|||||||
<Text type="xl" style={[pal.textLight, styles.profileCardFollowers]}>
|
<Text type="xl" style={[pal.textLight, styles.profileCardFollowers]}>
|
||||||
<Trans>
|
<Trans>
|
||||||
<Text type="xl-medium" style={pal.text}>
|
<Text type="xl-medium" style={pal.text}>
|
||||||
{formatCountShortOnly(profile?.followersCount ?? 0)}
|
{formatCount(i18n, profile?.followersCount ?? 0)}
|
||||||
</Text>{' '}
|
</Text>{' '}
|
||||||
<Plural
|
<Plural
|
||||||
value={profile?.followersCount || 0}
|
value={profile?.followersCount || 0}
|
||||||
@@ -111,7 +111,7 @@ let DrawerProfileCard = ({
|
|||||||
·{' '}
|
·{' '}
|
||||||
<Trans>
|
<Trans>
|
||||||
<Text type="xl-medium" style={pal.text}>
|
<Text type="xl-medium" style={pal.text}>
|
||||||
{formatCountShortOnly(profile?.followsCount ?? 0)}
|
{formatCount(i18n, profile?.followsCount ?? 0)}
|
||||||
</Text>{' '}
|
</Text>{' '}
|
||||||
<Plural
|
<Plural
|
||||||
value={profile?.followsCount || 0}
|
value={profile?.followsCount || 0}
|
||||||
|
|||||||
Reference in New Issue
Block a user