about summary refs log tree commit diff
diff options
context:
space:
mode:
authorPaul Frazee <pfrazee@gmail.com>2024-05-06 16:55:57 -0700
committerGitHub <noreply@github.com>2024-05-06 16:55:57 -0700
commit901feba6db9467ce9b75a78128fa305fc3370c7e (patch)
tree14dd8d8352f2a538e6b4c61b5bcf54924e6ab583
parent2ca4b74955244f23aa40ac2d98267e301a59793c (diff)
downloadvoidsky-901feba6db9467ce9b75a78128fa305fc3370c7e.tar.zst
Replace pluralize by plural by @tkusano (#3882)
* Replace pluralize with plural or Plural
* Replace all pluralize (defined by src/lib/strings/helpers.ts) with plural or Plural (defined by @lingui/macro) to make some UI elements translatable.
* Delete pluralize() and related test.

* Import @formatjs polyfill libraries for plural on ios and android

- ios and andorid: import `@formtjs/intl-locale` and `@formatjs/intl-pluralrules` to polyfill `Intl.Locale` and `Intl.PluralRules` which are used in `plural()` and '<Plural />'.
- update `plural` use in notification messages for better translation.

* Rewrite to pass lint

* Add Catalan plural polyfill

* more replacement

* import zh plural data for zh-CN

* Refactor feed header components (#2964)

* Move home-related files to view/com/home

* Add HomeHeader in front of FeedTabBar

* Move isDekstop check outside FeedsTabBar

* Remove PWI logic from tabbar

* Separate platform-specific layout from shared logic

* Rename Home Feed Prefs to Following Feed Prefs (#2965)

* use `useOpenLink` hook for links in ALF (#2975)

* use `useOpenLink` hook for links in ALF

* web only for `outline`

* increase timeout to 15s (#2958)

* Normalize relative day (#2874)

* fix: normalize relative date

* chore: add comments

* refactor: skip flooring normalized diff

* refactor: let -> const

* fix: get own copy of date to prevent mutating

* refactor: rounding does the same trick

* Add handle validation to create account UI (#2959)

* show uiState errors in the box as well

simplify copy

update ui for only letters and numbers

add ui validation to handle selection

* simplify names

* Fix accidental text-node render

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>

* Make dim theme dim (#2966)

* Make dim color scheme dim

* Tweaks

* Overall tweaks

* We have to go darker

* Tweak saturation of blues in dim

* Increase contrast on dark-dark mode

* adjust dim

---------

Co-authored-by: Eric Bailey <git@esb.lol>
Co-authored-by: Paul Frazee <pfrazee@gmail.com>
Co-authored-by: Hailey <me@haileyok.com>

* Fix dim mode unread notif color

* use `showControls` to show/hide live text icon on ios (#2982)

* Update .po files

* fix reversed icons in validator 🤦 (#2991)

* Adjust `windowSize` on `PostThread` `FlatList` (#2989)

* adjust window size, cells batching period

* rm batching period change

* Pluralize 'follow(s)'

* Include a space between the msgid count and "follower(s)/following(s)" so the translator can adjust the translated count line to fit within the Drawer.

* pluralie '# following'

* Fix & Update

* Rewrite to use Plural

* rmeove unused import

* When commiting changes, disable 'simple-import-sort' plugin in .eslintrc.js to sync with bluesky-social:main

* Revert simple-import-sort/imports related changes

* Move ProfileHoverCard web to plural util

* Followings -> following

* Add plural following to hovercard

* Followings -> Following

---------

Co-authored-by: Takayuki KUSANO <kusano@tkusano.jp>
Co-authored-by: Takayuki KUSANO <65759+tkusano@users.noreply.github.com>
Co-authored-by: dan <dan.abramov@gmail.com>
Co-authored-by: Hailey <me@haileyok.com>
Co-authored-by: Mary <148872143+mary-ext@users.noreply.github.com>
Co-authored-by: Eric Bailey <git@esb.lol>
-rw-r--r--__tests__/lib/string.test.ts31
-rw-r--r--modules/expo-scroll-forwarder/src/ExpoScrollForwarderView.ios.tsx1
-rw-r--r--package.json2
-rw-r--r--src/components/LabelingServiceCard/index.tsx7
-rw-r--r--src/components/ProfileHoverCard/index.web.tsx16
-rw-r--r--src/components/moderation/LabelsOnMe.tsx18
-rw-r--r--src/lib/strings/helpers.ts10
-rw-r--r--src/locale/i18n.ts20
-rw-r--r--src/screens/Deactivated.tsx13
-rw-r--r--src/screens/Profile/Header/Metrics.tsx35
-rw-r--r--src/screens/Profile/Header/ProfileHeaderLabeler.tsx21
-rw-r--r--src/view/com/feeds/FeedSourceCard.tsx12
-rw-r--r--src/view/com/notifications/FeedItem.tsx10
-rw-r--r--src/view/com/post-thread/PostThreadItem.tsx12
-rw-r--r--src/view/com/util/post-ctrls/PostCtrls.tsx24
-rw-r--r--src/view/com/util/post-ctrls/RepostButton.tsx5
-rw-r--r--src/view/screens/PreferencesFollowingFeed.tsx16
-rw-r--r--src/view/screens/ProfileFeed.tsx9
-rw-r--r--src/view/shell/Drawer.tsx24
-rw-r--r--yarn.lock48
20 files changed, 208 insertions, 126 deletions
diff --git a/__tests__/lib/string.test.ts b/__tests__/lib/string.test.ts
index 2f603a521..c8a209dfa 100644
--- a/__tests__/lib/string.test.ts
+++ b/__tests__/lib/string.test.ts
@@ -3,7 +3,7 @@ import {RichText} from '@atproto/api'
 import {parseEmbedPlayerFromUrl} from 'lib/strings/embed-player'
 import {cleanError} from '../../src/lib/strings/errors'
 import {createFullHandle, makeValidHandle} from '../../src/lib/strings/handles'
-import {enforceLen, pluralize} from '../../src/lib/strings/helpers'
+import {enforceLen} from '../../src/lib/strings/helpers'
 import {detectLinkables} from '../../src/lib/strings/rich-text-detection'
 import {shortenLinks} from '../../src/lib/strings/rich-text-manip'
 import {ago} from '../../src/lib/strings/time'
@@ -127,35 +127,6 @@ describe('detectLinkables', () => {
   })
 })
 
-describe('pluralize', () => {
-  const inputs: [number, string, string?][] = [
-    [1, 'follower'],
-    [1, 'member'],
-    [100, 'post'],
-    [1000, 'repost'],
-    [10000, 'upvote'],
-    [100000, 'other'],
-    [2, 'man', 'men'],
-  ]
-  const outputs = [
-    'follower',
-    'member',
-    'posts',
-    'reposts',
-    'upvotes',
-    'others',
-    'men',
-  ]
-
-  it('correctly pluralizes a set of words', () => {
-    for (let i = 0; i < inputs.length; i++) {
-      const input = inputs[i]
-      const output = pluralize(...input)
-      expect(output).toEqual(outputs[i])
-    }
-  })
-})
-
 describe('makeRecordUri', () => {
   const inputs: [string, string, string][] = [
     ['alice.test', 'app.bsky.feed.post', '3jk7x4irgv52r'],
diff --git a/modules/expo-scroll-forwarder/src/ExpoScrollForwarderView.ios.tsx b/modules/expo-scroll-forwarder/src/ExpoScrollForwarderView.ios.tsx
index a91aebd4d..6364d332c 100644
--- a/modules/expo-scroll-forwarder/src/ExpoScrollForwarderView.ios.tsx
+++ b/modules/expo-scroll-forwarder/src/ExpoScrollForwarderView.ios.tsx
@@ -1,5 +1,6 @@
 import {requireNativeViewManager} from 'expo-modules-core'
 import * as React from 'react'
+
 import {ExpoScrollForwarderViewProps} from './ExpoScrollForwarder.types'
 
 const NativeView: React.ComponentType<ExpoScrollForwarderViewProps> =
diff --git a/package.json b/package.json
index 39b90b3c3..e5d88be57 100644
--- a/package.json
+++ b/package.json
@@ -60,6 +60,8 @@
     "@expo/webpack-config": "^19.0.0",
     "@floating-ui/dom": "^1.6.3",
     "@floating-ui/react-dom": "^2.0.8",
+    "@formatjs/intl-locale": "^3.4.3",
+    "@formatjs/intl-pluralrules": "^5.2.10",
     "@fortawesome/fontawesome-svg-core": "^6.1.1",
     "@fortawesome/free-regular-svg-icons": "^6.1.1",
     "@fortawesome/free-solid-svg-icons": "^6.1.1",
diff --git a/src/components/LabelingServiceCard/index.tsx b/src/components/LabelingServiceCard/index.tsx
index f924f0f59..2bb7ed59c 100644
--- a/src/components/LabelingServiceCard/index.tsx
+++ b/src/components/LabelingServiceCard/index.tsx
@@ -1,6 +1,6 @@
 import React from 'react'
 import {View} from 'react-native'
-import {msg, Trans} from '@lingui/macro'
+import {msg, Plural, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {AppBskyLabelerDefs} from '@atproto/api'
 
@@ -13,7 +13,6 @@ import {RichText} from '#/components/RichText'
 import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '../icons/Chevron'
 import {UserAvatar} from '#/view/com/util/UserAvatar'
 import {sanitizeHandle} from '#/lib/strings/handles'
-import {pluralize} from '#/lib/strings/helpers'
 
 type LabelingServiceProps = {
   labeler: AppBskyLabelerDefs.LabelerViewDetailed
@@ -69,9 +68,7 @@ export function LikeCount({count}: {count: number}) {
         t.atoms.text_contrast_medium,
         {fontWeight: '500'},
       ]}>
-      <Trans>
-        Liked by {count} {pluralize(count, 'user')}
-      </Trans>
+      <Plural value={count} one="Liked by # user" other="Liked by # users" />
     </Text>
   )
 }
diff --git a/src/components/ProfileHoverCard/index.web.tsx b/src/components/ProfileHoverCard/index.web.tsx
index a22436879..305327d8b 100644
--- a/src/components/ProfileHoverCard/index.web.tsx
+++ b/src/components/ProfileHoverCard/index.web.tsx
@@ -2,13 +2,12 @@ import React from 'react'
 import {View} from 'react-native'
 import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
 import {flip, offset, shift, size, useFloating} from '@floating-ui/react-dom'
-import {msg, Trans} from '@lingui/macro'
+import {msg, plural, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
 import {makeProfileLink} from '#/lib/routes/links'
 import {sanitizeDisplayName} from '#/lib/strings/display-names'
 import {sanitizeHandle} from '#/lib/strings/handles'
-import {pluralize} from '#/lib/strings/helpers'
 import {useModerationOpts} from '#/state/preferences/moderation-opts'
 import {usePrefetchProfileQuery, useProfileQuery} from '#/state/queries/profile'
 import {useSession} from '#/state/session'
@@ -371,7 +370,14 @@ function Inner({
   const blockHide = profile.viewer?.blocking || profile.viewer?.blockedBy
   const following = formatCount(profile.followsCount || 0)
   const followers = formatCount(profile.followersCount || 0)
-  const pluralizedFollowers = pluralize(profile.followersCount || 0, 'follower')
+  const pluralizedFollowers = plural(profile.followersCount || 0, {
+    one: 'follower',
+    other: 'followers',
+  })
+  const pluralizedFollowings = plural(profile.followsCount || 0, {
+    one: 'following',
+    other: 'following',
+  })
   const profileURL = makeProfileLink({
     did: profile.did,
     handle: profile.handle,
@@ -448,7 +454,9 @@ function Inner({
               onPress={hide}>
               <Trans>
                 <Text style={[a.text_md, a.font_bold]}>{following} </Text>
-                <Text style={[t.atoms.text_contrast_medium]}>following</Text>
+                <Text style={[t.atoms.text_contrast_medium]}>
+                  {pluralizedFollowings}
+                </Text>
               </Trans>
             </InlineLinkText>
           </View>
diff --git a/src/components/moderation/LabelsOnMe.tsx b/src/components/moderation/LabelsOnMe.tsx
index 099769fa7..46825d761 100644
--- a/src/components/moderation/LabelsOnMe.tsx
+++ b/src/components/moderation/LabelsOnMe.tsx
@@ -1,7 +1,7 @@
 import React from 'react'
 import {StyleProp, View, ViewStyle} from 'react-native'
 import {AppBskyFeedDefs, ComAtprotoLabelDefs} from '@atproto/api'
-import {msg, Trans} from '@lingui/macro'
+import {msg, Plural} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useSession} from '#/state/session'
 
@@ -39,7 +39,6 @@ export function LabelsOnMe({
     return null
   }
 
-  const labelTarget = isAccount ? _(msg`account`) : _(msg`content`)
   return (
     <View style={[a.flex_row, style]}>
       <LabelsOnMeDialog control={control} subject={details} labels={labels} />
@@ -54,11 +53,18 @@ export function LabelsOnMe({
         }}>
         <ButtonIcon position="left" icon={CircleInfo} />
         <ButtonText style={[a.leading_snug]}>
-          {labels.length}{' '}
-          {labels.length === 1 ? (
-            <Trans>label has been placed on this {labelTarget}</Trans>
+          {isAccount ? (
+            <Plural
+              value={labels.length}
+              one="# label has been placed on this account"
+              other="# labels has been placed on this account"
+            />
           ) : (
-            <Trans>labels have been placed on this {labelTarget}</Trans>
+            <Plural
+              value={labels.length}
+              one="# label has been placed on this content"
+              other="# labels has been placed on this content"
+            />
           )}
         </ButtonText>
       </Button>
diff --git a/src/lib/strings/helpers.ts b/src/lib/strings/helpers.ts
index de4562d2c..b4ce64fa5 100644
--- a/src/lib/strings/helpers.ts
+++ b/src/lib/strings/helpers.ts
@@ -1,13 +1,3 @@
-export function pluralize(n: number, base: string, plural?: string): string {
-  if (n === 1) {
-    return base
-  }
-  if (plural) {
-    return plural
-  }
-  return base + 's'
-}
-
 export function enforceLen(
   str: string,
   len: number,
diff --git a/src/locale/i18n.ts b/src/locale/i18n.ts
index 725332de0..9f75f83f3 100644
--- a/src/locale/i18n.ts
+++ b/src/locale/i18n.ts
@@ -1,3 +1,7 @@
+import '@formatjs/intl-locale/polyfill'
+import '@formatjs/intl-pluralrules/polyfill'
+import '@formatjs/intl-pluralrules/locale-data/en'
+
 import {useEffect} from 'react'
 import {i18n} from '@lingui/core'
 
@@ -29,66 +33,82 @@ export async function dynamicActivate(locale: AppLanguage) {
   switch (locale) {
     case AppLanguage.ca: {
       i18n.loadAndActivate({locale, messages: messagesCa})
+      await import('@formatjs/intl-pluralrules/locale-data/ca')
       break
     }
     case AppLanguage.de: {
       i18n.loadAndActivate({locale, messages: messagesDe})
+      await import('@formatjs/intl-pluralrules/locale-data/de')
       break
     }
     case AppLanguage.es: {
       i18n.loadAndActivate({locale, messages: messagesEs})
+      await import('@formatjs/intl-pluralrules/locale-data/es')
       break
     }
     case AppLanguage.fi: {
       i18n.loadAndActivate({locale, messages: messagesFi})
+      await import('@formatjs/intl-pluralrules/locale-data/fi')
       break
     }
     case AppLanguage.fr: {
       i18n.loadAndActivate({locale, messages: messagesFr})
+      await import('@formatjs/intl-pluralrules/locale-data/fr')
       break
     }
     case AppLanguage.ga: {
       i18n.loadAndActivate({locale, messages: messagesGa})
+      await import('@formatjs/intl-pluralrules/locale-data/ga')
       break
     }
     case AppLanguage.hi: {
       i18n.loadAndActivate({locale, messages: messagesHi})
+      await import('@formatjs/intl-pluralrules/locale-data/hi')
       break
     }
     case AppLanguage.id: {
       i18n.loadAndActivate({locale, messages: messagesId})
+      await import('@formatjs/intl-pluralrules/locale-data/id')
       break
     }
     case AppLanguage.it: {
       i18n.loadAndActivate({locale, messages: messagesIt})
+      await import('@formatjs/intl-pluralrules/locale-data/it')
       break
     }
     case AppLanguage.ja: {
       i18n.loadAndActivate({locale, messages: messagesJa})
+      await import('@formatjs/intl-pluralrules/locale-data/ja')
       break
     }
     case AppLanguage.ko: {
       i18n.loadAndActivate({locale, messages: messagesKo})
+      await import('@formatjs/intl-pluralrules/locale-data/ko')
       break
     }
     case AppLanguage.pt_BR: {
       i18n.loadAndActivate({locale, messages: messagesPt_BR})
+      await import('@formatjs/intl-pluralrules/locale-data/pt')
       break
     }
     case AppLanguage.tr: {
       i18n.loadAndActivate({locale, messages: messagesTr})
+      await import('@formatjs/intl-pluralrules/locale-data/tr')
       break
     }
     case AppLanguage.uk: {
       i18n.loadAndActivate({locale, messages: messagesUk})
+      await import('@formatjs/intl-pluralrules/locale-data/uk')
       break
     }
     case AppLanguage.zh_CN: {
       i18n.loadAndActivate({locale, messages: messagesZh_CN})
+      await import('@formatjs/intl-pluralrules/locale-data/zh')
       break
     }
     case AppLanguage.zh_TW: {
       i18n.loadAndActivate({locale, messages: messagesZh_TW})
+      await import('@formatjs/intl-pluralrules/locale-data/zh')
       break
     }
     default: {
diff --git a/src/screens/Deactivated.tsx b/src/screens/Deactivated.tsx
index c2bac7715..08a2232df 100644
--- a/src/screens/Deactivated.tsx
+++ b/src/screens/Deactivated.tsx
@@ -1,10 +1,9 @@
 import React from 'react'
 import {View} from 'react-native'
 import {useSafeAreaInsets} from 'react-native-safe-area-context'
-import {msg, Trans} from '@lingui/macro'
+import {msg, plural, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
-import {pluralize} from '#/lib/strings/helpers'
 import {logger} from '#/logger'
 import {isWeb} from '#/platform/detection'
 import {isSessionDeactivated, useAgent, useSessionApi} from '#/state/session'
@@ -205,10 +204,16 @@ function msToString(ms: number | undefined): string | undefined {
         return undefined
       }
       // hours
-      return `${estimatedTimeHrs} ${pluralize(estimatedTimeHrs, 'hour')}`
+      return `${estimatedTimeHrs} ${plural(estimatedTimeHrs, {
+        one: 'hour',
+        other: 'hours',
+      })}`
     }
     // minutes
-    return `${estimatedTimeMins} ${pluralize(estimatedTimeMins, 'minute')}`
+    return `${estimatedTimeMins} ${plural(estimatedTimeMins, {
+      one: 'minute',
+      other: 'minutes',
+    })}`
   }
   return undefined
 }
diff --git a/src/screens/Profile/Header/Metrics.tsx b/src/screens/Profile/Header/Metrics.tsx
index 8789e0354..6d0a25182 100644
--- a/src/screens/Profile/Header/Metrics.tsx
+++ b/src/screens/Profile/Header/Metrics.tsx
@@ -1,10 +1,9 @@
 import React from 'react'
 import {View} from 'react-native'
 import {AppBskyActorDefs} from '@atproto/api'
-import {msg, Trans} from '@lingui/macro'
+import {msg, plural, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
-import {pluralize} from '#/lib/strings/helpers'
 import {Shadow} from '#/state/cache/types'
 import {makeProfileLink} from 'lib/routes/links'
 import {formatCount} from 'view/com/util/numeric/format'
@@ -21,7 +20,14 @@ export function ProfileHeaderMetrics({
   const {_} = useLingui()
   const following = formatCount(profile.followsCount || 0)
   const followers = formatCount(profile.followersCount || 0)
-  const pluralizedFollowers = pluralize(profile.followersCount || 0, 'follower')
+  const pluralizedFollowers = plural(profile.followersCount || 0, {
+    one: 'follower',
+    other: 'followers',
+  })
+  const pluralizedFollowings = plural(profile.followsCount || 0, {
+    one: 'following',
+    other: 'following',
+  })
 
   return (
     <View
@@ -32,10 +38,12 @@ export function ProfileHeaderMetrics({
         style={[a.flex_row, t.atoms.text]}
         to={makeProfileLink(profile, 'followers')}
         label={`${followers} ${pluralizedFollowers}`}>
-        <Text style={[a.font_bold, a.text_md]}>{followers} </Text>
-        <Text style={[t.atoms.text_contrast_medium, a.text_md]}>
-          {pluralizedFollowers}
-        </Text>
+        <Trans>
+          <Text style={[a.font_bold, a.text_md]}>{followers} </Text>
+          <Text style={[t.atoms.text_contrast_medium, a.text_md]}>
+            {pluralizedFollowers}
+          </Text>
+        </Trans>
       </InlineLinkText>
       <InlineLinkText
         testID="profileHeaderFollowsButton"
@@ -45,15 +53,18 @@ export function ProfileHeaderMetrics({
         <Trans>
           <Text style={[a.font_bold, a.text_md]}>{following} </Text>
           <Text style={[t.atoms.text_contrast_medium, a.text_md]}>
-            following
+            {pluralizedFollowings}
           </Text>
         </Trans>
       </InlineLinkText>
       <Text style={[a.font_bold, t.atoms.text, a.text_md]}>
-        {formatCount(profile.postsCount || 0)}{' '}
-        <Text style={[t.atoms.text_contrast_medium, a.font_normal, a.text_md]}>
-          {pluralize(profile.postsCount || 0, 'post')}
-        </Text>
+        <Trans>
+          {formatCount(profile.postsCount || 0)}{' '}
+          <Text
+            style={[t.atoms.text_contrast_medium, a.font_normal, a.text_md]}>
+            {plural(profile.postsCount || 0, {one: 'post', other: 'posts'})}
+          </Text>
+        </Trans>
       </Text>
     </View>
   )
diff --git a/src/screens/Profile/Header/ProfileHeaderLabeler.tsx b/src/screens/Profile/Header/ProfileHeaderLabeler.tsx
index cbac0b66c..459bd0d95 100644
--- a/src/screens/Profile/Header/ProfileHeaderLabeler.tsx
+++ b/src/screens/Profile/Header/ProfileHeaderLabeler.tsx
@@ -7,11 +7,10 @@ import {
   ModerationOpts,
   RichText as RichTextAPI,
 } from '@atproto/api'
-import {msg, Trans} from '@lingui/macro'
+import {msg, Plural, plural, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
 import {isAppLabeler} from '#/lib/moderation'
-import {pluralize} from '#/lib/strings/helpers'
 import {logger} from '#/logger'
 import {Shadow} from '#/state/cache/types'
 import {useModalControls} from '#/state/modals'
@@ -283,12 +282,10 @@ let ProfileHeaderLabeler = ({
                       },
                     }}
                     size="tiny"
-                    label={_(
-                      msg`Liked by ${likeCount} ${pluralize(
-                        likeCount,
-                        'user',
-                      )}`,
-                    )}>
+                    label={plural(likeCount, {
+                      one: 'Liked by # user',
+                      other: 'Liked by # users',
+                    })}>
                     {({hovered, focused, pressed}) => (
                       <Text
                         style={[
@@ -298,9 +295,11 @@ let ProfileHeaderLabeler = ({
                           (hovered || focused || pressed) &&
                             t.atoms.text_contrast_high,
                         ]}>
-                        <Trans>
-                          Liked by {likeCount} {pluralize(likeCount, 'user')}
-                        </Trans>
+                        <Plural
+                          value={likeCount}
+                          one="Liked by # user"
+                          other="Liked by # users"
+                        />
                       </Text>
                     )}
                   </Link>
diff --git a/src/view/com/feeds/FeedSourceCard.tsx b/src/view/com/feeds/FeedSourceCard.tsx
index 9300b4159..8a21d86ae 100644
--- a/src/view/com/feeds/FeedSourceCard.tsx
+++ b/src/view/com/feeds/FeedSourceCard.tsx
@@ -6,12 +6,11 @@ import {RichText} from '#/components/RichText'
 import {usePalette} from 'lib/hooks/usePalette'
 import {s} from 'lib/styles'
 import {UserAvatar} from '../util/UserAvatar'
-import {pluralize} from 'lib/strings/helpers'
 import {AtUri} from '@atproto/api'
 import * as Toast from 'view/com/util/Toast'
 import {sanitizeHandle} from 'lib/strings/handles'
 import {logger} from '#/logger'
-import {Trans, msg} from '@lingui/macro'
+import {Trans, msg, Plural} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {
   usePinFeedMutation,
@@ -265,10 +264,11 @@ export function FeedSourceCardLoaded({
 
         {showLikes && feed.type === 'feed' ? (
           <Text type="sm-medium" style={[pal.text, pal.textLight]}>
-            <Trans>
-              Liked by {feed.likeCount || 0}{' '}
-              {pluralize(feed.likeCount || 0, 'user')}
-            </Trans>
+            <Plural
+              value={feed.likeCount || 0}
+              one="Liked by # user"
+              other="Liked by # users"
+            />
           </Text>
         ) : null}
       </Pressable>
diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx
index 94844cb1a..c20a8e9ee 100644
--- a/src/view/com/notifications/FeedItem.tsx
+++ b/src/view/com/notifications/FeedItem.tsx
@@ -22,7 +22,7 @@ import {
   FontAwesomeIconStyle,
   Props,
 } from '@fortawesome/react-native-fontawesome'
-import {msg, Trans} from '@lingui/macro'
+import {msg, plural, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useQueryClient} from '@tanstack/react-query'
 
@@ -33,7 +33,6 @@ import {HeartIconSolid} from 'lib/icons'
 import {makeProfileLink} from 'lib/routes/links'
 import {sanitizeDisplayName} from 'lib/strings/display-names'
 import {sanitizeHandle} from 'lib/strings/handles'
-import {pluralize} from 'lib/strings/helpers'
 import {niceDate} from 'lib/strings/time'
 import {colors, s} from 'lib/styles'
 import {isWeb} from 'platform/detection'
@@ -176,6 +175,7 @@ let FeedItem = ({
     return null
   }
 
+  let formattedCount = authors.length > 1 ? formatCount(authors.length - 1) : ''
   return (
     <Link
       testID={`feedItem-by-${item.notification.author.handle}`}
@@ -236,8 +236,10 @@ let FeedItem = ({
                   <Trans>and</Trans>{' '}
                 </Text>
                 <Text style={[pal.text, s.bold]}>
-                  {formatCount(authors.length - 1)}{' '}
-                  {pluralize(authors.length - 1, 'other')}
+                  {plural(authors.length - 1, {
+                    one: `${formattedCount} other`,
+                    other: `${formattedCount} others`,
+                  })}
                 </Text>
               </>
             ) : undefined}
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
index cfb8bd93f..f644a5366 100644
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ b/src/view/com/post-thread/PostThreadItem.tsx
@@ -8,7 +8,7 @@ import {
   RichText as RichTextAPI,
 } from '@atproto/api'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {msg, Trans} from '@lingui/macro'
+import {msg, Plural, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
 import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
@@ -24,7 +24,7 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {makeProfileLink} from 'lib/routes/links'
 import {sanitizeDisplayName} from 'lib/strings/display-names'
 import {sanitizeHandle} from 'lib/strings/handles'
-import {countLines, pluralize} from 'lib/strings/helpers'
+import {countLines} from 'lib/strings/helpers'
 import {niceDate} from 'lib/strings/time'
 import {s} from 'lib/styles'
 import {isWeb} from 'platform/detection'
@@ -336,7 +336,11 @@ let PostThreadItemLoaded = ({
                       <Text type="xl-bold" style={pal.text}>
                         {formatCount(post.repostCount)}
                       </Text>{' '}
-                      {pluralize(post.repostCount, 'repost')}
+                      <Plural
+                        value={post.repostCount}
+                        one="repost"
+                        other="reposts"
+                      />
                     </Text>
                   </Link>
                 ) : null}
@@ -352,7 +356,7 @@ let PostThreadItemLoaded = ({
                       <Text type="xl-bold" style={pal.text}>
                         {formatCount(post.likeCount)}
                       </Text>{' '}
-                      {pluralize(post.likeCount, 'like')}
+                      <Plural value={post.likeCount} one="like" other="likes" />
                     </Text>
                   </Link>
                 ) : null}
diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx
index cb50ee6dc..7ebcde9a0 100644
--- a/src/view/com/util/post-ctrls/PostCtrls.tsx
+++ b/src/view/com/util/post-ctrls/PostCtrls.tsx
@@ -12,14 +12,13 @@ import {
   AtUri,
   RichText as RichTextAPI,
 } from '@atproto/api'
-import {msg} from '@lingui/macro'
+import {msg, plural} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
 import {HITSLOP_10, HITSLOP_20} from '#/lib/constants'
 import {CommentBottomArrow, HeartIcon, HeartIconSolid} from '#/lib/icons'
 import {makeProfileLink} from '#/lib/routes/links'
 import {shareUrl} from '#/lib/sharing'
-import {pluralize} from '#/lib/strings/helpers'
 import {toShareUrl} from '#/lib/strings/url-helpers'
 import {s} from '#/lib/styles'
 import {useTheme} from '#/lib/ThemeContext'
@@ -159,9 +158,10 @@ let PostCtrls = ({
             }
           }}
           accessibilityRole="button"
-          accessibilityLabel={`Reply (${post.replyCount} ${
-            post.replyCount === 1 ? 'reply' : 'replies'
-          })`}
+          accessibilityLabel={plural(post.replyCount || 0, {
+            one: 'Reply (# reply)',
+            other: 'Reply (# replies)',
+          })}
           accessibilityHint=""
           hitSlop={big ? HITSLOP_20 : HITSLOP_10}>
           <CommentBottomArrow
@@ -193,9 +193,17 @@ let PostCtrls = ({
             requireAuth(() => onPressToggleLike())
           }}
           accessibilityRole="button"
-          accessibilityLabel={`${
-            post.viewer?.like ? _(msg`Unlike`) : _(msg`Like`)
-          } (${post.likeCount} ${pluralize(post.likeCount || 0, 'like')})`}
+          accessibilityLabel={
+            post.viewer?.like
+              ? plural(post.likeCount || 0, {
+                  one: 'Unlike (# like)',
+                  other: 'Unlike (# likes)',
+                })
+              : plural(post.likeCount || 0, {
+                  one: 'Like (# like)',
+                  other: 'Like (# likes)',
+                })
+          }
           accessibilityHint=""
           hitSlop={big ? HITSLOP_20 : HITSLOP_10}>
           {post.viewer?.like ? (
diff --git a/src/view/com/util/post-ctrls/RepostButton.tsx b/src/view/com/util/post-ctrls/RepostButton.tsx
index cc3db50c8..c1af39a5d 100644
--- a/src/view/com/util/post-ctrls/RepostButton.tsx
+++ b/src/view/com/util/post-ctrls/RepostButton.tsx
@@ -4,11 +4,10 @@ import {RepostIcon} from 'lib/icons'
 import {s, colors} from 'lib/styles'
 import {useTheme} from 'lib/ThemeContext'
 import {Text} from '../text/Text'
-import {pluralize} from 'lib/strings/helpers'
 import {HITSLOP_10, HITSLOP_20} from 'lib/constants'
 import {useModalControls} from '#/state/modals'
 import {useRequireAuth} from '#/state/session'
-import {msg} from '@lingui/macro'
+import {msg, plural} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
 interface Props {
@@ -59,7 +58,7 @@ let RepostButton = ({
         isReposted
           ? _(msg`Undo repost`)
           : _(msg({message: 'Repost', context: 'action'}))
-      } (${repostCount} ${pluralize(repostCount || 0, 'repost')})`}
+      } (${plural(repostCount || 0, {one: '# repost', other: '# reposts'})})`}
       accessibilityHint=""
       hitSlop={big ? HITSLOP_20 : HITSLOP_10}>
       <RepostIcon
diff --git a/src/view/screens/PreferencesFollowingFeed.tsx b/src/view/screens/PreferencesFollowingFeed.tsx
index b4acbcd44..724c3f265 100644
--- a/src/view/screens/PreferencesFollowingFeed.tsx
+++ b/src/view/screens/PreferencesFollowingFeed.tsx
@@ -12,7 +12,7 @@ import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
 import {ViewHeader} from 'view/com/util/ViewHeader'
 import {CenteredView} from 'view/com/util/Views'
 import debounce from 'lodash.debounce'
-import {Trans, msg} from '@lingui/macro'
+import {Trans, msg, Plural} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {
   usePreferencesQuery,
@@ -27,7 +27,6 @@ function RepliesThresholdInput({
   initialValue: number
 }) {
   const pal = usePalette('default')
-  const {_} = useLingui()
   const [value, setValue] = useState(initialValue)
   const {mutate: setFeedViewPref} = useSetFeedViewPreferencesMutation()
   const preValue = React.useRef(initialValue)
@@ -64,13 +63,12 @@ function RepliesThresholdInput({
         thumbTintColor={colors.blue3}
       />
       <Text type="xs" style={pal.text}>
-        {value === 0
-          ? _(msg`Show all replies`)
-          : _(
-              msg`Show replies with at least ${value} ${
-                value > 1 ? `likes` : `like`
-              }`,
-            )}
+        <Plural
+          value={value}
+          _0="Show all replies"
+          one="Show replies with at least # like"
+          other="Show replies with at least # likes"
+        />
       </Text>
     </View>
   )
diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx
index 814c1e855..c6fac743a 100644
--- a/src/view/screens/ProfileFeed.tsx
+++ b/src/view/screens/ProfileFeed.tsx
@@ -1,6 +1,6 @@
 import React, {useCallback, useMemo} from 'react'
 import {Pressable, StyleSheet, View} from 'react-native'
-import {msg, Trans} from '@lingui/macro'
+import {msg, Plural, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useIsFocused, useNavigation} from '@react-navigation/native'
 import {NativeStackScreenProps} from '@react-navigation/native-stack'
@@ -35,7 +35,6 @@ import {makeCustomFeedLink} from 'lib/routes/links'
 import {CommonNavigatorParams} from 'lib/routes/types'
 import {NavigationProp} from 'lib/routes/types'
 import {shareUrl} from 'lib/sharing'
-import {pluralize} from 'lib/strings/helpers'
 import {makeRecordUri} from 'lib/strings/url-helpers'
 import {toShareUrl} from 'lib/strings/url-helpers'
 import {s} from 'lib/styles'
@@ -597,7 +596,11 @@ function AboutSection({
             label={_(msg`View users who like this feed`)}
             to={makeCustomFeedLink(feedOwnerDid, feedRkey, 'liked-by')}
             style={[t.atoms.text_contrast_medium, a.font_bold]}>
-            {_(msg`Liked by ${likeCount} ${pluralize(likeCount, 'user')}`)}
+            <Plural
+              value={likeCount}
+              one="Liked by # user"
+              other="Liked by # users"
+            />
           </InlineLinkText>
         )}
       </View>
diff --git a/src/view/shell/Drawer.tsx b/src/view/shell/Drawer.tsx
index d8e604ec3..04f144e87 100644
--- a/src/view/shell/Drawer.tsx
+++ b/src/view/shell/Drawer.tsx
@@ -13,7 +13,7 @@ import {
   FontAwesomeIcon,
   FontAwesomeIconStyle,
 } from '@fortawesome/react-native-fontawesome'
-import {msg, Trans} from '@lingui/macro'
+import {msg, Plural, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {StackActions, useNavigation} from '@react-navigation/native'
 
@@ -42,7 +42,6 @@ import {
 } from 'lib/icons'
 import {getTabState, TabState} from 'lib/routes/helpers'
 import {NavigationProp} from 'lib/routes/types'
-import {pluralize} from 'lib/strings/helpers'
 import {colors, s} from 'lib/styles'
 import {useTheme} from 'lib/ThemeContext'
 import {isWeb} from 'platform/detection'
@@ -90,15 +89,26 @@ let DrawerProfileCard = ({
         @{account.handle}
       </Text>
       <Text type="xl" style={[pal.textLight, styles.profileCardFollowers]}>
-        <Text type="xl-medium" style={pal.text}>
-          {formatCountShortOnly(profile?.followersCount ?? 0)}
-        </Text>{' '}
-        {pluralize(profile?.followersCount || 0, 'follower')} &middot;{' '}
+        <Trans>
+          <Text type="xl-medium" style={pal.text}>
+            {formatCountShortOnly(profile?.followersCount ?? 0)}
+          </Text>{' '}
+          <Plural
+            value={profile?.followersCount || 0}
+            one="follower"
+            other="followers"
+          />{' '}
+          &middot;{' '}
+        </Trans>
         <Trans>
           <Text type="xl-medium" style={pal.text}>
             {formatCountShortOnly(profile?.followsCount ?? 0)}
           </Text>{' '}
-          following
+          <Plural
+            value={profile?.followsCount || 0}
+            one="following"
+            other="following"
+          />
         </Trans>
       </Text>
     </TouchableOpacity>
diff --git a/yarn.lock b/yarn.lock
index e9c7fa571..2f9b59ecf 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3582,6 +3582,54 @@
   resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.1.tgz#16308cea045f0fc777b6ff20a9f25474dd8293d2"
   integrity sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==
 
+"@formatjs/ecma402-abstract@1.18.0":
+  version "1.18.0"
+  resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.18.0.tgz#e2120e7101020140661b58430a7ff4262705a2f2"
+  integrity sha512-PEVLoa3zBevWSCZzPIM/lvPCi8P5l4G+NXQMc/CjEiaCWgyHieUoo0nM7Bs0n/NbuQ6JpXEolivQ9pKSBHaDlA==
+  dependencies:
+    "@formatjs/intl-localematcher" "0.5.2"
+    tslib "^2.4.0"
+
+"@formatjs/intl-enumerator@1.4.3":
+  version "1.4.3"
+  resolved "https://registry.yarnpkg.com/@formatjs/intl-enumerator/-/intl-enumerator-1.4.3.tgz#8d278c273485d7c6219916509fbd51ce3142064d"
+  integrity sha512-0NpTmAQnDokPoB5aVtXvOdtrUq/uEuPPhBUAr57TYYDjI5MwfFXt8F6JCm6s6CPI0inL8+nxPLjjqH0qyNnP4Q==
+  dependencies:
+    tslib "^2.4.0"
+
+"@formatjs/intl-getcanonicallocales@2.3.0":
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/@formatjs/intl-getcanonicallocales/-/intl-getcanonicallocales-2.3.0.tgz#b6c6fa1c664e30a61f27fa6399a76159d82a5842"
+  integrity sha512-BOXbLwqQ7nKua/l7tKqDLRN84WupDXFDhGJQMFvsMVA2dKuOdRaWTxWpL3cJ7qPkoNw11Jf+Xpj4OSPBBvW0eQ==
+  dependencies:
+    tslib "^2.4.0"
+
+"@formatjs/intl-locale@^3.4.3":
+  version "3.4.3"
+  resolved "https://registry.yarnpkg.com/@formatjs/intl-locale/-/intl-locale-3.4.3.tgz#fdd2a3978b03aa76965abbca86526bb1d02973b6"
+  integrity sha512-g/35yMikkkRmLYmqE4W74gvZyKa768oC9OmUFzfLmH3CVYF3v2kvAZI0WsxWLbxYj8TT7wBDeLIL3aIlRw4Osw==
+  dependencies:
+    "@formatjs/ecma402-abstract" "1.18.0"
+    "@formatjs/intl-enumerator" "1.4.3"
+    "@formatjs/intl-getcanonicallocales" "2.3.0"
+    tslib "^2.4.0"
+
+"@formatjs/intl-localematcher@0.5.2":
+  version "0.5.2"
+  resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.5.2.tgz#5fcf029fd218905575e5080fa33facdcb623d532"
+  integrity sha512-txaaE2fiBMagLrR4jYhxzFO6wEdEG4TPMqrzBAcbr4HFUYzH/YC+lg6OIzKCHm8WgDdyQevxbAAV1OgcXctuGw==
+  dependencies:
+    tslib "^2.4.0"
+
+"@formatjs/intl-pluralrules@^5.2.10":
+  version "5.2.10"
+  resolved "https://registry.yarnpkg.com/@formatjs/intl-pluralrules/-/intl-pluralrules-5.2.10.tgz#379fc06133625df0cae715c1d902001974ff3279"
+  integrity sha512-wfJypePrbOByaZVPP1moLXHgS9LeAvi9coP95XZX7ySVrwdDGPnxz9Pw+o7J1o8AjLxjiqGrvAi74key5zzIjQ==
+  dependencies:
+    "@formatjs/ecma402-abstract" "1.18.0"
+    "@formatjs/intl-localematcher" "0.5.2"
+    tslib "^2.4.0"
+
 "@fortawesome/fontawesome-common-types@6.4.2":
   version "6.4.2"
   resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.2.tgz#1766039cad33f8ad87f9467b98e0d18fbc8f01c5"