An ATproto social media client -- with an independent Appview.

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>

+208 -126
+1 -30
__tests__/lib/string.test.ts
··· 3 3 import {parseEmbedPlayerFromUrl} from 'lib/strings/embed-player' 4 4 import {cleanError} from '../../src/lib/strings/errors' 5 5 import {createFullHandle, makeValidHandle} from '../../src/lib/strings/handles' 6 - import {enforceLen, pluralize} from '../../src/lib/strings/helpers' 6 + import {enforceLen} from '../../src/lib/strings/helpers' 7 7 import {detectLinkables} from '../../src/lib/strings/rich-text-detection' 8 8 import {shortenLinks} from '../../src/lib/strings/rich-text-manip' 9 9 import {ago} from '../../src/lib/strings/time' ··· 122 122 for (let i = 0; i < inputs.length; i++) { 123 123 const input = inputs[i] 124 124 const output = detectLinkables(input) 125 - expect(output).toEqual(outputs[i]) 126 - } 127 - }) 128 - }) 129 - 130 - describe('pluralize', () => { 131 - const inputs: [number, string, string?][] = [ 132 - [1, 'follower'], 133 - [1, 'member'], 134 - [100, 'post'], 135 - [1000, 'repost'], 136 - [10000, 'upvote'], 137 - [100000, 'other'], 138 - [2, 'man', 'men'], 139 - ] 140 - const outputs = [ 141 - 'follower', 142 - 'member', 143 - 'posts', 144 - 'reposts', 145 - 'upvotes', 146 - 'others', 147 - 'men', 148 - ] 149 - 150 - it('correctly pluralizes a set of words', () => { 151 - for (let i = 0; i < inputs.length; i++) { 152 - const input = inputs[i] 153 - const output = pluralize(...input) 154 125 expect(output).toEqual(outputs[i]) 155 126 } 156 127 })
+1
modules/expo-scroll-forwarder/src/ExpoScrollForwarderView.ios.tsx
··· 1 1 import {requireNativeViewManager} from 'expo-modules-core' 2 2 import * as React from 'react' 3 + 3 4 import {ExpoScrollForwarderViewProps} from './ExpoScrollForwarder.types' 4 5 5 6 const NativeView: React.ComponentType<ExpoScrollForwarderViewProps> =
+2
package.json
··· 60 60 "@expo/webpack-config": "^19.0.0", 61 61 "@floating-ui/dom": "^1.6.3", 62 62 "@floating-ui/react-dom": "^2.0.8", 63 + "@formatjs/intl-locale": "^3.4.3", 64 + "@formatjs/intl-pluralrules": "^5.2.10", 63 65 "@fortawesome/fontawesome-svg-core": "^6.1.1", 64 66 "@fortawesome/free-regular-svg-icons": "^6.1.1", 65 67 "@fortawesome/free-solid-svg-icons": "^6.1.1",
+2 -5
src/components/LabelingServiceCard/index.tsx
··· 1 1 import React from 'react' 2 2 import {View} from 'react-native' 3 - import {msg, Trans} from '@lingui/macro' 3 + import {msg, Plural, Trans} from '@lingui/macro' 4 4 import {useLingui} from '@lingui/react' 5 5 import {AppBskyLabelerDefs} from '@atproto/api' 6 6 ··· 13 13 import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '../icons/Chevron' 14 14 import {UserAvatar} from '#/view/com/util/UserAvatar' 15 15 import {sanitizeHandle} from '#/lib/strings/handles' 16 - import {pluralize} from '#/lib/strings/helpers' 17 16 18 17 type LabelingServiceProps = { 19 18 labeler: AppBskyLabelerDefs.LabelerViewDetailed ··· 69 68 t.atoms.text_contrast_medium, 70 69 {fontWeight: '500'}, 71 70 ]}> 72 - <Trans> 73 - Liked by {count} {pluralize(count, 'user')} 74 - </Trans> 71 + <Plural value={count} one="Liked by # user" other="Liked by # users" /> 75 72 </Text> 76 73 ) 77 74 }
+12 -4
src/components/ProfileHoverCard/index.web.tsx
··· 2 2 import {View} from 'react-native' 3 3 import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api' 4 4 import {flip, offset, shift, size, useFloating} from '@floating-ui/react-dom' 5 - import {msg, Trans} from '@lingui/macro' 5 + import {msg, plural, Trans} from '@lingui/macro' 6 6 import {useLingui} from '@lingui/react' 7 7 8 8 import {makeProfileLink} from '#/lib/routes/links' 9 9 import {sanitizeDisplayName} from '#/lib/strings/display-names' 10 10 import {sanitizeHandle} from '#/lib/strings/handles' 11 - import {pluralize} from '#/lib/strings/helpers' 12 11 import {useModerationOpts} from '#/state/preferences/moderation-opts' 13 12 import {usePrefetchProfileQuery, useProfileQuery} from '#/state/queries/profile' 14 13 import {useSession} from '#/state/session' ··· 371 370 const blockHide = profile.viewer?.blocking || profile.viewer?.blockedBy 372 371 const following = formatCount(profile.followsCount || 0) 373 372 const followers = formatCount(profile.followersCount || 0) 374 - const pluralizedFollowers = pluralize(profile.followersCount || 0, 'follower') 373 + const pluralizedFollowers = plural(profile.followersCount || 0, { 374 + one: 'follower', 375 + other: 'followers', 376 + }) 377 + const pluralizedFollowings = plural(profile.followsCount || 0, { 378 + one: 'following', 379 + other: 'following', 380 + }) 375 381 const profileURL = makeProfileLink({ 376 382 did: profile.did, 377 383 handle: profile.handle, ··· 448 454 onPress={hide}> 449 455 <Trans> 450 456 <Text style={[a.text_md, a.font_bold]}>{following} </Text> 451 - <Text style={[t.atoms.text_contrast_medium]}>following</Text> 457 + <Text style={[t.atoms.text_contrast_medium]}> 458 + {pluralizedFollowings} 459 + </Text> 452 460 </Trans> 453 461 </InlineLinkText> 454 462 </View>
+12 -6
src/components/moderation/LabelsOnMe.tsx
··· 1 1 import React from 'react' 2 2 import {StyleProp, View, ViewStyle} from 'react-native' 3 3 import {AppBskyFeedDefs, ComAtprotoLabelDefs} from '@atproto/api' 4 - import {msg, Trans} from '@lingui/macro' 4 + import {msg, Plural} from '@lingui/macro' 5 5 import {useLingui} from '@lingui/react' 6 6 import {useSession} from '#/state/session' 7 7 ··· 39 39 return null 40 40 } 41 41 42 - const labelTarget = isAccount ? _(msg`account`) : _(msg`content`) 43 42 return ( 44 43 <View style={[a.flex_row, style]}> 45 44 <LabelsOnMeDialog control={control} subject={details} labels={labels} /> ··· 54 53 }}> 55 54 <ButtonIcon position="left" icon={CircleInfo} /> 56 55 <ButtonText style={[a.leading_snug]}> 57 - {labels.length}{' '} 58 - {labels.length === 1 ? ( 59 - <Trans>label has been placed on this {labelTarget}</Trans> 56 + {isAccount ? ( 57 + <Plural 58 + value={labels.length} 59 + one="# label has been placed on this account" 60 + other="# labels has been placed on this account" 61 + /> 60 62 ) : ( 61 - <Trans>labels have been placed on this {labelTarget}</Trans> 63 + <Plural 64 + value={labels.length} 65 + one="# label has been placed on this content" 66 + other="# labels has been placed on this content" 67 + /> 62 68 )} 63 69 </ButtonText> 64 70 </Button>
-10
src/lib/strings/helpers.ts
··· 1 - export function pluralize(n: number, base: string, plural?: string): string { 2 - if (n === 1) { 3 - return base 4 - } 5 - if (plural) { 6 - return plural 7 - } 8 - return base + 's' 9 - } 10 - 11 1 export function enforceLen( 12 2 str: string, 13 3 len: number,
+20
src/locale/i18n.ts
··· 1 + import '@formatjs/intl-locale/polyfill' 2 + import '@formatjs/intl-pluralrules/polyfill' 3 + import '@formatjs/intl-pluralrules/locale-data/en' 4 + 1 5 import {useEffect} from 'react' 2 6 import {i18n} from '@lingui/core' 3 7 ··· 29 33 switch (locale) { 30 34 case AppLanguage.ca: { 31 35 i18n.loadAndActivate({locale, messages: messagesCa}) 36 + await import('@formatjs/intl-pluralrules/locale-data/ca') 32 37 break 33 38 } 34 39 case AppLanguage.de: { 35 40 i18n.loadAndActivate({locale, messages: messagesDe}) 41 + await import('@formatjs/intl-pluralrules/locale-data/de') 36 42 break 37 43 } 38 44 case AppLanguage.es: { 39 45 i18n.loadAndActivate({locale, messages: messagesEs}) 46 + await import('@formatjs/intl-pluralrules/locale-data/es') 40 47 break 41 48 } 42 49 case AppLanguage.fi: { 43 50 i18n.loadAndActivate({locale, messages: messagesFi}) 51 + await import('@formatjs/intl-pluralrules/locale-data/fi') 44 52 break 45 53 } 46 54 case AppLanguage.fr: { 47 55 i18n.loadAndActivate({locale, messages: messagesFr}) 56 + await import('@formatjs/intl-pluralrules/locale-data/fr') 48 57 break 49 58 } 50 59 case AppLanguage.ga: { 51 60 i18n.loadAndActivate({locale, messages: messagesGa}) 61 + await import('@formatjs/intl-pluralrules/locale-data/ga') 52 62 break 53 63 } 54 64 case AppLanguage.hi: { 55 65 i18n.loadAndActivate({locale, messages: messagesHi}) 66 + await import('@formatjs/intl-pluralrules/locale-data/hi') 56 67 break 57 68 } 58 69 case AppLanguage.id: { 59 70 i18n.loadAndActivate({locale, messages: messagesId}) 71 + await import('@formatjs/intl-pluralrules/locale-data/id') 60 72 break 61 73 } 62 74 case AppLanguage.it: { 63 75 i18n.loadAndActivate({locale, messages: messagesIt}) 76 + await import('@formatjs/intl-pluralrules/locale-data/it') 64 77 break 65 78 } 66 79 case AppLanguage.ja: { 67 80 i18n.loadAndActivate({locale, messages: messagesJa}) 81 + await import('@formatjs/intl-pluralrules/locale-data/ja') 68 82 break 69 83 } 70 84 case AppLanguage.ko: { 71 85 i18n.loadAndActivate({locale, messages: messagesKo}) 86 + await import('@formatjs/intl-pluralrules/locale-data/ko') 72 87 break 73 88 } 74 89 case AppLanguage.pt_BR: { 75 90 i18n.loadAndActivate({locale, messages: messagesPt_BR}) 91 + await import('@formatjs/intl-pluralrules/locale-data/pt') 76 92 break 77 93 } 78 94 case AppLanguage.tr: { 79 95 i18n.loadAndActivate({locale, messages: messagesTr}) 96 + await import('@formatjs/intl-pluralrules/locale-data/tr') 80 97 break 81 98 } 82 99 case AppLanguage.uk: { 83 100 i18n.loadAndActivate({locale, messages: messagesUk}) 101 + await import('@formatjs/intl-pluralrules/locale-data/uk') 84 102 break 85 103 } 86 104 case AppLanguage.zh_CN: { 87 105 i18n.loadAndActivate({locale, messages: messagesZh_CN}) 106 + await import('@formatjs/intl-pluralrules/locale-data/zh') 88 107 break 89 108 } 90 109 case AppLanguage.zh_TW: { 91 110 i18n.loadAndActivate({locale, messages: messagesZh_TW}) 111 + await import('@formatjs/intl-pluralrules/locale-data/zh') 92 112 break 93 113 } 94 114 default: {
+9 -4
src/screens/Deactivated.tsx
··· 1 1 import React from 'react' 2 2 import {View} from 'react-native' 3 3 import {useSafeAreaInsets} from 'react-native-safe-area-context' 4 - import {msg, Trans} from '@lingui/macro' 4 + import {msg, plural, Trans} from '@lingui/macro' 5 5 import {useLingui} from '@lingui/react' 6 6 7 - import {pluralize} from '#/lib/strings/helpers' 8 7 import {logger} from '#/logger' 9 8 import {isWeb} from '#/platform/detection' 10 9 import {isSessionDeactivated, useAgent, useSessionApi} from '#/state/session' ··· 205 204 return undefined 206 205 } 207 206 // hours 208 - return `${estimatedTimeHrs} ${pluralize(estimatedTimeHrs, 'hour')}` 207 + return `${estimatedTimeHrs} ${plural(estimatedTimeHrs, { 208 + one: 'hour', 209 + other: 'hours', 210 + })}` 209 211 } 210 212 // minutes 211 - return `${estimatedTimeMins} ${pluralize(estimatedTimeMins, 'minute')}` 213 + return `${estimatedTimeMins} ${plural(estimatedTimeMins, { 214 + one: 'minute', 215 + other: 'minutes', 216 + })}` 212 217 } 213 218 return undefined 214 219 }
+23 -12
src/screens/Profile/Header/Metrics.tsx
··· 1 1 import React from 'react' 2 2 import {View} from 'react-native' 3 3 import {AppBskyActorDefs} from '@atproto/api' 4 - import {msg, Trans} from '@lingui/macro' 4 + import {msg, plural, Trans} from '@lingui/macro' 5 5 import {useLingui} from '@lingui/react' 6 6 7 - import {pluralize} from '#/lib/strings/helpers' 8 7 import {Shadow} from '#/state/cache/types' 9 8 import {makeProfileLink} from 'lib/routes/links' 10 9 import {formatCount} from 'view/com/util/numeric/format' ··· 21 20 const {_} = useLingui() 22 21 const following = formatCount(profile.followsCount || 0) 23 22 const followers = formatCount(profile.followersCount || 0) 24 - const pluralizedFollowers = pluralize(profile.followersCount || 0, 'follower') 23 + const pluralizedFollowers = plural(profile.followersCount || 0, { 24 + one: 'follower', 25 + other: 'followers', 26 + }) 27 + const pluralizedFollowings = plural(profile.followsCount || 0, { 28 + one: 'following', 29 + other: 'following', 30 + }) 25 31 26 32 return ( 27 33 <View ··· 32 38 style={[a.flex_row, t.atoms.text]} 33 39 to={makeProfileLink(profile, 'followers')} 34 40 label={`${followers} ${pluralizedFollowers}`}> 35 - <Text style={[a.font_bold, a.text_md]}>{followers} </Text> 36 - <Text style={[t.atoms.text_contrast_medium, a.text_md]}> 37 - {pluralizedFollowers} 38 - </Text> 41 + <Trans> 42 + <Text style={[a.font_bold, a.text_md]}>{followers} </Text> 43 + <Text style={[t.atoms.text_contrast_medium, a.text_md]}> 44 + {pluralizedFollowers} 45 + </Text> 46 + </Trans> 39 47 </InlineLinkText> 40 48 <InlineLinkText 41 49 testID="profileHeaderFollowsButton" ··· 45 53 <Trans> 46 54 <Text style={[a.font_bold, a.text_md]}>{following} </Text> 47 55 <Text style={[t.atoms.text_contrast_medium, a.text_md]}> 48 - following 56 + {pluralizedFollowings} 49 57 </Text> 50 58 </Trans> 51 59 </InlineLinkText> 52 60 <Text style={[a.font_bold, t.atoms.text, a.text_md]}> 53 - {formatCount(profile.postsCount || 0)}{' '} 54 - <Text style={[t.atoms.text_contrast_medium, a.font_normal, a.text_md]}> 55 - {pluralize(profile.postsCount || 0, 'post')} 56 - </Text> 61 + <Trans> 62 + {formatCount(profile.postsCount || 0)}{' '} 63 + <Text 64 + style={[t.atoms.text_contrast_medium, a.font_normal, a.text_md]}> 65 + {plural(profile.postsCount || 0, {one: 'post', other: 'posts'})} 66 + </Text> 67 + </Trans> 57 68 </Text> 58 69 </View> 59 70 )
+10 -11
src/screens/Profile/Header/ProfileHeaderLabeler.tsx
··· 7 7 ModerationOpts, 8 8 RichText as RichTextAPI, 9 9 } from '@atproto/api' 10 - import {msg, Trans} from '@lingui/macro' 10 + import {msg, Plural, plural, Trans} from '@lingui/macro' 11 11 import {useLingui} from '@lingui/react' 12 12 13 13 import {isAppLabeler} from '#/lib/moderation' 14 - import {pluralize} from '#/lib/strings/helpers' 15 14 import {logger} from '#/logger' 16 15 import {Shadow} from '#/state/cache/types' 17 16 import {useModalControls} from '#/state/modals' ··· 283 282 }, 284 283 }} 285 284 size="tiny" 286 - label={_( 287 - msg`Liked by ${likeCount} ${pluralize( 288 - likeCount, 289 - 'user', 290 - )}`, 291 - )}> 285 + label={plural(likeCount, { 286 + one: 'Liked by # user', 287 + other: 'Liked by # users', 288 + })}> 292 289 {({hovered, focused, pressed}) => ( 293 290 <Text 294 291 style={[ ··· 298 295 (hovered || focused || pressed) && 299 296 t.atoms.text_contrast_high, 300 297 ]}> 301 - <Trans> 302 - Liked by {likeCount} {pluralize(likeCount, 'user')} 303 - </Trans> 298 + <Plural 299 + value={likeCount} 300 + one="Liked by # user" 301 + other="Liked by # users" 302 + /> 304 303 </Text> 305 304 )} 306 305 </Link>
+6 -6
src/view/com/feeds/FeedSourceCard.tsx
··· 6 6 import {usePalette} from 'lib/hooks/usePalette' 7 7 import {s} from 'lib/styles' 8 8 import {UserAvatar} from '../util/UserAvatar' 9 - import {pluralize} from 'lib/strings/helpers' 10 9 import {AtUri} from '@atproto/api' 11 10 import * as Toast from 'view/com/util/Toast' 12 11 import {sanitizeHandle} from 'lib/strings/handles' 13 12 import {logger} from '#/logger' 14 - import {Trans, msg} from '@lingui/macro' 13 + import {Trans, msg, Plural} from '@lingui/macro' 15 14 import {useLingui} from '@lingui/react' 16 15 import { 17 16 usePinFeedMutation, ··· 265 264 266 265 {showLikes && feed.type === 'feed' ? ( 267 266 <Text type="sm-medium" style={[pal.text, pal.textLight]}> 268 - <Trans> 269 - Liked by {feed.likeCount || 0}{' '} 270 - {pluralize(feed.likeCount || 0, 'user')} 271 - </Trans> 267 + <Plural 268 + value={feed.likeCount || 0} 269 + one="Liked by # user" 270 + other="Liked by # users" 271 + /> 272 272 </Text> 273 273 ) : null} 274 274 </Pressable>
+6 -4
src/view/com/notifications/FeedItem.tsx
··· 22 22 FontAwesomeIconStyle, 23 23 Props, 24 24 } from '@fortawesome/react-native-fontawesome' 25 - import {msg, Trans} from '@lingui/macro' 25 + import {msg, plural, Trans} from '@lingui/macro' 26 26 import {useLingui} from '@lingui/react' 27 27 import {useQueryClient} from '@tanstack/react-query' 28 28 ··· 33 33 import {makeProfileLink} from 'lib/routes/links' 34 34 import {sanitizeDisplayName} from 'lib/strings/display-names' 35 35 import {sanitizeHandle} from 'lib/strings/handles' 36 - import {pluralize} from 'lib/strings/helpers' 37 36 import {niceDate} from 'lib/strings/time' 38 37 import {colors, s} from 'lib/styles' 39 38 import {isWeb} from 'platform/detection' ··· 176 175 return null 177 176 } 178 177 178 + let formattedCount = authors.length > 1 ? formatCount(authors.length - 1) : '' 179 179 return ( 180 180 <Link 181 181 testID={`feedItem-by-${item.notification.author.handle}`} ··· 236 236 <Trans>and</Trans>{' '} 237 237 </Text> 238 238 <Text style={[pal.text, s.bold]}> 239 - {formatCount(authors.length - 1)}{' '} 240 - {pluralize(authors.length - 1, 'other')} 239 + {plural(authors.length - 1, { 240 + one: `${formattedCount} other`, 241 + other: `${formattedCount} others`, 242 + })} 241 243 </Text> 242 244 </> 243 245 ) : undefined}
+8 -4
src/view/com/post-thread/PostThreadItem.tsx
··· 8 8 RichText as RichTextAPI, 9 9 } from '@atproto/api' 10 10 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 11 - import {msg, Trans} from '@lingui/macro' 11 + import {msg, Plural, Trans} from '@lingui/macro' 12 12 import {useLingui} from '@lingui/react' 13 13 14 14 import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped' ··· 24 24 import {makeProfileLink} from 'lib/routes/links' 25 25 import {sanitizeDisplayName} from 'lib/strings/display-names' 26 26 import {sanitizeHandle} from 'lib/strings/handles' 27 - import {countLines, pluralize} from 'lib/strings/helpers' 27 + import {countLines} from 'lib/strings/helpers' 28 28 import {niceDate} from 'lib/strings/time' 29 29 import {s} from 'lib/styles' 30 30 import {isWeb} from 'platform/detection' ··· 336 336 <Text type="xl-bold" style={pal.text}> 337 337 {formatCount(post.repostCount)} 338 338 </Text>{' '} 339 - {pluralize(post.repostCount, 'repost')} 339 + <Plural 340 + value={post.repostCount} 341 + one="repost" 342 + other="reposts" 343 + /> 340 344 </Text> 341 345 </Link> 342 346 ) : null} ··· 352 356 <Text type="xl-bold" style={pal.text}> 353 357 {formatCount(post.likeCount)} 354 358 </Text>{' '} 355 - {pluralize(post.likeCount, 'like')} 359 + <Plural value={post.likeCount} one="like" other="likes" /> 356 360 </Text> 357 361 </Link> 358 362 ) : null}
+16 -8
src/view/com/util/post-ctrls/PostCtrls.tsx
··· 12 12 AtUri, 13 13 RichText as RichTextAPI, 14 14 } from '@atproto/api' 15 - import {msg} from '@lingui/macro' 15 + import {msg, plural} from '@lingui/macro' 16 16 import {useLingui} from '@lingui/react' 17 17 18 18 import {HITSLOP_10, HITSLOP_20} from '#/lib/constants' 19 19 import {CommentBottomArrow, HeartIcon, HeartIconSolid} from '#/lib/icons' 20 20 import {makeProfileLink} from '#/lib/routes/links' 21 21 import {shareUrl} from '#/lib/sharing' 22 - import {pluralize} from '#/lib/strings/helpers' 23 22 import {toShareUrl} from '#/lib/strings/url-helpers' 24 23 import {s} from '#/lib/styles' 25 24 import {useTheme} from '#/lib/ThemeContext' ··· 159 158 } 160 159 }} 161 160 accessibilityRole="button" 162 - accessibilityLabel={`Reply (${post.replyCount} ${ 163 - post.replyCount === 1 ? 'reply' : 'replies' 164 - })`} 161 + accessibilityLabel={plural(post.replyCount || 0, { 162 + one: 'Reply (# reply)', 163 + other: 'Reply (# replies)', 164 + })} 165 165 accessibilityHint="" 166 166 hitSlop={big ? HITSLOP_20 : HITSLOP_10}> 167 167 <CommentBottomArrow ··· 193 193 requireAuth(() => onPressToggleLike()) 194 194 }} 195 195 accessibilityRole="button" 196 - accessibilityLabel={`${ 197 - post.viewer?.like ? _(msg`Unlike`) : _(msg`Like`) 198 - } (${post.likeCount} ${pluralize(post.likeCount || 0, 'like')})`} 196 + accessibilityLabel={ 197 + post.viewer?.like 198 + ? plural(post.likeCount || 0, { 199 + one: 'Unlike (# like)', 200 + other: 'Unlike (# likes)', 201 + }) 202 + : plural(post.likeCount || 0, { 203 + one: 'Like (# like)', 204 + other: 'Like (# likes)', 205 + }) 206 + } 199 207 accessibilityHint="" 200 208 hitSlop={big ? HITSLOP_20 : HITSLOP_10}> 201 209 {post.viewer?.like ? (
+2 -3
src/view/com/util/post-ctrls/RepostButton.tsx
··· 4 4 import {s, colors} from 'lib/styles' 5 5 import {useTheme} from 'lib/ThemeContext' 6 6 import {Text} from '../text/Text' 7 - import {pluralize} from 'lib/strings/helpers' 8 7 import {HITSLOP_10, HITSLOP_20} from 'lib/constants' 9 8 import {useModalControls} from '#/state/modals' 10 9 import {useRequireAuth} from '#/state/session' 11 - import {msg} from '@lingui/macro' 10 + import {msg, plural} from '@lingui/macro' 12 11 import {useLingui} from '@lingui/react' 13 12 14 13 interface Props { ··· 59 58 isReposted 60 59 ? _(msg`Undo repost`) 61 60 : _(msg({message: 'Repost', context: 'action'})) 62 - } (${repostCount} ${pluralize(repostCount || 0, 'repost')})`} 61 + } (${plural(repostCount || 0, {one: '# repost', other: '# reposts'})})`} 63 62 accessibilityHint="" 64 63 hitSlop={big ? HITSLOP_20 : HITSLOP_10}> 65 64 <RepostIcon
+7 -9
src/view/screens/PreferencesFollowingFeed.tsx
··· 12 12 import {ViewHeader} from 'view/com/util/ViewHeader' 13 13 import {CenteredView} from 'view/com/util/Views' 14 14 import debounce from 'lodash.debounce' 15 - import {Trans, msg} from '@lingui/macro' 15 + import {Trans, msg, Plural} from '@lingui/macro' 16 16 import {useLingui} from '@lingui/react' 17 17 import { 18 18 usePreferencesQuery, ··· 27 27 initialValue: number 28 28 }) { 29 29 const pal = usePalette('default') 30 - const {_} = useLingui() 31 30 const [value, setValue] = useState(initialValue) 32 31 const {mutate: setFeedViewPref} = useSetFeedViewPreferencesMutation() 33 32 const preValue = React.useRef(initialValue) ··· 64 63 thumbTintColor={colors.blue3} 65 64 /> 66 65 <Text type="xs" style={pal.text}> 67 - {value === 0 68 - ? _(msg`Show all replies`) 69 - : _( 70 - msg`Show replies with at least ${value} ${ 71 - value > 1 ? `likes` : `like` 72 - }`, 73 - )} 66 + <Plural 67 + value={value} 68 + _0="Show all replies" 69 + one="Show replies with at least # like" 70 + other="Show replies with at least # likes" 71 + /> 74 72 </Text> 75 73 </View> 76 74 )
+6 -3
src/view/screens/ProfileFeed.tsx
··· 1 1 import React, {useCallback, useMemo} from 'react' 2 2 import {Pressable, StyleSheet, View} from 'react-native' 3 - import {msg, Trans} from '@lingui/macro' 3 + import {msg, Plural, Trans} from '@lingui/macro' 4 4 import {useLingui} from '@lingui/react' 5 5 import {useIsFocused, useNavigation} from '@react-navigation/native' 6 6 import {NativeStackScreenProps} from '@react-navigation/native-stack' ··· 35 35 import {CommonNavigatorParams} from 'lib/routes/types' 36 36 import {NavigationProp} from 'lib/routes/types' 37 37 import {shareUrl} from 'lib/sharing' 38 - import {pluralize} from 'lib/strings/helpers' 39 38 import {makeRecordUri} from 'lib/strings/url-helpers' 40 39 import {toShareUrl} from 'lib/strings/url-helpers' 41 40 import {s} from 'lib/styles' ··· 597 596 label={_(msg`View users who like this feed`)} 598 597 to={makeCustomFeedLink(feedOwnerDid, feedRkey, 'liked-by')} 599 598 style={[t.atoms.text_contrast_medium, a.font_bold]}> 600 - {_(msg`Liked by ${likeCount} ${pluralize(likeCount, 'user')}`)} 599 + <Plural 600 + value={likeCount} 601 + one="Liked by # user" 602 + other="Liked by # users" 603 + /> 601 604 </InlineLinkText> 602 605 )} 603 606 </View>
+17 -7
src/view/shell/Drawer.tsx
··· 13 13 FontAwesomeIcon, 14 14 FontAwesomeIconStyle, 15 15 } from '@fortawesome/react-native-fontawesome' 16 - import {msg, Trans} from '@lingui/macro' 16 + import {msg, Plural, Trans} from '@lingui/macro' 17 17 import {useLingui} from '@lingui/react' 18 18 import {StackActions, useNavigation} from '@react-navigation/native' 19 19 ··· 42 42 } from 'lib/icons' 43 43 import {getTabState, TabState} from 'lib/routes/helpers' 44 44 import {NavigationProp} from 'lib/routes/types' 45 - import {pluralize} from 'lib/strings/helpers' 46 45 import {colors, s} from 'lib/styles' 47 46 import {useTheme} from 'lib/ThemeContext' 48 47 import {isWeb} from 'platform/detection' ··· 90 89 @{account.handle} 91 90 </Text> 92 91 <Text type="xl" style={[pal.textLight, styles.profileCardFollowers]}> 93 - <Text type="xl-medium" style={pal.text}> 94 - {formatCountShortOnly(profile?.followersCount ?? 0)} 95 - </Text>{' '} 96 - {pluralize(profile?.followersCount || 0, 'follower')} &middot;{' '} 92 + <Trans> 93 + <Text type="xl-medium" style={pal.text}> 94 + {formatCountShortOnly(profile?.followersCount ?? 0)} 95 + </Text>{' '} 96 + <Plural 97 + value={profile?.followersCount || 0} 98 + one="follower" 99 + other="followers" 100 + />{' '} 101 + &middot;{' '} 102 + </Trans> 97 103 <Trans> 98 104 <Text type="xl-medium" style={pal.text}> 99 105 {formatCountShortOnly(profile?.followsCount ?? 0)} 100 106 </Text>{' '} 101 - following 107 + <Plural 108 + value={profile?.followsCount || 0} 109 + one="following" 110 + other="following" 111 + /> 102 112 </Trans> 103 113 </Text> 104 114 </TouchableOpacity>
+48
yarn.lock
··· 3582 3582 resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.1.tgz#16308cea045f0fc777b6ff20a9f25474dd8293d2" 3583 3583 integrity sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q== 3584 3584 3585 + "@formatjs/ecma402-abstract@1.18.0": 3586 + version "1.18.0" 3587 + resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.18.0.tgz#e2120e7101020140661b58430a7ff4262705a2f2" 3588 + integrity sha512-PEVLoa3zBevWSCZzPIM/lvPCi8P5l4G+NXQMc/CjEiaCWgyHieUoo0nM7Bs0n/NbuQ6JpXEolivQ9pKSBHaDlA== 3589 + dependencies: 3590 + "@formatjs/intl-localematcher" "0.5.2" 3591 + tslib "^2.4.0" 3592 + 3593 + "@formatjs/intl-enumerator@1.4.3": 3594 + version "1.4.3" 3595 + resolved "https://registry.yarnpkg.com/@formatjs/intl-enumerator/-/intl-enumerator-1.4.3.tgz#8d278c273485d7c6219916509fbd51ce3142064d" 3596 + integrity sha512-0NpTmAQnDokPoB5aVtXvOdtrUq/uEuPPhBUAr57TYYDjI5MwfFXt8F6JCm6s6CPI0inL8+nxPLjjqH0qyNnP4Q== 3597 + dependencies: 3598 + tslib "^2.4.0" 3599 + 3600 + "@formatjs/intl-getcanonicallocales@2.3.0": 3601 + version "2.3.0" 3602 + resolved "https://registry.yarnpkg.com/@formatjs/intl-getcanonicallocales/-/intl-getcanonicallocales-2.3.0.tgz#b6c6fa1c664e30a61f27fa6399a76159d82a5842" 3603 + integrity sha512-BOXbLwqQ7nKua/l7tKqDLRN84WupDXFDhGJQMFvsMVA2dKuOdRaWTxWpL3cJ7qPkoNw11Jf+Xpj4OSPBBvW0eQ== 3604 + dependencies: 3605 + tslib "^2.4.0" 3606 + 3607 + "@formatjs/intl-locale@^3.4.3": 3608 + version "3.4.3" 3609 + resolved "https://registry.yarnpkg.com/@formatjs/intl-locale/-/intl-locale-3.4.3.tgz#fdd2a3978b03aa76965abbca86526bb1d02973b6" 3610 + integrity sha512-g/35yMikkkRmLYmqE4W74gvZyKa768oC9OmUFzfLmH3CVYF3v2kvAZI0WsxWLbxYj8TT7wBDeLIL3aIlRw4Osw== 3611 + dependencies: 3612 + "@formatjs/ecma402-abstract" "1.18.0" 3613 + "@formatjs/intl-enumerator" "1.4.3" 3614 + "@formatjs/intl-getcanonicallocales" "2.3.0" 3615 + tslib "^2.4.0" 3616 + 3617 + "@formatjs/intl-localematcher@0.5.2": 3618 + version "0.5.2" 3619 + resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.5.2.tgz#5fcf029fd218905575e5080fa33facdcb623d532" 3620 + integrity sha512-txaaE2fiBMagLrR4jYhxzFO6wEdEG4TPMqrzBAcbr4HFUYzH/YC+lg6OIzKCHm8WgDdyQevxbAAV1OgcXctuGw== 3621 + dependencies: 3622 + tslib "^2.4.0" 3623 + 3624 + "@formatjs/intl-pluralrules@^5.2.10": 3625 + version "5.2.10" 3626 + resolved "https://registry.yarnpkg.com/@formatjs/intl-pluralrules/-/intl-pluralrules-5.2.10.tgz#379fc06133625df0cae715c1d902001974ff3279" 3627 + integrity sha512-wfJypePrbOByaZVPP1moLXHgS9LeAvi9coP95XZX7ySVrwdDGPnxz9Pw+o7J1o8AjLxjiqGrvAi74key5zzIjQ== 3628 + dependencies: 3629 + "@formatjs/ecma402-abstract" "1.18.0" 3630 + "@formatjs/intl-localematcher" "0.5.2" 3631 + tslib "^2.4.0" 3632 + 3585 3633 "@fortawesome/fontawesome-common-types@6.4.2": 3586 3634 version "6.4.2" 3587 3635 resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.2.tgz#1766039cad33f8ad87f9467b98e0d18fbc8f01c5"