refactor: consistent localized formatting

This commit is contained in:
Mary
2024-05-30 06:52:23 +07:00
committed by Eric Bailey
parent 4d39ef2e19
commit 6f0764de1d
11 changed files with 46 additions and 71 deletions
@@ -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',
+6 -6
View File
@@ -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>
-11
View File
@@ -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)
}
+4 -4
View File
@@ -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>
+3 -2
View File
@@ -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}`}
+3 -3
View File
@@ -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>
+12 -16
View File
@@ -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>
+5 -12
View File
@@ -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)
} }
+3 -8
View File
@@ -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 && (
+4 -4
View File
@@ -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 = ({
&middot;{' '} &middot;{' '}
<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}