about summary refs log tree commit diff
path: root/src/view
diff options
context:
space:
mode:
authorEric Bailey <git@esb.lol>2024-08-29 19:22:53 -0500
committerGitHub <noreply@github.com>2024-08-29 19:22:53 -0500
commit8651f31ebb7cf9c6a0949503f2c2c5755328ce46 (patch)
tree04f9c08a3770cee554a6cd421a53dc04957457fb /src/view
parentd5a76183746bc67f88b858add49c2dba52b99bb5 (diff)
downloadvoidsky-8651f31ebb7cf9c6a0949503f2c2c5755328ce46.tar.zst
Localize dates, counts (#5027)
* refactor: consistent localized formatting

* refactor: localized date time

* refactor: localize relative time with strings

* chore: fix typo from copy-paste

* Clean up useTimeAgo

* Remove old ago

* Const

* Reuse

* Prettier

---------

Co-authored-by: Mary <git@mary.my.id>
Diffstat (limited to 'src/view')
-rw-r--r--src/view/com/notifications/FeedItem.tsx6
-rw-r--r--src/view/com/post-thread/PostThreadItem.tsx14
-rw-r--r--src/view/com/util/PostMeta.tsx7
-rw-r--r--src/view/com/util/TimeElapsed.tsx12
-rw-r--r--src/view/com/util/forms/DateInput.tsx28
-rw-r--r--src/view/com/util/numeric/format.ts17
-rw-r--r--src/view/com/util/post-ctrls/PostCtrls.tsx6
-rw-r--r--src/view/com/util/post-ctrls/RepostButton.tsx4
-rw-r--r--src/view/com/util/post-ctrls/RepostButton.web.tsx3
-rw-r--r--src/view/screens/AppPasswords.tsx11
-rw-r--r--src/view/shell/Drawer.tsx8
11 files changed, 55 insertions, 61 deletions
diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx
index b591b4b7f..3e8f8d86d 100644
--- a/src/view/com/notifications/FeedItem.tsx
+++ b/src/view/com/notifications/FeedItem.tsx
@@ -84,7 +84,7 @@ let FeedItem = ({
 }): React.ReactNode => {
   const queryClient = useQueryClient()
   const pal = usePalette('default')
-  const {_} = useLingui()
+  const {_, i18n} = useLingui()
   const t = useTheme()
   const [isAuthorsExpanded, setAuthorsExpanded] = useState<boolean>(false)
   const itemHref = useMemo(() => {
@@ -225,11 +225,11 @@ let FeedItem = ({
   }
 
   const formattedCount =
-    authors.length > 1 ? formatCount(authors.length - 1) : ''
+    authors.length > 1 ? formatCount(i18n, authors.length - 1) : ''
   const firstAuthorName = sanitizeDisplayName(
     authors[0].profile.displayName || authors[0].profile.handle,
   )
-  const niceTimestamp = niceDate(item.notification.indexedAt)
+  const niceTimestamp = niceDate(i18n, item.notification.indexedAt)
   const a11yLabelUsers =
     authors.length > 1
       ? _(msg` and `) +
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
index a3cfebbab..3b5ddb1dc 100644
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ b/src/view/com/post-thread/PostThreadItem.tsx
@@ -181,7 +181,7 @@ let PostThreadItemLoaded = ({
   threadgateRecord?: AppBskyFeedThreadgate.Record
 }): React.ReactNode => {
   const pal = usePalette('default')
-  const {_} = useLingui()
+  const {_, i18n} = useLingui()
   const langPrefs = useLanguagePrefs()
   const {openComposer} = useComposerControls()
   const [limitLines, setLimitLines] = React.useState(
@@ -388,7 +388,7 @@ let PostThreadItemLoaded = ({
                       type="lg"
                       style={pal.textLight}>
                       <Text type="xl-bold" style={pal.text}>
-                        {formatCount(post.repostCount)}
+                        {formatCount(i18n, post.repostCount)}
                       </Text>{' '}
                       <Plural
                         value={post.repostCount}
@@ -410,7 +410,7 @@ let PostThreadItemLoaded = ({
                       type="lg"
                       style={pal.textLight}>
                       <Text type="xl-bold" style={pal.text}>
-                        {formatCount(post.quoteCount)}
+                        {formatCount(i18n, post.quoteCount)}
                       </Text>{' '}
                       <Plural
                         value={post.quoteCount}
@@ -430,7 +430,7 @@ let PostThreadItemLoaded = ({
                       type="lg"
                       style={pal.textLight}>
                       <Text type="xl-bold" style={pal.text}>
-                        {formatCount(post.likeCount)}
+                        {formatCount(i18n, post.likeCount)}
                       </Text>{' '}
                       <Plural value={post.likeCount} one="like" other="likes" />
                     </Text>
@@ -705,7 +705,7 @@ function ExpandedPostDetails({
   translatorUrl: string
 }) {
   const pal = usePalette('default')
-  const {_} = useLingui()
+  const {_, i18n} = useLingui()
   const openLink = useOpenLink()
   const isRootPost = !('reply' in post.record)
 
@@ -723,7 +723,9 @@ function ExpandedPostDetails({
         s.mt2,
         s.mb10,
       ]}>
-      <Text style={[a.text_sm, pal.textLight]}>{niceDate(post.indexedAt)}</Text>
+      <Text style={[a.text_sm, pal.textLight]}>
+        {niceDate(i18n, post.indexedAt)}
+      </Text>
       {isRootPost && (
         <WhoCanReply post={post} isThreadAuthor={isThreadAuthor} />
       )}
diff --git a/src/view/com/util/PostMeta.tsx b/src/view/com/util/PostMeta.tsx
index b1567c2c6..3bd350bf3 100644
--- a/src/view/com/util/PostMeta.tsx
+++ b/src/view/com/util/PostMeta.tsx
@@ -1,6 +1,7 @@
 import React, {memo, useCallback} from 'react'
 import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native'
 import {AppBskyActorDefs, ModerationDecision, ModerationUI} from '@atproto/api'
+import {useLingui} from '@lingui/react'
 import {useQueryClient} from '@tanstack/react-query'
 
 import {precacheProfile} from '#/state/queries/profile'
@@ -35,6 +36,8 @@ interface PostMetaOpts {
 }
 
 let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
+  const {i18n} = useLingui()
+
   const pal = usePalette('default')
   const displayName = opts.author.displayName || opts.author.handle
   const handle = opts.author.handle
@@ -101,8 +104,8 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
             type="md"
             style={pal.textLight}
             text={timeElapsed}
-            accessibilityLabel={niceDate(opts.timestamp)}
-            title={niceDate(opts.timestamp)}
+            accessibilityLabel={niceDate(i18n, opts.timestamp)}
+            title={niceDate(i18n, opts.timestamp)}
             accessibilityHint=""
             href={opts.postHref}
             onBeforePress={onBeforePressPost}
diff --git a/src/view/com/util/TimeElapsed.tsx b/src/view/com/util/TimeElapsed.tsx
index a49585182..70fed222f 100644
--- a/src/view/com/util/TimeElapsed.tsx
+++ b/src/view/com/util/TimeElapsed.tsx
@@ -1,4 +1,6 @@
 import React from 'react'
+import {I18n} from '@lingui/core'
+import {useLingui} from '@lingui/react'
 
 import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
 import {useTickEveryMinute} from '#/state/shell'
@@ -10,19 +12,21 @@ export function TimeElapsed({
 }: {
   timestamp: string
   children: ({timeElapsed}: {timeElapsed: string}) => JSX.Element
-  timeToString?: (timeElapsed: string) => string
+  timeToString?: (i18n: I18n, timeElapsed: string) => string
 }) {
+  const {i18n} = useLingui()
   const ago = useGetTimeAgo()
-  const format = timeToString ?? ago
   const tick = useTickEveryMinute()
   const [timeElapsed, setTimeAgo] = React.useState(() =>
-    format(timestamp, tick),
+    timeToString ? timeToString(i18n, timestamp) : ago(timestamp, tick),
   )
 
   const [prevTick, setPrevTick] = React.useState(tick)
   if (prevTick !== tick) {
     setPrevTick(tick)
-    setTimeAgo(format(timestamp, tick))
+    setTimeAgo(
+      timeToString ? timeToString(i18n, timestamp) : ago(timestamp, tick),
+    )
   }
 
   return children({timeElapsed})
diff --git a/src/view/com/util/forms/DateInput.tsx b/src/view/com/util/forms/DateInput.tsx
index 0104562aa..bfbb2ff55 100644
--- a/src/view/com/util/forms/DateInput.tsx
+++ b/src/view/com/util/forms/DateInput.tsx
@@ -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 DatePicker from 'react-native-date-picker'
 import {
   FontAwesomeIcon,
   FontAwesomeIconStyle,
 } from '@fortawesome/react-native-fontawesome'
-import {isIOS, isAndroid} from 'platform/detection'
-import {Button, ButtonType} from './Button'
-import {Text} from '../text/Text'
+import {useLingui} from '@lingui/react'
+
+import {usePalette} from 'lib/hooks/usePalette'
 import {TypographyVariant} from 'lib/ThemeContext'
 import {useTheme} from 'lib/ThemeContext'
-import {usePalette} from 'lib/hooks/usePalette'
-import {getLocales} from 'expo-localization'
-import DatePicker from 'react-native-date-picker'
-
-const LOCALE = getLocales()[0]
+import {isAndroid, isIOS} from 'platform/detection'
+import {Text} from '../text/Text'
+import {Button, ButtonType} from './Button'
 
 interface Props {
   testID?: string
@@ -30,16 +29,11 @@ interface Props {
 }
 
 export function DateInput(props: Props) {
+  const {i18n} = useLingui()
   const [show, setShow] = useState(false)
   const theme = useTheme()
   const pal = usePalette('default')
 
-  const formatter = React.useMemo(() => {
-    return new Intl.DateTimeFormat(LOCALE.languageTag, {
-      timeZone: props.handleAsUTC ? 'UTC' : undefined,
-    })
-  }, [props.handleAsUTC])
-
   const onChangeInternal = useCallback(
     (date: Date) => {
       setShow(false)
@@ -74,7 +68,9 @@ export function DateInput(props: Props) {
             <Text
               type={props.buttonLabelType}
               style={[pal.text, props.buttonLabelStyle]}>
-              {formatter.format(props.value)}
+              {i18n.date(props.value, {
+                timeZone: props.handleAsUTC ? 'UTC' : undefined,
+              })}
             </Text>
           </View>
         </Button>
diff --git a/src/view/com/util/numeric/format.ts b/src/view/com/util/numeric/format.ts
index 71d8d73e0..cca9fc7e7 100644
--- a/src/view/com/util/numeric/format.ts
+++ b/src/view/com/util/numeric/format.ts
@@ -1,19 +1,12 @@
-export const formatCount = (num: number) =>
-  Intl.NumberFormat('en-US', {
+import type {I18n} from '@lingui/core'
+
+export const formatCount = (i18n: I18n, num: number) => {
+  return i18n.number(num, {
     notation: 'compact',
     maximumFractionDigits: 1,
     // `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
     // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#roundingmode
     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)
+  })
 }
diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx
index a0cef8692..f577e1683 100644
--- a/src/view/com/util/post-ctrls/PostCtrls.tsx
+++ b/src/view/com/util/post-ctrls/PostCtrls.tsx
@@ -75,7 +75,7 @@ let PostCtrls = ({
   threadgateRecord?: AppBskyFeedThreadgate.Record
 }): React.ReactNode => {
   const t = useTheme()
-  const {_} = useLingui()
+  const {_, i18n} = useLingui()
   const {openComposer} = useComposerControls()
   const {currentAccount} = useSession()
   const [queueLike, queueUnlike] = usePostLikeMutationQueue(post, logContext)
@@ -247,7 +247,7 @@ let PostCtrls = ({
                 big ? a.text_md : {fontSize: 15},
                 a.user_select_none,
               ]}>
-              {formatCount(post.replyCount)}
+              {formatCount(i18n, post.replyCount)}
             </Text>
           ) : undefined}
         </Pressable>
@@ -300,7 +300,7 @@ let PostCtrls = ({
                     : defaultCtrlColor,
                 ],
               ]}>
-              {formatCount(post.likeCount)}
+              {formatCount(i18n, post.likeCount)}
             </Text>
           ) : undefined}
         </Pressable>
diff --git a/src/view/com/util/post-ctrls/RepostButton.tsx b/src/view/com/util/post-ctrls/RepostButton.tsx
index 5994b7ef6..d924adbe4 100644
--- a/src/view/com/util/post-ctrls/RepostButton.tsx
+++ b/src/view/com/util/post-ctrls/RepostButton.tsx
@@ -32,7 +32,7 @@ let RepostButton = ({
   embeddingDisabled,
 }: Props): React.ReactNode => {
   const t = useTheme()
-  const {_} = useLingui()
+  const {_, i18n} = useLingui()
   const requireAuth = useRequireAuth()
   const dialogControl = Dialog.useDialogControl()
   const playHaptic = useHaptics()
@@ -79,7 +79,7 @@ let RepostButton = ({
               big ? a.text_md : {fontSize: 15},
               isReposted && a.font_bold,
             ]}>
-            {formatCount(repostCount)}
+            {formatCount(i18n, repostCount)}
           </Text>
         ) : undefined}
       </Button>
diff --git a/src/view/com/util/post-ctrls/RepostButton.web.tsx b/src/view/com/util/post-ctrls/RepostButton.web.tsx
index 9a8776b9c..111b41dd7 100644
--- a/src/view/com/util/post-ctrls/RepostButton.web.tsx
+++ b/src/view/com/util/post-ctrls/RepostButton.web.tsx
@@ -128,6 +128,7 @@ const RepostInner = ({
   repostCount?: number
   big?: boolean
 }) => {
+  const {i18n} = useLingui()
   return (
     <View style={[a.flex_row, a.align_center, a.gap_xs, {padding: 5}]}>
       <Repost style={color} width={big ? 22 : 18} />
@@ -140,7 +141,7 @@ const RepostInner = ({
             isReposted && [a.font_bold],
             a.user_select_none,
           ]}>
-          {formatCount(repostCount)}
+          {formatCount(i18n, repostCount)}
         </Text>
       ) : undefined}
     </View>
diff --git a/src/view/screens/AppPasswords.tsx b/src/view/screens/AppPasswords.tsx
index 5bf9e8a16..6f1cd1bb8 100644
--- a/src/view/screens/AppPasswords.tsx
+++ b/src/view/screens/AppPasswords.tsx
@@ -18,7 +18,6 @@ import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
 import {CommonNavigatorParams} from '#/lib/routes/types'
 import {cleanError} from '#/lib/strings/errors'
 import {useModalControls} from '#/state/modals'
-import {useLanguagePrefs} from '#/state/preferences'
 import {
   useAppPasswordDeleteMutation,
   useAppPasswordsQuery,
@@ -218,9 +217,8 @@ function AppPassword({
   privileged?: boolean
 }) {
   const pal = usePalette('default')
-  const {_} = useLingui()
+  const {_, i18n} = useLingui()
   const control = useDialogControl()
-  const {contentLanguages} = useLanguagePrefs()
   const deleteMutation = useAppPasswordDeleteMutation()
 
   const onDelete = React.useCallback(async () => {
@@ -232,9 +230,6 @@ function AppPassword({
     control.open()
   }, [control])
 
-  const primaryLocale =
-    contentLanguages.length > 0 ? contentLanguages[0] : 'en-US'
-
   return (
     <TouchableOpacity
       testID={testID}
@@ -250,14 +245,14 @@ function AppPassword({
         <Text type="md" style={[pal.text, styles.pr10]} numberOfLines={1}>
           <Trans>
             Created{' '}
-            {Intl.DateTimeFormat(primaryLocale, {
+            {i18n.date(createdAt, {
               year: 'numeric',
               month: 'numeric',
               day: 'numeric',
               hour: '2-digit',
               minute: '2-digit',
               second: '2-digit',
-            }).format(new Date(createdAt))}
+            })}
           </Trans>
         </Text>
         {privileged && (
diff --git a/src/view/shell/Drawer.tsx b/src/view/shell/Drawer.tsx
index 0e852edd1..facead2c1 100644
--- a/src/view/shell/Drawer.tsx
+++ b/src/view/shell/Drawer.tsx
@@ -30,7 +30,7 @@ import {colors, s} from 'lib/styles'
 import {useTheme} from 'lib/ThemeContext'
 import {isWeb} from 'platform/detection'
 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 {UserAvatar} from 'view/com/util/UserAvatar'
 import {atoms as a} from '#/alf'
@@ -68,7 +68,7 @@ let DrawerProfileCard = ({
   account: SessionAccount
   onPressProfile: () => void
 }): React.ReactNode => {
-  const {_} = useLingui()
+  const {_, i18n} = useLingui()
   const pal = usePalette('default')
   const {data: profile} = useProfileQuery({did: account.did})
 
@@ -108,7 +108,7 @@ let DrawerProfileCard = ({
         <Text type="xl" style={pal.textLight}>
           <Trans>
             <Text type="xl-medium" style={pal.text}>
-              {formatCountShortOnly(profile?.followersCount ?? 0)}
+              {formatCount(i18n, profile?.followersCount ?? 0)}
             </Text>{' '}
             <Plural
               value={profile?.followersCount || 0}
@@ -123,7 +123,7 @@ let DrawerProfileCard = ({
         <Text type="xl" style={pal.textLight}>
           <Trans>
             <Text type="xl-medium" style={pal.text}>
-              {formatCountShortOnly(profile?.followsCount ?? 0)}
+              {formatCount(i18n, profile?.followsCount ?? 0)}
             </Text>{' '}
             <Plural
               value={profile?.followsCount || 0}