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>
zio/stable
Paul Frazee 2024-05-06 16:55:57 -07:00 committed by GitHub
parent 2ca4b74955
commit 901feba6db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 208 additions and 126 deletions

View File

@ -3,7 +3,7 @@ import {RichText} from '@atproto/api'
import {parseEmbedPlayerFromUrl} from 'lib/strings/embed-player' import {parseEmbedPlayerFromUrl} from 'lib/strings/embed-player'
import {cleanError} from '../../src/lib/strings/errors' import {cleanError} from '../../src/lib/strings/errors'
import {createFullHandle, makeValidHandle} from '../../src/lib/strings/handles' 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 {detectLinkables} from '../../src/lib/strings/rich-text-detection'
import {shortenLinks} from '../../src/lib/strings/rich-text-manip' import {shortenLinks} from '../../src/lib/strings/rich-text-manip'
import {ago} from '../../src/lib/strings/time' 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', () => { describe('makeRecordUri', () => {
const inputs: [string, string, string][] = [ const inputs: [string, string, string][] = [
['alice.test', 'app.bsky.feed.post', '3jk7x4irgv52r'], ['alice.test', 'app.bsky.feed.post', '3jk7x4irgv52r'],

View File

@ -1,5 +1,6 @@
import {requireNativeViewManager} from 'expo-modules-core' import {requireNativeViewManager} from 'expo-modules-core'
import * as React from 'react' import * as React from 'react'
import {ExpoScrollForwarderViewProps} from './ExpoScrollForwarder.types' import {ExpoScrollForwarderViewProps} from './ExpoScrollForwarder.types'
const NativeView: React.ComponentType<ExpoScrollForwarderViewProps> = const NativeView: React.ComponentType<ExpoScrollForwarderViewProps> =

View File

@ -60,6 +60,8 @@
"@expo/webpack-config": "^19.0.0", "@expo/webpack-config": "^19.0.0",
"@floating-ui/dom": "^1.6.3", "@floating-ui/dom": "^1.6.3",
"@floating-ui/react-dom": "^2.0.8", "@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/fontawesome-svg-core": "^6.1.1",
"@fortawesome/free-regular-svg-icons": "^6.1.1", "@fortawesome/free-regular-svg-icons": "^6.1.1",
"@fortawesome/free-solid-svg-icons": "^6.1.1", "@fortawesome/free-solid-svg-icons": "^6.1.1",

View File

@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro' import {msg, Plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {AppBskyLabelerDefs} from '@atproto/api' 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 {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '../icons/Chevron'
import {UserAvatar} from '#/view/com/util/UserAvatar' import {UserAvatar} from '#/view/com/util/UserAvatar'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
import {pluralize} from '#/lib/strings/helpers'
type LabelingServiceProps = { type LabelingServiceProps = {
labeler: AppBskyLabelerDefs.LabelerViewDetailed labeler: AppBskyLabelerDefs.LabelerViewDetailed
@ -69,9 +68,7 @@ export function LikeCount({count}: {count: number}) {
t.atoms.text_contrast_medium, t.atoms.text_contrast_medium,
{fontWeight: '500'}, {fontWeight: '500'},
]}> ]}>
<Trans> <Plural value={count} one="Liked by # user" other="Liked by # users" />
Liked by {count} {pluralize(count, 'user')}
</Trans>
</Text> </Text>
) )
} }

View File

