Internationalize more strings (#2440)

Co-authored-by: Ansh <anshnanda10@gmail.com>
This commit is contained in:
Stanislas Signoud 2024-01-09 23:37:15 +01:00 committed by GitHub
parent aeeacd10d3
commit 008893b911
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
108 changed files with 925 additions and 558 deletions

View file

@ -39,6 +39,8 @@ import {
import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread' import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread'
import * as persisted from '#/state/persisted' import * as persisted from '#/state/persisted'
import {Splash} from '#/Splash' import {Splash} from '#/Splash'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
SplashScreen.preventAutoHideAsync() SplashScreen.preventAutoHideAsync()
@ -46,17 +48,18 @@ function InnerApp() {
const colorMode = useColorMode() const colorMode = useColorMode()
const {isInitialLoad, currentAccount} = useSession() const {isInitialLoad, currentAccount} = useSession()
const {resumeSession} = useSessionApi() const {resumeSession} = useSessionApi()
const {_} = useLingui()
// init // init
useEffect(() => { useEffect(() => {
notifications.init(queryClient) notifications.init(queryClient)
listenSessionDropped(() => { listenSessionDropped(() => {
Toast.show('Sorry! Your session expired. Please log in again.') Toast.show(_(msg`Sorry! Your session expired. Please log in again.`))
}) })
const account = persisted.get('session').currentAccount const account = persisted.get('session').currentAccount
resumeSession(account) resumeSession(account)
}, [resumeSession]) }, [resumeSession, _])
return ( return (
<SafeAreaProvider initialMetrics={initialWindowMetrics}> <SafeAreaProvider initialMetrics={initialWindowMetrics}>

View file

@ -76,6 +76,8 @@ import {PreferencesHomeFeed} from 'view/screens/PreferencesHomeFeed'
import {PreferencesThreads} from 'view/screens/PreferencesThreads' import {PreferencesThreads} from 'view/screens/PreferencesThreads'
import {PreferencesExternalEmbeds} from '#/view/screens/PreferencesExternalEmbeds' import {PreferencesExternalEmbeds} from '#/view/screens/PreferencesExternalEmbeds'
import {createNativeStackNavigatorWithAuth} from './view/shell/createNativeStackNavigatorWithAuth' import {createNativeStackNavigatorWithAuth} from './view/shell/createNativeStackNavigatorWithAuth'
import {msg} from '@lingui/macro'
import {i18n, MessageDescriptor} from '@lingui/core'
const navigationRef = createNavigationContainerRef<AllNavigatorParams>() const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
@ -93,55 +95,56 @@ const Tab = createBottomTabNavigator<BottomTabNavigatorParams>()
* These "common screens" are reused across stacks. * These "common screens" are reused across stacks.
*/ */
function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
const title = (page: string) => bskyTitle(page, unreadCountLabel) const title = (page: MessageDescriptor) =>
bskyTitle(i18n._(page), unreadCountLabel)
return ( return (
<> <>
<Stack.Screen <Stack.Screen
name="NotFound" name="NotFound"
getComponent={() => NotFoundScreen} getComponent={() => NotFoundScreen}
options={{title: title('Not Found')}} options={{title: title(msg`Not Found`)}}
/> />
<Stack.Screen <Stack.Screen
name="Lists" name="Lists"
component={ListsScreen} component={ListsScreen}
options={{title: title('Lists'), requireAuth: true}} options={{title: title(msg`Lists`), requireAuth: true}}
/> />
<Stack.Screen <Stack.Screen
name="Moderation" name="Moderation"
getComponent={() => ModerationScreen} getComponent={() => ModerationScreen}
options={{title: title('Moderation'), requireAuth: true}} options={{title: title(msg`Moderation`), requireAuth: true}}
/> />
<Stack.Screen <Stack.Screen
name="ModerationModlists" name="ModerationModlists"
getComponent={() => ModerationModlistsScreen} getComponent={() => ModerationModlistsScreen}
options={{title: title('Moderation Lists'), requireAuth: true}} options={{title: title(msg`Moderation Lists`), requireAuth: true}}
/> />
<Stack.Screen <Stack.Screen
name="ModerationMutedAccounts" name="ModerationMutedAccounts"
getComponent={() => ModerationMutedAccounts} getComponent={() => ModerationMutedAccounts}
options={{title: title('Muted Accounts'), requireAuth: true}} options={{title: title(msg`Muted Accounts`), requireAuth: true}}
/> />
<Stack.Screen <Stack.Screen
name="ModerationBlockedAccounts" name="ModerationBlockedAccounts"
getComponent={() => ModerationBlockedAccounts} getComponent={() => ModerationBlockedAccounts}
options={{title: title('Blocked Accounts'), requireAuth: true}} options={{title: title(msg`Blocked Accounts`), requireAuth: true}}
/> />
<Stack.Screen <Stack.Screen
name="Settings" name="Settings"
getComponent={() => SettingsScreen} getComponent={() => SettingsScreen}
options={{title: title('Settings'), requireAuth: true}} options={{title: title(msg`Settings`), requireAuth: true}}
/> />
<Stack.Screen <Stack.Screen
name="LanguageSettings" name="LanguageSettings"
getComponent={() => LanguageSettingsScreen} getComponent={() => LanguageSettingsScreen}
options={{title: title('Language Settings'), requireAuth: true}} options={{title: title(msg`Language Settings`), requireAuth: true}}
/> />
<Stack.Screen <Stack.Screen
name="Profile" name="Profile"
getComponent={() => ProfileScreen} getComponent={() => ProfileScreen}
options={({route}) => ({ options={({route}) => ({
title: title(`@${route.params.name}`), title: title(msg`@${route.params.name}`),
animation: 'none', animation: 'none',
})} })}
/> />
@ -149,106 +152,112 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
name="ProfileFollowers" name="ProfileFollowers"
getComponent={() => ProfileFollowersScreen} getComponent={() => ProfileFollowersScreen}
options={({route}) => ({ options={({route}) => ({
title: title(`People following @${route.params.name}`), title: title(msg`People following @${route.params.name}`),
})} })}
/> />
<Stack.Screen <Stack.Screen
name="ProfileFollows" name="ProfileFollows"
getComponent={() => ProfileFollowsScreen} getComponent={() => ProfileFollowsScreen}
options={({route}) => ({ options={({route}) => ({
title: title(`People followed by @${route.params.name}`), title: title(msg`People followed by @${route.params.name}`),
})} })}
/> />
<Stack.Screen <Stack.Screen
name="ProfileList" name="ProfileList"
getComponent={() => ProfileListScreen} getComponent={() => ProfileListScreen}
options={{title: title('List'), requireAuth: true}} options={{title: title(msg`List`), requireAuth: true}}
/> />
<Stack.Screen <Stack.Screen
name="PostThread" name="PostThread"
getComponent={() => PostThreadScreen} getComponent={() => PostThreadScreen}
options={({route}) => ({title: title(`Post by @${route.params.name}`)})} options={({route}) => ({
title: title(msg`Post by @${route.params.name}`),
})}
/> />
<Stack.Screen <Stack.Screen
name="PostLikedBy" name="PostLikedBy"
getComponent={() => PostLikedByScreen} getComponent={() => PostLikedByScreen}
options={({route}) => ({title: title(`Post by @${route.params.name}`)})} options={({route}) => ({
title: title(msg`Post by @${route.params.name}`),
})}
/> />
<Stack.Screen <Stack.Screen
name="PostRepostedBy" name="PostRepostedBy"
getComponent={() => PostRepostedByScreen} getComponent={() => PostRepostedByScreen}
options={({route}) => ({title: title(`Post by @${route.params.name}`)})} options={({route}) => ({
title: title(msg`Post by @${route.params.name}`),
})}
/> />
<Stack.Screen <Stack.Screen
name="ProfileFeed" name="ProfileFeed"
getComponent={() => ProfileFeedScreen} getComponent={() => ProfileFeedScreen}
options={{title: title('Feed'), requireAuth: true}} options={{title: title(msg`Feed`), requireAuth: true}}
/> />
<Stack.Screen <Stack.Screen
name="ProfileFeedLikedBy" name="ProfileFeedLikedBy"
getComponent={() => ProfileFeedLikedByScreen} getComponent={() => ProfileFeedLikedByScreen}
options={{title: title('Liked by')}} options={{title: title(msg`Liked by`)}}
/> />
<Stack.Screen <Stack.Screen
name="Debug" name="Debug"
getComponent={() => DebugScreen} getComponent={() => DebugScreen}
options={{title: title('Debug'), requireAuth: true}} options={{title: title(msg`Debug`), requireAuth: true}}
/> />
<Stack.Screen <Stack.Screen
name="Log" name="Log"
getComponent={() => LogScreen} getComponent={() => LogScreen}
options={{title: title('Log'), requireAuth: true}} options={{title: title(msg`Log`), requireAuth: true}}
/> />
<Stack.Screen <Stack.Screen
name="Support" name="Support"
getComponent={() => SupportScreen} getComponent={() => SupportScreen}
options={{title: title('Support')}} options={{title: title(msg`Support`)}}
/> />
<Stack.Screen <Stack.Screen
name="PrivacyPolicy" name="PrivacyPolicy"
getComponent={() => PrivacyPolicyScreen} getComponent={() => PrivacyPolicyScreen}
options={{title: title('Privacy Policy')}} options={{title: title(msg`Privacy Policy`)}}
/> />
<Stack.Screen <Stack.Screen
name="TermsOfService" name="TermsOfService"
getComponent={() => TermsOfServiceScreen} getComponent={() => TermsOfServiceScreen}
options={{title: title('Terms of Service')}} options={{title: title(msg`Terms of Service`)}}
/> />
<Stack.Screen <Stack.Screen
name="CommunityGuidelines" name="CommunityGuidelines"
getComponent={() => CommunityGuidelinesScreen} getComponent={() => CommunityGuidelinesScreen}
options={{title: title('Community Guidelines')}} options={{title: title(msg`Community Guidelines`)}}
/> />
<Stack.Screen <Stack.Screen
name="CopyrightPolicy" name="CopyrightPolicy"
getComponent={() => CopyrightPolicyScreen} getComponent={() => CopyrightPolicyScreen}
options={{title: title('Copyright Policy')}} options={{title: title(msg`Copyright Policy`)}}
/> />
<Stack.Screen <Stack.Screen
name="AppPasswords" name="AppPasswords"
getComponent={() => AppPasswords} getComponent={() => AppPasswords}
options={{title: title('App Passwords'), requireAuth: true}} options={{title: title(msg`App Passwords`), requireAuth: true}}
/> />
<Stack.Screen <Stack.Screen
name="SavedFeeds" name="SavedFeeds"
getComponent={() => SavedFeeds} getComponent={() => SavedFeeds}
options={{title: title('Edit My Feeds'), requireAuth: true}} options={{title: title(msg`Edit My Feeds`), requireAuth: true}}
/> />
<Stack.Screen <Stack.Screen
name="PreferencesHomeFeed" name="PreferencesHomeFeed"
getComponent={() => PreferencesHomeFeed} getComponent={() => PreferencesHomeFeed}
options={{title: title('Home Feed Preferences'), requireAuth: true}} options={{title: title(msg`Home Feed Preferences`), requireAuth: true}}
/> />
<Stack.Screen <Stack.Screen
name="PreferencesThreads" name="PreferencesThreads"
getComponent={() => PreferencesThreads} getComponent={() => PreferencesThreads}
options={{title: title('Threads Preferences'), requireAuth: true}} options={{title: title(msg`Threads Preferences`), requireAuth: true}}
/> />
<Stack.Screen <Stack.Screen
name="PreferencesExternalEmbeds" name="PreferencesExternalEmbeds"
getComponent={() => PreferencesExternalEmbeds} getComponent={() => PreferencesExternalEmbeds}
options={{ options={{
title: title('External Media Preferences'), title: title(msg`External Media Preferences`),
requireAuth: true, requireAuth: true,
}} }}
/> />
@ -407,7 +416,7 @@ const FlatNavigator = () => {
const pal = usePalette('default') const pal = usePalette('default')
const numUnread = useUnreadNotifications() const numUnread = useUnreadNotifications()
const title = (page: string) => bskyTitle(page, numUnread) const title = (page: MessageDescriptor) => bskyTitle(i18n._(page), numUnread)
return ( return (
<Flat.Navigator <Flat.Navigator
screenOptions={{ screenOptions={{
@ -420,22 +429,22 @@ const FlatNavigator = () => {
<Flat.Screen <Flat.Screen
name="Home" name="Home"
getComponent={() => HomeScreen} getComponent={() => HomeScreen}
options={{title: title('Home'), requireAuth: true}} options={{title: title(msg`Home`), requireAuth: true}}
/> />
<Flat.Screen <Flat.Screen
name="Search" name="Search"
getComponent={() => SearchScreen} getComponent={() => SearchScreen}
options={{title: title('Search')}} options={{title: title(msg`Search`)}}
/> />
<Flat.Screen <Flat.Screen
name="Feeds" name="Feeds"
getComponent={() => FeedsScreen} getComponent={() => FeedsScreen}
options={{title: title('Feeds'), requireAuth: true}} options={{title: title(msg`Feeds`), requireAuth: true}}
/> />
<Flat.Screen <Flat.Screen
name="Notifications" name="Notifications"
getComponent={() => NotificationsScreen} getComponent={() => NotificationsScreen}
options={{title: title('Notifications'), requireAuth: true}} options={{title: title(msg`Notifications`), requireAuth: true}}
/> />
{commonScreens(Flat as typeof HomeTab, numUnread)} {commonScreens(Flat as typeof HomeTab, numUnread)}
</Flat.Navigator> </Flat.Navigator>

View file

@ -2,7 +2,7 @@ import React from 'react'
import {View, Pressable} from 'react-native' import {View, Pressable} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useNavigation} from '@react-navigation/native' import {useNavigation} from '@react-navigation/native'
import {isIOS, isNative} from 'platform/detection' import {isIOS, isNative} from 'platform/detection'
@ -119,7 +119,7 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
}} }}
onPress={onPressSearch}> onPress={onPressSearch}>
<Text type="lg-bold" style={[pal.text]}> <Text type="lg-bold" style={[pal.text]}>
Search{' '} <Trans>Search</Trans>{' '}
</Text> </Text>
<FontAwesomeIcon <FontAwesomeIcon
icon="search" icon="search"

View file

@ -74,7 +74,7 @@ export const SplashScreen = ({
// TODO: web accessibility // TODO: web accessibility
accessibilityRole="button"> accessibilityRole="button">
<Text style={[s.white, styles.btnLabel]}> <Text style={[s.white, styles.btnLabel]}>
Create a new account <Trans>Create a new account</Trans>
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity <TouchableOpacity

View file

@ -77,7 +77,7 @@ export function Step1({
value={uiState.serviceUrl} value={uiState.serviceUrl}
editable editable
onChange={onChangeServiceUrl} onChange={onChangeServiceUrl}
accessibilityHint="Input hosting provider address" accessibilityHint={_(msg`Input hosting provider address`)}
accessibilityLabel={_(msg`Hosting provider address`)} accessibilityLabel={_(msg`Hosting provider address`)}
accessibilityLabelledBy="addressProvider" accessibilityLabelledBy="addressProvider"
/> />
@ -125,6 +125,7 @@ function Option({
}>) { }>) {
const theme = useTheme() const theme = useTheme()
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui()
const circleFillStyle = React.useMemo( const circleFillStyle = React.useMemo(
() => ({ () => ({
backgroundColor: theme.palette.primary.background, backgroundColor: theme.palette.primary.background,
@ -139,7 +140,7 @@ function Option({
testID={testID} testID={testID}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={label} accessibilityLabel={label}
accessibilityHint={`Sets hosting provider to ${label}`}> accessibilityHint={_(msg`Sets hosting provider to ${label}`)}>
<View style={styles.optionHeading}> <View style={styles.optionHeading}>
<View style={[styles.circle, pal.border]}> <View style={[styles.circle, pal.border]}>
{isSelected ? ( {isSelected ? (

View file

@ -60,7 +60,7 @@ export function Step2({
{uiState.isInviteCodeRequired && ( {uiState.isInviteCodeRequired && (
<View style={s.pb20}> <View style={s.pb20}>
<Text type="md-medium" style={[pal.text, s.mb2]}> <Text type="md-medium" style={[pal.text, s.mb2]}>
Invite code <Trans>Invite code</Trans>
</Text> </Text>
<TextInput <TextInput
testID="inviteCodeInput" testID="inviteCodeInput"
@ -70,7 +70,7 @@ export function Step2({
editable editable
onChange={value => uiDispatch({type: 'set-invite-code', value})} onChange={value => uiDispatch({type: 'set-invite-code', value})}
accessibilityLabel={_(msg`Invite code`)} accessibilityLabel={_(msg`Invite code`)}
accessibilityHint="Input invite code to proceed" accessibilityHint={_(msg`Input invite code to proceed`)}
autoCapitalize="none" autoCapitalize="none"
autoComplete="off" autoComplete="off"
autoCorrect={false} autoCorrect={false}
@ -80,7 +80,7 @@ export function Step2({
{!uiState.inviteCode && uiState.isInviteCodeRequired ? ( {!uiState.inviteCode && uiState.isInviteCodeRequired ? (
<Text style={[s.alignBaseline, pal.text]}> <Text style={[s.alignBaseline, pal.text]}>
Don't have an invite code?{' '} <Trans>Don't have an invite code?</Trans>{' '}
<TouchableWithoutFeedback <TouchableWithoutFeedback
onPress={onPressWaitlist} onPress={onPressWaitlist}
accessibilityLabel={_(msg`Join the waitlist.`)} accessibilityLabel={_(msg`Join the waitlist.`)}
@ -106,7 +106,7 @@ export function Step2({
editable editable
onChange={value => uiDispatch({type: 'set-email', value})} onChange={value => uiDispatch({type: 'set-email', value})}
accessibilityLabel={_(msg`Email`)} accessibilityLabel={_(msg`Email`)}
accessibilityHint="Input email for Bluesky waitlist" accessibilityHint={_(msg`Input email for Bluesky waitlist`)}
accessibilityLabelledBy="email" accessibilityLabelledBy="email"
autoCapitalize="none" autoCapitalize="none"
autoComplete="off" autoComplete="off"
@ -130,7 +130,7 @@ export function Step2({
secureTextEntry secureTextEntry
onChange={value => uiDispatch({type: 'set-password', value})} onChange={value => uiDispatch({type: 'set-password', value})}
accessibilityLabel={_(msg`Password`)} accessibilityLabel={_(msg`Password`)}
accessibilityHint="Set password" accessibilityHint={_(msg`Set password`)}
accessibilityLabelledBy="password" accessibilityLabelledBy="password"
autoCapitalize="none" autoCapitalize="none"
autoComplete="off" autoComplete="off"
@ -154,7 +154,7 @@ export function Step2({
buttonStyle={[pal.border, styles.dateInputButton]} buttonStyle={[pal.border, styles.dateInputButton]}
buttonLabelType="lg" buttonLabelType="lg"
accessibilityLabel={_(msg`Birthday`)} accessibilityLabel={_(msg`Birthday`)}
accessibilityHint="Enter your birth date" accessibilityHint={_(msg`Enter your birth date`)}
accessibilityLabelledBy="birthDate" accessibilityLabelledBy="birthDate"
/> />
</View> </View>

View file

@ -36,7 +36,7 @@ export function Step3({
onChange={value => uiDispatch({type: 'set-handle', value})} onChange={value => uiDispatch({type: 'set-handle', value})}
// TODO: Add explicit text label // TODO: Add explicit text label
accessibilityLabel={_(msg`User handle`)} accessibilityLabel={_(msg`User handle`)}
accessibilityHint="Input your user handle" accessibilityHint={_(msg`Input your user handle`)}
/> />
<Text type="lg" style={[pal.text, s.pl5, s.pt10]}> <Text type="lg" style={[pal.text, s.pl5, s.pt10]}>
<Trans>Your full handle will be</Trans>{' '} <Trans>Your full handle will be</Trans>{' '}

View file

@ -2,13 +2,18 @@ import React from 'react'
import {StyleSheet, View} from 'react-native' import {StyleSheet, View} from 'react-native'
import {Text} from 'view/com/util/text/Text' import {Text} from 'view/com/util/text/Text'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {Trans} from '@lingui/macro'
export function StepHeader({step, title}: {step: string; title: string}) { export function StepHeader({step, title}: {step: string; title: string}) {
const pal = usePalette('default') const pal = usePalette('default')
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Text type="lg" style={[pal.textLight]}> <Text type="lg" style={[pal.textLight]}>
{step === '3' ? 'Last step!' : <>Step {step} of 3</>} {step === '3' ? (
<Trans>Last step!</Trans>
) : (
<Trans>Step {step} of 3</Trans>
)}
</Text> </Text>
<Text style={[pal.text]} type="title-xl"> <Text style={[pal.text]} type="title-xl">
{title} {title}

View file

@ -42,7 +42,7 @@ function AccountItem({
onPress={onPress} onPress={onPress}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_(msg`Sign in as ${account.handle}`)} accessibilityLabel={_(msg`Sign in as ${account.handle}`)}
accessibilityHint="Double tap to sign in"> accessibilityHint={_(msg`Double tap to sign in`)}>
<View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}> <View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
<View style={s.p10}> <View style={s.p10}>
<UserAvatar avatar={profile?.avatar} size={30} /> <UserAvatar avatar={profile?.avatar} size={30} />
@ -95,19 +95,19 @@ export const ChooseAccountForm = ({
if (account.accessJwt) { if (account.accessJwt) {
if (account.did === currentAccount?.did) { if (account.did === currentAccount?.did) {
setShowLoggedOut(false) setShowLoggedOut(false)
Toast.show(`Already signed in as @${account.handle}`) Toast.show(_(msg`Already signed in as @${account.handle}`))
} else { } else {
await initSession(account) await initSession(account)
track('Sign In', {resumedSession: true}) track('Sign In', {resumedSession: true})
setTimeout(() => { setTimeout(() => {
Toast.show(`Signed in as @${account.handle}`) Toast.show(_(msg`Signed in as @${account.handle}`))
}, 100) }, 100)
} }
} else { } else {
onSelectAccount(account) onSelectAccount(account)
} }
}, },
[currentAccount, track, initSession, onSelectAccount, setShowLoggedOut], [currentAccount, track, initSession, onSelectAccount, setShowLoggedOut, _],
) )
return ( return (

View file

@ -67,7 +67,7 @@ export const ForgotPasswordForm = ({
const onPressNext = async () => { const onPressNext = async () => {
if (!EmailValidator.validate(email)) { if (!EmailValidator.validate(email)) {
return setError('Your email appears to be invalid.') return setError(_(msg`Your email appears to be invalid.`))
} }
setError('') setError('')
@ -83,7 +83,9 @@ export const ForgotPasswordForm = ({
setIsProcessing(false) setIsProcessing(false)
if (isNetworkError(e)) { if (isNetworkError(e)) {
setError( setError(
'Unable to contact your service. Please check your Internet connection.', _(
msg`Unable to contact your service. Please check your Internet connection.`,
),
) )
} else { } else {
setError(cleanError(errMsg)) setError(cleanError(errMsg))
@ -112,7 +114,9 @@ export const ForgotPasswordForm = ({
onPress={onPressSelectService} onPress={onPressSelectService}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_(msg`Hosting provider`)} accessibilityLabel={_(msg`Hosting provider`)}
accessibilityHint="Sets hosting provider for password reset"> accessibilityHint={_(
msg`Sets hosting provider for password reset`,
)}>
<FontAwesomeIcon <FontAwesomeIcon
icon="globe" icon="globe"
style={[pal.textLight, styles.groupContentIcon]} style={[pal.textLight, styles.groupContentIcon]}
@ -136,7 +140,7 @@ export const ForgotPasswordForm = ({
<TextInput <TextInput
testID="forgotPasswordEmail" testID="forgotPasswordEmail"
style={[pal.text, styles.textInput]} style={[pal.text, styles.textInput]}
placeholder="Email address" placeholder={_(msg`Email address`)}
placeholderTextColor={pal.colors.textLight} placeholderTextColor={pal.colors.textLight}
autoCapitalize="none" autoCapitalize="none"
autoFocus autoFocus
@ -146,7 +150,7 @@ export const ForgotPasswordForm = ({
onChangeText={setEmail} onChangeText={setEmail}
editable={!isProcessing} editable={!isProcessing}
accessibilityLabel={_(msg`Email`)} accessibilityLabel={_(msg`Email`)}
accessibilityHint="Sets email for password reset" accessibilityHint={_(msg`Sets email for password reset`)}
/> />
</View> </View>
</View> </View>
@ -179,7 +183,7 @@ export const ForgotPasswordForm = ({
onPress={onPressNext} onPress={onPressNext}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_(msg`Go to next`)} accessibilityLabel={_(msg`Go to next`)}
accessibilityHint="Navigates to the next screen"> accessibilityHint={_(msg`Navigates to the next screen`)}>
<Text type="xl-bold" style={[pal.link, s.pr5]}> <Text type="xl-bold" style={[pal.link, s.pr5]}>
<Trans>Next</Trans> <Trans>Next</Trans>
</Text> </Text>

View file

@ -145,7 +145,7 @@ export const LoginForm = ({
onPress={onPressSelectService} onPress={onPressSelectService}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_(msg`Select service`)} accessibilityLabel={_(msg`Select service`)}
accessibilityHint="Sets server for the Bluesky client"> accessibilityHint={_(msg`Sets server for the Bluesky client`)}>
<Text type="xl" style={[pal.text, styles.textBtnLabel]}> <Text type="xl" style={[pal.text, styles.textBtnLabel]}>
{toNiceDomain(serviceUrl)} {toNiceDomain(serviceUrl)}
</Text> </Text>
@ -190,7 +190,9 @@ export const LoginForm = ({
} }
editable={!isProcessing} editable={!isProcessing}
accessibilityLabel={_(msg`Username or email address`)} accessibilityLabel={_(msg`Username or email address`)}
accessibilityHint="Input the username or email address you used at signup" accessibilityHint={_(
msg`Input the username or email address you used at signup`,
)}
/> />
</View> </View>
<View style={[pal.borderDark, styles.groupContent]}> <View style={[pal.borderDark, styles.groupContent]}>
@ -221,8 +223,8 @@ export const LoginForm = ({
accessibilityLabel={_(msg`Password`)} accessibilityLabel={_(msg`Password`)}
accessibilityHint={ accessibilityHint={
identifier === '' identifier === ''
? 'Input your password' ? _(msg`Input your password`)
: `Input the password tied to ${identifier}` : _(msg`Input the password tied to ${identifier}`)
} }
/> />
<TouchableOpacity <TouchableOpacity
@ -231,7 +233,7 @@ export const LoginForm = ({
onPress={onPressForgotPassword} onPress={onPressForgotPassword}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_(msg`Forgot password`)} accessibilityLabel={_(msg`Forgot password`)}
accessibilityHint="Opens password reset form"> accessibilityHint={_(msg`Opens password reset form`)}>
<Text style={pal.link}> <Text style={pal.link}>
<Trans>Forgot</Trans> <Trans>Forgot</Trans>
</Text> </Text>
@ -261,7 +263,7 @@ export const LoginForm = ({
onPress={onPressRetryConnect} onPress={onPressRetryConnect}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_(msg`Retry`)} accessibilityLabel={_(msg`Retry`)}
accessibilityHint="Retries login"> accessibilityHint={_(msg`Retries login`)}>
<Text type="xl-bold" style={[pal.link, s.pr5]}> <Text type="xl-bold" style={[pal.link, s.pr5]}>
<Trans>Retry</Trans> <Trans>Retry</Trans>
</Text> </Text>
@ -281,7 +283,7 @@ export const LoginForm = ({
onPress={onPressNext} onPress={onPressNext}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_(msg`Go to next`)} accessibilityLabel={_(msg`Go to next`)}
accessibilityHint="Navigates to the next screen"> accessibilityHint={_(msg`Navigates to the next screen`)}>
<Text type="xl-bold" style={[pal.link, s.pr5]}> <Text type="xl-bold" style={[pal.link, s.pr5]}>
<Trans>Next</Trans> <Trans>Next</Trans>
</Text> </Text>

View file

@ -36,7 +36,7 @@ export const PasswordUpdatedForm = ({
onPress={onPressNext} onPress={onPressNext}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_(msg`Close alert`)} accessibilityLabel={_(msg`Close alert`)}
accessibilityHint="Closes password update alert"> accessibilityHint={_(msg`Closes password update alert`)}>
<Text type="xl-bold" style={[pal.link, s.pr5]}> <Text type="xl-bold" style={[pal.link, s.pr5]}>
<Trans>Okay</Trans> <Trans>Okay</Trans>
</Text> </Text>

View file

@ -95,7 +95,7 @@ export const SetNewPasswordForm = ({
<TextInput <TextInput
testID="resetCodeInput" testID="resetCodeInput"
style={[pal.text, styles.textInput]} style={[pal.text, styles.textInput]}
placeholder="Reset code" placeholder={_(msg`Reset code`)}
placeholderTextColor={pal.colors.textLight} placeholderTextColor={pal.colors.textLight}
autoCapitalize="none" autoCapitalize="none"
autoCorrect={false} autoCorrect={false}
@ -106,7 +106,9 @@ export const SetNewPasswordForm = ({
editable={!isProcessing} editable={!isProcessing}
accessible={true} accessible={true}
accessibilityLabel={_(msg`Reset code`)} accessibilityLabel={_(msg`Reset code`)}
accessibilityHint="Input code sent to your email for password reset" accessibilityHint={_(
msg`Input code sent to your email for password reset`,
)}
/> />
</View> </View>
<View style={[pal.borderDark, styles.groupContent]}> <View style={[pal.borderDark, styles.groupContent]}>
@ -117,7 +119,7 @@ export const SetNewPasswordForm = ({
<TextInput <TextInput
testID="newPasswordInput" testID="newPasswordInput"
style={[pal.text, styles.textInput]} style={[pal.text, styles.textInput]}
placeholder="New password" placeholder={_(msg`New password`)}
placeholderTextColor={pal.colors.textLight} placeholderTextColor={pal.colors.textLight}
autoCapitalize="none" autoCapitalize="none"
autoCorrect={false} autoCorrect={false}
@ -128,7 +130,7 @@ export const SetNewPasswordForm = ({
editable={!isProcessing} editable={!isProcessing}
accessible={true} accessible={true}
accessibilityLabel={_(msg`Password`)} accessibilityLabel={_(msg`Password`)}
accessibilityHint="Input new password" accessibilityHint={_(msg`Input new password`)}
/> />
</View> </View>
</View> </View>
@ -161,7 +163,7 @@ export const SetNewPasswordForm = ({
onPress={onPressNext} onPress={onPressNext}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_(msg`Go to next`)} accessibilityLabel={_(msg`Go to next`)}
accessibilityHint="Navigates to the next screen"> accessibilityHint={_(msg`Navigates to the next screen`)}>
<Text type="xl-bold" style={[pal.link, s.pr5]}> <Text type="xl-bold" style={[pal.link, s.pr5]}>
<Trans>Next</Trans> <Trans>Next</Trans>
</Text> </Text>

View file

@ -18,6 +18,8 @@ import {
} from '#/state/queries/preferences' } from '#/state/queries/preferences'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useAnalytics} from '#/lib/analytics/analytics' import {useAnalytics} from '#/lib/analytics/analytics'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
export function RecommendedFeedsItem({ export function RecommendedFeedsItem({
item, item,
@ -26,6 +28,7 @@ export function RecommendedFeedsItem({
}) { }) {
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui()
const {data: preferences} = usePreferencesQuery() const {data: preferences} = usePreferencesQuery()
const { const {
mutateAsync: pinFeed, mutateAsync: pinFeed,
@ -51,7 +54,7 @@ export function RecommendedFeedsItem({
await removeFeed({uri: item.uri}) await removeFeed({uri: item.uri})
resetRemoveFeed() resetRemoveFeed()
} catch (e) { } catch (e) {
Toast.show('There was an issue contacting your server') Toast.show(_(msg`There was an issue contacting your server`))
logger.error('Failed to unsave feed', {error: e}) logger.error('Failed to unsave feed', {error: e})
} }
} else { } else {
@ -60,7 +63,7 @@ export function RecommendedFeedsItem({
resetPinFeed() resetPinFeed()
track('Onboarding:CustomFeedAdded') track('Onboarding:CustomFeedAdded')
} catch (e) { } catch (e) {
Toast.show('There was an issue contacting your server') Toast.show(_(msg`There was an issue contacting your server`))
logger.error('Failed to pin feed', {error: e}) logger.error('Failed to pin feed', {error: e})
} }
} }
@ -94,7 +97,7 @@ export function RecommendedFeedsItem({
</Text> </Text>
<Text style={[pal.textLight, {marginBottom: 8}]} numberOfLines={1}> <Text style={[pal.textLight, {marginBottom: 8}]} numberOfLines={1}>
by {sanitizeHandle(item.creator.handle, '@')} <Trans>by {sanitizeHandle(item.creator.handle, '@')}</Trans>
</Text> </Text>
{item.description ? ( {item.description ? (
@ -133,7 +136,7 @@ export function RecommendedFeedsItem({
color={pal.colors.textInverted} color={pal.colors.textInverted}
/> />
<Text type="lg-medium" style={pal.textInverted}> <Text type="lg-medium" style={pal.textInverted}>
Added <Trans>Added</Trans>
</Text> </Text>
</> </>
) : ( ) : (
@ -144,7 +147,7 @@ export function RecommendedFeedsItem({
color={pal.colors.textInverted} color={pal.colors.textInverted}
/> />
<Text type="lg-medium" style={pal.textInverted}> <Text type="lg-medium" style={pal.textInverted}>
Add <Trans>Add</Trans>
</Text> </Text>
</> </>
)} )}

View file

@ -83,7 +83,7 @@ export function RecommendedFollows({next}: Props) {
<Text <Text
type="2xl-medium" type="2xl-medium"
style={{color: '#fff', position: 'relative', top: -1}}> style={{color: '#fff', position: 'relative', top: -1}}>
<Trans>Done</Trans> <Trans context="action">Done</Trans>
</Text> </Text>
<FontAwesomeIcon icon="angle-right" color="#fff" size={14} /> <FontAwesomeIcon icon="angle-right" color="#fff" size={14} />
</View> </View>

View file

@ -7,6 +7,7 @@ import {usePalette} from 'lib/hooks/usePalette'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {TitleColumnLayout} from 'view/com/util/layouts/TitleColumnLayout' import {TitleColumnLayout} from 'view/com/util/layouts/TitleColumnLayout'
import {Button} from 'view/com/util/forms/Button' import {Button} from 'view/com/util/forms/Button'
import {Trans} from '@lingui/macro'
type Props = { type Props = {
next: () => void next: () => void
@ -17,7 +18,7 @@ export function WelcomeDesktop({next}: Props) {
const pal = usePalette('default') const pal = usePalette('default')
const horizontal = useMediaQuery({minWidth: 1300}) const horizontal = useMediaQuery({minWidth: 1300})
const title = ( const title = (
<> <Trans>
<Text <Text
style={[ style={[
pal.textLight, pal.textLight,
@ -40,7 +41,7 @@ export function WelcomeDesktop({next}: Props) {
]}> ]}>
Bluesky Bluesky
</Text> </Text>
</> </Trans>
) )
return ( return (
<TitleColumnLayout <TitleColumnLayout
@ -52,10 +53,12 @@ export function WelcomeDesktop({next}: Props) {
<FontAwesomeIcon icon={'globe'} size={36} color={pal.colors.link} /> <FontAwesomeIcon icon={'globe'} size={36} color={pal.colors.link} />
<View style={[styles.rowText]}> <View style={[styles.rowText]}>
<Text type="xl-bold" style={[pal.text]}> <Text type="xl-bold" style={[pal.text]}>
Bluesky is public. <Trans>Bluesky is public.</Trans>
</Text> </Text>
<Text type="xl" style={[pal.text, s.pt2]}> <Text type="xl" style={[pal.text, s.pt2]}>
Your posts, likes, and blocks are public. Mutes are private. <Trans>
Your posts, likes, and blocks are public. Mutes are private.
</Trans>
</Text> </Text>
</View> </View>
</View> </View>
@ -63,10 +66,10 @@ export function WelcomeDesktop({next}: Props) {
<FontAwesomeIcon icon={'at'} size={36} color={pal.colors.link} /> <FontAwesomeIcon icon={'at'} size={36} color={pal.colors.link} />
<View style={[styles.rowText]}> <View style={[styles.rowText]}>
<Text type="xl-bold" style={[pal.text]}> <Text type="xl-bold" style={[pal.text]}>
Bluesky is open. <Trans>Bluesky is open.</Trans>
</Text> </Text>
<Text type="xl" style={[pal.text, s.pt2]}> <Text type="xl" style={[pal.text, s.pt2]}>
Never lose access to your followers and data. <Trans>Never lose access to your followers and data.</Trans>
</Text> </Text>
</View> </View>
</View> </View>
@ -74,10 +77,13 @@ export function WelcomeDesktop({next}: Props) {
<FontAwesomeIcon icon={'gear'} size={36} color={pal.colors.link} /> <FontAwesomeIcon icon={'gear'} size={36} color={pal.colors.link} />
<View style={[styles.rowText]}> <View style={[styles.rowText]}>
<Text type="xl-bold" style={[pal.text]}> <Text type="xl-bold" style={[pal.text]}>
Bluesky is flexible. <Trans>Bluesky is flexible.</Trans>
</Text> </Text>
<Text type="xl" style={[pal.text, s.pt2]}> <Text type="xl" style={[pal.text, s.pt2]}>
Choose the algorithms that power your experience with custom feeds. <Trans>
Choose the algorithms that power your experience with custom
feeds.
</Trans>
</Text> </Text>
</View> </View>
</View> </View>
@ -94,7 +100,7 @@ export function WelcomeDesktop({next}: Props) {
<Text <Text
type="2xl-medium" type="2xl-medium"
style={{color: '#fff', position: 'relative', top: -1}}> style={{color: '#fff', position: 'relative', top: -1}}>
Next <Trans context="action">Next</Trans>
</Text> </Text>
<FontAwesomeIcon icon="angle-right" color="#fff" size={14} /> <FontAwesomeIcon icon="angle-right" color="#fff" size={14} />
</View> </View>

View file

@ -260,7 +260,11 @@ export const ComposePost = observer(function ComposePost({
setLangPrefs.savePostLanguageToHistory() setLangPrefs.savePostLanguageToHistory()
onPost?.() onPost?.()
onClose() onClose()
Toast.show(`Your ${replyTo ? 'reply' : 'post'} has been published`) Toast.show(
replyTo
? _(msg`Your reply has been published`)
: _(msg`Your post has been published`),
)
} }
const canPost = useMemo( const canPost = useMemo(
@ -269,7 +273,9 @@ export const ComposePost = observer(function ComposePost({
(!requireAltTextEnabled || !gallery.needsAltText), (!requireAltTextEnabled || !gallery.needsAltText),
[graphemeLength, requireAltTextEnabled, gallery.needsAltText], [graphemeLength, requireAltTextEnabled, gallery.needsAltText],
) )
const selectTextInputPlaceholder = replyTo ? 'Write your reply' : `What's up?` const selectTextInputPlaceholder = replyTo
? _(msg`Write your reply`)
: _(msg`What's up?`)
const canSelectImages = useMemo(() => gallery.size < 4, [gallery.size]) const canSelectImages = useMemo(() => gallery.size < 4, [gallery.size])
const hasMedia = gallery.size > 0 || Boolean(extLink) const hasMedia = gallery.size > 0 || Boolean(extLink)
@ -291,7 +297,9 @@ export const ComposePost = observer(function ComposePost({
onAccessibilityEscape={onPressCancel} onAccessibilityEscape={onPressCancel}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_(msg`Cancel`)} accessibilityLabel={_(msg`Cancel`)}
accessibilityHint="Closes post composer and discards post draft"> accessibilityHint={_(
msg`Closes post composer and discards post draft`,
)}>
<Text style={[pal.link, s.f18]}> <Text style={[pal.link, s.f18]}>
<Trans>Cancel</Trans> <Trans>Cancel</Trans>
</Text> </Text>
@ -323,7 +331,7 @@ export const ComposePost = observer(function ComposePost({
onPress={onPressPublish} onPress={onPressPublish}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={ accessibilityLabel={
replyTo ? 'Publish reply' : 'Publish post' replyTo ? _(msg`Publish reply`) : _(msg`Publish post`)
} }
accessibilityHint=""> accessibilityHint="">
<LinearGradient <LinearGradient
@ -335,14 +343,18 @@ export const ComposePost = observer(function ComposePost({
end={{x: 1, y: 1}} end={{x: 1, y: 1}}
style={styles.postBtn}> style={styles.postBtn}>
<Text style={[s.white, s.f16, s.bold]}> <Text style={[s.white, s.f16, s.bold]}>
{replyTo ? 'Reply' : 'Post'} {replyTo ? (
<Trans context="action">Reply</Trans>
) : (
<Trans context="action">Post</Trans>
)}
</Text> </Text>
</LinearGradient> </LinearGradient>
</TouchableOpacity> </TouchableOpacity>
) : ( ) : (
<View style={[styles.postBtn, pal.btn]}> <View style={[styles.postBtn, pal.btn]}>
<Text style={[pal.textLight, s.f16, s.bold]}> <Text style={[pal.textLight, s.f16, s.bold]}>
<Trans>Post</Trans> <Trans context="action">Post</Trans>
</Text> </Text>
</View> </View>
)} )}
@ -400,7 +412,9 @@ export const ComposePost = observer(function ComposePost({
onError={setError} onError={setError}
accessible={true} accessible={true}
accessibilityLabel={_(msg`Write post`)} accessibilityLabel={_(msg`Write post`)}
accessibilityHint={`Compose posts up to ${MAX_GRAPHEME_LENGTH} characters in length`} accessibilityHint={_(
msg`Compose posts up to ${MAX_GRAPHEME_LENGTH} characters in length`,
)}
/> />
</View> </View>
@ -429,7 +443,9 @@ export const ComposePost = observer(function ComposePost({
onPress={() => onPressAddLinkCard(url)} onPress={() => onPressAddLinkCard(url)}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_(msg`Add link card`)} accessibilityLabel={_(msg`Add link card`)}
accessibilityHint={`Creates a card with a thumbnail. The card links to ${url}`}> accessibilityHint={_(
msg`Creates a card with a thumbnail. The card links to ${url}`,
)}>
<Text style={pal.text}> <Text style={pal.text}>
<Trans>Add link card:</Trans>{' '} <Trans>Add link card:</Trans>{' '}
<Text style={[pal.link, s.ml5]}>{toShortUrl(url)}</Text> <Text style={[pal.link, s.ml5]}>{toShortUrl(url)}</Text>

View file

@ -68,7 +68,7 @@ export const ExternalEmbed = ({
onPress={onRemove} onPress={onRemove}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_(msg`Remove image preview`)} accessibilityLabel={_(msg`Remove image preview`)}
accessibilityHint={`Removes default thumbnail from ${link.uri}`} accessibilityHint={_(msg`Removes default thumbnail from ${link.uri}`)}
onAccessibilityEscape={onRemove}> onAccessibilityEscape={onRemove}>
<FontAwesomeIcon size={18} icon="xmark" style={s.white} /> <FontAwesomeIcon size={18} icon="xmark" style={s.white} />
</TouchableOpacity> </TouchableOpacity>

View file

@ -22,7 +22,7 @@ export function ComposePrompt({onPressCompose}: {onPressCompose: () => void}) {
onPress={() => onPressCompose()} onPress={() => onPressCompose()}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_(msg`Compose reply`)} accessibilityLabel={_(msg`Compose reply`)}
accessibilityHint="Opens composer"> accessibilityHint={_(msg`Opens composer`)}>
<UserAvatar avatar={profile?.avatar} size={38} /> <UserAvatar avatar={profile?.avatar} size={38} />
<Text <Text
type="xl" type="xl"

View file

@ -58,7 +58,7 @@ export function OpenCameraBtn({gallery}: Props) {
hitSlop={HITSLOP_10} hitSlop={HITSLOP_10}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_(msg`Camera`)} accessibilityLabel={_(msg`Camera`)}
accessibilityHint="Opens camera on device"> accessibilityHint={_(msg`Opens camera on device`)}>
<FontAwesomeIcon <FontAwesomeIcon
icon="camera" icon="camera"
style={pal.link as FontAwesomeIconStyle} style={pal.link as FontAwesomeIconStyle}

View file

@ -41,7 +41,7 @@ export function SelectPhotoBtn({gallery}: Props) {
hitSlop={HITSLOP_10} hitSlop={HITSLOP_10}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_(msg`Gallery`)} accessibilityLabel={_(msg`Gallery`)}
accessibilityHint="Opens device photo gallery"> accessibilityHint={_(msg`Opens device photo gallery`)}>
<FontAwesomeIcon <FontAwesomeIcon
icon={['far', 'image']} icon={['far', 'image']}
style={pal.link as FontAwesomeIconStyle} style={pal.link as FontAwesomeIconStyle}

View file

@ -17,6 +17,7 @@ import {usePalette} from 'lib/hooks/usePalette'
import {Text} from 'view/com/util/text/Text' import {Text} from 'view/com/util/text/Text'
import {UserAvatar} from 'view/com/util/UserAvatar' import {UserAvatar} from 'view/com/util/UserAvatar'
import {useGrapheme} from '../hooks/useGrapheme' import {useGrapheme} from '../hooks/useGrapheme'
import {Trans} from '@lingui/macro'
interface MentionListRef { interface MentionListRef {
onKeyDown: (props: SuggestionKeyDownProps) => boolean onKeyDown: (props: SuggestionKeyDownProps) => boolean
@ -187,7 +188,7 @@ const MentionList = forwardRef<MentionListRef, SuggestionProps>(
}) })
) : ( ) : (
<Text type="sm" style={[pal.text, styles.noResult]}> <Text type="sm" style={[pal.text, styles.noResult]}>
No result <Trans>No result</Trans>
</Text> </Text>
)} )}
</View> </View>

View file

@ -197,7 +197,7 @@ export function FeedPage({
onPress={onPressCompose} onPress={onPressCompose}
icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />} icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_(msg`New post`)} accessibilityLabel={_(msg({message: `New post`, context: 'action'}))}
accessibilityHint="" accessibilityHint=""
/> />
)} )}

View file

@ -14,7 +14,7 @@ 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 {useModalControls} from '#/state/modals' import {useModalControls} from '#/state/modals'
import {msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import { import {
usePinFeedMutation, usePinFeedMutation,
@ -108,9 +108,9 @@ export function FeedSourceCardLoaded({
try { try {
await removeFeed({uri: feed.uri}) await removeFeed({uri: feed.uri})
// await item.unsave() // await item.unsave()
Toast.show('Removed from my feeds') Toast.show(_(msg`Removed from my feeds`))
} catch (e) { } catch (e) {
Toast.show('There was an issue contacting your server') Toast.show(_(msg`There was an issue contacting your server`))
logger.error('Failed to unsave feed', {error: e}) logger.error('Failed to unsave feed', {error: e})
} }
}, },
@ -122,9 +122,9 @@ export function FeedSourceCardLoaded({
} else { } else {
await saveFeed({uri: feed.uri}) await saveFeed({uri: feed.uri})
} }
Toast.show('Added to my feeds') Toast.show(_(msg`Added to my feeds`))
} catch (e) { } catch (e) {
Toast.show('There was an issue contacting your server') Toast.show(_(msg`There was an issue contacting your server`))
logger.error('Failed to save feed', {error: e}) logger.error('Failed to save feed', {error: e})
} }
} }
@ -164,7 +164,7 @@ export function FeedSourceCardLoaded({
testID={`feed-${feedUri}-toggleSave`} testID={`feed-${feedUri}-toggleSave`}
disabled={isRemovePending} disabled={isRemovePending}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={'Remove from my feeds'} accessibilityLabel={_(msg`Remove from my feeds`)}
accessibilityHint="" accessibilityHint=""
onPress={() => { onPress={() => {
openModal({ openModal({
@ -175,9 +175,11 @@ export function FeedSourceCardLoaded({
try { try {
await removeFeed({uri: feedUri}) await removeFeed({uri: feedUri})
// await item.unsave() // await item.unsave()
Toast.show('Removed from my feeds') Toast.show(_(msg`Removed from my feeds`))
} catch (e) { } catch (e) {
Toast.show('There was an issue contacting your server') Toast.show(
_(msg`There was an issue contacting your server`),
)
logger.error('Failed to unsave feed', {error: e}) logger.error('Failed to unsave feed', {error: e})
} }
}, },
@ -223,8 +225,11 @@ export function FeedSourceCardLoaded({
{feed.displayName} {feed.displayName}
</Text> </Text>
<Text style={[pal.textLight]} numberOfLines={3}> <Text style={[pal.textLight]} numberOfLines={3}>
{feed.type === 'feed' ? 'Feed' : 'List'} by{' '} {feed.type === 'feed' ? (
{sanitizeHandle(feed.creatorHandle, '@')} <Trans>Feed by {sanitizeHandle(feed.creatorHandle, '@')}</Trans>
) : (
<Trans>List by {sanitizeHandle(feed.creatorHandle, '@')}</Trans>
)}
</Text> </Text>
</View> </View>
@ -235,7 +240,7 @@ export function FeedSourceCardLoaded({
disabled={isSavePending || isPinPending || isRemovePending} disabled={isSavePending || isPinPending || isRemovePending}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={ accessibilityLabel={
isSaved ? 'Remove from my feeds' : 'Add to my feeds' isSaved ? _(msg`Remove from my feeds`) : _(msg`Add to my feeds`)
} }
accessibilityHint="" accessibilityHint=""
onPress={onToggleSaved} onPress={onToggleSaved}
@ -269,8 +274,10 @@ 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]}>
Liked by {feed.likeCount || 0}{' '} <Trans>
{pluralize(feed.likeCount || 0, 'user')} Liked by {feed.likeCount || 0}{' '}
{pluralize(feed.likeCount || 0, 'user')}
</Trans>
</Text> </Text>
) : null} ) : null}
</Pressable> </Pressable>

View file

@ -9,13 +9,14 @@ import {Text} from '../util/text/Text'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useProfileFeedgensQuery, RQKEY} from '#/state/queries/profile-feedgens' import {useProfileFeedgensQuery, RQKEY} from '#/state/queries/profile-feedgens'
import {logger} from '#/logger' import {logger} from '#/logger'
import {Trans} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
import {useTheme} from '#/lib/ThemeContext' import {useTheme} from '#/lib/ThemeContext'
import {usePreferencesQuery} from '#/state/queries/preferences' import {usePreferencesQuery} from '#/state/queries/preferences'
import {hydrateFeedGenerator} from '#/state/queries/feed' import {hydrateFeedGenerator} from '#/state/queries/feed'
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {isNative} from '#/platform/detection' import {isNative} from '#/platform/detection'
import {useLingui} from '@lingui/react'
const LOADING = {_reactKey: '__loading__'} const LOADING = {_reactKey: '__loading__'}
const EMPTY = {_reactKey: '__empty__'} const EMPTY = {_reactKey: '__empty__'}
@ -43,6 +44,7 @@ export const ProfileFeedgens = React.forwardRef<
ref, ref,
) { ) {
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui()
const theme = useTheme() const theme = useTheme()
const [isPTRing, setIsPTRing] = React.useState(false) const [isPTRing, setIsPTRing] = React.useState(false)
const opts = React.useMemo(() => ({enabled}), [enabled]) const opts = React.useMemo(() => ({enabled}), [enabled])
@ -142,7 +144,9 @@ export const ProfileFeedgens = React.forwardRef<
} else if (item === LOAD_MORE_ERROR_ITEM) { } else if (item === LOAD_MORE_ERROR_ITEM) {
return ( return (
<LoadMoreRetryBtn <LoadMoreRetryBtn
label="There was an issue fetching your lists. Tap here to try again." label={_(
msg`There was an issue fetching your lists. Tap here to try again.`,
)}
onPress={onPressRetryLoadMore} onPress={onPressRetryLoadMore}
/> />
) )
@ -162,7 +166,7 @@ export const ProfileFeedgens = React.forwardRef<
} }
return null return null
}, },
[error, refetch, onPressRetryLoadMore, pal, preferences], [error, refetch, onPressRetryLoadMore, pal, preferences, _],
) )
return ( return (

View file

@ -24,7 +24,7 @@ const ImageDefaultHeader = ({onRequestClose}: Props) => (
hitSlop={HIT_SLOP} hitSlop={HIT_SLOP}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={t`Close image`} accessibilityLabel={t`Close image`}
accessibilityHint="Closes viewer for header image" accessibilityHint={t`Closes viewer for header image`}
onAccessibilityEscape={onRequestClose}> onAccessibilityEscape={onRequestClose}>
<Text style={styles.closeText}></Text> <Text style={styles.closeText}></Text>
</TouchableOpacity> </TouchableOpacity>

View file

@ -15,6 +15,8 @@ import {
ProfileImageLightbox, ProfileImageLightbox,
ImagesLightbox, ImagesLightbox,
} from '#/state/lightbox' } from '#/state/lightbox'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
export function Lightbox() { export function Lightbox() {
const {activeLightbox} = useLightbox() const {activeLightbox} = useLightbox()
@ -53,6 +55,7 @@ export function Lightbox() {
} }
function LightboxFooter({imageIndex}: {imageIndex: number}) { function LightboxFooter({imageIndex}: {imageIndex: number}) {
const {_} = useLingui()
const {activeLightbox} = useLightbox() const {activeLightbox} = useLightbox()
const [isAltExpanded, setAltExpanded] = React.useState(false) const [isAltExpanded, setAltExpanded] = React.useState(false)
const [permissionResponse, requestPermission] = MediaLibrary.usePermissions() const [permissionResponse, requestPermission] = MediaLibrary.usePermissions()
@ -60,12 +63,14 @@ function LightboxFooter({imageIndex}: {imageIndex: number}) {
const saveImageToAlbumWithToasts = React.useCallback( const saveImageToAlbumWithToasts = React.useCallback(
async (uri: string) => { async (uri: string) => {
if (!permissionResponse || permissionResponse.granted === false) { if (!permissionResponse || permissionResponse.granted === false) {
Toast.show('Permission to access camera roll is required.') Toast.show(_(msg`Permission to access camera roll is required.`))
if (permissionResponse?.canAskAgain) { if (permissionResponse?.canAskAgain) {
requestPermission() requestPermission()
} else { } else {
Toast.show( Toast.show(
'Permission to access camera roll was denied. Please enable it in your system settings.', _(
msg`Permission to access camera roll was denied. Please enable it in your system settings.`,
),
) )
} }
return return
@ -78,7 +83,7 @@ function LightboxFooter({imageIndex}: {imageIndex: number}) {
Toast.show(`Failed to save image: ${String(e)}`) Toast.show(`Failed to save image: ${String(e)}`)
} }
}, },
[permissionResponse, requestPermission], [permissionResponse, requestPermission, _],
) )
const lightbox = activeLightbox const lightbox = activeLightbox
@ -117,7 +122,7 @@ function LightboxFooter({imageIndex}: {imageIndex: number}) {
onPress={() => saveImageToAlbumWithToasts(uri)}> onPress={() => saveImageToAlbumWithToasts(uri)}>
<FontAwesomeIcon icon={['far', 'floppy-disk']} style={s.white} /> <FontAwesomeIcon icon={['far', 'floppy-disk']} style={s.white} />
<Text type="xl" style={s.white}> <Text type="xl" style={s.white}>
Save <Trans context="action">Save</Trans>
</Text> </Text>
</Button> </Button>
<Button <Button
@ -126,7 +131,7 @@ function LightboxFooter({imageIndex}: {imageIndex: number}) {
onPress={() => shareImageModal({uri})}> onPress={() => shareImageModal({uri})}>
<FontAwesomeIcon icon="arrow-up-from-bracket" style={s.white} /> <FontAwesomeIcon icon="arrow-up-from-bracket" style={s.white} />
<Text type="xl" style={s.white}> <Text type="xl" style={s.white}>
Share <Trans context="action">Share</Trans>
</Text> </Text>
</Button> </Button>
</View> </View>

View file

@ -110,7 +110,7 @@ function LightboxInner({
onPress={onClose} onPress={onClose}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_(msg`Close image viewer`)} accessibilityLabel={_(msg`Close image viewer`)}
accessibilityHint="Exits image view" accessibilityHint={_(msg`Exits image view`)}
onAccessibilityEscape={onClose}> onAccessibilityEscape={onClose}>
<View style={styles.imageCenterer}> <View style={styles.imageCenterer}>
<Image <Image
@ -154,7 +154,9 @@ function LightboxInner({
<View style={styles.footer}> <View style={styles.footer}>
<Pressable <Pressable
accessibilityLabel={_(msg`Expand alt text`)} accessibilityLabel={_(msg`Expand alt text`)}
accessibilityHint="If alt text is long, toggles alt text expanded state" accessibilityHint={_(
msg`If alt text is long, toggles alt text expanded state`,
)}
onPress={() => { onPress={() => {
setAltExpanded(!isAltExpanded) setAltExpanded(!isAltExpanded)
}}> }}>

View file

@ -11,6 +11,7 @@ import {useSession} from '#/state/session'
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 {makeProfileLink} from 'lib/routes/links' import {makeProfileLink} from 'lib/routes/links'
import {Trans} from '@lingui/macro'
export const ListCard = ({ export const ListCard = ({
testID, testID,
@ -76,19 +77,28 @@ export const ListCard = ({
{sanitizeDisplayName(list.name)} {sanitizeDisplayName(list.name)}
</Text> </Text>
<Text type="md" style={[pal.textLight]} numberOfLines={1}> <Text type="md" style={[pal.textLight]} numberOfLines={1}>
{list.purpose === 'app.bsky.graph.defs#curatelist' && 'User list '} {list.purpose === 'app.bsky.graph.defs#curatelist' &&
(list.creator.did === currentAccount?.did ? (
<Trans>User list by you</Trans>
) : (
<Trans>
User list by {sanitizeHandle(list.creator.handle, '@')}
</Trans>
))}
{list.purpose === 'app.bsky.graph.defs#modlist' && {list.purpose === 'app.bsky.graph.defs#modlist' &&
'Moderation list '} (list.creator.did === currentAccount?.did ? (
by{' '} <Trans>Moderation list by you</Trans>
{list.creator.did === currentAccount?.did ) : (
? 'you' <Trans>
: sanitizeHandle(list.creator.handle, '@')} Moderation list by {sanitizeHandle(list.creator.handle, '@')}
</Trans>
))}
</Text> </Text>
{!!list.viewer?.muted && ( {!!list.viewer?.muted && (
<View style={s.flexRow}> <View style={s.flexRow}>
<View style={[s.mt5, pal.btn, styles.pill]}> <View style={[s.mt5, pal.btn, styles.pill]}>
<Text type="xs" style={pal.text}> <Text type="xs" style={pal.text}>
Subscribed <Trans>Subscribed</Trans>
</Text> </Text>
</View> </View>
</View> </View>

View file

@ -20,6 +20,8 @@ import {logger} from '#/logger'
import {useModalControls} from '#/state/modals' import {useModalControls} from '#/state/modals'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
const LOADING_ITEM = {_reactKey: '__loading__'} const LOADING_ITEM = {_reactKey: '__loading__'}
const EMPTY_ITEM = {_reactKey: '__empty__'} const EMPTY_ITEM = {_reactKey: '__empty__'}
@ -50,6 +52,7 @@ export function ListMembers({
desktopFixedHeightOffset?: number desktopFixedHeightOffset?: number
}) { }) {
const {track} = useAnalytics() const {track} = useAnalytics()
const {_} = useLingui()
const [isRefreshing, setIsRefreshing] = React.useState(false) const [isRefreshing, setIsRefreshing] = React.useState(false)
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
const {openModal} = useModalControls() const {openModal} = useModalControls()
@ -143,12 +146,12 @@ export function ListMembers({
<Button <Button
testID={`user-${profile.handle}-editBtn`} testID={`user-${profile.handle}-editBtn`}
type="default" type="default"
label="Edit" label={_(msg({message: 'Edit', context: 'action'}))}
onPress={() => onPressEditMembership(profile)} onPress={() => onPressEditMembership(profile)}
/> />
) )
}, },
[isOwner, onPressEditMembership], [isOwner, onPressEditMembership, _],
) )
const renderItem = React.useCallback( const renderItem = React.useCallback(
@ -165,7 +168,9 @@ export function ListMembers({
} else if (item === LOAD_MORE_ERROR_ITEM) { } else if (item === LOAD_MORE_ERROR_ITEM) {
return ( return (
<LoadMoreRetryBtn <LoadMoreRetryBtn
label="There was an issue fetching the list. Tap here to try again." label={_(
msg`There was an issue fetching the list. Tap here to try again.`,
)}
onPress={onPressRetryLoadMore} onPress={onPressRetryLoadMore}
/> />
) )
@ -191,6 +196,7 @@ export function ListMembers({
onPressTryAgain, onPressTryAgain,
onPressRetryLoadMore, onPressRetryLoadMore,
isMobile, isMobile,
_,
], ],
) )

View file

@ -10,11 +10,12 @@ import {useAnalytics} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useProfileListsQuery, RQKEY} from '#/state/queries/profile-lists' import {useProfileListsQuery, RQKEY} from '#/state/queries/profile-lists'
import {logger} from '#/logger' import {logger} from '#/logger'
import {Trans} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
import {useTheme} from '#/lib/ThemeContext' import {useTheme} from '#/lib/ThemeContext'
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {isNative} from '#/platform/detection' import {isNative} from '#/platform/detection'
import {useLingui} from '@lingui/react'
const LOADING = {_reactKey: '__loading__'} const LOADING = {_reactKey: '__loading__'}
const EMPTY = {_reactKey: '__empty__'} const EMPTY = {_reactKey: '__empty__'}
@ -42,6 +43,7 @@ export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>(
const pal = usePalette('default') const pal = usePalette('default')
const theme = useTheme() const theme = useTheme()
const {track} = useAnalytics() const {track} = useAnalytics()
const {_} = useLingui()
const [isPTRing, setIsPTRing] = React.useState(false) const [isPTRing, setIsPTRing] = React.useState(false)
const opts = React.useMemo(() => ({enabled}), [enabled]) const opts = React.useMemo(() => ({enabled}), [enabled])
const { const {
@ -149,7 +151,9 @@ export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>(
} else if (item === LOAD_MORE_ERROR_ITEM) { } else if (item === LOAD_MORE_ERROR_ITEM) {
return ( return (
<LoadMoreRetryBtn <LoadMoreRetryBtn
label="There was an issue fetching your lists. Tap here to try again." label={_(
msg`There was an issue fetching your lists. Tap here to try again.`,
)}
onPress={onPressRetryLoadMore} onPress={onPressRetryLoadMore}
/> />
) )
@ -164,7 +168,7 @@ export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>(
/> />
) )
}, },
[error, refetch, onPressRetryLoadMore, pal], [error, refetch, onPressRetryLoadMore, pal, _],
) )
return ( return (

View file

@ -72,10 +72,10 @@ export function Component({}: {}) {
const onCopy = React.useCallback(() => { const onCopy = React.useCallback(() => {
if (appPassword) { if (appPassword) {
Clipboard.setString(appPassword) Clipboard.setString(appPassword)
Toast.show('Copied to clipboard') Toast.show(_(msg`Copied to clipboard`))
setWasCopied(true) setWasCopied(true)
} }
}, [appPassword]) }, [appPassword, _])
const onDone = React.useCallback(() => { const onDone = React.useCallback(() => {
closeModal() closeModal()
@ -85,7 +85,9 @@ export function Component({}: {}) {
// if name is all whitespace, we don't allow it // if name is all whitespace, we don't allow it
if (!name || !name.trim()) { if (!name || !name.trim()) {
Toast.show( Toast.show(
'Please enter a name for your app password. All spaces is not allowed.', _(
msg`Please enter a name for your app password. All spaces is not allowed.`,
),
'times', 'times',
) )
return return
@ -93,14 +95,14 @@ export function Component({}: {}) {
// if name is too short (under 4 chars), we don't allow it // if name is too short (under 4 chars), we don't allow it
if (name.length < 4) { if (name.length < 4) {
Toast.show( Toast.show(
'App Password names must be at least 4 characters long.', _(msg`App Password names must be at least 4 characters long.`),
'times', 'times',
) )
return return
} }
if (passwords?.find(p => p.name === name)) { if (passwords?.find(p => p.name === name)) {
Toast.show('This name is already in use', 'times') Toast.show(_(msg`This name is already in use`), 'times')
return return
} }
@ -109,11 +111,11 @@ export function Component({}: {}) {
if (newPassword) { if (newPassword) {
setAppPassword(newPassword.password) setAppPassword(newPassword.password)
} else { } else {
Toast.show('Failed to create app password.', 'times') Toast.show(_(msg`Failed to create app password.`), 'times')
// TODO: better error handling (?) // TODO: better error handling (?)
} }
} catch (e) { } catch (e) {
Toast.show('Failed to create app password.', 'times') Toast.show(_(msg`Failed to create app password.`), 'times')
logger.error('Failed to create app password', {error: e}) logger.error('Failed to create app password', {error: e})
} }
} }
@ -127,7 +129,9 @@ export function Component({}: {}) {
setName(text) setName(text)
} else { } else {
Toast.show( Toast.show(
'App Password names can only contain letters, numbers, spaces, dashes, and underscores.', _(
msg`App Password names can only contain letters, numbers, spaces, dashes, and underscores.`,
),
) )
} }
} }
@ -158,7 +162,7 @@ export function Component({}: {}) {
style={[styles.input, pal.text]} style={[styles.input, pal.text]}
onChangeText={_onChangeText} onChangeText={_onChangeText}
value={name} value={name}
placeholder="Enter a name for this App Password" placeholder={_(msg`Enter a name for this App Password`)}
placeholderTextColor={pal.colors.textLight} placeholderTextColor={pal.colors.textLight}
autoCorrect={false} autoCorrect={false}
autoComplete="off" autoComplete="off"
@ -175,7 +179,7 @@ export function Component({}: {}) {
onEndEditing={createAppPassword} onEndEditing={createAppPassword}
accessible={true} accessible={true}
accessibilityLabel={_(msg`Name`)} accessibilityLabel={_(msg`Name`)}
accessibilityHint="Input name for app password" accessibilityHint={_(msg`Input name for app password`)}
/> />
</View> </View>
) : ( ) : (
@ -184,7 +188,7 @@ export function Component({}: {}) {
onPress={onCopy} onPress={onCopy}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_(msg`Copy`)} accessibilityLabel={_(msg`Copy`)}
accessibilityHint="Copies app password"> accessibilityHint={_(msg`Copies app password`)}>
<Text type="2xl-bold" style={[pal.text]}> <Text type="2xl-bold" style={[pal.text]}>
{appPassword} {appPassword}
</Text> </Text>
@ -221,7 +225,7 @@ export function Component({}: {}) {
<View style={styles.btnContainer}> <View style={styles.btnContainer}>
<Button <Button
type="primary" type="primary"
label={!appPassword ? 'Create App Password' : 'Done'} label={!appPassword ? _(msg`Create App Password`) : _(msg`Done`)}
style={styles.btn} style={styles.btn}
labelStyle={styles.btnLabel} labelStyle={styles.btnLabel}
onPress={!appPassword ? createAppPassword : onDone} onPress={!appPassword ? createAppPassword : onDone}

View file

@ -45,7 +45,7 @@ export function Component(props: ReportComponentProps) {
}, },
reason: details, reason: details,
}) })
Toast.show("We'll look into your appeal promptly.") Toast.show(_(msg`We'll look into your appeal promptly.`))
} finally { } finally {
closeModal() closeModal()
} }

View file

@ -71,7 +71,7 @@ function Inner({preferences}: {preferences: UsePreferencesQueryResponse}) {
buttonStyle={[pal.border, styles.dateInputButton]} buttonStyle={[pal.border, styles.dateInputButton]}
buttonLabelType="lg" buttonLabelType="lg"
accessibilityLabel={_(msg`Birthday`)} accessibilityLabel={_(msg`Birthday`)}
accessibilityHint="Enter your birth date" accessibilityHint={_(msg`Enter your birth date`)}
accessibilityLabelledBy="birthDate" accessibilityLabelledBy="birthDate"
/> />
</View> </View>

View file

@ -38,7 +38,7 @@ export function Component() {
const onRequestChange = async () => { const onRequestChange = async () => {
if (email === currentAccount?.email) { if (email === currentAccount?.email) {
setError('Enter your new email above') setError(_(msg`Enter your new email above`))
return return
} }
setError('') setError('')
@ -53,7 +53,7 @@ export function Component() {
email: email.trim(), email: email.trim(),
emailConfirmed: false, emailConfirmed: false,
}) })
Toast.show('Email updated') Toast.show(_(msg`Email updated`))
setStage(Stages.Done) setStage(Stages.Done)
} }
} catch (e) { } catch (e) {
@ -85,7 +85,7 @@ export function Component() {
email: email.trim(), email: email.trim(),
emailConfirmed: false, emailConfirmed: false,
}) })
Toast.show('Email updated') Toast.show(_(msg`Email updated`))
setStage(Stages.Done) setStage(Stages.Done)
} catch (e) { } catch (e) {
setError(cleanError(String(e))) setError(cleanError(String(e)))

View file

@ -147,7 +147,7 @@ export function Inner({
onPress={onPressCancel} onPress={onPressCancel}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_(msg`Cancel change handle`)} accessibilityLabel={_(msg`Cancel change handle`)}
accessibilityHint="Exits handle change process" accessibilityHint={_(msg`Exits handle change process`)}
onAccessibilityEscape={onPressCancel}> onAccessibilityEscape={onPressCancel}>
<Text type="lg" style={pal.textLight}> <Text type="lg" style={pal.textLight}>
Cancel Cancel
@ -168,7 +168,7 @@ export function Inner({
onPress={onPressSave} onPress={onPressSave}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_(msg`Save handle change`)} accessibilityLabel={_(msg`Save handle change`)}
accessibilityHint={`Saves handle change to ${handle}`}> accessibilityHint={_(msg`Saves handle change to ${handle}`)}>
<Text type="2xl-medium" style={pal.link}> <Text type="2xl-medium" style={pal.link}>
<Trans>Save</Trans> <Trans>Save</Trans>
</Text> </Text>
@ -263,14 +263,16 @@ function ProvidedHandleForm({
editable={!isProcessing} editable={!isProcessing}
accessible={true} accessible={true}
accessibilityLabel={_(msg`Handle`)} accessibilityLabel={_(msg`Handle`)}
accessibilityHint="Sets Bluesky username" accessibilityHint={_(msg`Sets Bluesky username`)}
/> />
</View> </View>
<Text type="md" style={[pal.textLight, s.pl10, s.pt10]}> <Text type="md" style={[pal.textLight, s.pl10, s.pt10]}>
<Trans>Your full handle will be</Trans>{' '} <Trans>
<Text type="md-bold" style={pal.textLight}> Your full handle will be{' '}
@{createFullHandle(handle, userDomain)} <Text type="md-bold" style={pal.textLight}>
</Text> @{createFullHandle(handle, userDomain)}
</Text>
</Trans>
</Text> </Text>
<TouchableOpacity <TouchableOpacity
onPress={onToggleCustom} onPress={onToggleCustom}

View file

@ -12,7 +12,7 @@ import {cleanError} from 'lib/strings/errors'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {isWeb} from 'platform/detection' import {isWeb} from 'platform/detection'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import type {ConfirmModal} from '#/state/modals' import type {ConfirmModal} from '#/state/modals'
import {useModalControls} from '#/state/modals' import {useModalControls} from '#/state/modals'
@ -72,10 +72,10 @@ export function Component({
onPress={onPress} onPress={onPress}
style={[styles.btn, confirmBtnStyle]} style={[styles.btn, confirmBtnStyle]}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_(msg`Confirm`)} accessibilityLabel={_(msg({message: 'Confirm', context: 'action'}))}
accessibilityHint=""> accessibilityHint="">
<Text style={[s.white, s.bold, s.f18]}> <Text style={[s.white, s.bold, s.f18]}>
{confirmBtnText ?? 'Confirm'} {confirmBtnText ?? <Trans context="action">Confirm</Trans>}
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
)} )}
@ -85,10 +85,10 @@ export function Component({
onPress={onPressCancel} onPress={onPressCancel}
style={[styles.btnCancel, s.mt10]} style={[styles.btnCancel, s.mt10]}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_(msg`Cancel`)} accessibilityLabel={_(msg({message: 'Cancel', context: 'action'}))}
accessibilityHint=""> accessibilityHint="">
<Text type="button-lg" style={pal.textLight}> <Text type="button-lg" style={pal.textLight}>
{cancelBtnText ?? 'Cancel'} {cancelBtnText ?? <Trans context="action">Cancel</Trans>}
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
)} )}

View file

@ -148,9 +148,13 @@ function AdultContentEnabledPref() {
) : typeof preferences?.birthDate === 'undefined' ? ( ) : typeof preferences?.birthDate === 'undefined' ? (
<View style={[pal.viewLight, styles.agePrompt]}> <View style={[pal.viewLight, styles.agePrompt]}>
<Text type="md" style={[pal.text, {flex: 1}]}> <Text type="md" style={[pal.text, {flex: 1}]}>
Confirm your age to enable adult content. <Trans>Confirm your age to enable adult content.</Trans>
</Text> </Text>
<Button type="primary" label="Set Age" onPress={onSetAge} /> <Button
type="primary"
label={_(msg({message: 'Set Age', context: 'action'}))}
onPress={onSetAge}
/>
</View> </View>
) : (preferences.userAge || 0) >= 18 ? ( ) : (preferences.userAge || 0) >= 18 ? (
<ToggleButton <ToggleButton
@ -165,7 +169,11 @@ function AdultContentEnabledPref() {
<Text type="md" style={[pal.text, {flex: 1}]}> <Text type="md" style={[pal.text, {flex: 1}]}>
<Trans>You must be 18 or older to enable adult content.</Trans> <Trans>You must be 18 or older to enable adult content.</Trans>
</Text> </Text>
<Button type="primary" label="Set Age" onPress={onSetAge} /> <Button
type="primary"
label={_(msg({message: 'Set Age', context: 'action'}))}
onPress={onSetAge}
/>
</View> </View>
)} )}
</View> </View>
@ -208,7 +216,7 @@ function ContentLabelPref({
{disabled || !visibility ? ( {disabled || !visibility ? (
<Text type="sm-bold" style={pal.textLight}> <Text type="sm-bold" style={pal.textLight}>
<Trans>Hide</Trans> <Trans context="action">Hide</Trans>
</Text> </Text>
) : ( ) : (
<SelectGroup <SelectGroup
@ -229,6 +237,7 @@ interface SelectGroupProps {
function SelectGroup({current, onChange, labelGroup}: SelectGroupProps) { function SelectGroup({current, onChange, labelGroup}: SelectGroupProps) {
const {_} = useLingui() const {_} = useLingui()
return ( return (
<View style={styles.selectableBtns}> <View style={styles.selectableBtns}>
<SelectableBtn <SelectableBtn
@ -279,6 +288,8 @@ function SelectableBtn({
}: SelectableBtnProps) { }: SelectableBtnProps) {
const pal = usePalette('default') const pal = usePalette('default')
const palPrimary = usePalette('inverted') const palPrimary = usePalette('inverted')
const {_} = useLingui()
return ( return (
<Pressable <Pressable
style={[ style={[
@ -291,7 +302,9 @@ function SelectableBtn({
onPress={() => onChange(value)} onPress={() => onChange(value)}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={value} accessibilityLabel={value}
accessibilityHint={`Set ${value} for ${labelGroup} content moderation policy`}> accessibilityHint={_(
msg`Set ${value} for ${labelGroup} content moderation policy`,
)}>
<Text style={current === value ? palPrimary.text : pal.text}> <Text style={current === value ? palPrimary.text : pal.text}>
{label} {label}
</Text> </Text>

View file

@ -65,7 +65,6 @@ export function Component({
return 'app.bsky.graph.defs#curatelist' return 'app.bsky.graph.defs#curatelist'
}, [list, purpose]) }, [list, purpose])
const isCurateList = activePurpose === 'app.bsky.graph.defs#curatelist' const isCurateList = activePurpose === 'app.bsky.graph.defs#curatelist'
const purposeLabel = isCurateList ? 'User' : 'Moderation'
const [isProcessing, setProcessing] = useState<boolean>(false) const [isProcessing, setProcessing] = useState<boolean>(false)
const [name, setName] = useState<string>(list?.name || '') const [name, setName] = useState<string>(list?.name || '')
@ -106,7 +105,7 @@ export function Component({
} }
const nameTrimmed = name.trim() const nameTrimmed = name.trim()
if (!nameTrimmed) { if (!nameTrimmed) {
setError('Name is required') setError(_(msg`Name is required`))
return return
} }
setProcessing(true) setProcessing(true)
@ -121,7 +120,11 @@ export function Component({
description: description.trim(), description: description.trim(),
avatar: newAvatar, avatar: newAvatar,
}) })
Toast.show(`${purposeLabel} list updated`) Toast.show(
isCurateList
? _(msg`User list updated`)
: _(msg`Moderation list updated`),
)
onSave?.(list.uri) onSave?.(list.uri)
} else { } else {
const res = await listCreateMutation.mutateAsync({ const res = await listCreateMutation.mutateAsync({
@ -130,14 +133,20 @@ export function Component({
description, description,
avatar: newAvatar, avatar: newAvatar,
}) })
Toast.show(`${purposeLabel} list created`) Toast.show(
isCurateList
? _(msg`User list created`)
: _(msg`Moderation list created`),
)
onSave?.(res.uri) onSave?.(res.uri)
} }
closeModal() closeModal()
} catch (e: any) { } catch (e: any) {
if (isNetworkError(e)) { if (isNetworkError(e)) {
setError( setError(
'Failed to create the list. Check your internet connection and try again.', _(
msg`Failed to create the list. Check your internet connection and try again.`,
),
) )
} else { } else {
setError(cleanError(e)) setError(cleanError(e))
@ -153,13 +162,13 @@ export function Component({
closeModal, closeModal,
activePurpose, activePurpose,
isCurateList, isCurateList,
purposeLabel,
name, name,
description, description,
newAvatar, newAvatar,
list, list,
listMetadataMutation, listMetadataMutation,
listCreateMutation, listCreateMutation,
_,
]) ])
return ( return (
@ -174,7 +183,17 @@ export function Component({
testID="createOrEditListModal"> testID="createOrEditListModal">
<Text style={[styles.title, pal.text]}> <Text style={[styles.title, pal.text]}>
<Trans> <Trans>
{list ? 'Edit' : 'New'} {purposeLabel} List {isCurateList ? (
list ? (
<Trans>Edit User List</Trans>
) : (
<Trans>New User List</Trans>
)
) : list ? (
<Trans>Edit Moderation List</Trans>
) : (
<Trans>New Moderation List</Trans>
)}
</Trans> </Trans>
</Text> </Text>
{error !== '' && ( {error !== '' && (
@ -202,7 +221,9 @@ export function Component({
testID="editNameInput" testID="editNameInput"
style={[styles.textInput, pal.border, pal.text]} style={[styles.textInput, pal.border, pal.text]}
placeholder={ placeholder={
isCurateList ? 'e.g. Great Posters' : 'e.g. Spammers' isCurateList
? _(msg`e.g. Great Posters`)
: _(msg`e.g. Spammers`)
} }
placeholderTextColor={colors.gray4} placeholderTextColor={colors.gray4}
value={name} value={name}
@ -222,8 +243,8 @@ export function Component({
style={[styles.textArea, pal.border, pal.text]} style={[styles.textArea, pal.border, pal.text]}
placeholder={ placeholder={
isCurateList isCurateList
? 'e.g. The posters who never miss.' ? _(msg`e.g. The posters who never miss.`)
: 'e.g. Users that repeatedly reply with ads.' : _(msg`e.g. Users that repeatedly reply with ads.`)
} }
placeholderTextColor={colors.gray4} placeholderTextColor={colors.gray4}
keyboardAppearance={theme.colorScheme} keyboardAppearance={theme.colorScheme}
@ -254,7 +275,7 @@ export function Component({
end={{x: 1, y: 1}} end={{x: 1, y: 1}}
style={[styles.btn]}> style={[styles.btn]}>
<Text style={[s.white, s.bold]}> <Text style={[s.white, s.bold]}>
<Trans>Save</Trans> <Trans context="action">Save</Trans>
</Text> </Text>
</LinearGradient> </LinearGradient>
</TouchableOpacity> </TouchableOpacity>
@ -269,7 +290,7 @@ export function Component({
onAccessibilityEscape={onPressCancel}> onAccessibilityEscape={onPressCancel}>
<View style={[styles.btn]}> <View style={[styles.btn]}>
<Text style={[s.black, s.bold, pal.text]}> <Text style={[s.black, s.bold, pal.text]}>
<Trans>Cancel</Trans> <Trans context="action">Cancel</Trans>
</Text> </Text>
</View> </View>
</TouchableOpacity> </TouchableOpacity>

View file

@ -62,7 +62,7 @@ export function Component({}: {}) {
password, password,
token, token,
}) })
Toast.show('Your account has been deleted') Toast.show(_(msg`Your account has been deleted`))
resetToTab('HomeTab') resetToTab('HomeTab')
removeAccount(currentAccount) removeAccount(currentAccount)
clearCurrentAccount() clearCurrentAccount()
@ -125,7 +125,9 @@ export function Component({}: {}) {
onPress={onPressSendEmail} onPress={onPressSendEmail}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_(msg`Send email`)} accessibilityLabel={_(msg`Send email`)}
accessibilityHint="Sends email with confirmation code for account deletion"> accessibilityHint={_(
msg`Sends email with confirmation code for account deletion`,
)}>
<LinearGradient <LinearGradient
colors={[ colors={[
gradients.blueLight.start, gradients.blueLight.start,
@ -135,7 +137,7 @@ export function Component({}: {}) {
end={{x: 1, y: 1}} end={{x: 1, y: 1}}
style={[styles.btn]}> style={[styles.btn]}>
<Text type="button-lg" style={[s.white, s.bold]}> <Text type="button-lg" style={[s.white, s.bold]}>
<Trans>Send Email</Trans> <Trans context="action">Send Email</Trans>
</Text> </Text>
</LinearGradient> </LinearGradient>
</TouchableOpacity> </TouchableOpacity>
@ -147,7 +149,7 @@ export function Component({}: {}) {
accessibilityHint="" accessibilityHint=""
onAccessibilityEscape={onCancel}> onAccessibilityEscape={onCancel}>
<Text type="button-lg" style={pal.textLight}> <Text type="button-lg" style={pal.textLight}>
<Trans>Cancel</Trans> <Trans context="action">Cancel</Trans>
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
</> </>
@ -174,7 +176,9 @@ export function Component({}: {}) {
onChangeText={setConfirmCode} onChangeText={setConfirmCode}
accessibilityLabelledBy="confirmationCode" accessibilityLabelledBy="confirmationCode"
accessibilityLabel={_(msg`Confirmation code`)} accessibilityLabel={_(msg`Confirmation code`)}
accessibilityHint="Input confirmation code for account deletion" accessibilityHint={_(
msg`Input confirmation code for account deletion`,
)}
/> />
<Text type="lg" style={styles.description} nativeID="password"> <Text type="lg" style={styles.description} nativeID="password">
<Trans>Please enter your password as well:</Trans> <Trans>Please enter your password as well:</Trans>
@ -189,7 +193,7 @@ export function Component({}: {}) {
onChangeText={setPassword} onChangeText={setPassword}
accessibilityLabelledBy="password" accessibilityLabelledBy="password"
accessibilityLabel={_(msg`Password`)} accessibilityLabel={_(msg`Password`)}
accessibilityHint="Input password for account deletion" accessibilityHint={_(msg`Input password for account deletion`)}
/> />
{error ? ( {error ? (
<View style={styles.mt20}> <View style={styles.mt20}>
@ -220,7 +224,7 @@ export function Component({}: {}) {
accessibilityHint="Exits account deletion process" accessibilityHint="Exits account deletion process"
onAccessibilityEscape={onCancel}> onAccessibilityEscape={onCancel}>
<Text type="button-lg" style={pal.textLight}> <Text type="button-lg" style={pal.textLight}>
<Trans>Cancel</Trans> <Trans context="action">Cancel</Trans>
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
</> </>

View file

@ -112,16 +112,16 @@ export const Component = observer(function EditImageImpl({
// }, // },
{ {
name: 'flip' as const, name: 'flip' as const,
label: 'Flip horizontal', label: _(msg`Flip horizontal`),
onPress: onFlipHorizontal, onPress: onFlipHorizontal,
}, },
{ {
name: 'flip' as const, name: 'flip' as const,
label: 'Flip vertically', label: _(msg`Flip vertically`),
onPress: onFlipVertical, onPress: onFlipVertical,
}, },
], ],
[onFlipHorizontal, onFlipVertical], [onFlipHorizontal, onFlipVertical, _],
) )
useEffect(() => { useEffect(() => {
@ -284,7 +284,7 @@ export const Component = observer(function EditImageImpl({
size={label?.startsWith('Flip') ? 22 : 24} size={label?.startsWith('Flip') ? 22 : 24}
style={[ style={[
pal.text, pal.text,
label === 'Flip vertically' label === _(msg`Flip vertically`)
? styles.flipVertical ? styles.flipVertical
: undefined, : undefined,
]} ]}
@ -330,7 +330,7 @@ export const Component = observer(function EditImageImpl({
end={{x: 1, y: 1}} end={{x: 1, y: 1}}
style={[styles.btn]}> style={[styles.btn]}>
<Text type="xl-medium" style={s.white}> <Text type="xl-medium" style={s.white}>
<Trans>Done</Trans> <Trans context="action">Done</Trans>
</Text> </Text>
</LinearGradient> </LinearGradient>
</Pressable> </Pressable>

View file

@ -125,7 +125,7 @@ export function Component({
newUserAvatar, newUserAvatar,
newUserBanner, newUserBanner,
}) })
Toast.show('Profile updated') Toast.show(_(msg`Profile updated`))
onUpdate?.() onUpdate?.()
closeModal() closeModal()
} catch (e: any) { } catch (e: any) {
@ -142,6 +142,7 @@ export function Component({
newUserAvatar, newUserAvatar,
newUserBanner, newUserBanner,
setImageError, setImageError,
_,
]) ])
return ( return (
@ -181,7 +182,7 @@ export function Component({
<TextInput <TextInput
testID="editProfileDisplayNameInput" testID="editProfileDisplayNameInput"
style={[styles.textInput, pal.border, pal.text]} style={[styles.textInput, pal.border, pal.text]}
placeholder="e.g. Alice Roberts" placeholder={_(msg`e.g. Alice Roberts`)}
placeholderTextColor={colors.gray4} placeholderTextColor={colors.gray4}
value={displayName} value={displayName}
onChangeText={v => onChangeText={v =>
@ -189,7 +190,7 @@ export function Component({
} }
accessible={true} accessible={true}
accessibilityLabel={_(msg`Display name`)} accessibilityLabel={_(msg`Display name`)}
accessibilityHint="Edit your display name" accessibilityHint={_(msg`Edit your display name`)}
/> />
</View> </View>
<View style={s.pb10}> <View style={s.pb10}>
@ -199,7 +200,7 @@ export function Component({
<TextInput <TextInput
testID="editProfileDescriptionInput" testID="editProfileDescriptionInput"
style={[styles.textArea, pal.border, pal.text]} style={[styles.textArea, pal.border, pal.text]}
placeholder="e.g. Artist, dog-lover, and avid reader." placeholder={_(msg`e.g. Artist, dog-lover, and avid reader.`)}
placeholderTextColor={colors.gray4} placeholderTextColor={colors.gray4}
keyboardAppearance={theme.colorScheme} keyboardAppearance={theme.colorScheme}
multiline multiline
@ -207,7 +208,7 @@ export function Component({
onChangeText={v => setDescription(enforceLen(v, MAX_DESCRIPTION))} onChangeText={v => setDescription(enforceLen(v, MAX_DESCRIPTION))}
accessible={true} accessible={true}
accessibilityLabel={_(msg`Description`)} accessibilityLabel={_(msg`Description`)}
accessibilityHint="Edit your profile description" accessibilityHint={_(msg`Edit your profile description`)}
/> />
</View> </View>
{updateMutation.isPending ? ( {updateMutation.isPending ? (
@ -221,7 +222,7 @@ export function Component({
onPress={onPressSave} onPress={onPressSave}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_(msg`Save`)} accessibilityLabel={_(msg`Save`)}
accessibilityHint="Saves any changes to your profile"> accessibilityHint={_(msg`Saves any changes to your profile`)}>
<LinearGradient <LinearGradient
colors={[gradients.blueLight.start, gradients.blueLight.end]} colors={[gradients.blueLight.start, gradients.blueLight.end]}
start={{x: 0, y: 0}} start={{x: 0, y: 0}}

View file

@ -19,7 +19,6 @@ import {usePalette} from 'lib/hooks/usePalette'
import {isWeb} from 'platform/detection' import {isWeb} from 'platform/detection'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {cleanError} from 'lib/strings/errors' import {cleanError} from 'lib/strings/errors'
import {useModalControls} from '#/state/modals' import {useModalControls} from '#/state/modals'
import {useInvitesState, useInvitesAPI} from '#/state/invites' import {useInvitesState, useInvitesAPI} from '#/state/invites'
@ -31,6 +30,7 @@ import {
useInviteCodesQuery, useInviteCodesQuery,
InviteCodesQueryResponse, InviteCodesQueryResponse,
} from '#/state/queries/invites' } from '#/state/queries/invites'
import {useLingui} from '@lingui/react'
export const snapPoints = ['70%'] export const snapPoints = ['70%']
@ -166,10 +166,10 @@ function InviteCode({
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={ accessibilityLabel={
invites.available.length === 1 invites.available.length === 1
? 'Invite codes: 1 available' ? _(msg`Invite codes: 1 available`)
: `Invite codes: ${invites.available.length} available` : _(msg`Invite codes: ${invites.available.length} available`)
} }
accessibilityHint="Opens list of invite codes"> accessibilityHint={_(msg`Opens list of invite codes`)}>
<Text <Text
testID={`${testID}-code`} testID={`${testID}-code`}
type={used ? 'md' : 'md-bold'} type={used ? 'md' : 'md-bold'}

View file

@ -67,7 +67,7 @@ export function Component({
<TextInput <TextInput
testID="searchInput" testID="searchInput"
style={[styles.searchInput, pal.border, pal.text]} style={[styles.searchInput, pal.border, pal.text]}
placeholder="Search for users" placeholder={_(msg`Search for users`)}
placeholderTextColor={pal.colors.textLight} placeholderTextColor={pal.colors.textLight}
value={query} value={query}
onChangeText={setQuery} onChangeText={setQuery}
@ -85,7 +85,7 @@ export function Component({
onPress={onPressCancelSearch} onPress={onPressCancelSearch}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_(msg`Cancel search`)} accessibilityLabel={_(msg`Cancel search`)}
accessibilityHint="Exits inputting search query" accessibilityHint={_(msg`Exits inputting search query`)}
onAccessibilityEscape={onPressCancelSearch} onAccessibilityEscape={onPressCancelSearch}
hitSlop={HITSLOP_20}> hitSlop={HITSLOP_20}>
<FontAwesomeIcon <FontAwesomeIcon
@ -141,7 +141,7 @@ export function Component({
}} }}
accessibilityLabel={_(msg`Done`)} accessibilityLabel={_(msg`Done`)}
accessibilityHint="" accessibilityHint=""
label="Done" label={_(msg({message: 'Done', context: 'action'}))}
labelContainerStyle={{justifyContent: 'center', padding: 4}} labelContainerStyle={{justifyContent: 'center', padding: 4}}
labelStyle={[s.f18]} labelStyle={[s.f18]}
/> />

View file

@ -10,6 +10,8 @@ import {isWeb} from 'platform/detection'
import {listUriToHref} from 'lib/strings/url-helpers' import {listUriToHref} from 'lib/strings/url-helpers'
import {Button} from '../util/forms/Button' import {Button} from '../util/forms/Button'
import {useModalControls} from '#/state/modals' import {useModalControls} from '#/state/modals'
import {useLingui} from '@lingui/react'
import {Trans, msg} from '@lingui/macro'
export const snapPoints = [300] export const snapPoints = [300]
@ -23,19 +25,21 @@ export function Component({
const {closeModal} = useModalControls() const {closeModal} = useModalControls()
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui()
let name let name
let description let description
if (!moderation.cause) { if (!moderation.cause) {
name = 'Content Warning' name = _(msg`Content Warning`)
description = description = _(
'Moderator has chosen to set a general warning on the content.' msg`Moderator has chosen to set a general warning on the content.`,
)
} else if (moderation.cause.type === 'blocking') { } else if (moderation.cause.type === 'blocking') {
if (moderation.cause.source.type === 'list') { if (moderation.cause.source.type === 'list') {
const list = moderation.cause.source.list const list = moderation.cause.source.list
name = 'User Blocked by List' name = _(msg`User Blocked by List`)
description = ( description = (
<> <Trans>
This user is included in the{' '} This user is included in the{' '}
<TextLink <TextLink
type="2xl" type="2xl"
@ -44,25 +48,30 @@ export function Component({
style={pal.link} style={pal.link}
/>{' '} />{' '}
list which you have blocked. list which you have blocked.
</> </Trans>
) )
} else { } else {
name = 'User Blocked' name = _(msg`User Blocked`)
description = 'You have blocked this user. You cannot view their content.' description = _(
msg`You have blocked this user. You cannot view their content.`,
)
} }
} else if (moderation.cause.type === 'blocked-by') { } else if (moderation.cause.type === 'blocked-by') {
name = 'User Blocks You' name = _(msg`User Blocks You`)
description = 'This user has blocked you. You cannot view their content.' description = _(
msg`This user has blocked you. You cannot view their content.`,
)
} else if (moderation.cause.type === 'block-other') { } else if (moderation.cause.type === 'block-other') {
name = 'Content Not Available' name = _(msg`Content Not Available`)
description = description = _(
'This content is not available because one of the users involved has blocked the other.' msg`This content is not available because one of the users involved has blocked the other.`,
)
} else if (moderation.cause.type === 'muted') { } else if (moderation.cause.type === 'muted') {
if (moderation.cause.source.type === 'list') { if (moderation.cause.source.type === 'list') {
const list = moderation.cause.source.list const list = moderation.cause.source.list
name = <>Account Muted by List</> name = _(msg`Account Muted by List`)
description = ( description = (
<> <Trans>
This user is included the{' '} This user is included the{' '}
<TextLink <TextLink
type="2xl" type="2xl"
@ -71,11 +80,11 @@ export function Component({
style={pal.link} style={pal.link}
/>{' '} />{' '}
list which you have muted. list which you have muted.
</> </Trans>
) )
} else { } else {
name = 'Account Muted' name = _(msg`Account Muted`)
description = 'You have muted this user.' description = _(msg`You have muted this user.`)
} }
} else { } else {
name = moderation.cause.labelDef.strings[context].en.name name = moderation.cause.labelDef.strings[context].en.name

View file

@ -14,11 +14,14 @@ import {ErrorScreen} from '../util/error/ErrorScreen'
import {CenteredView} from '../util/Views' import {CenteredView} from '../util/Views'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
import {useProfileShadow} from '#/state/cache/profile-shadow' import {useProfileShadow} from '#/state/cache/profile-shadow'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
export const snapPoints = [520, '100%'] export const snapPoints = [520, '100%']
export function Component({did}: {did: string}) { export function Component({did}: {did: string}) {
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui()
const moderationOpts = useModerationOpts() const moderationOpts = useModerationOpts()
const { const {
data: profile, data: profile,
@ -43,7 +46,7 @@ export function Component({did}: {did: string}) {
if (profileError) { if (profileError) {
return ( return (
<ErrorScreen <ErrorScreen
title="Oops!" title={_(msg`Oops!`)}
message={cleanError(profileError)} message={cleanError(profileError)}
onPressTryAgain={refetchProfile} onPressTryAgain={refetchProfile}
/> />
@ -55,8 +58,8 @@ export function Component({did}: {did: string}) {
// should never happen // should never happen
return ( return (
<ErrorScreen <ErrorScreen
title="Oops!" title={_(msg`Oops!`)}
message="Something went wrong and we're not sure what." message={_(msg`Something went wrong and we're not sure what.`)}
onPressTryAgain={refetchProfile} onPressTryAgain={refetchProfile}
/> />
) )
@ -104,7 +107,7 @@ function ComponentLoaded({
<> <>
<InfoCircleIcon size={21} style={pal.textLight} /> <InfoCircleIcon size={21} style={pal.textLight} />
<ThemedText type="xl" fg="light"> <ThemedText type="xl" fg="light">
Swipe up to see more <Trans>Swipe up to see more</Trans>
</ThemedText> </ThemedText>
</> </>
)} )}

View file

@ -37,11 +37,23 @@ export function Component({
style={[styles.actionBtn]} style={[styles.actionBtn]}
onPress={onRepost} onPress={onRepost}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={isReposted ? 'Undo repost' : 'Repost'} accessibilityLabel={
accessibilityHint={isReposted ? 'Remove repost' : 'Repost '}> isReposted
? _(msg`Undo repost`)
: _(msg({message: `Repost`, context: 'action'}))
}
accessibilityHint={
isReposted
? _(msg`Remove repost`)
: _(msg({message: `Repost`, context: 'action'}))
}>
<RepostIcon strokeWidth={2} size={24} style={s.blue3} /> <RepostIcon strokeWidth={2} size={24} style={s.blue3} />
<Text type="title-lg" style={[styles.actionBtnLabel, pal.text]}> <Text type="title-lg" style={[styles.actionBtnLabel, pal.text]}>
<Trans>{!isReposted ? 'Repost' : 'Undo repost'}</Trans> {!isReposted ? (
<Trans context="action">Repost</Trans>
) : (
<Trans>Undo repost</Trans>
)}
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity <TouchableOpacity
@ -49,11 +61,13 @@ export function Component({
style={[styles.actionBtn]} style={[styles.actionBtn]}
onPress={onQuote} onPress={onQuote}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_(msg`Quote post`)} accessibilityLabel={_(
msg({message: `Quote post`, context: 'action'}),
)}
accessibilityHint=""> accessibilityHint="">
<FontAwesomeIcon icon="quote-left" size={24} style={s.blue3} /> <FontAwesomeIcon icon="quote-left" size={24} style={s.blue3} />
<Text type="title-lg" style={[styles.actionBtnLabel, pal.text]}> <Text type="title-lg" style={[styles.actionBtnLabel, pal.text]}>
<Trans>Quote Post</Trans> <Trans context="action">Quote Post</Trans>
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>

View file

@ -92,7 +92,7 @@ export function Component({
testID="sexualLabelBtn" testID="sexualLabelBtn"
selected={selected.includes('sexual')} selected={selected.includes('sexual')}
left left
label="Suggestive" label={_(msg`Suggestive`)}
onSelect={() => toggleAdultLabel('sexual')} onSelect={() => toggleAdultLabel('sexual')}
accessibilityHint="" accessibilityHint=""
style={s.flex1} style={s.flex1}
@ -100,7 +100,7 @@ export function Component({
<SelectableBtn <SelectableBtn
testID="nudityLabelBtn" testID="nudityLabelBtn"
selected={selected.includes('nudity')} selected={selected.includes('nudity')}
label="Nudity" label={_(msg`Nudity`)}
onSelect={() => toggleAdultLabel('nudity')} onSelect={() => toggleAdultLabel('nudity')}
accessibilityHint="" accessibilityHint=""
style={s.flex1} style={s.flex1}
@ -108,7 +108,7 @@ export function Component({
<SelectableBtn <SelectableBtn
testID="pornLabelBtn" testID="pornLabelBtn"
selected={selected.includes('porn')} selected={selected.includes('porn')}
label="Porn" label={_(msg`Porn`)}
right right
onSelect={() => toggleAdultLabel('porn')} onSelect={() => toggleAdultLabel('porn')}
accessibilityHint="" accessibilityHint=""
@ -154,7 +154,7 @@ export function Component({
accessibilityLabel={_(msg`Confirm`)} accessibilityLabel={_(msg`Confirm`)}
accessibilityHint=""> accessibilityHint="">
<Text style={[s.white, s.bold, s.f18]}> <Text style={[s.white, s.bold, s.f18]}>
<Trans>Done</Trans> <Trans context="action">Done</Trans>
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>

View file

@ -101,7 +101,9 @@ export function Component({onSelect}: {onSelect: (url: string) => void}) {
onChangeText={setCustomUrl} onChangeText={setCustomUrl}
accessibilityLabel={_(msg`Custom domain`)} accessibilityLabel={_(msg`Custom domain`)}
// TODO: Simplify this wording further to be understandable by everyone // TODO: Simplify this wording further to be understandable by everyone
accessibilityHint="Use your domain as your Bluesky client service provider" accessibilityHint={_(
msg`Use your domain as your Bluesky client service provider`,
)}
/> />
<TouchableOpacity <TouchableOpacity
testID="customServerSelectBtn" testID="customServerSelectBtn"
@ -110,7 +112,7 @@ export function Component({onSelect}: {onSelect: (url: string) => void}) {
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={`Confirm service. ${ accessibilityLabel={`Confirm service. ${
customUrl === '' customUrl === ''
? 'Button disabled. Input custom domain to proceed.' ? _(msg`Button disabled. Input custom domain to proceed.`)
: '' : ''
}`} }`}
accessibilityHint="" accessibilityHint=""

View file

@ -62,7 +62,9 @@ function SwitchAccountCard({account}: {account: SessionAccount}) {
onPress={isSwitchingAccounts ? undefined : onPressSignout} onPress={isSwitchingAccounts ? undefined : onPressSignout}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_(msg`Sign out`)} accessibilityLabel={_(msg`Sign out`)}
accessibilityHint={`Signs ${profile?.displayName} out of Bluesky`}> accessibilityHint={_(
msg`Signs ${profile?.displayName} out of Bluesky`,
)}>
<Text type="lg" style={pal.link}> <Text type="lg" style={pal.link}>
<Trans>Sign out</Trans> <Trans>Sign out</Trans>
</Text> </Text>
@ -92,8 +94,8 @@ function SwitchAccountCard({account}: {account: SessionAccount}) {
isSwitchingAccounts ? undefined : () => onPressSwitchAccount(account) isSwitchingAccounts ? undefined : () => onPressSwitchAccount(account)
} }
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={`Switch to ${account.handle}`} accessibilityLabel={_(msg`Switch to ${account.handle}`)}
accessibilityHint="Switches the account you are logged in to"> accessibilityHint={_(msg`Switches the account you are logged in to`)}>
{contents} {contents}
</TouchableOpacity> </TouchableOpacity>
) )

View file

@ -126,10 +126,10 @@ export function Component({
}} }}
style={styles.btn} style={styles.btn}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_(msg`Done`)} accessibilityLabel={_(msg({message: `Done`, context: 'action'}))}
accessibilityHint=""> accessibilityHint="">
<Text style={[s.white, s.bold, s.f18]}> <Text style={[s.white, s.bold, s.f18]}>
<Trans>Done</Trans> <Trans context="action">Done</Trans>
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>

View file

@ -76,10 +76,10 @@ export function Component({
type="default" type="default"
onPress={onPressDone} onPress={onPressDone}
style={styles.footerBtn} style={styles.footerBtn}
accessibilityLabel={_(msg`Done`)} accessibilityLabel={_(msg({message: `Done`, context: 'action'}))}
accessibilityHint="" accessibilityHint=""
onAccessibilityEscape={onPressDone} onAccessibilityEscape={onPressDone}
label={_(msg`Done`)} label={_(msg({message: `Done`, context: 'action'}))}
/> />
</View> </View>
</View> </View>
@ -175,12 +175,22 @@ function ListItem({
{sanitizeDisplayName(list.name)} {sanitizeDisplayName(list.name)}
</Text> </Text>
<Text type="md" style={[pal.textLight]} numberOfLines={1}> <Text type="md" style={[pal.textLight]} numberOfLines={1}>
{list.purpose === 'app.bsky.graph.defs#curatelist' && 'User list '} {list.purpose === 'app.bsky.graph.defs#curatelist' &&
{list.purpose === 'app.bsky.graph.defs#modlist' && 'Moderation list '} (list.creator.did === currentAccount?.did ? (
by{' '} <Trans>User list by you</Trans>
{list.creator.did === currentAccount?.did ) : (
? 'you' <Trans>
: sanitizeHandle(list.creator.handle, '@')} User list by {sanitizeHandle(list.creator.handle, '@')}
</Trans>
))}
{list.purpose === 'app.bsky.graph.defs#modlist' &&
(list.creator.did === currentAccount?.did ? (
<Trans>Moderation list by you</Trans>
) : (
<Trans>
Moderation list by {sanitizeHandle(list.creator.handle, '@')}
</Trans>
))}
</Text> </Text>
</View> </View>
<View> <View>

View file

@ -75,7 +75,7 @@ export function Component({showReminder}: {showReminder?: boolean}) {
token: confirmationCode.trim(), token: confirmationCode.trim(),
}) })
updateCurrentAccount({emailConfirmed: true}) updateCurrentAccount({emailConfirmed: true})
Toast.show('Email verified') Toast.show(_(msg`Email verified`))
closeModal() closeModal()
} catch (e) { } catch (e) {
setError(cleanError(String(e))) setError(cleanError(String(e)))
@ -97,9 +97,15 @@ export function Component({showReminder}: {showReminder?: boolean}) {
{stage === Stages.Reminder && <ReminderIllustration />} {stage === Stages.Reminder && <ReminderIllustration />}
<View style={styles.titleSection}> <View style={styles.titleSection}>
<Text type="title-lg" style={[pal.text, styles.title]}> <Text type="title-lg" style={[pal.text, styles.title]}>
{stage === Stages.Reminder ? 'Please Verify Your Email' : ''} {stage === Stages.Reminder ? (
{stage === Stages.ConfirmCode ? 'Enter Confirmation Code' : ''} <Trans>Please Verify Your Email</Trans>
{stage === Stages.Email ? 'Verify Your Email' : ''} ) : stage === Stages.Email ? (
<Trans>Verify Your Email</Trans>
) : stage === Stages.ConfirmCode ? (
<Trans>Enter Confirmation Code</Trans>
) : (
''
)}
</Text> </Text>
</View> </View>
@ -133,7 +139,7 @@ export function Component({showReminder}: {showReminder?: boolean}) {
size={16} size={16}
/> />
<Text type="xl-medium" style={[pal.text, s.flex1, {minWidth: 0}]}> <Text type="xl-medium" style={[pal.text, s.flex1, {minWidth: 0}]}>
{currentAccount?.email || '(no email)'} {currentAccount?.email || _(msg`(no email)`)}
</Text> </Text>
</View> </View>
<Pressable <Pressable
@ -182,7 +188,7 @@ export function Component({showReminder}: {showReminder?: boolean}) {
onPress={() => setStage(Stages.Email)} onPress={() => setStage(Stages.Email)}
accessibilityLabel={_(msg`Get Started`)} accessibilityLabel={_(msg`Get Started`)}
accessibilityHint="" accessibilityHint=""
label="Get Started" label={_(msg`Get Started`)}
labelContainerStyle={{justifyContent: 'center', padding: 4}} labelContainerStyle={{justifyContent: 'center', padding: 4}}
labelStyle={[s.f18]} labelStyle={[s.f18]}
/> />
@ -195,7 +201,7 @@ export function Component({showReminder}: {showReminder?: boolean}) {
onPress={onSendEmail} onPress={onSendEmail}
accessibilityLabel={_(msg`Send Confirmation Email`)} accessibilityLabel={_(msg`Send Confirmation Email`)}
accessibilityHint="" accessibilityHint=""
label="Send Confirmation Email" label={_(msg`Send Confirmation Email`)}
labelContainerStyle={{ labelContainerStyle={{
justifyContent: 'center', justifyContent: 'center',
padding: 4, padding: 4,
@ -207,7 +213,7 @@ export function Component({showReminder}: {showReminder?: boolean}) {
type="default" type="default"
accessibilityLabel={_(msg`I have a code`)} accessibilityLabel={_(msg`I have a code`)}
accessibilityHint="" accessibilityHint=""
label="I have a confirmation code" label={_(msg`I have a confirmation code`)}
labelContainerStyle={{ labelContainerStyle={{
justifyContent: 'center', justifyContent: 'center',
padding: 4, padding: 4,
@ -224,7 +230,7 @@ export function Component({showReminder}: {showReminder?: boolean}) {
onPress={onConfirm} onPress={onConfirm}
accessibilityLabel={_(msg`Confirm`)} accessibilityLabel={_(msg`Confirm`)}
accessibilityHint="" accessibilityHint=""
label="Confirm" label={_(msg`Confirm`)}
labelContainerStyle={{justifyContent: 'center', padding: 4}} labelContainerStyle={{justifyContent: 'center', padding: 4}}
labelStyle={[s.f18]} labelStyle={[s.f18]}
/> />
@ -236,10 +242,16 @@ export function Component({showReminder}: {showReminder?: boolean}) {
closeModal() closeModal()
}} }}
accessibilityLabel={ accessibilityLabel={
stage === Stages.Reminder ? 'Not right now' : 'Cancel' stage === Stages.Reminder
? _(msg`Not right now`)
: _(msg`Cancel`)
} }
accessibilityHint="" accessibilityHint=""
label={stage === Stages.Reminder ? 'Not right now' : 'Cancel'} label={
stage === Stages.Reminder
? _(msg`Not right now`)
: _(msg`Cancel`)
}
labelContainerStyle={{justifyContent: 'center', padding: 4}} labelContainerStyle={{justifyContent: 'center', padding: 4}}
labelStyle={[s.f18]} labelStyle={[s.f18]}
/> />

View file

@ -48,7 +48,7 @@ export function Component({}: {}) {
} else { } else {
setError( setError(
resBody.error || resBody.error ||
'Something went wrong. Check your email and try again.', _(msg`Something went wrong. Check your email and try again.`),
) )
} }
} catch (e: any) { } catch (e: any) {
@ -75,7 +75,7 @@ export function Component({}: {}) {
</Text> </Text>
<TextInput <TextInput
style={[styles.textInput, pal.borderDark, pal.text, s.mb10, s.mt10]} style={[styles.textInput, pal.borderDark, pal.text, s.mb10, s.mt10]}
placeholder="Enter your email" placeholder={_(msg`Enter your email`)}
placeholderTextColor={pal.textLight.color} placeholderTextColor={pal.textLight.color}
autoCapitalize="none" autoCapitalize="none"
autoCorrect={false} autoCorrect={false}
@ -86,7 +86,9 @@ export function Component({}: {}) {
enterKeyHint="done" enterKeyHint="done"
accessible={true} accessible={true}
accessibilityLabel={_(msg`Email`)} accessibilityLabel={_(msg`Email`)}
accessibilityHint="Input your email to get on the Bluesky waitlist" accessibilityHint={_(
msg`Input your email to get on the Bluesky waitlist`,
)}
/> />
{error ? ( {error ? (
<View style={s.mt10}> <View style={s.mt10}>
@ -114,7 +116,9 @@ export function Component({}: {}) {
<TouchableOpacity <TouchableOpacity
onPress={onPressSignup} onPress={onPressSignup}
accessibilityRole="button" accessibilityRole="button"
accessibilityHint={`Confirms signing up ${email} to the waitlist`}> accessibilityHint={_(
msg`Confirms signing up ${email} to the waitlist`,
)}>
<LinearGradient <LinearGradient
colors={[gradients.blueLight.start, gradients.blueLight.end]} colors={[gradients.blueLight.start, gradients.blueLight.end]}
start={{x: 0, y: 0}} start={{x: 0, y: 0}}
@ -130,7 +134,9 @@ export function Component({}: {}) {
onPress={onCancel} onPress={onCancel}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_(msg`Cancel waitlist signup`)} accessibilityLabel={_(msg`Cancel waitlist signup`)}
accessibilityHint={`Exits signing up for waitlist with ${email}`} accessibilityHint={_(
msg`Exits signing up for waitlist with ${email}`,
)}
onAccessibilityEscape={onCancel}> onAccessibilityEscape={onCancel}>
<Text type="button-lg" style={pal.textLight}> <Text type="button-lg" style={pal.textLight}>
<Trans>Cancel</Trans> <Trans>Cancel</Trans>

View file

@ -13,6 +13,8 @@ import {logger} from '#/logger'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
import {useModerationOpts} from '#/state/queries/preferences' import {useModerationOpts} from '#/state/queries/preferences'
import {List, ListRef} from '../util/List' import {List, ListRef} from '../util/List'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'} const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'} const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
@ -31,6 +33,7 @@ export function Feed({
}) { }) {
const [isPTRing, setIsPTRing] = React.useState(false) const [isPTRing, setIsPTRing] = React.useState(false)
const {_} = useLingui()
const moderationOpts = useModerationOpts() const moderationOpts = useModerationOpts()
const {checkUnread} = useUnreadNotificationsApi() const {checkUnread} = useUnreadNotificationsApi()
const { const {
@ -101,14 +104,16 @@ export function Feed({
return ( return (
<EmptyState <EmptyState
icon="bell" icon="bell"
message="No notifications yet!" message={_(msg`No notifications yet!`)}
style={styles.emptyState} style={styles.emptyState}
/> />
) )
} else if (item === LOAD_MORE_ERROR_ITEM) { } else if (item === LOAD_MORE_ERROR_ITEM) {
return ( return (
<LoadMoreRetryBtn <LoadMoreRetryBtn
label="There was an issue fetching notifications. Tap here to try again." label={_(
msg`There was an issue fetching notifications. Tap here to try again.`,
)}
onPress={onPressRetryLoadMore} onPress={onPressRetryLoadMore}
/> />
) )
@ -117,7 +122,7 @@ export function Feed({
} }
return <FeedItem item={item} moderationOpts={moderationOpts!} /> return <FeedItem item={item} moderationOpts={moderationOpts!} />
}, },
[onPressRetryLoadMore, moderationOpts], [onPressRetryLoadMore, moderationOpts, _],
) )
const FeedFooter = React.useCallback( const FeedFooter = React.useCallback(

View file

@ -65,6 +65,7 @@ let FeedItem = ({
moderationOpts: ModerationOpts moderationOpts: ModerationOpts
}): React.ReactNode => { }): React.ReactNode => {
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui()
const [isAuthorsExpanded, setAuthorsExpanded] = useState<boolean>(false) const [isAuthorsExpanded, setAuthorsExpanded] = useState<boolean>(false)
const itemHref = useMemo(() => { const itemHref = useMemo(() => {
if (item.type === 'post-like' || item.type === 'repost') { if (item.type === 'post-like' || item.type === 'repost') {
@ -151,24 +152,26 @@ let FeedItem = ({
let icon: Props['icon'] | 'HeartIconSolid' let icon: Props['icon'] | 'HeartIconSolid'
let iconStyle: Props['style'] = [] let iconStyle: Props['style'] = []
if (item.type === 'post-like') { if (item.type === 'post-like') {
action = 'liked your post' action = _(msg`liked your post`)
icon = 'HeartIconSolid' icon = 'HeartIconSolid'
iconStyle = [ iconStyle = [
s.likeColor as FontAwesomeIconStyle, s.likeColor as FontAwesomeIconStyle,
{position: 'relative', top: -4}, {position: 'relative', top: -4},
] ]
} else if (item.type === 'repost') { } else if (item.type === 'repost') {
action = 'reposted your post' action = _(msg`reposted your post`)
icon = 'retweet' icon = 'retweet'
iconStyle = [s.green3 as FontAwesomeIconStyle] iconStyle = [s.green3 as FontAwesomeIconStyle]
} else if (item.type === 'follow') { } else if (item.type === 'follow') {
action = 'followed you' action = _(msg`followed you`)
icon = 'user-plus' icon = 'user-plus'
iconStyle = [s.blue3 as FontAwesomeIconStyle] iconStyle = [s.blue3 as FontAwesomeIconStyle]
} else if (item.type === 'feedgen-like') { } else if (item.type === 'feedgen-like') {
action = `liked your custom feed${ action = _(
item.subjectUri ? ` '${new AtUri(item.subjectUri).rkey}'` : '' msg`liked your custom feed${
}` item.subjectUri ? ` '${new AtUri(item.subjectUri).rkey}'` : ''
}`,
)
icon = 'HeartIconSolid' icon = 'HeartIconSolid'
iconStyle = [ iconStyle = [
s.likeColor as FontAwesomeIconStyle, s.likeColor as FontAwesomeIconStyle,
@ -314,14 +317,16 @@ function CondensedAuthorsList({
onPress={onToggleAuthorsExpanded} onPress={onToggleAuthorsExpanded}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_(msg`Hide user list`)} accessibilityLabel={_(msg`Hide user list`)}
accessibilityHint="Collapses list of users for a given notification"> accessibilityHint={_(
msg`Collapses list of users for a given notification`,
)}>
<FontAwesomeIcon <FontAwesomeIcon
icon="angle-up" icon="angle-up"
size={18} size={18}
style={[styles.expandedAuthorsCloseBtnIcon, pal.text]} style={[styles.expandedAuthorsCloseBtnIcon, pal.text]}
/> />
<Text type="sm-medium" style={pal.text}> <Text type="sm-medium" style={pal.text}>
<Trans>Hide</Trans> <Trans context="action">Hide</Trans>
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
@ -343,7 +348,9 @@ function CondensedAuthorsList({
return ( return (
<TouchableOpacity <TouchableOpacity
accessibilityLabel={_(msg`Show users`)} accessibilityLabel={_(msg`Show users`)}
accessibilityHint="Opens an expanded list of users in this notification" accessibilityHint={_(
msg`Opens an expanded list of users in this notification`,
)}
onPress={onToggleAuthorsExpanded}> onPress={onToggleAuthorsExpanded}>
<View style={styles.avis}> <View style={styles.avis}>
{authors.slice(0, MAX_AUTHORS).map(author => ( {authors.slice(0, MAX_AUTHORS).map(author => (

View file

@ -74,7 +74,9 @@ export function FeedsTabBar(
onPress={onPressAvi} onPress={onPressAvi}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_(msg`Open navigation`)} accessibilityLabel={_(msg`Open navigation`)}
accessibilityHint="Access profile and other navigation links" accessibilityHint={_(
msg`Access profile and other navigation links`,
)}
hitSlop={HITSLOP_10}> hitSlop={HITSLOP_10}>
<FontAwesomeIcon <FontAwesomeIcon
icon="bars" icon="bars"

View file

@ -222,7 +222,11 @@ function PostThreadLoaded({
const renderItem = React.useCallback( const renderItem = React.useCallback(
({item, index}: {item: YieldedItem; index: number}) => { ({item, index}: {item: YieldedItem; index: number}) => {
if (item === TOP_COMPONENT) { if (item === TOP_COMPONENT) {
return isTablet ? <ViewHeader title={_(msg`Post`)} /> : null return isTablet ? (
<ViewHeader
title={_(msg({message: `Post`, context: 'description'}))}
/>
) : null
} else if (item === PARENT_SPINNER) { } else if (item === PARENT_SPINNER) {
return ( return (
<View style={styles.parentSpinner}> <View style={styles.parentSpinner}>
@ -393,7 +397,7 @@ function PostThreadBlocked() {
style={[pal.link as FontAwesomeIconStyle, s.mr5]} style={[pal.link as FontAwesomeIconStyle, s.mr5]}
size={14} size={14}
/> />
Back <Trans context="action">Back</Trans>
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>

View file

@ -158,6 +158,7 @@ let PostThreadItemLoaded = ({
onPostReply: () => void onPostReply: () => void
}): React.ReactNode => { }): React.ReactNode => {
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui()
const langPrefs = useLanguagePrefs() const langPrefs = useLanguagePrefs()
const {openComposer} = useComposerControls() const {openComposer} = useComposerControls()
const {currentAccount} = useSession() const {currentAccount} = useSession()
@ -172,7 +173,7 @@ let PostThreadItemLoaded = ({
const urip = new AtUri(post.uri) const urip = new AtUri(post.uri)
return makeProfileLink(post.author, 'post', urip.rkey) return makeProfileLink(post.author, 'post', urip.rkey)
}, [post.uri, post.author]) }, [post.uri, post.author])
const itemTitle = `Post by ${post.author.handle}` const itemTitle = _(msg`Post by ${post.author.handle}`)
const authorHref = makeProfileLink(post.author) const authorHref = makeProfileLink(post.author)
const authorTitle = post.author.handle const authorTitle = post.author.handle
const isAuthorMuted = post.author.viewer?.muted const isAuthorMuted = post.author.viewer?.muted
@ -180,12 +181,12 @@ let PostThreadItemLoaded = ({
const urip = new AtUri(post.uri) const urip = new AtUri(post.uri)
return makeProfileLink(post.author, 'post', urip.rkey, 'liked-by') return makeProfileLink(post.author, 'post', urip.rkey, 'liked-by')
}, [post.uri, post.author]) }, [post.uri, post.author])
const likesTitle = 'Likes on this post' const likesTitle = _(msg`Likes on this post`)
const repostsHref = React.useMemo(() => { const repostsHref = React.useMemo(() => {
const urip = new AtUri(post.uri) const urip = new AtUri(post.uri)
return makeProfileLink(post.author, 'post', urip.rkey, 'reposted-by') return makeProfileLink(post.author, 'post', urip.rkey, 'reposted-by')
}, [post.uri, post.author]) }, [post.uri, post.author])
const repostsTitle = 'Reposts of this post' const repostsTitle = _(msg`Reposts of this post`)
const isModeratedPost = const isModeratedPost =
moderation.decisions.post.cause?.type === 'label' && moderation.decisions.post.cause?.type === 'label' &&
moderation.decisions.post.cause.label.src !== currentAccount?.did moderation.decisions.post.cause.label.src !== currentAccount?.did
@ -225,7 +226,7 @@ let PostThreadItemLoaded = ({
}, [setLimitLines]) }, [setLimitLines])
if (!record) { if (!record) {
return <ErrorMessage message="Invalid or unsupported post record" /> return <ErrorMessage message={_(msg`Invalid or unsupported post record`)} />
} }
if (isHighlightedPost) { if (isHighlightedPost) {
@ -563,7 +564,7 @@ let PostThreadItemLoaded = ({
) : undefined} ) : undefined}
{limitLines ? ( {limitLines ? (
<TextLink <TextLink
text="Show More" text={_(msg`Show More`)}
style={pal.link} style={pal.link}
onPress={onPressShowMore} onPress={onPressShowMore}
href="#" href="#"

View file

@ -27,6 +27,8 @@ import {countLines} from 'lib/strings/helpers'
import {useModerationOpts} from '#/state/queries/preferences' import {useModerationOpts} from '#/state/queries/preferences'
import {useComposerControls} from '#/state/shell/composer' import {useComposerControls} from '#/state/shell/composer'
import {Shadow, usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow' import {Shadow, usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
export function Post({ export function Post({
post, post,
@ -95,6 +97,7 @@ function PostInner({
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
}) { }) {
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui()
const {openComposer} = useComposerControls() const {openComposer} = useComposerControls()
const [limitLines, setLimitLines] = useState( const [limitLines, setLimitLines] = useState(
() => countLines(richText?.text) >= MAX_POST_LINES, () => countLines(richText?.text) >= MAX_POST_LINES,
@ -159,13 +162,15 @@ function PostInner({
style={[pal.textLight, s.mr2]} style={[pal.textLight, s.mr2]}
lineHeight={1.2} lineHeight={1.2}
numberOfLines={1}> numberOfLines={1}>
Reply to{' '} <Trans context="description">
<UserInfoText Reply to{' '}
type="sm" <UserInfoText
did={replyAuthorDid} type="sm"
attr="displayName" did={replyAuthorDid}
style={[pal.textLight]} attr="displayName"
/> style={[pal.textLight]}
/>
</Trans>
</Text> </Text>
</View> </View>
)} )}
@ -188,7 +193,7 @@ function PostInner({
) : undefined} ) : undefined}
{limitLines ? ( {limitLines ? (
<TextLink <TextLink
text="Show More" text={_(msg`Show More`)}
style={pal.link} style={pal.link}
onPress={onPressShowMore} onPress={onPressShowMore}
href="#" href="#"

View file

@ -12,6 +12,7 @@ import {NavigationProp} from 'lib/routes/types'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {isWeb} from 'platform/detection' import {isWeb} from 'platform/detection'
import {Trans} from '@lingui/macro'
export function CustomFeedEmptyState() { export function CustomFeedEmptyState() {
const pal = usePalette('default') const pal = usePalette('default')
@ -33,15 +34,17 @@ export function CustomFeedEmptyState() {
<MagnifyingGlassIcon style={[styles.emptyIcon, pal.text]} size={62} /> <MagnifyingGlassIcon style={[styles.emptyIcon, pal.text]} size={62} />
</View> </View>
<Text type="xl-medium" style={[s.textCenter, pal.text]}> <Text type="xl-medium" style={[s.textCenter, pal.text]}>
This feed is empty! You may need to follow more users or tune your <Trans>
language settings. This feed is empty! You may need to follow more users or tune your
language settings.
</Trans>
</Text> </Text>
<Button <Button
type="inverted" type="inverted"
style={styles.emptyBtn} style={styles.emptyBtn}
onPress={onPressFindAccounts}> onPress={onPressFindAccounts}>
<Text type="lg-medium" style={palInverted.text}> <Text type="lg-medium" style={palInverted.text}>
Find accounts to follow <Trans>Find accounts to follow</Trans>
</Text> </Text>
<FontAwesomeIcon <FontAwesomeIcon
icon="angle-right" icon="angle-right"

View file

@ -28,6 +28,8 @@ import {isWeb} from '#/platform/detection'
import {listenPostCreated} from '#/state/events' import {listenPostCreated} from '#/state/events'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {STALE} from '#/state/queries' import {STALE} from '#/state/queries'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
const LOADING_ITEM = {_reactKey: '__loading__'} const LOADING_ITEM = {_reactKey: '__loading__'}
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'} const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
@ -74,6 +76,7 @@ let Feed = ({
}): React.ReactNode => { }): React.ReactNode => {
const theme = useTheme() const theme = useTheme()
const {track} = useAnalytics() const {track} = useAnalytics()
const {_} = useLingui()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const {currentAccount} = useSession() const {currentAccount} = useSession()
const [isPTRing, setIsPTRing] = React.useState(false) const [isPTRing, setIsPTRing] = React.useState(false)
@ -250,7 +253,9 @@ let Feed = ({
} else if (item === LOAD_MORE_ERROR_ITEM) { } else if (item === LOAD_MORE_ERROR_ITEM) {
return ( return (
<LoadMoreRetryBtn <LoadMoreRetryBtn
label="There was an issue fetching posts. Tap here to try again." label={_(
msg`There was an issue fetching posts. Tap here to try again.`,
)}
onPress={onPressRetryLoadMore} onPress={onPressRetryLoadMore}
/> />
) )
@ -259,7 +264,7 @@ let Feed = ({
} }
return <FeedSlice slice={item} /> return <FeedSlice slice={item} />
}, },
[feed, error, onPressTryAgain, onPressRetryLoadMore, renderEmptyState], [feed, error, onPressTryAgain, onPressRetryLoadMore, renderEmptyState, _],
) )
const shouldRenderEndOfFeed = const shouldRenderEndOfFeed =

View file

@ -38,6 +38,7 @@ export function FeedErrorMessage({
error?: Error error?: Error
onPressTryAgain: () => void onPressTryAgain: () => void
}) { }) {
const {_: _l} = useLingui()
const knownError = React.useMemo( const knownError = React.useMemo(
() => detectKnownError(feedDesc, error), () => detectKnownError(feedDesc, error),
[feedDesc, error], [feedDesc, error],
@ -60,7 +61,7 @@ export function FeedErrorMessage({
return ( return (
<EmptyState <EmptyState
icon="ban" icon="ban"
message="Posts hidden" message={_l(msgLingui`Posts hidden`)}
style={{paddingVertical: 40}} style={{paddingVertical: 40}}
/> />
) )
@ -134,7 +135,9 @@ function FeedgenErrorMessage({
await removeFeed({uri}) await removeFeed({uri})
} catch (err) { } catch (err) {
Toast.show( Toast.show(
'There was an an issue removing this feed. Please check your internet connection and try again.', _l(
msgLingui`There was an an issue removing this feed. Please check your internet connection and try again.`,
),
) )
logger.error('Failed to remove feed', {error: err}) logger.error('Failed to remove feed', {error: err})
} }
@ -160,20 +163,20 @@ function FeedgenErrorMessage({
{knownError === KnownError.FeedgenDoesNotExist && ( {knownError === KnownError.FeedgenDoesNotExist && (
<Button <Button
type="inverted" type="inverted"
label="Remove feed" label={_l(msgLingui`Remove feed`)}
onPress={onRemoveFeed} onPress={onRemoveFeed}
/> />
)} )}
<Button <Button
type="default-light" type="default-light"
label="View profile" label={_l(msgLingui`View profile`)}
onPress={onViewProfile} onPress={onViewProfile}
/> />
</View> </View>
) )
} }
} }
}, [knownError, onViewProfile, onRemoveFeed]) }, [knownError, onViewProfile, onRemoveFeed, _l])
return ( return (
<View <View
@ -191,7 +194,7 @@ function FeedgenErrorMessage({
{rawError?.message && ( {rawError?.message && (
<Text style={pal.textLight}> <Text style={pal.textLight}>
<Trans>Message from server</Trans>: {rawError.message} <Trans>Message from server: {rawError.message}</Trans>
</Text> </Text>
)} )}

View file

@ -35,6 +35,8 @@ import {useComposerControls} from '#/state/shell/composer'
import {Shadow, usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow' import {Shadow, usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow'
import {FeedNameText} from '../util/FeedInfoText' import {FeedNameText} from '../util/FeedInfoText'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
export function FeedItem({ export function FeedItem({
post, post,
@ -103,6 +105,7 @@ let FeedItemInner = ({
}): React.ReactNode => { }): React.ReactNode => {
const {openComposer} = useComposerControls() const {openComposer} = useComposerControls()
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui()
const {currentAccount} = useSession() const {currentAccount} = useSession()
const href = useMemo(() => { const href = useMemo(() => {
const urip = new AtUri(post.uri) const urip = new AtUri(post.uri)
@ -182,24 +185,28 @@ let FeedItemInner = ({
style={pal.textLight} style={pal.textLight}
lineHeight={1.2} lineHeight={1.2}
numberOfLines={1}> numberOfLines={1}>
From{' '} <Trans context="from-feed">
<FeedNameText From{' '}
type="sm-bold" <FeedNameText
uri={reason.uri} type="sm-bold"
href={reason.href} uri={reason.uri}
lineHeight={1.2} href={reason.href}
numberOfLines={1} lineHeight={1.2}
style={pal.textLight} numberOfLines={1}
/> style={pal.textLight}
/>
</Trans>
</Text> </Text>
</Link> </Link>
) : AppBskyFeedDefs.isReasonRepost(reason) ? ( ) : AppBskyFeedDefs.isReasonRepost(reason) ? (
<Link <Link
style={styles.includeReason} style={styles.includeReason}
href={makeProfileLink(reason.by)} href={makeProfileLink(reason.by)}
title={`Reposted by ${sanitizeDisplayName( title={_(
reason.by.displayName || reason.by.handle, msg`Reposted by ${sanitizeDisplayName(
)}`}> reason.by.displayName || reason.by.handle,
)})`,
)}>
<FontAwesomeIcon <FontAwesomeIcon
icon="retweet" icon="retweet"
style={{ style={{
@ -213,17 +220,19 @@ let FeedItemInner = ({
style={pal.textLight} style={pal.textLight}
lineHeight={1.2} lineHeight={1.2}
numberOfLines={1}> numberOfLines={1}>
Reposted by{' '} <Trans>
<TextLinkOnWebOnly Reposted by{' '}
type="sm-bold" <TextLinkOnWebOnly
style={pal.textLight} type="sm-bold"
lineHeight={1.2} style={pal.textLight}
numberOfLines={1} lineHeight={1.2}
text={sanitizeDisplayName( numberOfLines={1}
reason.by.displayName || sanitizeHandle(reason.by.handle), text={sanitizeDisplayName(
)} reason.by.displayName || sanitizeHandle(reason.by.handle),
href={makeProfileLink(reason.by)} )}
/> href={makeProfileLink(reason.by)}
/>
</Trans>
</Text> </Text>
</Link> </Link>
) : null} ) : null}
@ -274,13 +283,15 @@ let FeedItemInner = ({
style={[pal.textLight, s.mr2]} style={[pal.textLight, s.mr2]}
lineHeight={1.2} lineHeight={1.2}
numberOfLines={1}> numberOfLines={1}>
Reply to{' '} <Trans context="description">
<UserInfoText Reply to{' '}
type="md" <UserInfoText
did={replyAuthorDid} type="md"
attr="displayName" did={replyAuthorDid}
style={[pal.textLight, s.ml2]} attr="displayName"
/> style={[pal.textLight, s.ml2]}
/>
</Trans>
</Text> </Text>
</View> </View>
)} )}
@ -317,6 +328,7 @@ let PostContent = ({
postAuthor: AppBskyFeedDefs.PostView['author'] postAuthor: AppBskyFeedDefs.PostView['author']
}): React.ReactNode => { }): React.ReactNode => {
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui()
const [limitLines, setLimitLines] = useState( const [limitLines, setLimitLines] = useState(
() => countLines(richText.text) >= MAX_POST_LINES, () => countLines(richText.text) >= MAX_POST_LINES,
) )
@ -346,7 +358,7 @@ let PostContent = ({
) : undefined} ) : undefined}
{limitLines ? ( {limitLines ? (
<TextLink <TextLink
text="Show More" text={_(msg`Show More`)}
style={pal.link} style={pal.link}
onPress={onPressShowMore} onPress={onPressShowMore}
href="#" href="#"

View file

@ -8,6 +8,7 @@ import Svg, {Circle, Line} from 'react-native-svg'
import {FeedItem} from './FeedItem' import {FeedItem} from './FeedItem'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {makeProfileLink} from 'lib/routes/links' import {makeProfileLink} from 'lib/routes/links'
import {Trans} from '@lingui/macro'
let FeedSlice = ({slice}: {slice: FeedPostSlice}): React.ReactNode => { let FeedSlice = ({slice}: {slice: FeedPostSlice}): React.ReactNode => {
if (slice.isThread && slice.items.length > 3) { if (slice.isThread && slice.items.length > 3) {
@ -99,7 +100,7 @@ function ViewFullThread({slice}: {slice: FeedPostSlice}) {
</View> </View>
<Text type="md" style={[pal.link, {paddingTop: 18, paddingBottom: 4}]}> <Text type="md" style={[pal.link, {paddingTop: 18, paddingBottom: 4}]}>
View full thread <Trans>View full thread</Trans>
</Text> </Text>
</Link> </Link>
) )

View file

@ -12,6 +12,7 @@ import {NavigationProp} from 'lib/routes/types'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {isWeb} from 'platform/detection' import {isWeb} from 'platform/detection'
import {Trans} from '@lingui/macro'
export function FollowingEmptyState() { export function FollowingEmptyState() {
const pal = usePalette('default') const pal = usePalette('default')
@ -43,15 +44,17 @@ export function FollowingEmptyState() {
<MagnifyingGlassIcon style={[styles.icon, pal.text]} size={62} /> <MagnifyingGlassIcon style={[styles.icon, pal.text]} size={62} />
</View> </View>
<Text type="xl-medium" style={[s.textCenter, pal.text]}> <Text type="xl-medium" style={[s.textCenter, pal.text]}>
Your following feed is empty! Follow more users to see what's <Trans>
happening. Your following feed is empty! Follow more users to see what's
happening.
</Trans>
</Text> </Text>
<Button <Button
type="inverted" type="inverted"
style={styles.emptyBtn} style={styles.emptyBtn}
onPress={onPressFindAccounts}> onPress={onPressFindAccounts}>
<Text type="lg-medium" style={palInverted.text}> <Text type="lg-medium" style={palInverted.text}>
Find accounts to follow <Trans>Find accounts to follow</Trans>
</Text> </Text>
<FontAwesomeIcon <FontAwesomeIcon
icon="angle-right" icon="angle-right"
@ -61,14 +64,14 @@ export function FollowingEmptyState() {
</Button> </Button>
<Text type="xl-medium" style={[s.textCenter, pal.text, s.mt20]}> <Text type="xl-medium" style={[s.textCenter, pal.text, s.mt20]}>
You can also discover new Custom Feeds to follow. <Trans>You can also discover new Custom Feeds to follow.</Trans>
</Text> </Text>
<Button <Button
type="inverted" type="inverted"
style={[styles.emptyBtn, s.mt10]} style={[styles.emptyBtn, s.mt10]}
onPress={onPressDiscoverFeeds}> onPress={onPressDiscoverFeeds}>
<Text type="lg-medium" style={palInverted.text}> <Text type="lg-medium" style={palInverted.text}>
Discover new custom feeds <Trans>Discover new custom feeds</Trans>
</Text> </Text>
<FontAwesomeIcon <FontAwesomeIcon
icon="angle-right" icon="angle-right"

View file

@ -11,6 +11,7 @@ import {NavigationProp} from 'lib/routes/types'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {isWeb} from 'platform/detection' import {isWeb} from 'platform/detection'
import {Trans} from '@lingui/macro'
export function FollowingEndOfFeed() { export function FollowingEndOfFeed() {
const pal = usePalette('default') const pal = usePalette('default')
@ -44,15 +45,17 @@ export function FollowingEndOfFeed() {
]}> ]}>
<View style={styles.inner}> <View style={styles.inner}>
<Text type="xl-medium" style={[s.textCenter, pal.text]}> <Text type="xl-medium" style={[s.textCenter, pal.text]}>
You've reached the end of your feed! Find some more accounts to <Trans>
follow. You've reached the end of your feed! Find some more accounts to
follow.
</Trans>
</Text> </Text>
<Button <Button
type="inverted" type="inverted"
style={styles.emptyBtn} style={styles.emptyBtn}
onPress={onPressFindAccounts}> onPress={onPressFindAccounts}>
<Text type="lg-medium" style={palInverted.text}> <Text type="lg-medium" style={palInverted.text}>
Find accounts to follow <Trans>Find accounts to follow</Trans>
</Text> </Text>
<FontAwesomeIcon <FontAwesomeIcon
icon="angle-right" icon="angle-right"
@ -62,14 +65,14 @@ export function FollowingEndOfFeed() {
</Button> </Button>
<Text type="xl-medium" style={[s.textCenter, pal.text, s.mt20]}> <Text type="xl-medium" style={[s.textCenter, pal.text, s.mt20]}>
You can also discover new Custom Feeds to follow. <Trans>You can also discover new Custom Feeds to follow.</Trans>
</Text> </Text>
<Button <Button
type="inverted" type="inverted"
style={[styles.emptyBtn, s.mt10]} style={[styles.emptyBtn, s.mt10]}
onPress={onPressDiscoverFeeds}> onPress={onPressDiscoverFeeds}>
<Text type="lg-medium" style={palInverted.text}> <Text type="lg-medium" style={palInverted.text}>
Discover new custom feeds <Trans>Discover new custom feeds</Trans>
</Text> </Text>
<FontAwesomeIcon <FontAwesomeIcon
icon="angle-right" icon="angle-right"

View file

@ -5,6 +5,8 @@ import {Button, ButtonType} from '../util/forms/Button'
import * as Toast from '../util/Toast' import * as Toast from '../util/Toast'
import {useProfileFollowMutationQueue} from '#/state/queries/profile' import {useProfileFollowMutationQueue} from '#/state/queries/profile'
import {Shadow} from '#/state/cache/types' import {Shadow} from '#/state/cache/types'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
export function FollowButton({ export function FollowButton({
unfollowedType = 'inverted', unfollowedType = 'inverted',
@ -18,13 +20,14 @@ export function FollowButton({
labelStyle?: StyleProp<TextStyle> labelStyle?: StyleProp<TextStyle>
}) { }) {
const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(profile) const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(profile)
const {_} = useLingui()
const onPressFollow = async () => { const onPressFollow = async () => {
try { try {
await queueFollow() await queueFollow()
} catch (e: any) { } catch (e: any) {
if (e?.name !== 'AbortError') { if (e?.name !== 'AbortError') {
Toast.show(`An issue occurred, please try again.`) Toast.show(_(msg`An issue occurred, please try again.`))
} }
} }
} }
@ -34,7 +37,7 @@ export function FollowButton({
await queueUnfollow() await queueUnfollow()
} catch (e: any) { } catch (e: any) {
if (e?.name !== 'AbortError') { if (e?.name !== 'AbortError') {
Toast.show(`An issue occurred, please try again.`) Toast.show(_(msg`An issue occurred, please try again.`))
} }
} }
} }
@ -49,7 +52,7 @@ export function FollowButton({
type={followedType} type={followedType}
labelStyle={labelStyle} labelStyle={labelStyle}
onPress={onPressUnfollow} onPress={onPressUnfollow}
label="Unfollow" label={_(msg({message: 'Unfollow', context: 'action'}))}
/> />
) )
} else { } else {
@ -58,7 +61,7 @@ export function FollowButton({
type={unfollowedType} type={unfollowedType}
labelStyle={labelStyle} labelStyle={labelStyle}
onPress={onPressFollow} onPress={onPressFollow}
label="Follow" label={_(msg({message: 'Follow', context: 'action'}))}
/> />
) )
} }

View file

@ -23,6 +23,7 @@ import {Shadow} from '#/state/cache/types'
import {useModerationOpts} from '#/state/queries/preferences' import {useModerationOpts} from '#/state/queries/preferences'
import {useProfileShadow} from '#/state/cache/profile-shadow' import {useProfileShadow} from '#/state/cache/profile-shadow'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {Trans} from '@lingui/macro'
export function ProfileCard({ export function ProfileCard({
testID, testID,
@ -137,7 +138,7 @@ function ProfileCardPills({
{followedBy && ( {followedBy && (
<View style={[s.mt5, pal.btn, styles.pill]}> <View style={[s.mt5, pal.btn, styles.pill]}>
<Text type="xs" style={pal.text}> <Text type="xs" style={pal.text}>
Follows You <Trans>Follows You</Trans>
</Text> </Text>
</View> </View>
)} )}
@ -190,8 +191,10 @@ function FollowersList({
style={[styles.followsByDesc, pal.textLight]} style={[styles.followsByDesc, pal.textLight]}
numberOfLines={2} numberOfLines={2}
lineHeight={1.2}> lineHeight={1.2}>
Followed by{' '} <Trans>
{followersWithMods.map(({f}) => f.displayName || f.handle).join(', ')} Followed by{' '}
{followersWithMods.map(({f}) => f.displayName || f.handle).join(', ')}
</Trans>
</Text> </Text>
{followersWithMods.slice(0, 3).map(({f, mod}) => ( {followersWithMods.slice(0, 3).map(({f, mod}) => (
<View key={f.did} style={styles.followedByAviContainer}> <View key={f.did} style={styles.followedByAviContainer}>

View file

@ -192,14 +192,16 @@ let ProfileHeaderLoaded = ({
track('ProfileHeader:FollowButtonClicked') track('ProfileHeader:FollowButtonClicked')
await queueFollow() await queueFollow()
Toast.show( Toast.show(
`Following ${sanitizeDisplayName( _(
profile.displayName || profile.handle, msg`Following ${sanitizeDisplayName(
)}`, profile.displayName || profile.handle,
)}`,
),
) )
} catch (e: any) { } catch (e: any) {
if (e?.name !== 'AbortError') { if (e?.name !== 'AbortError') {
logger.error('Failed to follow', {error: String(e)}) logger.error('Failed to follow', {error: String(e)})
Toast.show(`There was an issue! ${e.toString()}`) Toast.show(_(msg`There was an issue! ${e.toString()}`))
} }
} }
}) })
@ -211,14 +213,16 @@ let ProfileHeaderLoaded = ({
track('ProfileHeader:UnfollowButtonClicked') track('ProfileHeader:UnfollowButtonClicked')
await queueUnfollow() await queueUnfollow()
Toast.show( Toast.show(
`No longer following ${sanitizeDisplayName( _(
profile.displayName || profile.handle, msg`No longer following ${sanitizeDisplayName(
)}`, profile.displayName || profile.handle,
)}`,
),
) )
} catch (e: any) { } catch (e: any) {
if (e?.name !== 'AbortError') { if (e?.name !== 'AbortError') {
logger.error('Failed to unfollow', {error: String(e)}) logger.error('Failed to unfollow', {error: String(e)})
Toast.show(`There was an issue! ${e.toString()}`) Toast.show(_(msg`There was an issue! ${e.toString()}`))
} }
} }
}) })
@ -253,27 +257,27 @@ let ProfileHeaderLoaded = ({
track('ProfileHeader:MuteAccountButtonClicked') track('ProfileHeader:MuteAccountButtonClicked')
try { try {
await queueMute() await queueMute()
Toast.show('Account muted') Toast.show(_(msg`Account muted`))
} catch (e: any) { } catch (e: any) {
if (e?.name !== 'AbortError') { if (e?.name !== 'AbortError') {
logger.error('Failed to mute account', {error: e}) logger.error('Failed to mute account', {error: e})
Toast.show(`There was an issue! ${e.toString()}`) Toast.show(_(msg`There was an issue! ${e.toString()}`))
} }
} }
}, [track, queueMute]) }, [track, queueMute, _])
const onPressUnmuteAccount = React.useCallback(async () => { const onPressUnmuteAccount = React.useCallback(async () => {
track('ProfileHeader:UnmuteAccountButtonClicked') track('ProfileHeader:UnmuteAccountButtonClicked')
try { try {
await queueUnmute() await queueUnmute()
Toast.show('Account unmuted') Toast.show(_(msg`Account unmuted`))
} catch (e: any) { } catch (e: any) {
if (e?.name !== 'AbortError') { if (e?.name !== 'AbortError') {
logger.error('Failed to unmute account', {error: e}) logger.error('Failed to unmute account', {error: e})
Toast.show(`There was an issue! ${e.toString()}`) Toast.show(_(msg`There was an issue! ${e.toString()}`))
} }
} }
}, [track, queueUnmute]) }, [track, queueUnmute, _])
const onPressBlockAccount = React.useCallback(async () => { const onPressBlockAccount = React.useCallback(async () => {
track('ProfileHeader:BlockAccountButtonClicked') track('ProfileHeader:BlockAccountButtonClicked')
@ -286,11 +290,11 @@ let ProfileHeaderLoaded = ({
onPressConfirm: async () => { onPressConfirm: async () => {
try { try {
await queueBlock() await queueBlock()
Toast.show('Account blocked') Toast.show(_(msg`Account blocked`))
} catch (e: any) { } catch (e: any) {
if (e?.name !== 'AbortError') { if (e?.name !== 'AbortError') {
logger.error('Failed to block account', {error: e}) logger.error('Failed to block account', {error: e})
Toast.show(`There was an issue! ${e.toString()}`) Toast.show(_(msg`There was an issue! ${e.toString()}`))
} }
} }
}, },
@ -308,11 +312,11 @@ let ProfileHeaderLoaded = ({
onPressConfirm: async () => { onPressConfirm: async () => {
try { try {
await queueUnblock() await queueUnblock()
Toast.show('Account unblocked') Toast.show(_(msg`Account unblocked`))
} catch (e: any) { } catch (e: any) {
if (e?.name !== 'AbortError') { if (e?.name !== 'AbortError') {
logger.error('Failed to unblock account', {error: e}) logger.error('Failed to unblock account', {error: e})
Toast.show(`There was an issue! ${e.toString()}`) Toast.show(_(msg`There was an issue! ${e.toString()}`))
} }
} }
}, },
@ -451,7 +455,9 @@ let ProfileHeaderLoaded = ({
style={[styles.btn, styles.mainBtn, pal.btn]} style={[styles.btn, styles.mainBtn, pal.btn]}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_(msg`Edit profile`)} accessibilityLabel={_(msg`Edit profile`)}
accessibilityHint="Opens editor for profile display name, avatar, background image, and description"> accessibilityHint={_(
msg`Opens editor for profile display name, avatar, background image, and description`,
)}>
<Text type="button" style={pal.text}> <Text type="button" style={pal.text}>
<Trans>Edit Profile</Trans> <Trans>Edit Profile</Trans>
</Text> </Text>
@ -466,7 +472,7 @@ let ProfileHeaderLoaded = ({
accessibilityLabel={_(msg`Unblock`)} accessibilityLabel={_(msg`Unblock`)}
accessibilityHint=""> accessibilityHint="">
<Text type="button" style={[pal.text, s.bold]}> <Text type="button" style={[pal.text, s.bold]}>
<Trans>Unblock</Trans> <Trans context="action">Unblock</Trans>
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
) )
@ -488,8 +494,12 @@ let ProfileHeaderLoaded = ({
}, },
]} ]}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={`Show follows similar to ${profile.handle}`} accessibilityLabel={_(
accessibilityHint={`Shows a list of users similar to this user.`}> msg`Show follows similar to ${profile.handle}`,
)}
accessibilityHint={_(
msg`Shows a list of users similar to this user.`,
)}>
<FontAwesomeIcon <FontAwesomeIcon
icon="user-plus" icon="user-plus"
style={[ style={[
@ -511,8 +521,10 @@ let ProfileHeaderLoaded = ({
onPress={onPressUnfollow} onPress={onPressUnfollow}
style={[styles.btn, styles.mainBtn, pal.btn]} style={[styles.btn, styles.mainBtn, pal.btn]}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={`Unfollow ${profile.handle}`} accessibilityLabel={_(msg`Unfollow ${profile.handle}`)}
accessibilityHint={`Hides posts from ${profile.handle} in your feed`}> accessibilityHint={_(
msg`Hides posts from ${profile.handle} in your feed`,
)}>
<FontAwesomeIcon <FontAwesomeIcon
icon="check" icon="check"
style={[pal.text, s.mr5]} style={[pal.text, s.mr5]}
@ -528,8 +540,10 @@ let ProfileHeaderLoaded = ({
onPress={onPressFollow} onPress={onPressFollow}
style={[styles.btn, styles.mainBtn, palInverted.view]} style={[styles.btn, styles.mainBtn, palInverted.view]}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={`Follow ${profile.handle}`} accessibilityLabel={_(msg`Follow ${profile.handle}`)}
accessibilityHint={`Shows posts from ${profile.handle} in your feed`}> accessibilityHint={_(
msg`Shows posts from ${profile.handle} in your feed`,
)}>
<FontAwesomeIcon <FontAwesomeIcon
icon="plus" icon="plus"
style={[palInverted.text, s.mr5]} style={[palInverted.text, s.mr5]}
@ -580,7 +594,7 @@ let ProfileHeaderLoaded = ({
invalidHandle ? styles.invalidHandle : undefined, invalidHandle ? styles.invalidHandle : undefined,
styles.handle, styles.handle,
]}> ]}>
{invalidHandle ? '⚠Invalid Handle' : `@${profile.handle}`} {invalidHandle ? _(msg`⚠Invalid Handle`) : `@${profile.handle}`}
</ThemedText> </ThemedText>
</View> </View>
{!blockHide && ( {!blockHide && (
@ -597,7 +611,7 @@ let ProfileHeaderLoaded = ({
} }
asAnchor asAnchor
accessibilityLabel={`${followers} ${pluralizedFollowers}`} accessibilityLabel={`${followers} ${pluralizedFollowers}`}
accessibilityHint={'Opens followers list'}> accessibilityHint={_(msg`Opens followers list`)}>
<Text type="md" style={[s.bold, pal.text]}> <Text type="md" style={[s.bold, pal.text]}>
{followers}{' '} {followers}{' '}
</Text> </Text>
@ -615,14 +629,16 @@ let ProfileHeaderLoaded = ({
}) })
} }
asAnchor asAnchor
accessibilityLabel={`${following} following`} accessibilityLabel={_(msg`${following} following`)}
accessibilityHint={'Opens following list'}> accessibilityHint={_(msg`Opens following list`)}>
<Text type="md" style={[s.bold, pal.text]}> <Trans>
{following}{' '} <Text type="md" style={[s.bold, pal.text]}>
</Text> {following}{' '}
<Text type="md" style={[pal.textLight]}> </Text>
<Trans>following</Trans> <Text type="md" style={[pal.textLight]}>
</Text> following
</Text>
</Trans>
</Link> </Link>
<Text type="md" style={[s.bold, pal.text]}> <Text type="md" style={[s.bold, pal.text]}>
{formatCount(profile.postsCount || 0)}{' '} {formatCount(profile.postsCount || 0)}{' '}
@ -682,7 +698,7 @@ let ProfileHeaderLoaded = ({
testID="profileHeaderAviButton" testID="profileHeaderAviButton"
onPress={onPressAvi} onPress={onPressAvi}
accessibilityRole="image" accessibilityRole="image"
accessibilityLabel={`View ${profile.handle}'s avatar`} accessibilityLabel={_(msg`View ${profile.handle}'s avatar`)}
accessibilityHint=""> accessibilityHint="">
<View <View
style={[pal.view, {borderColor: pal.colors.background}, styles.avi]}> style={[pal.view, {borderColor: pal.colors.background}, styles.avi]}>

View file

@ -21,6 +21,7 @@ import {useModerationOpts} from '#/state/queries/preferences'
import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows' import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows'
import {useProfileShadow} from '#/state/cache/profile-shadow' import {useProfileShadow} from '#/state/cache/profile-shadow'
import {useProfileFollowMutationQueue} from '#/state/queries/profile' import {useProfileFollowMutationQueue} from '#/state/queries/profile'
import {Trans} from '@lingui/macro'
const OUTER_PADDING = 10 const OUTER_PADDING = 10
const INNER_PADDING = 14 const INNER_PADDING = 14
@ -60,7 +61,7 @@ export function ProfileHeaderSuggestedFollows({
paddingRight: INNER_PADDING / 2, paddingRight: INNER_PADDING / 2,
}}> }}>
<Text type="sm-bold" style={[pal.textLight]}> <Text type="sm-bold" style={[pal.textLight]}>
Suggested for you <Trans>Suggested for you</Trans>
</Text> </Text>
<Pressable <Pressable

View file

@ -16,7 +16,7 @@ import {BACK_HITSLOP} from 'lib/constants'
import {isNative} from 'platform/detection' import {isNative} from 'platform/detection'
import {useLightboxControls, ImagesLightbox} from '#/state/lightbox' import {useLightboxControls, ImagesLightbox} from '#/state/lightbox'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useSetDrawerOpen} from '#/state/shell' import {useSetDrawerOpen} from '#/state/shell'
import {emitSoftReset} from '#/state/events' import {emitSoftReset} from '#/state/events'
@ -153,17 +153,19 @@ export function ProfileSubpageHeader({
<LoadingPlaceholder width={50} height={8} /> <LoadingPlaceholder width={50} height={8} />
) : ( ) : (
<Text type="xl" style={[pal.textLight]} numberOfLines={1}> <Text type="xl" style={[pal.textLight]} numberOfLines={1}>
by{' '}
{!creator ? ( {!creator ? (
'—' <Trans>by </Trans>
) : isOwner ? ( ) : isOwner ? (
'you' <Trans>by you</Trans>
) : ( ) : (
<TextLink <Trans>
text={sanitizeHandle(creator.handle, '@')} by{' '}
href={makeProfileLink(creator)} <TextLink
style={pal.textLight} text={sanitizeHandle(creator.handle, '@')}
/> href={makeProfileLink(creator)}
style={pal.textLight}
/>
</Trans>
)} )}
</Text> </Text>
)} )}

View file

@ -22,7 +22,7 @@ export function AccountDropdownBtn({account}: {account: SessionAccount}) {
label: _(msg`Remove account`), label: _(msg`Remove account`),
onPress: () => { onPress: () => {
removeAccount(account) removeAccount(account)
Toast.show('Account removed from quick access') Toast.show(_(msg`Account removed from quick access`))
}, },
icon: { icon: {
ios: { ios: {

View file

@ -2,6 +2,8 @@ import React, {createRef, useState, useMemo, useRef} from 'react'
import {Animated, Pressable, StyleSheet, View} from 'react-native' import {Animated, Pressable, StyleSheet, View} from 'react-native'
import {Text} from './text/Text' import {Text} from './text/Text'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
interface Layout { interface Layout {
x: number x: number
@ -19,6 +21,7 @@ export function Selector({
panX: Animated.Value panX: Animated.Value
onSelect?: (index: number) => void onSelect?: (index: number) => void
}) { }) {
const {_} = useLingui()
const containerRef = useRef<View>(null) const containerRef = useRef<View>(null)
const pal = usePalette('default') const pal = usePalette('default')
const [itemLayouts, setItemLayouts] = useState<undefined | Layout[]>( const [itemLayouts, setItemLayouts] = useState<undefined | Layout[]>(
@ -100,8 +103,8 @@ export function Selector({
testID={`selector-${i}`} testID={`selector-${i}`}
key={item} key={item}
onPress={() => onPressItem(i)} onPress={() => onPressItem(i)}
accessibilityLabel={`Select ${item}`} accessibilityLabel={_(msg`Select ${item}`)}
accessibilityHint={`Select option ${i} of ${numItems}`}> accessibilityHint={_(msg`Select option ${i} of ${numItems}`)}>
<View style={styles.item} ref={itemRefs[i]}> <View style={styles.item} ref={itemRefs[i]}>
<Text <Text
style={ style={

View file

@ -11,6 +11,8 @@ import {NavigationProp} from 'lib/routes/types'
import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
import Animated from 'react-native-reanimated' import Animated from 'react-native-reanimated'
import {useSetDrawerOpen} from '#/state/shell' import {useSetDrawerOpen} from '#/state/shell'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20} const BACK_HITSLOP = {left: 20, top: 20, right: 50, bottom: 20}
@ -32,6 +34,7 @@ export function ViewHeader({
renderButton?: () => JSX.Element renderButton?: () => JSX.Element
}) { }) {
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui()
const setDrawerOpen = useSetDrawerOpen() const setDrawerOpen = useSetDrawerOpen()
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
const {track} = useAnalytics() const {track} = useAnalytics()
@ -75,9 +78,9 @@ export function ViewHeader({
hitSlop={BACK_HITSLOP} hitSlop={BACK_HITSLOP}
style={canGoBack ? styles.backBtn : styles.backBtnWide} style={canGoBack ? styles.backBtn : styles.backBtnWide}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={canGoBack ? 'Back' : 'Menu'} accessibilityLabel={canGoBack ? _(msg`Back`) : _(msg`Menu`)}
accessibilityHint={ accessibilityHint={
canGoBack ? '' : 'Access navigation links and settings' canGoBack ? '' : _(msg`Access navigation links and settings`)
}> }>
{canGoBack ? ( {canGoBack ? (
<FontAwesomeIcon <FontAwesomeIcon

View file

@ -53,7 +53,9 @@ export function ErrorMessage({
onPress={onPressTryAgain} onPress={onPressTryAgain}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_(msg`Retry`)} accessibilityLabel={_(msg`Retry`)}
accessibilityHint="Retries the last action, which errored out"> accessibilityHint={_(
msg`Retries the last action, which errored out`,
)}>
<FontAwesomeIcon <FontAwesomeIcon
icon="arrows-rotate" icon="arrows-rotate"
style={{color: theme.palette.error.icon}} style={{color: theme.palette.error.icon}}

View file

@ -63,14 +63,16 @@ export function ErrorScreen({
style={[styles.btn]} style={[styles.btn]}
onPress={onPressTryAgain} onPress={onPressTryAgain}
accessibilityLabel={_(msg`Retry`)} accessibilityLabel={_(msg`Retry`)}
accessibilityHint="Retries the last action, which errored out"> accessibilityHint={_(
msg`Retries the last action, which errored out`,
)}>
<FontAwesomeIcon <FontAwesomeIcon
icon="arrows-rotate" icon="arrows-rotate"
style={pal.link as FontAwesomeIconStyle} style={pal.link as FontAwesomeIconStyle}
size={16} size={16}
/> />
<Text type="button" style={[styles.btnText, pal.link]}> <Text type="button" style={[styles.btnText, pal.link]}>
<Trans>Try again</Trans> <Trans context="action">Try again</Trans>
</Text> </Text>
</Button> </Button>
</View> </View>

View file

@ -75,6 +75,8 @@ export function DropdownButton({
bottomOffset = 0, bottomOffset = 0,
accessibilityLabel, accessibilityLabel,
}: PropsWithChildren<DropdownButtonProps>) { }: PropsWithChildren<DropdownButtonProps>) {
const {_} = useLingui()
const ref1 = useRef<TouchableOpacity>(null) const ref1 = useRef<TouchableOpacity>(null)
const ref2 = useRef<View>(null) const ref2 = useRef<View>(null)
@ -141,7 +143,9 @@ export function DropdownButton({
hitSlop={HITSLOP_10} hitSlop={HITSLOP_10}
ref={ref1} ref={ref1}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={accessibilityLabel || `Opens ${numItems} options`} accessibilityLabel={
accessibilityLabel || _(msg`Opens ${numItems} options`)
}
accessibilityHint=""> accessibilityHint="">
{children} {children}
</TouchableOpacity> </TouchableOpacity>
@ -247,7 +251,7 @@ const DropdownItems = ({
onPress={() => onPressItem(index)} onPress={() => onPressItem(index)}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={item.label} accessibilityLabel={item.label}
accessibilityHint={`Option ${index + 1} of ${numItems}`}> accessibilityHint={_(msg`Option ${index + 1} of ${numItems}`)}>
{item.icon && ( {item.icon && (
<FontAwesomeIcon <FontAwesomeIcon
style={styles.icon} style={styles.icon}

View file

@ -71,32 +71,34 @@ let PostDropdownBtn = ({
const onDeletePost = React.useCallback(() => { const onDeletePost = React.useCallback(() => {
postDeleteMutation.mutateAsync({uri: postUri}).then( postDeleteMutation.mutateAsync({uri: postUri}).then(
() => { () => {
Toast.show('Post deleted') Toast.show(_(msg`Post deleted`))
}, },
e => { e => {
logger.error('Failed to delete post', {error: e}) logger.error('Failed to delete post', {error: e})
Toast.show('Failed to delete post, please try again') Toast.show(_(msg`Failed to delete post, please try again`))
}, },
) )
}, [postUri, postDeleteMutation]) }, [postUri, postDeleteMutation, _])
const onToggleThreadMute = React.useCallback(() => { const onToggleThreadMute = React.useCallback(() => {
try { try {
const muted = toggleThreadMute(rootUri) const muted = toggleThreadMute(rootUri)
if (muted) { if (muted) {
Toast.show('You will no longer receive notifications for this thread') Toast.show(
_(msg`You will no longer receive notifications for this thread`),
)
} else { } else {
Toast.show('You will now receive notifications for this thread') Toast.show(_(msg`You will now receive notifications for this thread`))
} }
} catch (e) { } catch (e) {
logger.error('Failed to toggle thread mute', {error: e}) logger.error('Failed to toggle thread mute', {error: e})
} }
}, [rootUri, toggleThreadMute]) }, [rootUri, toggleThreadMute, _])
const onCopyPostText = React.useCallback(() => { const onCopyPostText = React.useCallback(() => {
Clipboard.setString(record?.text || '') Clipboard.setString(record?.text || '')
Toast.show('Copied to clipboard') Toast.show(_(msg`Copied to clipboard`))
}, [record]) }, [record, _])
const onOpenTranslate = React.useCallback(() => { const onOpenTranslate = React.useCallback(() => {
Linking.openURL(translatorUrl) Linking.openURL(translatorUrl)
@ -253,7 +255,7 @@ let PostDropdownBtn = ({
<NativeDropdown <NativeDropdown
testID={testID} testID={testID}
items={dropdownItems} items={dropdownItems}
accessibilityLabel="More post options" accessibilityLabel={_(msg`More post options`)}
accessibilityHint=""> accessibilityHint="">
<View style={style}> <View style={style}>
<FontAwesomeIcon icon="ellipsis" size={20} color={defaultCtrlColor} /> <FontAwesomeIcon icon="ellipsis" size={20} color={defaultCtrlColor} />

View file

@ -50,7 +50,7 @@ export function SearchInput({
<TextInput <TextInput
testID="searchTextInput" testID="searchTextInput"
ref={textInput} ref={textInput}
placeholder="Search" placeholder={_(msg`Search`)}
placeholderTextColor={pal.colors.textLight} placeholderTextColor={pal.colors.textLight}
selectTextOnFocus selectTextOnFocus
returnKeyType="search" returnKeyType="search"

View file

@ -4,6 +4,8 @@ import {Image} from 'expo-image'
import {clamp} from 'lib/numbers' import {clamp} from 'lib/numbers'
import {Dimensions} from 'lib/media/types' import {Dimensions} from 'lib/media/types'
import * as imageSizes from 'lib/media/image-sizes' import * as imageSizes from 'lib/media/image-sizes'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
const MIN_ASPECT_RATIO = 0.33 // 1/3 const MIN_ASPECT_RATIO = 0.33 // 1/3
const MAX_ASPECT_RATIO = 10 // 10/1 const MAX_ASPECT_RATIO = 10 // 10/1
@ -29,6 +31,7 @@ export function AutoSizedImage({
style, style,
children = null, children = null,
}: Props) { }: Props) {
const {_} = useLingui()
const [dim, setDim] = React.useState<Dimensions | undefined>( const [dim, setDim] = React.useState<Dimensions | undefined>(
dimensionsHint || imageSizes.get(uri), dimensionsHint || imageSizes.get(uri),
) )
@ -64,7 +67,7 @@ export function AutoSizedImage({
accessible={true} // Must set for `accessibilityLabel` to work accessible={true} // Must set for `accessibilityLabel` to work
accessibilityIgnoresInvertColors accessibilityIgnoresInvertColors
accessibilityLabel={alt} accessibilityLabel={alt}
accessibilityHint="Tap to view fully" accessibilityHint={_(msg`Tap to view fully`)}
/> />
{children} {children}
</Pressable> </Pressable>

View file

@ -2,6 +2,8 @@ import {AppBskyEmbedImages} from '@atproto/api'
import React, {ComponentProps, FC} from 'react' import React, {ComponentProps, FC} from 'react'
import {StyleSheet, Text, Pressable, View} from 'react-native' import {StyleSheet, Text, Pressable, View} from 'react-native'
import {Image} from 'expo-image' import {Image} from 'expo-image'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
type EventFunction = (index: number) => void type EventFunction = (index: number) => void
@ -22,6 +24,7 @@ export const GalleryItem: FC<GalleryItemProps> = ({
onPressIn, onPressIn,
onLongPress, onLongPress,
}) => { }) => {
const {_} = useLingui()
const image = images[index] const image = images[index]
return ( return (
<View style={styles.fullWidth}> <View style={styles.fullWidth}>
@ -31,7 +34,7 @@ export const GalleryItem: FC<GalleryItemProps> = ({
onLongPress={onLongPress ? () => onLongPress(index) : undefined} onLongPress={onLongPress ? () => onLongPress(index) : undefined}
style={styles.fullWidth} style={styles.fullWidth}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={image.alt || 'Image'} accessibilityLabel={image.alt || _(msg`Image`)}
accessibilityHint=""> accessibilityHint="">
<Image <Image
source={{uri: image.thumb}} source={{uri: image.thumb}}

View file

@ -63,7 +63,9 @@ export function ContentHider({
} }
}} }}
accessibilityRole="button" accessibilityRole="button"
accessibilityHint={override ? 'Hide the content' : 'Show the content'} accessibilityHint={
override ? _(msg`Hide the content`) : _(msg`Show the content`)
}
accessibilityLabel="" accessibilityLabel=""
style={[ style={[
styles.cover, styles.cover,

View file

@ -9,7 +9,7 @@ import {addStyle} from 'lib/styles'
import {describeModerationCause} from 'lib/moderation' import {describeModerationCause} from 'lib/moderation'
import {ShieldExclamation} from 'lib/icons' import {ShieldExclamation} from 'lib/icons'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useModalControls} from '#/state/modals' import {useModalControls} from '#/state/modals'
interface Props extends ComponentProps<typeof Link> { interface Props extends ComponentProps<typeof Link> {
@ -57,7 +57,9 @@ export function PostHider({
} }
}} }}
accessibilityRole="button" accessibilityRole="button"
accessibilityHint={override ? 'Hide the content' : 'Show the content'} accessibilityHint={
override ? _(msg`Hide the content`) : _(msg`Show the content`)
}
accessibilityLabel="" accessibilityLabel=""
style={[ style={[
styles.description, styles.description,
@ -103,7 +105,7 @@ export function PostHider({
</Text> </Text>
{!moderation.noOverride && ( {!moderation.noOverride && (
<Text type="sm" style={[styles.showBtn, pal.link]}> <Text type="sm" style={[styles.showBtn, pal.link]}>
{override ? 'Hide' : 'Show'} {override ? <Trans>Hide</Trans> : <Trans>Show</Trans>}
</Text> </Text>
)} )}
</Pressable> </Pressable>

View file

@ -26,6 +26,8 @@ import {
import {useComposerControls} from '#/state/shell/composer' import {useComposerControls} from '#/state/shell/composer'
import {Shadow} from '#/state/cache/types' import {Shadow} from '#/state/cache/types'
import {useRequireAuth} from '#/state/session' import {useRequireAuth} from '#/state/session'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
let PostCtrls = ({ let PostCtrls = ({
big, big,
@ -43,6 +45,7 @@ let PostCtrls = ({
onPressReply: () => void onPressReply: () => void
}): React.ReactNode => { }): React.ReactNode => {
const theme = useTheme() const theme = useTheme()
const {_} = useLingui()
const {openComposer} = useComposerControls() const {openComposer} = useComposerControls()
const {closeModal} = useModalControls() const {closeModal} = useModalControls()
const postLikeMutation = usePostLikeMutation() const postLikeMutation = usePostLikeMutation()
@ -176,9 +179,9 @@ let PostCtrls = ({
requireAuth(() => onPressToggleLike()) requireAuth(() => onPressToggleLike())
}} }}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={`${post.viewer?.like ? 'Unlike' : 'Like'} (${ accessibilityLabel={`${
post.likeCount post.viewer?.like ? _(msg`Unlike`) : _(msg`Like`)
} ${pluralize(post.likeCount || 0, 'like')})`} } (${post.likeCount} ${pluralize(post.likeCount || 0, 'like')})`}
accessibilityHint="" accessibilityHint=""
hitSlop={big ? HITSLOP_20 : HITSLOP_10}> hitSlop={big ? HITSLOP_20 : HITSLOP_10}>
{post.viewer?.like ? ( {post.viewer?.like ? (

View file

@ -8,6 +8,8 @@ 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 {useLingui} from '@lingui/react'
interface Props { interface Props {
isReposted: boolean isReposted: boolean
@ -25,6 +27,7 @@ let RepostButton = ({
onQuote, onQuote,
}: Props): React.ReactNode => { }: Props): React.ReactNode => {
const theme = useTheme() const theme = useTheme()
const {_} = useLingui()
const {openModal} = useModalControls() const {openModal} = useModalControls()
const requireAuth = useRequireAuth() const requireAuth = useRequireAuth()
@ -53,7 +56,9 @@ let RepostButton = ({
style={[styles.control, !big && styles.controlPad]} style={[styles.control, !big && styles.controlPad]}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={`${ accessibilityLabel={`${
isReposted ? 'Undo repost' : 'Repost' isReposted
? _(msg`Undo repost`)
: _(msg({message: 'Repost', context: 'action'}))
} (${repostCount} ${pluralize(repostCount || 0, 'repost')})`} } (${repostCount} ${pluralize(repostCount || 0, 'repost')})`}
accessibilityHint="" accessibilityHint=""
hitSlop={big ? HITSLOP_20 : HITSLOP_10}> hitSlop={big ? HITSLOP_20 : HITSLOP_10}>

View file

@ -17,6 +17,7 @@ import {PostEmbeds} from '.'
import {PostAlerts} from '../moderation/PostAlerts' import {PostAlerts} from '../moderation/PostAlerts'
import {makeProfileLink} from 'lib/routes/links' import {makeProfileLink} from 'lib/routes/links'
import {InfoCircleIcon} from 'lib/icons' import {InfoCircleIcon} from 'lib/icons'
import {Trans} from '@lingui/macro'
export function MaybeQuoteEmbed({ export function MaybeQuoteEmbed({
embed, embed,
@ -52,7 +53,7 @@ export function MaybeQuoteEmbed({
<View style={[styles.errorContainer, pal.borderDark]}> <View style={[styles.errorContainer, pal.borderDark]}>
<InfoCircleIcon size={18} style={pal.text} /> <InfoCircleIcon size={18} style={pal.text} />
<Text type="lg" style={pal.text}> <Text type="lg" style={pal.text}>
Blocked <Trans>Blocked</Trans>
</Text> </Text>
</View> </View>
) )
@ -61,7 +62,7 @@ export function MaybeQuoteEmbed({
<View style={[styles.errorContainer, pal.borderDark]}> <View style={[styles.errorContainer, pal.borderDark]}>
<InfoCircleIcon size={18} style={pal.text} /> <InfoCircleIcon size={18} style={pal.text} />
<Text type="lg" style={pal.text}> <Text type="lg" style={pal.text}>
Deleted <Trans>Deleted</Trans>
</Text> </Text>
</View> </View>
) )

View file

@ -62,8 +62,8 @@ export function AppPasswords({}: Props) {
]} ]}
testID="appPasswordsScreen"> testID="appPasswordsScreen">
<ErrorScreen <ErrorScreen
title="Oops!" title={_(msg`Oops!`)}
message="There was an issue with fetching your app passwords" message={_(msg`There was an issue with fetching your app passwords`)}
details={cleanError(error)} details={cleanError(error)}
/> />
</CenteredView> </CenteredView>

View file

@ -16,6 +16,8 @@ import {ToggleButton} from '../com/util/forms/ToggleButton'
import {RadioGroup} from '../com/util/forms/RadioGroup' import {RadioGroup} from '../com/util/forms/RadioGroup'
import {ErrorScreen} from '../com/util/error/ErrorScreen' import {ErrorScreen} from '../com/util/error/ErrorScreen'
import {ErrorMessage} from '../com/util/error/ErrorMessage' import {ErrorMessage} from '../com/util/error/ErrorMessage'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
const MAIN_VIEWS = ['Base', 'Controls', 'Error', 'Notifs'] const MAIN_VIEWS = ['Base', 'Controls', 'Error', 'Notifs']
@ -48,6 +50,7 @@ function DebugInner({
}) { }) {
const [currentView, setCurrentView] = React.useState<number>(0) const [currentView, setCurrentView] = React.useState<number>(0)
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui()
const renderItem = (item: any) => { const renderItem = (item: any) => {
return ( return (
@ -57,7 +60,7 @@ function DebugInner({
type="default-light" type="default-light"
onPress={onToggleColorScheme} onPress={onToggleColorScheme}
isSelected={colorScheme === 'dark'} isSelected={colorScheme === 'dark'}
label="Dark mode" label={_(msg`Dark mode`)}
/> />
</View> </View>
{item.currentView === 3 ? ( {item.currentView === 3 ? (
@ -77,7 +80,7 @@ function DebugInner({
return ( return (
<View style={[s.hContentRegion, pal.view]}> <View style={[s.hContentRegion, pal.view]}>
<ViewHeader title="Debug panel" /> <ViewHeader title={_(msg`Debug panel`)} />
<ViewSelector <ViewSelector
swipeEnabled swipeEnabled
sections={MAIN_VIEWS} sections={MAIN_VIEWS}

View file

@ -328,7 +328,7 @@ export function FeedsScreen(_props: Props) {
hitSlop={10} hitSlop={10}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_(msg`Edit Saved Feeds`)} accessibilityLabel={_(msg`Edit Saved Feeds`)}
accessibilityHint="Opens screen to edit Saved Feeds"> accessibilityHint={_(msg`Opens screen to edit Saved Feeds`)}>
<CogIcon size={22} strokeWidth={2} style={pal.textLight} /> <CogIcon size={22} strokeWidth={2} style={pal.textLight} />
</Link> </Link>
) )

View file

@ -73,7 +73,7 @@ export function ListsScreen({}: Props) {
}}> }}>
<FontAwesomeIcon icon="plus" color={pal.colors.text} /> <FontAwesomeIcon icon="plus" color={pal.colors.text} />
<Text type="button" style={pal.text}> <Text type="button" style={pal.text}>
<Trans>New</Trans> <Trans context="action">New</Trans>
</Text> </Text>
</Button> </Button>
</View> </View>

View file

@ -50,7 +50,9 @@ export function LogScreen({}: NativeStackScreenProps<
style={[styles.entry, pal.border, pal.view]} style={[styles.entry, pal.border, pal.view]}
onPress={toggler(entry.id)} onPress={toggler(entry.id)}
accessibilityLabel={_(msg`View debug entry`)} accessibilityLabel={_(msg`View debug entry`)}
accessibilityHint="Opens additional details for a debug entry"> accessibilityHint={_(
msg`Opens additional details for a debug entry`,
)}>
{entry.level === 'debug' ? ( {entry.level === 'debug' ? (
<FontAwesomeIcon icon="info" /> <FontAwesomeIcon icon="info" />
) : ( ) : (

View file

@ -78,7 +78,9 @@ export function PostThreadScreen({route}: Props) {
return ( return (
<View style={s.hContentRegion}> <View style={s.hContentRegion}>
{isMobile && <ViewHeader title={_(msg`Post`)} />} {isMobile && (
<ViewHeader title={_(msg({message: 'Post', context: 'description'}))} />
)}
<View style={s.flex1}> <View style={s.flex1}>
{uriError ? ( {uriError ? (
<CenteredView> <CenteredView>

View file

@ -72,7 +72,7 @@ export function PreferencesExternalEmbeds({}: Props) {
</View> </View>
</View> </View>
<Text type="xl-bold" style={[pal.text, styles.heading]}> <Text type="xl-bold" style={[pal.text, styles.heading]}>
Enable media players for <Trans>Enable media players for</Trans>
</Text> </Text>
{Object.entries(externalEmbedLabels).map(([key, label]) => ( {Object.entries(externalEmbedLabels).map(([key, label]) => (
<PrefSelector <PrefSelector

View file

@ -27,6 +27,7 @@ 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,10 +65,12 @@ function RepliesThresholdInput({
/> />
<Text type="xs" style={pal.text}> <Text type="xs" style={pal.text}>
{value === 0 {value === 0
? `Show all replies` ? _(msg`Show all replies`)
: `Show replies with at least ${value} ${ : _(
value > 1 ? `likes` : `like` msg`Show replies with at least ${value} ${
}`} value > 1 ? `likes` : `like`
}`,
)}
</Text> </Text>
</View> </View>
) )

View file

@ -159,7 +159,7 @@ export function PreferencesThreads({navigation}: Props) {
accessibilityLabel={_(msg`Confirm`)} accessibilityLabel={_(msg`Confirm`)}
accessibilityHint=""> accessibilityHint="">
<Text style={[s.white, s.bold, s.f18]}> <Text style={[s.white, s.bold, s.f18]}>
<Trans>Done</Trans> <Trans context="action">Done</Trans>
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>

View file

@ -371,6 +371,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
{feed, headerHeight, isFocused, scrollElRef, ignoreFilterFor}, {feed, headerHeight, isFocused, scrollElRef, ignoreFilterFor},
ref, ref,
) { ) {
const {_} = useLingui()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const [hasNew, setHasNew] = React.useState(false) const [hasNew, setHasNew] = React.useState(false)
const [isScrolledDown, setIsScrolledDown] = React.useState(false) const [isScrolledDown, setIsScrolledDown] = React.useState(false)
@ -388,8 +389,8 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
})) }))
const renderPostsEmpty = React.useCallback(() => { const renderPostsEmpty = React.useCallback(() => {
return <EmptyState icon="feed" message="This feed is empty!" /> return <EmptyState icon="feed" message={_(msg`This feed is empty!`)} />
}, []) }, [_])
return ( return (
<View> <View>
@ -408,7 +409,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
{(isScrolledDown || hasNew) && ( {(isScrolledDown || hasNew) && (
<LoadLatestBtn <LoadLatestBtn
onPress={onScrollToTop} onPress={onScrollToTop}
label="Load new posts" label={_(msg`Load new posts`)}
showIndicator={hasNew} showIndicator={hasNew}
/> />
)} )}

View file

@ -214,11 +214,21 @@ export function ProfileFeedScreenInner({
} }
} catch (err) { } catch (err) {
Toast.show( Toast.show(
'There was an an issue updating your feeds, please check your internet connection and try again.', _(
msg`There was an an issue updating your feeds, please check your internet connection and try again.`,
),
) )
logger.error('Failed up update feeds', {error: err}) logger.error('Failed up update feeds', {error: err})
} }
}, [feedInfo, isSaved, saveFeed, removeFeed, resetSaveFeed, resetRemoveFeed]) }, [
feedInfo,
isSaved,
saveFeed,
removeFeed,
resetSaveFeed,
resetRemoveFeed,
_,
])
const onTogglePinned = React.useCallback(async () => { const onTogglePinned = React.useCallback(async () => {
try { try {
@ -232,10 +242,10 @@ export function ProfileFeedScreenInner({
resetPinFeed() resetPinFeed()
} }
} catch (e) { } catch (e) {
Toast.show('There was an issue contacting the server') Toast.show(_(msg`There was an issue contacting the server`))
logger.error('Failed to toggle pinned feed', {error: e}) logger.error('Failed to toggle pinned feed', {error: e})
} }
}, [isPinned, feedInfo, pinFeed, unpinFeed, resetPinFeed, resetUnpinFeed]) }, [isPinned, feedInfo, pinFeed, unpinFeed, resetPinFeed, resetUnpinFeed, _])
const onPressShare = React.useCallback(() => { const onPressShare = React.useCallback(() => {
const url = toShareUrl(feedInfo.route.href) const url = toShareUrl(feedInfo.route.href)
@ -341,7 +351,7 @@ export function ProfileFeedScreenInner({
<Button <Button
disabled={isSavePending || isRemovePending} disabled={isSavePending || isRemovePending}
type="default" type="default"
label={isSaved ? 'Unsave' : 'Save'} label={isSaved ? _(msg`Unsave`) : _(msg`Save`)}
onPress={onToggleSaved} onPress={onToggleSaved}
style={styles.btn} style={styles.btn}
/> />
@ -349,7 +359,7 @@ export function ProfileFeedScreenInner({
testID={isPinned ? 'unpinBtn' : 'pinBtn'} testID={isPinned ? 'unpinBtn' : 'pinBtn'}
disabled={isPinPending || isUnpinPending} disabled={isPinPending || isUnpinPending}
type={isPinned ? 'default' : 'inverted'} type={isPinned ? 'default' : 'inverted'}
label={isPinned ? 'Unpin' : 'Pin to home'} label={isPinned ? _(msg`Unpin`) : _(msg`Pin to home`)}
onPress={onTogglePinned} onPress={onTogglePinned}
style={styles.btn} style={styles.btn}
/> />
@ -444,6 +454,7 @@ interface FeedSectionProps {
} }
const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>( const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
function FeedSectionImpl({feed, headerHeight, scrollElRef, isFocused}, ref) { function FeedSectionImpl({feed, headerHeight, scrollElRef, isFocused}, ref) {
const {_} = useLingui()
const [hasNew, setHasNew] = React.useState(false) const [hasNew, setHasNew] = React.useState(false)
const [isScrolledDown, setIsScrolledDown] = React.useState(false) const [isScrolledDown, setIsScrolledDown] = React.useState(false)
const queryClient = useQueryClient() const queryClient = useQueryClient()
@ -470,8 +481,8 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
}, [onScrollToTop, isScreenFocused]) }, [onScrollToTop, isScreenFocused])
const renderPostsEmpty = useCallback(() => { const renderPostsEmpty = useCallback(() => {
return <EmptyState icon="feed" message="This feed is empty!" /> return <EmptyState icon="feed" message={_(msg`This feed is empty!`)} />
}, []) }, [_])
return ( return (
<View> <View>
@ -488,7 +499,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
{(isScrolledDown || hasNew) && ( {(isScrolledDown || hasNew) && (
<LoadLatestBtn <LoadLatestBtn
onPress={onScrollToTop} onPress={onScrollToTop}
label="Load new posts" label={_(msg`Load new posts`)}
showIndicator={hasNew} showIndicator={hasNew}
/> />
)} )}
@ -542,11 +553,13 @@ function AboutSection({
} }
} catch (err) { } catch (err) {
Toast.show( Toast.show(
'There was an an issue contacting the server, please check your internet connection and try again.', _(
msg`There was an an issue contacting the server, please check your internet connection and try again.`,
),
) )
logger.error('Failed up toggle like', {error: err}) logger.error('Failed up toggle like', {error: err})
} }
}, [likeUri, isLiked, feedInfo, likeFeed, unlikeFeed, track]) }, [likeUri, isLiked, feedInfo, likeFeed, unlikeFeed, track, _])
return ( return (
<ScrollView <ScrollView
@ -597,24 +610,28 @@ function AboutSection({
{typeof likeCount === 'number' && ( {typeof likeCount === 'number' && (
<TextLink <TextLink
href={makeCustomFeedLink(feedOwnerDid, feedRkey, 'liked-by')} href={makeCustomFeedLink(feedOwnerDid, feedRkey, 'liked-by')}
text={`Liked by ${likeCount} ${pluralize(likeCount, 'user')}`} text={_(
msg`Liked by ${likeCount} ${pluralize(likeCount, 'user')}`,
)}
style={[pal.textLight, s.semiBold]} style={[pal.textLight, s.semiBold]}
/> />
)} )}
</View> </View>
<Text type="md" style={[pal.textLight]} numberOfLines={1}> <Text type="md" style={[pal.textLight]} numberOfLines={1}>
Created by{' '}
{isOwner ? ( {isOwner ? (
'you' <Trans>Created by you</Trans>
) : ( ) : (
<TextLink <Trans>
text={sanitizeHandle(feedInfo.creatorHandle, '@')} Created by{' '}
href={makeProfileLink({ <TextLink
did: feedInfo.creatorDid, text={sanitizeHandle(feedInfo.creatorHandle, '@')}
handle: feedInfo.creatorHandle, href={makeProfileLink({
})} did: feedInfo.creatorDid,
style={pal.textLight} handle: feedInfo.creatorHandle,
/> })}
style={pal.textLight}
/>
</Trans>
)} )}
</Text> </Text>
</View> </View>

View file

@ -68,6 +68,7 @@ interface SectionRef {
type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileList'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileList'>
export function ProfileListScreen(props: Props) { export function ProfileListScreen(props: Props) {
const {_} = useLingui()
const {name: handleOrDid, rkey} = props.route.params const {name: handleOrDid, rkey} = props.route.params
const {data: resolvedUri, error: resolveError} = useResolveUriQuery( const {data: resolvedUri, error: resolveError} = useResolveUriQuery(
AtUri.make(handleOrDid, 'app.bsky.graph.list', rkey).toString(), AtUri.make(handleOrDid, 'app.bsky.graph.list', rkey).toString(),
@ -78,7 +79,9 @@ export function ProfileListScreen(props: Props) {
return ( return (
<CenteredView> <CenteredView>
<ErrorScreen <ErrorScreen
error={`We're sorry, but we were unable to resolve this list. If this persists, please contact the list creator, @${handleOrDid}.`} error={_(
msg`We're sorry, but we were unable to resolve this list. If this persists, please contact the list creator, @${handleOrDid}.`,
)}
/> />
</CenteredView> </CenteredView>
) )
@ -260,10 +263,10 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
await pinFeed({uri: list.uri}) await pinFeed({uri: list.uri})
} }
} catch (e) { } catch (e) {
Toast.show('There was an issue contacting the server') Toast.show(_(msg`There was an issue contacting the server`))
logger.error('Failed to toggle pinned feed', {error: e}) logger.error('Failed to toggle pinned feed', {error: e})
} }
}, [list.uri, isPinned, pinFeed, unpinFeed]) }, [list.uri, isPinned, pinFeed, unpinFeed, _])
const onSubscribeMute = useCallback(() => { const onSubscribeMute = useCallback(() => {
openModal({ openModal({
@ -272,15 +275,17 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
message: _( message: _(
msg`Muting is private. Muted accounts can interact with you, but you will not see their posts or receive notifications from them.`, msg`Muting is private. Muted accounts can interact with you, but you will not see their posts or receive notifications from them.`,
), ),
confirmBtnText: 'Mute this List', confirmBtnText: _(msg`Mute this List`),
async onPressConfirm() { async onPressConfirm() {
try { try {
await listMuteMutation.mutateAsync({uri: list.uri, mute: true}) await listMuteMutation.mutateAsync({uri: list.uri, mute: true})
Toast.show('List muted') Toast.show(_(msg`List muted`))
track('Lists:Mute') track('Lists:Mute')
} catch { } catch {
Toast.show( Toast.show(
'There was an issue. Please check your internet connection and try again.', _(
msg`There was an issue. Please check your internet connection and try again.`,
),
) )
} }
}, },
@ -293,14 +298,16 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
const onUnsubscribeMute = useCallback(async () => { const onUnsubscribeMute = useCallback(async () => {
try { try {
await listMuteMutation.mutateAsync({uri: list.uri, mute: false}) await listMuteMutation.mutateAsync({uri: list.uri, mute: false})
Toast.show('List unmuted') Toast.show(_(msg`List unmuted`))
track('Lists:Unmute') track('Lists:Unmute')
} catch { } catch {
Toast.show( Toast.show(
'There was an issue. Please check your internet connection and try again.', _(
msg`There was an issue. Please check your internet connection and try again.`,
),
) )
} }
}, [list, listMuteMutation, track]) }, [list, listMuteMutation, track, _])
const onSubscribeBlock = useCallback(() => { const onSubscribeBlock = useCallback(() => {
openModal({ openModal({
@ -309,15 +316,17 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
message: _( message: _(
msg`Blocking is public. Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`, msg`Blocking is public. Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`,
), ),
confirmBtnText: 'Block this List', confirmBtnText: _(msg`Block this List`),
async onPressConfirm() { async onPressConfirm() {
try { try {
await listBlockMutation.mutateAsync({uri: list.uri, block: true}) await listBlockMutation.mutateAsync({uri: list.uri, block: true})
Toast.show('List blocked') Toast.show(_(msg`List blocked`))
track('Lists:Block') track('Lists:Block')
} catch { } catch {
Toast.show( Toast.show(
'There was an issue. Please check your internet connection and try again.', _(
msg`There was an issue. Please check your internet connection and try again.`,
),
) )
} }
}, },
@ -330,14 +339,16 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
const onUnsubscribeBlock = useCallback(async () => { const onUnsubscribeBlock = useCallback(async () => {
try { try {
await listBlockMutation.mutateAsync({uri: list.uri, block: false}) await listBlockMutation.mutateAsync({uri: list.uri, block: false})
Toast.show('List unblocked') Toast.show(_(msg`List unblocked`))
track('Lists:Unblock') track('Lists:Unblock')
} catch { } catch {
Toast.show( Toast.show(
'There was an issue. Please check your internet connection and try again.', _(
msg`There was an issue. Please check your internet connection and try again.`,
),
) )
} }
}, [list, listBlockMutation, track]) }, [list, listBlockMutation, track, _])
const onPressEdit = useCallback(() => { const onPressEdit = useCallback(() => {
openModal({ openModal({
@ -353,7 +364,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
message: _(msg`Are you sure?`), message: _(msg`Are you sure?`),
async onPressConfirm() { async onPressConfirm() {
await listDeleteMutation.mutateAsync({uri: list.uri}) await listDeleteMutation.mutateAsync({uri: list.uri})
Toast.show('List deleted') Toast.show(_(msg`List deleted`))
track('Lists:Delete') track('Lists:Delete')
if (navigation.canGoBack()) { if (navigation.canGoBack()) {
navigation.goBack() navigation.goBack()
@ -545,7 +556,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
<Button <Button
testID={isPinned ? 'unpinBtn' : 'pinBtn'} testID={isPinned ? 'unpinBtn' : 'pinBtn'}
type={isPinned ? 'default' : 'inverted'} type={isPinned ? 'default' : 'inverted'}
label={isPinned ? 'Unpin' : 'Pin to home'} label={isPinned ? _(msg`Unpin`) : _(msg`Pin to home`)}
onPress={onTogglePinned} onPress={onTogglePinned}
disabled={isPending} disabled={isPending}
/> />
@ -554,14 +565,14 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
<Button <Button
testID="unblockBtn" testID="unblockBtn"
type="default" type="default"
label="Unblock" label={_(msg`Unblock`)}
onPress={onUnsubscribeBlock} onPress={onUnsubscribeBlock}
/> />
) : isMuting ? ( ) : isMuting ? (
<Button <Button
testID="unmuteBtn" testID="unmuteBtn"
type="default" type="default"
label="Unmute" label={_(msg`Unmute`)}
onPress={onUnsubscribeMute} onPress={onUnsubscribeMute}
/> />
) : ( ) : (
@ -603,6 +614,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
const [hasNew, setHasNew] = React.useState(false) const [hasNew, setHasNew] = React.useState(false)
const [isScrolledDown, setIsScrolledDown] = React.useState(false) const [isScrolledDown, setIsScrolledDown] = React.useState(false)
const isScreenFocused = useIsFocused() const isScreenFocused = useIsFocused()
const {_} = useLingui()
const onScrollToTop = useCallback(() => { const onScrollToTop = useCallback(() => {
scrollElRef.current?.scrollToOffset({ scrollElRef.current?.scrollToOffset({
@ -624,8 +636,8 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
}, [onScrollToTop, isScreenFocused]) }, [onScrollToTop, isScreenFocused])
const renderPostsEmpty = useCallback(() => { const renderPostsEmpty = useCallback(() => {
return <EmptyState icon="feed" message="This feed is empty!" /> return <EmptyState icon="feed" message={_(msg`This feed is empty!`)} />
}, []) }, [_])
return ( return (
<View> <View>
@ -643,7 +655,7 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
{(isScrolledDown || hasNew) && ( {(isScrolledDown || hasNew) && (
<LoadLatestBtn <LoadLatestBtn
onPress={onScrollToTop} onPress={onScrollToTop}
label="Load new posts" label={_(msg`Load new posts`)}
showIndicator={hasNew} showIndicator={hasNew}
/> />
)} )}
@ -721,15 +733,30 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
</Text> </Text>
)} )}
<Text type="md" style={[pal.textLight]} numberOfLines={1}> <Text type="md" style={[pal.textLight]} numberOfLines={1}>
{isCurateList ? 'User list' : 'Moderation list'} by{' '} {isCurateList ? (
{isOwner ? ( isOwner ? (
'you' <Trans>User list by you</Trans>
) : (
<Trans>
User list by{' '}
<TextLink
text={sanitizeHandle(list.creator.handle || '', '@')}
href={makeProfileLink(list.creator)}
style={pal.textLight}
/>
</Trans>
)
) : isOwner ? (
<Trans>Moderation list by you</Trans>
) : ( ) : (
<TextLink <Trans>
text={sanitizeHandle(list.creator.handle || '', '@')} Moderation list by{' '}
href={makeProfileLink(list.creator)} <TextLink
style={pal.textLight} text={sanitizeHandle(list.creator.handle || '', '@')}
/> href={makeProfileLink(list.creator)}
style={pal.textLight}
/>
</Trans>
)} )}
</Text> </Text>
</View> </View>
@ -782,11 +809,11 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
return ( return (
<EmptyState <EmptyState
icon="users-slash" icon="users-slash"
message="This list is empty!" message={_(msg`This list is empty!`)}
style={{paddingTop: 40}} style={{paddingTop: 40}}
/> />
) )
}, []) }, [_])
return ( return (
<View> <View>
@ -802,7 +829,7 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
{isScrolledDown && ( {isScrolledDown && (
<LoadLatestBtn <LoadLatestBtn
onPress={onScrollToTop} onPress={onScrollToTop}
label="Scroll to top" label={_(msg`Scroll to top`)}
showIndicator={false} showIndicator={false}
/> />
)} )}
@ -846,7 +873,7 @@ function ErrorScreen({error}: {error: string}) {
<Button <Button
type="default" type="default"
accessibilityLabel={_(msg`Go Back`)} accessibilityLabel={_(msg`Go Back`)}
accessibilityHint="Return to previous page" accessibilityHint={_(msg`Return to previous page`)}
onPress={onPressBack} onPress={onPressBack}
style={{flexShrink: 1}}> style={{flexShrink: 1}}>
<Text type="button" style={pal.text}> <Text type="button" style={pal.text}>

View file

@ -160,7 +160,7 @@ export function SavedFeeds({}: Props) {
type="sm" type="sm"
style={pal.link} style={pal.link}
href="https://github.com/bluesky-social/feed-generator" href="https://github.com/bluesky-social/feed-generator"
text="See this guide" text={_(msg`See this guide`)}
/>{' '} />{' '}
for more information. for more information.
</Trans> </Trans>
@ -188,6 +188,7 @@ function ListItem({
>['reset'] >['reset']
}) { }) {
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui()
const {isPending: isPinPending, mutateAsync: pinFeed} = usePinFeedMutation() const {isPending: isPinPending, mutateAsync: pinFeed} = usePinFeedMutation()
const {isPending: isUnpinPending, mutateAsync: unpinFeed} = const {isPending: isUnpinPending, mutateAsync: unpinFeed} =
useUnpinFeedMutation() useUnpinFeedMutation()
@ -205,10 +206,10 @@ function ListItem({
await pinFeed({uri: feedUri}) await pinFeed({uri: feedUri})
} }
} catch (e) { } catch (e) {
Toast.show('There was an issue contacting the server') Toast.show(_(msg`There was an issue contacting the server`))
logger.error('Failed to toggle pinned feed', {error: e}) logger.error('Failed to toggle pinned feed', {error: e})
} }
}, [feedUri, isPinned, pinFeed, unpinFeed, resetSaveFeedsMutationState]) }, [feedUri, isPinned, pinFeed, unpinFeed, resetSaveFeedsMutationState, _])
const onPressUp = React.useCallback(async () => { const onPressUp = React.useCallback(async () => {
if (!isPinned) return if (!isPinned) return
@ -227,10 +228,10 @@ function ListItem({
index: pinned.indexOf(feedUri), index: pinned.indexOf(feedUri),
}) })
} catch (e) { } catch (e) {
Toast.show('There was an issue contacting the server') Toast.show(_(msg`There was an issue contacting the server`))
logger.error('Failed to set pinned feed order', {error: e}) logger.error('Failed to set pinned feed order', {error: e})
} }
}, [feedUri, isPinned, setSavedFeeds, currentFeeds]) }, [feedUri, isPinned, setSavedFeeds, currentFeeds, _])
const onPressDown = React.useCallback(async () => { const onPressDown = React.useCallback(async () => {
if (!isPinned) return if (!isPinned) return
@ -248,10 +249,10 @@ function ListItem({
index: pinned.indexOf(feedUri), index: pinned.indexOf(feedUri),
}) })
} catch (e) { } catch (e) {
Toast.show('There was an issue contacting the server') Toast.show(_(msg`There was an issue contacting the server`))
logger.error('Failed to set pinned feed order', {error: e}) logger.error('Failed to set pinned feed order', {error: e})
} }
}, [feedUri, isPinned, setSavedFeeds, currentFeeds]) }, [feedUri, isPinned, setSavedFeeds, currentFeeds, _])
return ( return (
<Pressable <Pressable

Some files were not shown because too many files have changed in this diff Show more