@ -2,13 +2,12 @@ import React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api' import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
import {flip, offset, shift, size, useFloating} from '@floating-ui/react-dom' 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 {useLingui} from '@lingui/react'
import {makeProfileLink} from '#/lib/routes/links' import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
import {pluralize} from '#/lib/strings/helpers'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {usePrefetchProfileQuery, useProfileQuery} from '#/state/queries/profile' import {usePrefetchProfileQuery, useProfileQuery} from '#/state/queries/profile'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
@ -371,7 +370,14 @@ function Inner({
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(profile.followsCount || 0)
const followers = formatCount(profile.followersCount || 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({ const profileURL = makeProfileLink({
did: profile.did, did: profile.did,
handle: profile.handle, handle: profile.handle,
@ -448,7 +454,9 @@ function Inner({
onPress={hide}> onPress={hide}>
<Trans> <Trans>
<Text style={[a.text_md, a.font_bold]}>{following} </Text> <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> </Trans>
</InlineLinkText> </InlineLinkText>
</View> </View>

View File

@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import {StyleProp, View, ViewStyle} from 'react-native' import {StyleProp, View, ViewStyle} from 'react-native'
import {AppBskyFeedDefs, ComAtprotoLabelDefs} from '@atproto/api' import {AppBskyFeedDefs, ComAtprotoLabelDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro' import {msg, Plural} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
@ -39,7 +39,6 @@ export function LabelsOnMe({
return null return null
} }
const labelTarget = isAccount ? _(msg`account`) : _(msg`content`)
return ( return (
<View style={[a.flex_row, style]}> <View style={[a.flex_row, style]}>
<LabelsOnMeDialog control={control} subject={details} labels={labels} /> <LabelsOnMeDialog control={control} subject={details} labels={labels} />
@ -54,11 +53,18 @@ export function LabelsOnMe({
}}> }}>
<ButtonIcon position="left" icon={CircleInfo} /> <ButtonIcon position="left" icon={CircleInfo} />
<ButtonText style={[a.leading_snug]}> <ButtonText style={[a.leading_snug]}>
{labels.length}{' '} {isAccount ? (
{labels.length === 1 ? ( <Plural
<Trans>label has been placed on this {labelTarget}</Trans> 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> </ButtonText>
</Button> </Button>

View File

@ -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( export function enforceLen(
str: string, str: string,
len: number, len: number,

View File

@ -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 {useEffect} from 'react'
import {i18n} from '@lingui/core' import {i18n} from '@lingui/core'
@ -29,66 +33,82 @@ export async function dynamicActivate(locale: AppLanguage) {
switch (locale) { switch (locale) {
case AppLanguage.ca: { case AppLanguage.ca: {
i18n.loadAndActivate({locale, messages: messagesCa}) i18n.loadAndActivate({locale, messages: messagesCa})
await import('@formatjs/intl-pluralrules/locale-data/ca')
break break
} }
case AppLanguage.de: { case AppLanguage.de: {
i18n.loadAndActivate({locale, messages: messagesDe}) i18n.loadAndActivate({locale, messages: messagesDe})
await import('@formatjs/intl-pluralrules/locale-data/de')
break break
} }
case AppLanguage.es: { case AppLanguage.es: {
i18n.loadAndActivate({locale, messages: messagesEs}) i18n.loadAndActivate({locale, messages: messagesEs})
await import('@formatjs/intl-pluralrules/locale-data/es')
break break
} }
case AppLanguage.fi: { case AppLanguage.fi: {
i18n.loadAndActivate({locale, messages: messagesFi}) i18n.loadAndActivate({locale, messages: messagesFi})
await import('@formatjs/intl-pluralrules/locale-data/fi')
break break
} }
case AppLanguage.fr: { case AppLanguage.fr: {
i18n.loadAndActivate({locale, messages: messagesFr}) i18n.loadAndActivate({locale, messages: messagesFr})
await import('@formatjs/intl-pluralrules/locale-data/fr')
break break
} }
case AppLanguage.ga: { case AppLanguage.ga: {
i18n.loadAndActivate({locale, messages: messagesGa}) i18n.loadAndActivate({locale, messages: messagesGa})
await import('@formatjs/intl-pluralrules/locale-data/ga')
break break
} }
case AppLanguage.hi: { case AppLanguage.hi: {
i18n.loadAndActivate({locale, messages: messagesHi}) i18n.loadAndActivate({locale, messages: messagesHi})
await import('@formatjs/intl-pluralrules/locale-data/hi')
break break
} }
case AppLanguage.id: { case AppLanguage.id: {
i18n.loadAndActivate({locale, messages: messagesId}) i18n.loadAndActivate({locale, messages: messagesId})
await import('@formatjs/intl-pluralrules/locale-data/id')
break break
} }
case AppLanguage.it: { case AppLanguage.it: {
i18n.loadAndActivate({locale, messages: messagesIt}) i18n.loadAndActivate({locale, messages: messagesIt})
await import('@formatjs/intl-pluralrules/locale-data/it')
break break
} }
case AppLanguage.ja: { case AppLanguage.ja: {
i18n.loadAndActivate({locale, messages: messagesJa}) i18n.loadAndActivate({locale, messages: messagesJa})
await import('@formatjs/intl-pluralrules/locale-data/ja')
break break
} }
case AppLanguage.ko: { case AppLanguage.ko: {
i18n.loadAndActivate({locale, messages: messagesKo}) i18n.loadAndActivate({locale, messages: messagesKo})
await import('@formatjs/intl-pluralrules/locale-data/ko')
break break
} }
case AppLanguage.pt_BR: { case AppLanguage.pt_BR: {
i18n.loadAndActivate({locale, messages: messagesPt_BR}) i18n.loadAndActivate({locale, messages: messagesPt_BR})
await import('@formatjs/intl-pluralrules/locale-data/pt')
break break
} }
case AppLanguage.tr: { case AppLanguage.tr: {
i18n.loadAndActivate({locale, messages: messagesTr}) i18n.loadAndActivate({locale, messages: messagesTr})
await import('@formatjs/intl-pluralrules/locale-data/tr')
break break
} }
case AppLanguage.uk: { case AppLanguage.uk: {
i18n.loadAndActivate({locale, messages: messagesUk}) i18n.loadAndActivate({locale, messages: messagesUk})
await import('@formatjs/intl-pluralrules/locale-data/uk')
break break
} }
case AppLanguage.zh_CN: { case AppLanguage.zh_CN: {
i18n.loadAndActivate({locale, messages: messagesZh_CN}) i18n.loadAndActivate({locale, messages: messagesZh_CN})
await import('@formatjs/intl-pluralrules/locale-data/zh')
break break
} }
case AppLanguage.zh_TW: { case AppLanguage.zh_TW: {
i18n.loadAndActivate({locale, messages: messagesZh_TW}) i18n.loadAndActivate({locale, messages: messagesZh_TW})
await import('@formatjs/intl-pluralrules/locale-data/zh')
break break
} }
default: { default: {

View File

@ -1,10 +1,9 @@
import React from 'react' import React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context' 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 {useLingui} from '@lingui/react'
import {pluralize} from '#/lib/strings/helpers'
import {logger} from '#/logger' import {logger} from '#/logger'
import {isWeb} from '#/platform/detection' import {isWeb} from '#/platform/detection'
import {isSessionDeactivated, useAgent, useSessionApi} from '#/state/session' import {isSessionDeactivated, useAgent, useSessionApi} from '#/state/session'
@ -205,10 +204,16 @@ function msToString(ms: number | undefined): string | undefined {
return undefined return undefined
} }
// hours // hours
return `${estimatedTimeHrs} ${pluralize(estimatedTimeHrs, 'hour')}` return `${estimatedTimeHrs} ${plural(estimatedTimeHrs, {
one: 'hour',
other: 'hours',
})}`
} }
// minutes // minutes
return `${estimatedTimeMins} ${pluralize(estimatedTimeMins, 'minute')}` return `${estimatedTimeMins} ${plural(estimatedTimeMins, {
one: 'minute',
other: 'minutes',
})}`
} }
return undefined return undefined
} }

View File

@ -1,10 +1,9 @@
import React from 'react' import React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {AppBskyActorDefs} from '@atproto/api' import {AppBskyActorDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro' import {msg, plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {pluralize} from '#/lib/strings/helpers'
import {Shadow} from '#/state/cache/types' import {Shadow} from '#/state/cache/types'
import {makeProfileLink} from 'lib/routes/links' import {makeProfileLink} from 'lib/routes/links'
import {formatCount} from 'view/com/util/numeric/format' import {formatCount} from 'view/com/util/numeric/format'
@ -21,7 +20,14 @@ export function ProfileHeaderMetrics({
const {_} = useLingui() const {_} = useLingui()
const following = formatCount(profile.followsCount || 0) const following = formatCount(profile.followsCount || 0)
const followers = formatCount(profile.followersCount || 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 ( return (
<View <View
@ -32,10 +38,12 @@ export function ProfileHeaderMetrics({
style={[a.flex_row, t.atoms.text]} style={[a.flex_row, t.atoms.text]}
to={makeProfileLink(profile, 'followers')} to={makeProfileLink(profile, 'followers')}
label={`${followers} ${pluralizedFollowers}`}> label={`${followers} ${pluralizedFollowers}`}>
<Trans>
<Text style={[a.font_bold, a.text_md]}>{followers} </Text> <Text style={[a.font_bold, a.text_md]}>{followers} </Text>
<Text style={[t.atoms.text_contrast_medium, a.text_md]}> <Text style={[t.atoms.text_contrast_medium, a.text_md]}>
{pluralizedFollowers} {pluralizedFollowers}
</Text> </Text>
</Trans>
</InlineLinkText> </InlineLinkText>
<InlineLinkText <InlineLinkText
testID="profileHeaderFollowsButton" testID="profileHeaderFollowsButton"
@ -45,15 +53,18 @@ export function ProfileHeaderMetrics({
<Trans> <Trans>
<Text style={[a.font_bold, a.text_md]}>{following} </Text> <Text style={[a.font_bold, a.text_md]}>{following} </Text>
<Text style={[t.atoms.text_contrast_medium, a.text_md]}> <Text style={[t.atoms.text_contrast_medium, a.text_md]}>
following {pluralizedFollowings}
</Text> </Text>
</Trans> </Trans>
</InlineLinkText> </InlineLinkText>
<Text style={[a.font_bold, t.atoms.text, a.text_md]}> <Text style={[a.font_bold, t.atoms.text, a.text_md]}>
<Trans>
{formatCount(profile.postsCount || 0)}{' '} {formatCount(profile.postsCount || 0)}{' '}
<Text style={[t.atoms.text_contrast_medium, a.font_normal, a.text_md]}> <Text
{pluralize(profile.postsCount || 0, 'post')} style={[t.atoms.text_contrast_medium, a.font_normal, a.text_md]}>
{plural(profile.postsCount || 0, {one: 'post', other: 'posts'})}
</Text> </Text>
</Trans>
</Text> </Text>
</View> </View>
) )

View File

@ -7,11 +7,10 @@ import {
ModerationOpts, ModerationOpts,
RichText as RichTextAPI, RichText as RichTextAPI,
} from '@atproto/api' } from '@atproto/api'
import {msg, Trans} from '@lingui/macro' import {msg, Plural, plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {isAppLabeler} from '#/lib/moderation' import {isAppLabeler} from '#/lib/moderation'
import {pluralize} from '#/lib/strings/helpers'
import {logger} from '#/logger' import {logger} from '#/logger'
import {Shadow} from '#/state/cache/types' import {Shadow} from '#/state/cache/types'
import {useModalControls} from '#/state/modals' import {useModalControls} from '#/state/modals'
@ -283,12 +282,10 @@ let ProfileHeaderLabeler = ({
}, },
}} }}
size="tiny" size="tiny"
label={_( label={plural(likeCount, {
msg`Liked by ${likeCount} ${pluralize( one: 'Liked by # user',
likeCount, other: 'Liked by # users',
'user', })}>
)}`,
)}>
{({hovered, focused, pressed}) => ( {({hovered, focused, pressed}) => (
<Text <Text
style={[ style={[
@ -298,9 +295,11 @@ let ProfileHeaderLabeler = ({
(hovered || focused || pressed) && (hovered || focused || pressed) &&
t.atoms.text_contrast_high, t.atoms.text_contrast_high,
]}> ]}>
<Trans> <Plural
Liked by {likeCount} {pluralize(likeCount, 'user')} value={likeCount}
</Trans> one="Liked by # user"
other="Liked by # users"
/>
</Text> </Text>
)} )}
</Link> </Link>

View File

@ -6,12 +6,11 @@ import {RichText} from '#/components/RichText'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {UserAvatar} from '../util/UserAvatar' import {UserAvatar} from '../util/UserAvatar'
import {pluralize} from 'lib/strings/helpers'
import {AtUri} from '@atproto/api' import {AtUri} from '@atproto/api'
import * as Toast from 'view/com/util/Toast' import * as Toast from 'view/com/util/Toast'
import {sanitizeHandle} from 'lib/strings/handles' import {sanitizeHandle} from 'lib/strings/handles'
import {logger} from '#/logger' import {logger} from '#/logger'
import {Trans, msg} from '@lingui/macro' import {Trans, msg, Plural} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import { import {
usePinFeedMutation, usePinFeedMutation,
@ -265,10 +264,11 @@ export function FeedSourceCardLoaded({
{showLikes && feed.type === 'feed' ? ( {showLikes && feed.type === 'feed' ? (
<Text type="sm-medium" style={[pal.text, pal.textLight]}> <Text type="sm-medium" style={[pal.text, pal.textLight]}>
<Trans> <Plural
Liked by {feed.likeCount || 0}{' '} value={feed.likeCount || 0}
{pluralize(feed.likeCount || 0, 'user')} one="Liked by # user"
</Trans> other="Liked by # users"
/>
</Text> </Text>
) : null} ) : null}
</Pressable> </Pressable>

View File

@ -22,7 +22,7 @@ import {
FontAwesomeIconStyle, FontAwesomeIconStyle,
Props, Props,
} from '@fortawesome/react-native-fontawesome' } from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro' import {msg, plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
@ -33,7 +33,6 @@ import {HeartIconSolid} from 'lib/icons'
import {makeProfileLink} from 'lib/routes/links' import {makeProfileLink} from 'lib/routes/links'
import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles' import {sanitizeHandle} from 'lib/strings/handles'
import {pluralize} from 'lib/strings/helpers'
import {niceDate} from 'lib/strings/time' import {niceDate} from 'lib/strings/time'
import {colors, s} from 'lib/styles' import {colors, s} from 'lib/styles'
import {isWeb} from 'platform/detection' import {isWeb} from 'platform/detection'
@ -176,6 +175,7 @@ let FeedItem = ({
return null return null
} }
let formattedCount = authors.length > 1 ? formatCount(authors.length - 1) : ''
return ( return (
<Link <Link
testID={`feedItem-by-${item.notification.author.handle}`} testID={`feedItem-by-${item.notification.author.handle}`}
@ -236,8 +236,10 @@ let FeedItem = ({
<Trans>and</Trans>{' '} <Trans>and</Trans>{' '}
</Text> </Text>
<Text style={[pal.text, s.bold]}> <Text style={[pal.text, s.bold]}>
{formatCount(authors.length - 1)}{' '} {plural(authors.length - 1, {
{pluralize(authors.length - 1, 'other')} one: `${formattedCount} other`,
other: `${formattedCount} others`,
})}
</Text> </Text>
</> </>
) : undefined} ) : undefined}

View File

@ -8,7 +8,7 @@ import {
RichText as RichTextAPI, RichText as RichTextAPI,
} from '@atproto/api' } from '@atproto/api'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 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 {useLingui} from '@lingui/react'
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped' 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 {makeProfileLink} from 'lib/routes/links'
import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles' 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 {niceDate} from 'lib/strings/time'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {isWeb} from 'platform/detection' import {isWeb} from 'platform/detection'
@ -336,7 +336,11 @@ let PostThreadItemLoaded = ({
<Text type="xl-bold" style={pal.text}> <Text type="xl-bold" style={pal.text}>
{formatCount(post.repostCount)} {formatCount(post.repostCount)}
</Text>{' '} </Text>{' '}
{pluralize(post.repostCount, 'repost')} <Plural
value={post.repostCount}
one="repost"
other="reposts"
/>
</Text> </Text>
</Link> </Link>
) : null} ) : null}
@ -352,7 +356,7 @@ let PostThreadItemLoaded = ({
<Text type="xl-bold" style={pal.text}> <Text type="xl-bold" style={pal.text}>
{formatCount(post.likeCount)} {formatCount(post.likeCount)}
</Text>{' '} </Text>{' '}
{pluralize(post.likeCount, 'like')} <Plural value={post.likeCount} one="like" other="likes" />
</Text> </Text>
</Link> </Link>
) : null} ) : null}

View File

@ -12,14 +12,13 @@ import {
AtUri, AtUri,
RichText as RichTextAPI, RichText as RichTextAPI,
} from '@atproto/api' } from '@atproto/api'
import {msg} from '@lingui/macro' import {msg, plural} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {HITSLOP_10, HITSLOP_20} from '#/lib/constants' import {HITSLOP_10, HITSLOP_20} from '#/lib/constants'
import {CommentBottomArrow, HeartIcon, HeartIconSolid} from '#/lib/icons' import {CommentBottomArrow, HeartIcon, HeartIconSolid} from '#/lib/icons'
import {makeProfileLink} from '#/lib/routes/links' import {makeProfileLink} from '#/lib/routes/links'
import {shareUrl} from '#/lib/sharing' import {shareUrl} from '#/lib/sharing'
import {pluralize} from '#/lib/strings/helpers'
import {toShareUrl} from '#/lib/strings/url-helpers' import {toShareUrl} from '#/lib/strings/url-helpers'
import {s} from '#/lib/styles' import {s} from '#/lib/styles'
import {useTheme} from '#/lib/ThemeContext' import {useTheme} from '#/lib/ThemeContext'
@ -159,9 +158,10 @@ let PostCtrls = ({
} }
}} }}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={`Reply (${post.replyCount} ${ accessibilityLabel={plural(post.replyCount || 0, {
post.replyCount === 1 ? 'reply' : 'replies' one: 'Reply (# reply)',
})`} other: 'Reply (# replies)',
})}
accessibilityHint="" accessibilityHint=""
hitSlop={big ? HITSLOP_20 : HITSLOP_10}> hitSlop={big ? HITSLOP_20 : HITSLOP_10}>
<CommentBottomArrow <CommentBottomArrow
@ -193,9 +193,17 @@ let PostCtrls = ({
requireAuth(() => onPressToggleLike()) requireAuth(() => onPressToggleLike())
}} }}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={`${ accessibilityLabel={
post.viewer?.like ? _(msg`Unlike`) : _(msg`Like`) post.viewer?.like
} (${post.likeCount} ${pluralize(post.likeCount || 0, 'like')})`} ? plural(post.likeCount || 0, {
one: 'Unlike (# like)',
other: 'Unlike (# likes)',
})
: plural(post.likeCount || 0, {
one: 'Like (# like)',
other: 'Like (# likes)',
})
}
accessibilityHint="" accessibilityHint=""
hitSlop={big ? HITSLOP_20 : HITSLOP_10}> hitSlop={big ? HITSLOP_20 : HITSLOP_10}>
{post.viewer?.like ? ( {post.viewer?.like ? (

View File

@ -4,11 +4,10 @@ import {RepostIcon} from 'lib/icons'
import {s, colors} from 'lib/styles' import {s, colors} from 'lib/styles'
import {useTheme} from 'lib/ThemeContext' import {useTheme} from 'lib/ThemeContext'
import {Text} from '../text/Text' import {Text} from '../text/Text'
import {pluralize} from 'lib/strings/helpers'
import {HITSLOP_10, HITSLOP_20} from 'lib/constants' import {HITSLOP_10, HITSLOP_20} from 'lib/constants'
import {useModalControls} from '#/state/modals' import {useModalControls} from '#/state/modals'
import {useRequireAuth} from '#/state/session' import {useRequireAuth} from '#/state/session'
import {msg} from '@lingui/macro' import {msg, plural} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
interface Props { interface Props {
@ -59,7 +58,7 @@ let RepostButton = ({
isReposted isReposted
? _(msg`Undo repost`) ? _(msg`Undo repost`)
: _(msg({message: 'Repost', context: 'action'})) : _(msg({message: 'Repost', context: 'action'}))
} (${repostCount} ${pluralize(repostCount || 0, 'repost')})`} } (${plural(repostCount || 0, {one: '# repost', other: '# reposts'})})`}
accessibilityHint="" accessibilityHint=""
hitSlop={big ? HITSLOP_20 : HITSLOP_10}> hitSlop={big ? HITSLOP_20 : HITSLOP_10}>
<RepostIcon <RepostIcon

View File

@ -12,7 +12,7 @@ import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
import {ViewHeader} from 'view/com/util/ViewHeader' import {ViewHeader} from 'view/com/util/ViewHeader'
import {CenteredView} from 'view/com/util/Views' import {CenteredView} from 'view/com/util/Views'
import debounce from 'lodash.debounce' import debounce from 'lodash.debounce'
import {Trans, msg} from '@lingui/macro' import {Trans, msg, Plural} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import { import {
usePreferencesQuery, usePreferencesQuery,
@ -27,7 +27,6 @@ function RepliesThresholdInput({
initialValue: number initialValue: number
}) { }) {
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui()
const [value, setValue] = useState(initialValue) const [value, setValue] = useState(initialValue)
const {mutate: setFeedViewPref} = useSetFeedViewPreferencesMutation() const {mutate: setFeedViewPref} = useSetFeedViewPreferencesMutation()
const preValue = React.useRef(initialValue) const preValue = React.useRef(initialValue)
@ -64,13 +63,12 @@ function RepliesThresholdInput({
thumbTintColor={colors.blue3} thumbTintColor={colors.blue3}
/> />
<Text type="xs" style={pal.text}> <Text type="xs" style={pal.text}>
{value === 0 <Plural
? _(msg`Show all replies`) value={value}
: _( _0="Show all replies"
msg`Show replies with at least ${value} ${ one="Show replies with at least # like"
value > 1 ? `likes` : `like` other="Show replies with at least # likes"
}`, />
)}
</Text> </Text>
</View> </View>
) )

View File

@ -1,6 +1,6 @@
import React, {useCallback, useMemo} from 'react' import React, {useCallback, useMemo} from 'react'
import {Pressable, StyleSheet, View} from 'react-native' 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 {useLingui} from '@lingui/react'
import {useIsFocused, useNavigation} from '@react-navigation/native' import {useIsFocused, useNavigation} from '@react-navigation/native'
import {NativeStackScreenProps} from '@react-navigation/native-stack' import {NativeStackScreenProps} from '@react-navigation/native-stack'
@ -35,7 +35,6 @@ import {makeCustomFeedLink} from 'lib/routes/links'
import {CommonNavigatorParams} from 'lib/routes/types' import {CommonNavigatorParams} from 'lib/routes/types'
import {NavigationProp} from 'lib/routes/types' import {NavigationProp} from 'lib/routes/types'
import {shareUrl} from 'lib/sharing' import {shareUrl} from 'lib/sharing'
import {pluralize} from 'lib/strings/helpers'
import {makeRecordUri} from 'lib/strings/url-helpers' import {makeRecordUri} from 'lib/strings/url-helpers'
import {toShareUrl} from 'lib/strings/url-helpers' import {toShareUrl} from 'lib/strings/url-helpers'
import {s} from 'lib/styles' import {s} from 'lib/styles'
@ -597,7 +596,11 @@ function AboutSection({
label={_(msg`View users who like this feed`)} label={_(msg`View users who like this feed`)}
to={makeCustomFeedLink(feedOwnerDid, feedRkey, 'liked-by')} to={makeCustomFeedLink(feedOwnerDid, feedRkey, 'liked-by')}
style={[t.atoms.text_contrast_medium, a.font_bold]}> 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> </InlineLinkText>
)} )}
</View> </View>

View File

@ -13,7 +13,7 @@ import {
FontAwesomeIcon, FontAwesomeIcon,
FontAwesomeIconStyle, FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome' } from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro' import {msg, Plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {StackActions, useNavigation} from '@react-navigation/native' import {StackActions, useNavigation} from '@react-navigation/native'
@ -42,7 +42,6 @@ import {
} from 'lib/icons' } from 'lib/icons'
import {getTabState, TabState} from 'lib/routes/helpers' import {getTabState, TabState} from 'lib/routes/helpers'
import {NavigationProp} from 'lib/routes/types' import {NavigationProp} from 'lib/routes/types'
import {pluralize} from 'lib/strings/helpers'
import {colors, s} from 'lib/styles' 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'
@ -90,15 +89,26 @@ let DrawerProfileCard = ({
@{account.handle} @{account.handle}
</Text> </Text>
<Text type="xl" style={[pal.textLight, styles.profileCardFollowers]}> <Text type="xl" style={[pal.textLight, styles.profileCardFollowers]}>
<Trans>
<Text type="xl-medium" style={pal.text}> <Text type="xl-medium" style={pal.text}>
{formatCountShortOnly(profile?.followersCount ?? 0)} {formatCountShortOnly(profile?.followersCount ?? 0)}
</Text>{' '} </Text>{' '}
{pluralize(profile?.followersCount || 0, 'follower')} &middot;{' '} <Plural
value={profile?.followersCount || 0}
one="follower"
other="followers"
/>{' '}
&middot;{' '}
</Trans>
<Trans> <Trans>
<Text type="xl-medium" style={pal.text}> <Text type="xl-medium" style={pal.text}>
{formatCountShortOnly(profile?.followsCount ?? 0)} {formatCountShortOnly(profile?.followsCount ?? 0)}
</Text>{' '} </Text>{' '}
following <Plural
value={profile?.followsCount || 0}
one="following"
other="following"
/>
</Trans> </Trans>
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>

View File

@ -3582,6 +3582,54 @@
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.1.tgz#16308cea045f0fc777b6ff20a9f25474dd8293d2" resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.1.tgz#16308cea045f0fc777b6ff20a9f25474dd8293d2"
integrity sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q== 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": "@fortawesome/fontawesome-common-types@6.4.2":
version "6.4.2" version "6.4.2"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.2.tgz#1766039cad33f8ad87f9467b98e0d18fbc8f01c5" resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.2.tgz#1766039cad33f8ad87f9467b98e0d18fbc8f01c5"