Internationalization & localization (#1822)

* install and setup lingui

* setup dynamic locale activation and async loading

* first pass of automated replacement of text messages

* add some more documentaton

* fix nits

* add `es` and `hi`locales for testing purposes

* make accessibilityLabel localized

* compile and extract new messages

* fix merge conflicts

* fix eslint warning

* change instructions from sending email to opening PR

* fix comments
This commit is contained in:
Ansh 2023-11-09 10:04:16 -08:00 committed by GitHub
parent 82059b7ee1
commit 4c7850f8c4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
108 changed files with 10334 additions and 1365 deletions

View file

@ -5,6 +5,8 @@ import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
import {s, colors} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
import {CenteredView} from '../util/Views'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
export const SplashScreen = ({
onPressSignin,
@ -14,14 +16,18 @@ export const SplashScreen = ({
onPressCreateAccount: () => void
}) => {
const pal = usePalette('default')
const {_} = useLingui()
return (
<CenteredView style={[styles.container, pal.view]}>
<SafeAreaView testID="noSessionView" style={styles.container}>
<ErrorBoundary>
<View style={styles.hero}>
<Text style={[styles.title, pal.link]}>Bluesky</Text>
<Text style={[styles.title, pal.link]}>
<Trans>Bluesky</Trans>
</Text>
<Text style={[styles.subtitle, pal.textLight]}>
See what's next
<Trans>See what's next</Trans>
</Text>
</View>
<View testID="signinOrCreateAccount" style={styles.btns}>
@ -30,10 +36,10 @@ export const SplashScreen = ({
style={[styles.btn, {backgroundColor: colors.blue3}]}
onPress={onPressCreateAccount}
accessibilityRole="button"
accessibilityLabel="Create new account"
accessibilityLabel={_(msg`Create new account`)}
accessibilityHint="Opens flow to create a new Bluesky account">
<Text style={[s.white, styles.btnLabel]}>
Create a new account
<Trans>Create a new account</Trans>
</Text>
</TouchableOpacity>
<TouchableOpacity
@ -41,9 +47,11 @@ export const SplashScreen = ({
style={[styles.btn, pal.btn]}
onPress={onPressSignin}
accessibilityRole="button"
accessibilityLabel="Sign in"
accessibilityLabel={_(msg`Sign in`)}
accessibilityHint="Opens flow to sign into your existing Bluesky account">
<Text style={[pal.text, styles.btnLabel]}>Sign In</Text>
<Text style={[pal.text, styles.btnLabel]}>
<Trans>Sign In</Trans>
</Text>
</TouchableOpacity>
</View>
</ErrorBoundary>

View file

@ -8,6 +8,7 @@ import {usePalette} from 'lib/hooks/usePalette'
import {CenteredView} from '../util/Views'
import {isWeb} from 'platform/detection'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {Trans} from '@lingui/macro'
export const SplashScreen = ({
onPressSignin,
@ -54,7 +55,9 @@ export const SplashScreen = ({
onPress={onPressSignin}
// TODO: web accessibility
accessibilityRole="button">
<Text style={[pal.text, styles.btnLabel]}>Sign In</Text>
<Text style={[pal.text, styles.btnLabel]}>
<Trans>Sign In</Trans>
</Text>
</TouchableOpacity>
</View>
</ErrorBoundary>

View file

@ -15,6 +15,8 @@ import {s} from 'lib/styles'
import {useStores} from 'state/index'
import {CreateAccountModel} from 'state/models/ui/create-account'
import {usePalette} from 'lib/hooks/usePalette'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useOnboardingDispatch} from '#/state/shell'
import {Step1} from './Step1'
@ -30,6 +32,7 @@ export const CreateAccount = observer(function CreateAccountImpl({
const pal = usePalette('default')
const store = useStores()
const model = React.useMemo(() => new CreateAccountModel(store), [store])
const {_} = useLingui()
const onboardingDispatch = useOnboardingDispatch()
React.useEffect(() => {
@ -73,8 +76,8 @@ export const CreateAccount = observer(function CreateAccountImpl({
return (
<LoggedOutLayout
leadin={`Step ${model.step}`}
title="Create Account"
description="We're so excited to have you join us!">
title={_(msg`Create Account`)}
description={_(msg`We're so excited to have you join us!`)}>
<ScrollView testID="createAccount" style={pal.view}>
<KeyboardAvoidingView behavior="padding">
<View style={styles.stepContainer}>
@ -88,7 +91,7 @@ export const CreateAccount = observer(function CreateAccountImpl({
testID="backBtn"
accessibilityRole="button">
<Text type="xl" style={pal.link}>
Back
<Trans>Back</Trans>
</Text>
</TouchableOpacity>
<View style={s.flex1} />
@ -101,7 +104,7 @@ export const CreateAccount = observer(function CreateAccountImpl({
<ActivityIndicator />
) : (
<Text type="xl-bold" style={[pal.link, s.pr5]}>
Next
<Trans>Next</Trans>
</Text>
)}
</TouchableOpacity>
@ -110,18 +113,18 @@ export const CreateAccount = observer(function CreateAccountImpl({
testID="retryConnectBtn"
onPress={onPressRetryConnect}
accessibilityRole="button"
accessibilityLabel="Retry"
accessibilityLabel={_(msg`Retry`)}
accessibilityHint="Retries account creation"
accessibilityLiveRegion="polite">
<Text type="xl-bold" style={[pal.link, s.pr5]}>
Retry
<Trans>Retry</Trans>
</Text>
</TouchableOpacity>
) : model.isFetchingServiceDescription ? (
<>
<ActivityIndicator color="#fff" />
<Text type="xl" style={[pal.text, s.pr5]}>
Connecting...
<Trans>Connecting...</Trans>
</Text>
</>
) : undefined}

View file

@ -12,6 +12,8 @@ import {HelpTip} from '../util/HelpTip'
import {TextInput} from '../util/TextInput'
import {Button} from 'view/com/util/forms/Button'
import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {LOCAL_DEV_SERVICE, STAGING_SERVICE, PROD_SERVICE} from 'state/index'
import {LOGIN_INCLUDE_DEV_SERVERS} from 'lib/build-flags'
@ -27,6 +29,7 @@ export const Step1 = observer(function Step1Impl({
}) {
const pal = usePalette('default')
const [isDefaultSelected, setIsDefaultSelected] = React.useState(true)
const {_} = useLingui()
const onPressDefault = React.useCallback(() => {
setIsDefaultSelected(true)
@ -63,9 +66,9 @@ export const Step1 = observer(function Step1Impl({
return (
<View>
<StepHeader step="1" title="Your hosting provider" />
<StepHeader step="1" title={_(msg`Your hosting provider`)} />
<Text style={[pal.text, s.mb10]}>
This is the service that keeps you online.
<Trans>This is the service that keeps you online.</Trans>
</Text>
<Option
testID="blueskyServerBtn"
@ -81,17 +84,17 @@ export const Step1 = observer(function Step1Impl({
onPress={onPressOther}>
<View style={styles.otherForm}>
<Text nativeID="addressProvider" style={[pal.text, s.mb5]}>
Enter the address of your provider:
<Trans>Enter the address of your provider:</Trans>
</Text>
<TextInput
testID="customServerInput"
icon="globe"
placeholder="Hosting provider address"
placeholder={_(msg`Hosting provider address`)}
value={model.serviceUrl}
editable
onChange={onChangeServiceUrl}
accessibilityHint="Input hosting provider address"
accessibilityLabel="Hosting provider address"
accessibilityLabel={_(msg`Hosting provider address`)}
accessibilityLabelledBy="addressProvider"
/>
{LOGIN_INCLUDE_DEV_SERVERS && (
@ -100,13 +103,13 @@ export const Step1 = observer(function Step1Impl({
testID="stagingServerBtn"
type="default"
style={s.mr5}
label="Staging"
label={_(msg`Staging`)}
onPress={() => onDebugChangeServiceUrl(STAGING_SERVICE)}
/>
<Button
testID="localDevServerBtn"
type="default"
label="Dev Server"
label={_(msg`Dev Server`)}
onPress={() => onDebugChangeServiceUrl(LOCAL_DEV_SERVICE)}
/>
</View>
@ -116,7 +119,7 @@ export const Step1 = observer(function Step1Impl({
{model.error ? (
<ErrorMessage message={model.error} style={styles.error} />
) : (
<HelpTip text="You can change hosting providers at any time." />
<HelpTip text={_(msg`You can change hosting providers at any time.`)} />
)}
</View>
)

View file

@ -11,6 +11,8 @@ import {TextInput} from '../util/TextInput'
import {Policies} from './Policies'
import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
import {isWeb} from 'platform/detection'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
/** STEP 2: Your account
@ -28,6 +30,7 @@ export const Step2 = observer(function Step2Impl({
model: CreateAccountModel
}) {
const pal = usePalette('default')
const {_} = useLingui()
const {openModal} = useModalControls()
const onPressWaitlist = React.useCallback(() => {
@ -36,7 +39,7 @@ export const Step2 = observer(function Step2Impl({
return (
<View>
<StepHeader step="2" title="Your account" />
<StepHeader step="2" title={_(msg`Your account`)} />
{model.isInviteCodeRequired && (
<View style={s.pb20}>
@ -46,11 +49,11 @@ export const Step2 = observer(function Step2Impl({
<TextInput
testID="inviteCodeInput"
icon="ticket"
placeholder="Required for this provider"
placeholder={_(msg`Required for this provider`)}
value={model.inviteCode}
editable
onChange={model.setInviteCode}
accessibilityLabel="Invite code"
accessibilityLabel={_(msg`Invite code`)}
accessibilityHint="Input invite code to proceed"
/>
</View>
@ -61,10 +64,12 @@ export const Step2 = observer(function Step2Impl({
Don't have an invite code?{' '}
<TouchableWithoutFeedback
onPress={onPressWaitlist}
accessibilityLabel="Join the waitlist."
accessibilityLabel={_(msg`Join the waitlist.`)}
accessibilityHint="">
<View style={styles.touchable}>
<Text style={pal.link}>Join the waitlist.</Text>
<Text style={pal.link}>
<Trans>Join the waitlist.</Trans>
</Text>
</View>
</TouchableWithoutFeedback>
</Text>
@ -72,16 +77,16 @@ export const Step2 = observer(function Step2Impl({
<>
<View style={s.pb20}>
<Text type="md-medium" style={[pal.text, s.mb2]} nativeID="email">
Email address
<Trans>Email address</Trans>
</Text>
<TextInput
testID="emailInput"
icon="envelope"
placeholder="Enter your email address"
placeholder={_(msg`Enter your email address`)}
value={model.email}
editable
onChange={model.setEmail}
accessibilityLabel="Email"
accessibilityLabel={_(msg`Email`)}
accessibilityHint="Input email for Bluesky waitlist"
accessibilityLabelledBy="email"
/>
@ -92,17 +97,17 @@ export const Step2 = observer(function Step2Impl({
type="md-medium"
style={[pal.text, s.mb2]}
nativeID="password">
Password
<Trans>Password</Trans>
</Text>
<TextInput
testID="passwordInput"
icon="lock"
placeholder="Choose your password"
placeholder={_(msg`Choose your password`)}
value={model.password}
editable
secureTextEntry
onChange={model.setPassword}
accessibilityLabel="Password"
accessibilityLabel={_(msg`Password`)}
accessibilityHint="Set password"
accessibilityLabelledBy="password"
/>
@ -113,7 +118,7 @@ export const Step2 = observer(function Step2Impl({
type="md-medium"
style={[pal.text, s.mb2]}
nativeID="birthDate">
Your birth date
<Trans>Your birth date</Trans>
</Text>
<DateInput
testID="birthdayInput"
@ -122,7 +127,7 @@ export const Step2 = observer(function Step2Impl({
buttonType="default-light"
buttonStyle={[pal.border, styles.dateInputButton]}
buttonLabelType="lg"
accessibilityLabel="Birthday"
accessibilityLabel={_(msg`Birthday`)}
accessibilityHint="Enter your birth date"
accessibilityLabelledBy="birthDate"
/>

View file

@ -9,6 +9,8 @@ import {TextInput} from '../util/TextInput'
import {createFullHandle} from 'lib/strings/handles'
import {usePalette} from 'lib/hooks/usePalette'
import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
/** STEP 3: Your user handle
* @field User handle
@ -19,9 +21,10 @@ export const Step3 = observer(function Step3Impl({
model: CreateAccountModel
}) {
const pal = usePalette('default')
const {_} = useLingui()
return (
<View>
<StepHeader step="3" title="Your user handle" />
<StepHeader step="3" title={_(msg`Your user handle`)} />
<View style={s.pb10}>
<TextInput
testID="handleInput"
@ -31,12 +34,12 @@ export const Step3 = observer(function Step3Impl({
editable
onChange={model.setHandle}
// TODO: Add explicit text label
accessibilityLabel="User handle"
accessibilityLabel={_(msg`User handle`)}
accessibilityHint="Input your user handle"
/>
<Text type="lg" style={[pal.text, s.pl5, s.pt10]}>
Your full handle will be{' '}
<Text type="lg-bold" style={pal.text}>
<Trans>Your full handle will be</Trans>
<Text type="lg-bold" style={[pal.text, s.ml5]}>
@{createFullHandle(model.handle, model.userDomain)}
</Text>
</Text>

View file

@ -0,0 +1,119 @@
import React from 'react'
import {
ActivityIndicator,
ScrollView,
TouchableOpacity,
View,
} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {useAnalytics} from 'lib/analytics/analytics'
import {Text} from '../../util/text/Text'
import {UserAvatar} from '../../util/UserAvatar'
import {s} from 'lib/styles'
import {RootStoreModel} from 'state/index'
import {AccountData} from 'state/models/session'
import {usePalette} from 'lib/hooks/usePalette'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {styles} from './styles'
export const ChooseAccountForm = ({
store,
onSelectAccount,
onPressBack,
}: {
store: RootStoreModel
onSelectAccount: (account?: AccountData) => void
onPressBack: () => void
}) => {
const {track, screen} = useAnalytics()
const pal = usePalette('default')
const [isProcessing, setIsProcessing] = React.useState(false)
const {_} = useLingui()
React.useEffect(() => {
screen('Choose Account')
}, [screen])
const onTryAccount = async (account: AccountData) => {
if (account.accessJwt && account.refreshJwt) {
setIsProcessing(true)
if (await store.session.resumeSession(account)) {
track('Sign In', {resumedSession: true})
setIsProcessing(false)
return
}
setIsProcessing(false)
}
onSelectAccount(account)
}
return (
<ScrollView testID="chooseAccountForm" style={styles.maxHeight}>
<Text
type="2xl-medium"
style={[pal.text, styles.groupLabel, s.mt5, s.mb10]}>
<Trans>Sign in as...</Trans>
</Text>
{store.session.accounts.map(account => (
<TouchableOpacity
testID={`chooseAccountBtn-${account.handle}`}
key={account.did}
style={[pal.view, pal.border, styles.account]}
onPress={() => onTryAccount(account)}
accessibilityRole="button"
accessibilityLabel={_(msg`Sign in as ${account.handle}`)}
accessibilityHint="Double tap to sign in">
<View
style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
<View style={s.p10}>
<UserAvatar avatar={account.aviUrl} size={30} />
</View>
<Text style={styles.accountText}>
<Text type="lg-bold" style={pal.text}>
{account.displayName || account.handle}{' '}
</Text>
<Text type="lg" style={[pal.textLight]}>
{account.handle}
</Text>
</Text>
<FontAwesomeIcon
icon="angle-right"
size={16}
style={[pal.text, s.mr10]}
/>
</View>
</TouchableOpacity>
))}
<TouchableOpacity
testID="chooseNewAccountBtn"
style={[pal.view, pal.border, styles.account, styles.accountLast]}
onPress={() => onSelectAccount(undefined)}
accessibilityRole="button"
accessibilityLabel={_(msg`Login to account that is not listed`)}
accessibilityHint="">
<View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
<Text style={[styles.accountText, styles.accountTextOther]}>
<Text type="lg" style={pal.text}>
<Trans>Other account</Trans>
</Text>
</Text>
<FontAwesomeIcon
icon="angle-right"
size={16}
style={[pal.text, s.mr10]}
/>
</View>
</TouchableOpacity>
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
<TouchableOpacity onPress={onPressBack} accessibilityRole="button">
<Text type="xl" style={[pal.link, s.pl5]}>
<Trans>Back</Trans>
</Text>
</TouchableOpacity>
<View style={s.flex1} />
{isProcessing && <ActivityIndicator />}
</View>
</ScrollView>
)
}

View file

@ -0,0 +1,197 @@
import React, {useState, useEffect} from 'react'
import {
ActivityIndicator,
TextInput,
TouchableOpacity,
View,
} from 'react-native'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import * as EmailValidator from 'email-validator'
import {BskyAgent} from '@atproto/api'
import {useAnalytics} from 'lib/analytics/analytics'
import {Text} from '../../util/text/Text'
import {s} from 'lib/styles'
import {toNiceDomain} from 'lib/strings/url-helpers'
import {RootStoreModel} from 'state/index'
import {ServiceDescription} from 'state/models/session'
import {isNetworkError} from 'lib/strings/errors'
import {usePalette} from 'lib/hooks/usePalette'
import {useTheme} from 'lib/ThemeContext'
import {cleanError} from 'lib/strings/errors'
import {logger} from '#/logger'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {styles} from './styles'
import {useModalControls} from '#/state/modals'
export const ForgotPasswordForm = ({
error,
serviceUrl,
serviceDescription,
setError,
setServiceUrl,
onPressBack,
onEmailSent,
}: {
store: RootStoreModel
error: string
serviceUrl: string
serviceDescription: ServiceDescription | undefined
setError: (v: string) => void
setServiceUrl: (v: string) => void
onPressBack: () => void
onEmailSent: () => void
}) => {
const pal = usePalette('default')
const theme = useTheme()
const [isProcessing, setIsProcessing] = useState<boolean>(false)
const [email, setEmail] = useState<string>('')
const {screen} = useAnalytics()
const {_} = useLingui()
const {openModal} = useModalControls()
useEffect(() => {
screen('Signin:ForgotPassword')
}, [screen])
const onPressSelectService = () => {
openModal({
name: 'server-input',
initialService: serviceUrl,
onSelect: setServiceUrl,
})
}
const onPressNext = async () => {
if (!EmailValidator.validate(email)) {
return setError('Your email appears to be invalid.')
}
setError('')
setIsProcessing(true)
try {
const agent = new BskyAgent({service: serviceUrl})
await agent.com.atproto.server.requestPasswordReset({email})
onEmailSent()
} catch (e: any) {
const errMsg = e.toString()
logger.warn('Failed to request password reset', {error: e})
setIsProcessing(false)
if (isNetworkError(e)) {
setError(
'Unable to contact your service. Please check your Internet connection.',
)
} else {
setError(cleanError(errMsg))
}
}
}
return (
<>
<View>
<Text type="title-lg" style={[pal.text, styles.screenTitle]}>
<Trans>Reset password</Trans>
</Text>
<Text type="md" style={[pal.text, styles.instructions]}>
<Trans>
Enter the email you used to create your account. We'll send you a
"reset code" so you can set a new password.
</Trans>
</Text>
<View
testID="forgotPasswordView"
style={[pal.borderDark, pal.view, styles.group]}>
<TouchableOpacity
testID="forgotPasswordSelectServiceButton"
style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}
onPress={onPressSelectService}
accessibilityRole="button"
accessibilityLabel={_(msg`Hosting provider`)}
accessibilityHint="Sets hosting provider for password reset">
<FontAwesomeIcon
icon="globe"
style={[pal.textLight, styles.groupContentIcon]}
/>
<Text style={[pal.text, styles.textInput]} numberOfLines={1}>
{toNiceDomain(serviceUrl)}
</Text>
<View style={[pal.btn, styles.textBtnFakeInnerBtn]}>
<FontAwesomeIcon
icon="pen"
size={12}
style={pal.text as FontAwesomeIconStyle}
/>
</View>
</TouchableOpacity>
<View style={[pal.borderDark, styles.groupContent]}>
<FontAwesomeIcon
icon="envelope"
style={[pal.textLight, styles.groupContentIcon]}
/>
<TextInput
testID="forgotPasswordEmail"
style={[pal.text, styles.textInput]}
placeholder="Email address"
placeholderTextColor={pal.colors.textLight}
autoCapitalize="none"
autoFocus
autoCorrect={false}
keyboardAppearance={theme.colorScheme}
value={email}
onChangeText={setEmail}
editable={!isProcessing}
accessibilityLabel={_(msg`Email`)}
accessibilityHint="Sets email for password reset"
/>
</View>
</View>
{error ? (
<View style={styles.error}>
<View style={styles.errorIcon}>
<FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
</View>
<View style={s.flex1}>
<Text style={[s.white, s.bold]}>{error}</Text>
</View>
</View>
) : undefined}
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
<TouchableOpacity onPress={onPressBack} accessibilityRole="button">
<Text type="xl" style={[pal.link, s.pl5]}>
<Trans>Back</Trans>
</Text>
</TouchableOpacity>
<View style={s.flex1} />
{!serviceDescription || isProcessing ? (
<ActivityIndicator />
) : !email ? (
<Text type="xl-bold" style={[pal.link, s.pr5, styles.dimmed]}>
<Trans>Next</Trans>
</Text>
) : (
<TouchableOpacity
testID="newPasswordButton"
onPress={onPressNext}
accessibilityRole="button"
accessibilityLabel={_(msg`Go to next`)}
accessibilityHint="Navigates to the next screen">
<Text type="xl-bold" style={[pal.link, s.pr5]}>
<Trans>Next</Trans>
</Text>
</TouchableOpacity>
)}
{!serviceDescription || isProcessing ? (
<Text type="xl" style={[pal.textLight, s.pl10]}>
<Trans>Processing...</Trans>
</Text>
) : undefined}
</View>
</View>
</>
)
}

View file

@ -1,37 +1,19 @@
import React, {useState, useEffect, useRef} from 'react'
import {
ActivityIndicator,
Keyboard,
KeyboardAvoidingView,
ScrollView,
StyleSheet,
TextInput,
TouchableOpacity,
View,
} from 'react-native'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import * as EmailValidator from 'email-validator'
import {BskyAgent} from '@atproto/api'
import React, {useState, useEffect} from 'react'
import {KeyboardAvoidingView} from 'react-native'
import {useAnalytics} from 'lib/analytics/analytics'
import {Text} from '../../util/text/Text'
import {UserAvatar} from '../../util/UserAvatar'
import {LoggedOutLayout} from 'view/com/util/layouts/LoggedOutLayout'
import {s, colors} from 'lib/styles'
import {createFullHandle} from 'lib/strings/handles'
import {toNiceDomain} from 'lib/strings/url-helpers'
import {useStores, RootStoreModel, DEFAULT_SERVICE} from 'state/index'
import {useStores, DEFAULT_SERVICE} from 'state/index'
import {ServiceDescription} from 'state/models/session'
import {AccountData} from 'state/models/session'
import {isNetworkError} from 'lib/strings/errors'
import {usePalette} from 'lib/hooks/usePalette'
import {useTheme} from 'lib/ThemeContext'
import {cleanError} from 'lib/strings/errors'
import {isWeb} from 'platform/detection'
import {logger} from '#/logger'
import {useModalControls} from '#/state/modals'
import {ChooseAccountForm} from './ChooseAccountForm'
import {LoginForm} from './LoginForm'
import {ForgotPasswordForm} from './ForgotPasswordForm'
import {SetNewPasswordForm} from './SetNewPasswordForm'
import {PasswordUpdatedForm} from './PasswordUpdatedForm'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
enum Forms {
Login,
@ -45,6 +27,7 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
const pal = usePalette('default')
const store = useStores()
const {track} = useAnalytics()
const {_} = useLingui()
const [error, setError] = useState<string>('')
const [retryDescribeTrigger, setRetryDescribeTrigger] = useState<any>({})
const [serviceUrl, setServiceUrl] = useState<string>(DEFAULT_SERVICE)
@ -87,14 +70,16 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
error: err,
})
setError(
'Unable to contact your service. Please check your Internet connection.',
_(
msg`Unable to contact your service. Please check your Internet connection.`,
),
)
},
)
return () => {
aborted = true
}
}, [store.session, serviceUrl, retryDescribeTrigger])
}, [store.session, serviceUrl, retryDescribeTrigger, _])
const onPressRetryConnect = () => setRetryDescribeTrigger({})
const onPressForgotPassword = () => {
@ -107,8 +92,8 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
{currentForm === Forms.Login ? (
<LoggedOutLayout
leadin=""
title="Sign in"
description="Enter your username and password">
title={_(msg`Sign in`)}
description={_(msg`Enter your username and password`)}>
<LoginForm
store={store}
error={error}
@ -126,8 +111,8 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
{currentForm === Forms.ChooseAccount ? (
<LoggedOutLayout
leadin=""
title="Sign in as..."
description="Select from an existing account">
title={_(msg`Sign in as...`)}
description={_(msg`Select from an existing account`)}>
<ChooseAccountForm
store={store}
onSelectAccount={onSelectAccount}
@ -138,8 +123,8 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
{currentForm === Forms.ForgotPassword ? (
<LoggedOutLayout
leadin=""
title="Forgot Password"
description="Let's get your password reset!">
title={_(msg`Forgot Password`)}
description={_(msg`Let's get your password reset!`)}>
<ForgotPasswordForm
store={store}
error={error}
@ -155,8 +140,8 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
{currentForm === Forms.SetNewPassword ? (
<LoggedOutLayout
leadin=""
title="Forgot Password"
description="Let's get your password reset!">
title={_(msg`Forgot Password`)}
description={_(msg`Let's get your password reset!`)}>
<SetNewPasswordForm
store={store}
error={error}
@ -173,830 +158,3 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
</KeyboardAvoidingView>
)
}
const ChooseAccountForm = ({
store,
onSelectAccount,
onPressBack,
}: {
store: RootStoreModel
onSelectAccount: (account?: AccountData) => void
onPressBack: () => void
}) => {
const {track, screen} = useAnalytics()
const pal = usePalette('default')
const [isProcessing, setIsProcessing] = React.useState(false)
React.useEffect(() => {
screen('Choose Account')
}, [screen])
const onTryAccount = async (account: AccountData) => {
if (account.accessJwt && account.refreshJwt) {
setIsProcessing(true)
if (await store.session.resumeSession(account)) {
track('Sign In', {resumedSession: true})
setIsProcessing(false)
return
}
setIsProcessing(false)
}
onSelectAccount(account)
}
return (
<ScrollView testID="chooseAccountForm" style={styles.maxHeight}>
<Text
type="2xl-medium"
style={[pal.text, styles.groupLabel, s.mt5, s.mb10]}>
Sign in as...
</Text>
{store.session.accounts.map(account => (
<TouchableOpacity
testID={`chooseAccountBtn-${account.handle}`}
key={account.did}
style={[pal.view, pal.border, styles.account]}
onPress={() => onTryAccount(account)}
accessibilityRole="button"
accessibilityLabel={`Sign in as ${account.handle}`}
accessibilityHint="Double tap to sign in">
<View
style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
<View style={s.p10}>
<UserAvatar avatar={account.aviUrl} size={30} />
</View>
<Text style={styles.accountText}>
<Text type="lg-bold" style={pal.text}>
{account.displayName || account.handle}{' '}
</Text>
<Text type="lg" style={[pal.textLight]}>
{account.handle}
</Text>
</Text>
<FontAwesomeIcon
icon="angle-right"
size={16}
style={[pal.text, s.mr10]}
/>
</View>
</TouchableOpacity>
))}
<TouchableOpacity
testID="chooseNewAccountBtn"
style={[pal.view, pal.border, styles.account, styles.accountLast]}
onPress={() => onSelectAccount(undefined)}
accessibilityRole="button"
accessibilityLabel="Login to account that is not listed"
accessibilityHint="">
<View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
<Text style={[styles.accountText, styles.accountTextOther]}>
<Text type="lg" style={pal.text}>
Other account
</Text>
</Text>
<FontAwesomeIcon
icon="angle-right"
size={16}
style={[pal.text, s.mr10]}
/>
</View>
</TouchableOpacity>
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
<TouchableOpacity onPress={onPressBack} accessibilityRole="button">
<Text type="xl" style={[pal.link, s.pl5]}>
Back
</Text>
</TouchableOpacity>
<View style={s.flex1} />
{isProcessing && <ActivityIndicator />}
</View>
</ScrollView>
)
}
const LoginForm = ({
store,
error,
serviceUrl,
serviceDescription,
initialHandle,
setError,
setServiceUrl,
onPressRetryConnect,
onPressBack,
onPressForgotPassword,
}: {
store: RootStoreModel
error: string
serviceUrl: string
serviceDescription: ServiceDescription | undefined
initialHandle: string
setError: (v: string) => void
setServiceUrl: (v: string) => void
onPressRetryConnect: () => void
onPressBack: () => void
onPressForgotPassword: () => void
}) => {
const {track} = useAnalytics()
const pal = usePalette('default')
const theme = useTheme()
const [isProcessing, setIsProcessing] = useState<boolean>(false)
const [identifier, setIdentifier] = useState<string>(initialHandle)
const [password, setPassword] = useState<string>('')
const passwordInputRef = useRef<TextInput>(null)
const {openModal} = useModalControls()
const onPressSelectService = () => {
openModal({
name: 'server-input',
initialService: serviceUrl,
onSelect: setServiceUrl,
})
Keyboard.dismiss()
track('Signin:PressedSelectService')
}
const onPressNext = async () => {
Keyboard.dismiss()
setError('')
setIsProcessing(true)
try {
// try to guess the handle if the user just gave their own username
let fullIdent = identifier
if (
!identifier.includes('@') && // not an email
!identifier.includes('.') && // not a domain
serviceDescription &&
serviceDescription.availableUserDomains.length > 0
) {
let matched = false
for (const domain of serviceDescription.availableUserDomains) {
if (fullIdent.endsWith(domain)) {
matched = true
}
}
if (!matched) {
fullIdent = createFullHandle(
identifier,
serviceDescription.availableUserDomains[0],
)
}
}
await store.session.login({
service: serviceUrl,
identifier: fullIdent,
password,
})
} catch (e: any) {
const errMsg = e.toString()
logger.warn('Failed to login', {error: e})
setIsProcessing(false)
if (errMsg.includes('Authentication Required')) {
setError('Invalid username or password')
} else if (isNetworkError(e)) {
setError(
'Unable to contact your service. Please check your Internet connection.',
)
} else {
setError(cleanError(errMsg))
}
} finally {
track('Sign In', {resumedSession: false})
}
}
const isReady = !!serviceDescription && !!identifier && !!password
return (
<View testID="loginForm">
<Text type="sm-bold" style={[pal.text, styles.groupLabel]}>
Sign into
</Text>
<View style={[pal.borderDark, styles.group]}>
<View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
<FontAwesomeIcon
icon="globe"
style={[pal.textLight, styles.groupContentIcon]}
/>
<TouchableOpacity
testID="loginSelectServiceButton"
style={styles.textBtn}
onPress={onPressSelectService}
accessibilityRole="button"
accessibilityLabel="Select service"
accessibilityHint="Sets server for the Bluesky client">
<Text type="xl" style={[pal.text, styles.textBtnLabel]}>
{toNiceDomain(serviceUrl)}
</Text>
<View style={[pal.btn, styles.textBtnFakeInnerBtn]}>
<FontAwesomeIcon
icon="pen"
size={12}
style={pal.textLight as FontAwesomeIconStyle}
/>
</View>
</TouchableOpacity>
</View>
</View>
<Text type="sm-bold" style={[pal.text, styles.groupLabel]}>
Account
</Text>
<View style={[pal.borderDark, styles.group]}>
<View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
<FontAwesomeIcon
icon="at"
style={[pal.textLight, styles.groupContentIcon]}
/>
<TextInput
testID="loginUsernameInput"
style={[pal.text, styles.textInput]}
placeholder="Username or email address"
placeholderTextColor={pal.colors.textLight}
autoCapitalize="none"
autoFocus
autoCorrect={false}
autoComplete="username"
returnKeyType="next"
onSubmitEditing={() => {
passwordInputRef.current?.focus()
}}
blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field
keyboardAppearance={theme.colorScheme}
value={identifier}
onChangeText={str =>
setIdentifier((str || '').toLowerCase().trim())
}
editable={!isProcessing}
accessibilityLabel="Username or email address"
accessibilityHint="Input the username or email address you used at signup"
/>
</View>
<View style={[pal.borderDark, styles.groupContent]}>
<FontAwesomeIcon
icon="lock"
style={[pal.textLight, styles.groupContentIcon]}
/>
<TextInput
testID="loginPasswordInput"
ref={passwordInputRef}
style={[pal.text, styles.textInput]}
placeholder="Password"
placeholderTextColor={pal.colors.textLight}
autoCapitalize="none"
autoCorrect={false}
autoComplete="password"
returnKeyType="done"
enablesReturnKeyAutomatically={true}
keyboardAppearance={theme.colorScheme}
secureTextEntry={true}
textContentType="password"
clearButtonMode="while-editing"
value={password}
onChangeText={setPassword}
onSubmitEditing={onPressNext}
blurOnSubmit={false} // HACK: https://github.com/facebook/react-native/issues/21911#issuecomment-558343069 Keyboard blur behavior is now handled in onSubmitEditing
editable={!isProcessing}
accessibilityLabel="Password"
accessibilityHint={
identifier === ''
? 'Input your password'
: `Input the password tied to ${identifier}`
}
/>
<TouchableOpacity
testID="forgotPasswordButton"
style={styles.textInputInnerBtn}
onPress={onPressForgotPassword}
accessibilityRole="button"
accessibilityLabel="Forgot password"
accessibilityHint="Opens password reset form">
<Text style={pal.link}>Forgot</Text>
</TouchableOpacity>
</View>
</View>
{error ? (
<View style={styles.error}>
<View style={styles.errorIcon}>
<FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
</View>
<View style={s.flex1}>
<Text style={[s.white, s.bold]}>{error}</Text>
</View>
</View>
) : undefined}
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
<TouchableOpacity onPress={onPressBack} accessibilityRole="button">
<Text type="xl" style={[pal.link, s.pl5]}>
Back
</Text>
</TouchableOpacity>
<View style={s.flex1} />
{!serviceDescription && error ? (
<TouchableOpacity
testID="loginRetryButton"
onPress={onPressRetryConnect}
accessibilityRole="button"
accessibilityLabel="Retry"
accessibilityHint="Retries login">
<Text type="xl-bold" style={[pal.link, s.pr5]}>
Retry
</Text>
</TouchableOpacity>
) : !serviceDescription ? (
<>
<ActivityIndicator />
<Text type="xl" style={[pal.textLight, s.pl10]}>
Connecting...
</Text>
</>
) : isProcessing ? (
<ActivityIndicator />
) : isReady ? (
<TouchableOpacity
testID="loginNextButton"
onPress={onPressNext}
accessibilityRole="button"
accessibilityLabel="Go to next"
accessibilityHint="Navigates to the next screen">
<Text type="xl-bold" style={[pal.link, s.pr5]}>
Next
</Text>
</TouchableOpacity>
) : undefined}
</View>
</View>
)
}
const ForgotPasswordForm = ({
error,
serviceUrl,
serviceDescription,
setError,
setServiceUrl,
onPressBack,
onEmailSent,
}: {
store: RootStoreModel
error: string
serviceUrl: string
serviceDescription: ServiceDescription | undefined
setError: (v: string) => void
setServiceUrl: (v: string) => void
onPressBack: () => void
onEmailSent: () => void
}) => {
const pal = usePalette('default')
const theme = useTheme()
const [isProcessing, setIsProcessing] = useState<boolean>(false)
const [email, setEmail] = useState<string>('')
const {screen} = useAnalytics()
const {openModal} = useModalControls()
useEffect(() => {
screen('Signin:ForgotPassword')
}, [screen])
const onPressSelectService = () => {
openModal({
name: 'server-input',
initialService: serviceUrl,
onSelect: setServiceUrl,
})
}
const onPressNext = async () => {
if (!EmailValidator.validate(email)) {
return setError('Your email appears to be invalid.')
}
setError('')
setIsProcessing(true)
try {
const agent = new BskyAgent({service: serviceUrl})
await agent.com.atproto.server.requestPasswordReset({email})
onEmailSent()
} catch (e: any) {
const errMsg = e.toString()
logger.warn('Failed to request password reset', {error: e})
setIsProcessing(false)
if (isNetworkError(e)) {
setError(
'Unable to contact your service. Please check your Internet connection.',
)
} else {
setError(cleanError(errMsg))
}
}
}
return (
<>
<View>
<Text type="title-lg" style={[pal.text, styles.screenTitle]}>
Reset password
</Text>
<Text type="md" style={[pal.text, styles.instructions]}>
Enter the email you used to create your account. We'll send you a
"reset code" so you can set a new password.
</Text>
<View
testID="forgotPasswordView"
style={[pal.borderDark, pal.view, styles.group]}>
<TouchableOpacity
testID="forgotPasswordSelectServiceButton"
style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}
onPress={onPressSelectService}
accessibilityRole="button"
accessibilityLabel="Hosting provider"
accessibilityHint="Sets hosting provider for password reset">
<FontAwesomeIcon
icon="globe"
style={[pal.textLight, styles.groupContentIcon]}
/>
<Text style={[pal.text, styles.textInput]} numberOfLines={1}>
{toNiceDomain(serviceUrl)}
</Text>
<View style={[pal.btn, styles.textBtnFakeInnerBtn]}>
<FontAwesomeIcon
icon="pen"
size={12}
style={pal.text as FontAwesomeIconStyle}
/>
</View>
</TouchableOpacity>
<View style={[pal.borderDark, styles.groupContent]}>
<FontAwesomeIcon
icon="envelope"
style={[pal.textLight, styles.groupContentIcon]}
/>
<TextInput
testID="forgotPasswordEmail"
style={[pal.text, styles.textInput]}
placeholder="Email address"
placeholderTextColor={pal.colors.textLight}
autoCapitalize="none"
autoFocus
autoCorrect={false}
keyboardAppearance={theme.colorScheme}
value={email}
onChangeText={setEmail}
editable={!isProcessing}
accessibilityLabel="Email"
accessibilityHint="Sets email for password reset"
/>
</View>
</View>
{error ? (
<View style={styles.error}>
<View style={styles.errorIcon}>
<FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
</View>
<View style={s.flex1}>
<Text style={[s.white, s.bold]}>{error}</Text>
</View>
</View>
) : undefined}
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
<TouchableOpacity onPress={onPressBack} accessibilityRole="button">
<Text type="xl" style={[pal.link, s.pl5]}>
Back
</Text>
</TouchableOpacity>
<View style={s.flex1} />
{!serviceDescription || isProcessing ? (
<ActivityIndicator />
) : !email ? (
<Text type="xl-bold" style={[pal.link, s.pr5, styles.dimmed]}>
Next
</Text>
) : (
<TouchableOpacity
testID="newPasswordButton"
onPress={onPressNext}
accessibilityRole="button"
accessibilityLabel="Go to next"
accessibilityHint="Navigates to the next screen">
<Text type="xl-bold" style={[pal.link, s.pr5]}>
Next
</Text>
</TouchableOpacity>
)}
{!serviceDescription || isProcessing ? (
<Text type="xl" style={[pal.textLight, s.pl10]}>
Processing...
</Text>
) : undefined}
</View>
</View>
</>
)
}
const SetNewPasswordForm = ({
error,
serviceUrl,
setError,
onPressBack,
onPasswordSet,
}: {
store: RootStoreModel
error: string
serviceUrl: string
setError: (v: string) => void
onPressBack: () => void
onPasswordSet: () => void
}) => {
const pal = usePalette('default')
const theme = useTheme()
const {screen} = useAnalytics()
useEffect(() => {
screen('Signin:SetNewPasswordForm')
}, [screen])
const [isProcessing, setIsProcessing] = useState<boolean>(false)
const [resetCode, setResetCode] = useState<string>('')
const [password, setPassword] = useState<string>('')
const onPressNext = async () => {
setError('')
setIsProcessing(true)
try {
const agent = new BskyAgent({service: serviceUrl})
const token = resetCode.replace(/\s/g, '')
await agent.com.atproto.server.resetPassword({
token,
password,
})
onPasswordSet()
} catch (e: any) {
const errMsg = e.toString()
logger.warn('Failed to set new password', {error: e})
setIsProcessing(false)
if (isNetworkError(e)) {
setError(
'Unable to contact your service. Please check your Internet connection.',
)
} else {
setError(cleanError(errMsg))
}
}
}
return (
<>
<View>
<Text type="title-lg" style={[pal.text, styles.screenTitle]}>
Set new password
</Text>
<Text type="lg" style={[pal.text, styles.instructions]}>
You will receive an email with a "reset code." Enter that code here,
then enter your new password.
</Text>
<View
testID="newPasswordView"
style={[pal.view, pal.borderDark, styles.group]}>
<View
style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
<FontAwesomeIcon
icon="ticket"
style={[pal.textLight, styles.groupContentIcon]}
/>
<TextInput
testID="resetCodeInput"
style={[pal.text, styles.textInput]}
placeholder="Reset code"
placeholderTextColor={pal.colors.textLight}
autoCapitalize="none"
autoCorrect={false}
keyboardAppearance={theme.colorScheme}
autoFocus
value={resetCode}
onChangeText={setResetCode}
editable={!isProcessing}
accessible={true}
accessibilityLabel="Reset code"
accessibilityHint="Input code sent to your email for password reset"
/>
</View>
<View style={[pal.borderDark, styles.groupContent]}>
<FontAwesomeIcon
icon="lock"
style={[pal.textLight, styles.groupContentIcon]}
/>
<TextInput
testID="newPasswordInput"
style={[pal.text, styles.textInput]}
placeholder="New password"
placeholderTextColor={pal.colors.textLight}
autoCapitalize="none"
autoCorrect={false}
keyboardAppearance={theme.colorScheme}
secureTextEntry
value={password}
onChangeText={setPassword}
editable={!isProcessing}
accessible={true}
accessibilityLabel="Password"
accessibilityHint="Input new password"
/>
</View>
</View>
{error ? (
<View style={styles.error}>
<View style={styles.errorIcon}>
<FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
</View>
<View style={s.flex1}>
<Text style={[s.white, s.bold]}>{error}</Text>
</View>
</View>
) : undefined}
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
<TouchableOpacity onPress={onPressBack} accessibilityRole="button">
<Text type="xl" style={[pal.link, s.pl5]}>
Back
</Text>
</TouchableOpacity>
<View style={s.flex1} />
{isProcessing ? (
<ActivityIndicator />
) : !resetCode || !password ? (
<Text type="xl-bold" style={[pal.link, s.pr5, styles.dimmed]}>
Next
</Text>
) : (
<TouchableOpacity
testID="setNewPasswordButton"
onPress={onPressNext}
accessibilityRole="button"
accessibilityLabel="Go to next"
accessibilityHint="Navigates to the next screen">
<Text type="xl-bold" style={[pal.link, s.pr5]}>
Next
</Text>
</TouchableOpacity>
)}
{isProcessing ? (
<Text type="xl" style={[pal.textLight, s.pl10]}>
Updating...
</Text>
) : undefined}
</View>
</View>
</>
)
}
const PasswordUpdatedForm = ({onPressNext}: {onPressNext: () => void}) => {
const {screen} = useAnalytics()
useEffect(() => {
screen('Signin:PasswordUpdatedForm')
}, [screen])
const pal = usePalette('default')
return (
<>
<View>
<Text type="title-lg" style={[pal.text, styles.screenTitle]}>
Password updated!
</Text>
<Text type="lg" style={[pal.text, styles.instructions]}>
You can now sign in with your new password.
</Text>
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
<View style={s.flex1} />
<TouchableOpacity
onPress={onPressNext}
accessibilityRole="button"
accessibilityLabel="Close alert"
accessibilityHint="Closes password update alert">
<Text type="xl-bold" style={[pal.link, s.pr5]}>
Okay
</Text>
</TouchableOpacity>
</View>
</View>
</>
)
}
const styles = StyleSheet.create({
screenTitle: {
marginBottom: 10,
marginHorizontal: 20,
},
instructions: {
marginBottom: 20,
marginHorizontal: 20,
},
group: {
borderWidth: 1,
borderRadius: 10,
marginBottom: 20,
marginHorizontal: 20,
},
groupLabel: {
paddingHorizontal: 20,
paddingBottom: 5,
},
groupContent: {
borderTopWidth: 1,
flexDirection: 'row',
alignItems: 'center',
},
noTopBorder: {
borderTopWidth: 0,
},
groupContentIcon: {
marginLeft: 10,
},
account: {
borderTopWidth: 1,
paddingHorizontal: 20,
paddingVertical: 4,
},
accountLast: {
borderBottomWidth: 1,
marginBottom: 20,
paddingVertical: 8,
},
textInput: {
flex: 1,
width: '100%',
paddingVertical: 10,
paddingHorizontal: 12,
fontSize: 17,
letterSpacing: 0.25,
fontWeight: '400',
borderRadius: 10,
},
textInputInnerBtn: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 6,
paddingHorizontal: 8,
marginHorizontal: 6,
},
textBtn: {
flexDirection: 'row',
flex: 1,
alignItems: 'center',
},
textBtnLabel: {
flex: 1,
paddingVertical: 10,
paddingHorizontal: 12,
},
textBtnFakeInnerBtn: {
flexDirection: 'row',
alignItems: 'center',
borderRadius: 6,
paddingVertical: 6,
paddingHorizontal: 8,
marginHorizontal: 6,
},
accountText: {
flex: 1,
flexDirection: 'row',
alignItems: 'baseline',
paddingVertical: 10,
},
accountTextOther: {
paddingLeft: 12,
},
error: {
backgroundColor: colors.red4,
flexDirection: 'row',
alignItems: 'center',
marginTop: -5,
marginHorizontal: 20,
marginBottom: 15,
borderRadius: 8,
paddingHorizontal: 8,
paddingVertical: 8,
},
errorIcon: {
borderWidth: 1,
borderColor: colors.white,
color: colors.white,
borderRadius: 30,
width: 16,
height: 16,
alignItems: 'center',
justifyContent: 'center',
marginRight: 5,
},
dimmed: {opacity: 0.5},
maxHeight: {
// @ts-ignore web only -prf
maxHeight: isWeb ? '100vh' : undefined,
height: !isWeb ? '100%' : undefined,
},
})

View file

@ -0,0 +1,288 @@
import React, {useState, useRef} from 'react'
import {
ActivityIndicator,
Keyboard,
TextInput,
TouchableOpacity,
View,
} from 'react-native'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {useAnalytics} from 'lib/analytics/analytics'
import {Text} from '../../util/text/Text'
import {s} from 'lib/styles'
import {createFullHandle} from 'lib/strings/handles'
import {toNiceDomain} from 'lib/strings/url-helpers'
import {RootStoreModel} from 'state/index'
import {ServiceDescription} from 'state/models/session'
import {isNetworkError} from 'lib/strings/errors'
import {usePalette} from 'lib/hooks/usePalette'
import {useTheme} from 'lib/ThemeContext'
import {cleanError} from 'lib/strings/errors'
import {logger} from '#/logger'
import {Trans, msg} from '@lingui/macro'
import {styles} from './styles'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
export const LoginForm = ({
store,
error,
serviceUrl,
serviceDescription,
initialHandle,
setError,
setServiceUrl,
onPressRetryConnect,
onPressBack,
onPressForgotPassword,
}: {
store: RootStoreModel
error: string
serviceUrl: string
serviceDescription: ServiceDescription | undefined
initialHandle: string
setError: (v: string) => void
setServiceUrl: (v: string) => void
onPressRetryConnect: () => void
onPressBack: () => void
onPressForgotPassword: () => void
}) => {
const {track} = useAnalytics()
const pal = usePalette('default')
const theme = useTheme()
const [isProcessing, setIsProcessing] = useState<boolean>(false)
const [identifier, setIdentifier] = useState<string>(initialHandle)
const [password, setPassword] = useState<string>('')
const passwordInputRef = useRef<TextInput>(null)
const {_} = useLingui()
const {openModal} = useModalControls()
const onPressSelectService = () => {
openModal({
name: 'server-input',
initialService: serviceUrl,
onSelect: setServiceUrl,
})
Keyboard.dismiss()
track('Signin:PressedSelectService')
}
const onPressNext = async () => {
Keyboard.dismiss()
setError('')
setIsProcessing(true)
try {
// try to guess the handle if the user just gave their own username
let fullIdent = identifier
if (
!identifier.includes('@') && // not an email
!identifier.includes('.') && // not a domain
serviceDescription &&
serviceDescription.availableUserDomains.length > 0
) {
let matched = false
for (const domain of serviceDescription.availableUserDomains) {
if (fullIdent.endsWith(domain)) {
matched = true
}
}
if (!matched) {
fullIdent = createFullHandle(
identifier,
serviceDescription.availableUserDomains[0],
)
}
}
await store.session.login({
service: serviceUrl,
identifier: fullIdent,
password,
})
} catch (e: any) {
const errMsg = e.toString()
logger.warn('Failed to login', {error: e})
setIsProcessing(false)
if (errMsg.includes('Authentication Required')) {
setError(_(msg`Invalid username or password`))
} else if (isNetworkError(e)) {
setError(
_(
msg`Unable to contact your service. Please check your Internet connection.`,
),
)
} else {
setError(cleanError(errMsg))
}
} finally {
track('Sign In', {resumedSession: false})
}
}
const isReady = !!serviceDescription && !!identifier && !!password
return (
<View testID="loginForm">
<Text type="sm-bold" style={[pal.text, styles.groupLabel]}>
<Trans>Sign into</Trans>
</Text>
<View style={[pal.borderDark, styles.group]}>
<View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
<FontAwesomeIcon
icon="globe"
style={[pal.textLight, styles.groupContentIcon]}
/>
<TouchableOpacity
testID="loginSelectServiceButton"
style={styles.textBtn}
onPress={onPressSelectService}
accessibilityRole="button"
accessibilityLabel={_(msg`Select service`)}
accessibilityHint="Sets server for the Bluesky client">
<Text type="xl" style={[pal.text, styles.textBtnLabel]}>
{toNiceDomain(serviceUrl)}
</Text>
<View style={[pal.btn, styles.textBtnFakeInnerBtn]}>
<FontAwesomeIcon
icon="pen"
size={12}
style={pal.textLight as FontAwesomeIconStyle}
/>
</View>
</TouchableOpacity>
</View>
</View>
<Text type="sm-bold" style={[pal.text, styles.groupLabel]}>
<Trans>Account</Trans>
</Text>
<View style={[pal.borderDark, styles.group]}>
<View style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
<FontAwesomeIcon
icon="at"
style={[pal.textLight, styles.groupContentIcon]}
/>
<TextInput
testID="loginUsernameInput"
style={[pal.text, styles.textInput]}
placeholder={_(msg`Username or email address`)}
placeholderTextColor={pal.colors.textLight}
autoCapitalize="none"
autoFocus
autoCorrect={false}
autoComplete="username"
returnKeyType="next"
onSubmitEditing={() => {
passwordInputRef.current?.focus()
}}
blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field
keyboardAppearance={theme.colorScheme}
value={identifier}
onChangeText={str =>
setIdentifier((str || '').toLowerCase().trim())
}
editable={!isProcessing}
accessibilityLabel={_(msg`Username or email address`)}
accessibilityHint="Input the username or email address you used at signup"
/>
</View>
<View style={[pal.borderDark, styles.groupContent]}>
<FontAwesomeIcon
icon="lock"
style={[pal.textLight, styles.groupContentIcon]}
/>
<TextInput
testID="loginPasswordInput"
ref={passwordInputRef}
style={[pal.text, styles.textInput]}
placeholder="Password"
placeholderTextColor={pal.colors.textLight}
autoCapitalize="none"
autoCorrect={false}
autoComplete="password"
returnKeyType="done"
enablesReturnKeyAutomatically={true}
keyboardAppearance={theme.colorScheme}
secureTextEntry={true}
textContentType="password"
clearButtonMode="while-editing"
value={password}
onChangeText={setPassword}
onSubmitEditing={onPressNext}
blurOnSubmit={false} // HACK: https://github.com/facebook/react-native/issues/21911#issuecomment-558343069 Keyboard blur behavior is now handled in onSubmitEditing
editable={!isProcessing}
accessibilityLabel={_(msg`Password`)}
accessibilityHint={
identifier === ''
? 'Input your password'
: `Input the password tied to ${identifier}`
}
/>
<TouchableOpacity
testID="forgotPasswordButton"
style={styles.textInputInnerBtn}
onPress={onPressForgotPassword}
accessibilityRole="button"
accessibilityLabel={_(msg`Forgot password`)}
accessibilityHint="Opens password reset form">
<Text style={pal.link}>
<Trans>Forgot</Trans>
</Text>
</TouchableOpacity>
</View>
</View>
{error ? (
<View style={styles.error}>
<View style={styles.errorIcon}>
<FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
</View>
<View style={s.flex1}>
<Text style={[s.white, s.bold]}>{error}</Text>
</View>
</View>
) : undefined}
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
<TouchableOpacity onPress={onPressBack} accessibilityRole="button">
<Text type="xl" style={[pal.link, s.pl5]}>
<Trans>Back</Trans>
</Text>
</TouchableOpacity>
<View style={s.flex1} />
{!serviceDescription && error ? (
<TouchableOpacity
testID="loginRetryButton"
onPress={onPressRetryConnect}
accessibilityRole="button"
accessibilityLabel={_(msg`Retry`)}
accessibilityHint="Retries login">
<Text type="xl-bold" style={[pal.link, s.pr5]}>
<Trans>Retry</Trans>
</Text>
</TouchableOpacity>
) : !serviceDescription ? (
<>
<ActivityIndicator />
<Text type="xl" style={[pal.textLight, s.pl10]}>
<Trans>Connecting...</Trans>
</Text>
</>
) : isProcessing ? (
<ActivityIndicator />
) : isReady ? (
<TouchableOpacity
testID="loginNextButton"
onPress={onPressNext}
accessibilityRole="button"
accessibilityLabel={_(msg`Go to next`)}
accessibilityHint="Navigates to the next screen">
<Text type="xl-bold" style={[pal.link, s.pr5]}>
<Trans>Next</Trans>
</Text>
</TouchableOpacity>
) : undefined}
</View>
</View>
)
}

View file

@ -0,0 +1,48 @@
import React, {useEffect} from 'react'
import {TouchableOpacity, View} from 'react-native'
import {useAnalytics} from 'lib/analytics/analytics'
import {Text} from '../../util/text/Text'
import {s} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
import {styles} from './styles'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
export const PasswordUpdatedForm = ({
onPressNext,
}: {
onPressNext: () => void
}) => {
const {screen} = useAnalytics()
const pal = usePalette('default')
const {_} = useLingui()
useEffect(() => {
screen('Signin:PasswordUpdatedForm')
}, [screen])
return (
<>
<View>
<Text type="title-lg" style={[pal.text, styles.screenTitle]}>
<Trans>Password updated!</Trans>
</Text>
<Text type="lg" style={[pal.text, styles.instructions]}>
<Trans>You can now sign in with your new password.</Trans>
</Text>
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
<View style={s.flex1} />
<TouchableOpacity
onPress={onPressNext}
accessibilityRole="button"
accessibilityLabel={_(msg`Close alert`)}
accessibilityHint="Closes password update alert">
<Text type="xl-bold" style={[pal.link, s.pr5]}>
<Trans>Okay</Trans>
</Text>
</TouchableOpacity>
</View>
</View>
</>
)
}

View file

@ -0,0 +1,181 @@
import React, {useState, useEffect} from 'react'
import {
ActivityIndicator,
TextInput,
TouchableOpacity,
View,
} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {BskyAgent} from '@atproto/api'
import {useAnalytics} from 'lib/analytics/analytics'
import {Text} from '../../util/text/Text'
import {s} from 'lib/styles'
import {RootStoreModel} from 'state/index'
import {isNetworkError} from 'lib/strings/errors'
import {usePalette} from 'lib/hooks/usePalette'
import {useTheme} from 'lib/ThemeContext'
import {cleanError} from 'lib/strings/errors'
import {logger} from '#/logger'
import {styles} from './styles'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
export const SetNewPasswordForm = ({
error,
serviceUrl,
setError,
onPressBack,
onPasswordSet,
}: {
store: RootStoreModel
error: string
serviceUrl: string
setError: (v: string) => void
onPressBack: () => void
onPasswordSet: () => void
}) => {
const pal = usePalette('default')
const theme = useTheme()
const {screen} = useAnalytics()
const {_} = useLingui()
useEffect(() => {
screen('Signin:SetNewPasswordForm')
}, [screen])
const [isProcessing, setIsProcessing] = useState<boolean>(false)
const [resetCode, setResetCode] = useState<string>('')
const [password, setPassword] = useState<string>('')
const onPressNext = async () => {
setError('')
setIsProcessing(true)
try {
const agent = new BskyAgent({service: serviceUrl})
const token = resetCode.replace(/\s/g, '')
await agent.com.atproto.server.resetPassword({
token,
password,
})
onPasswordSet()
} catch (e: any) {
const errMsg = e.toString()
logger.warn('Failed to set new password', {error: e})
setIsProcessing(false)
if (isNetworkError(e)) {
setError(
'Unable to contact your service. Please check your Internet connection.',
)
} else {
setError(cleanError(errMsg))
}
}
}
return (
<>
<View>
<Text type="title-lg" style={[pal.text, styles.screenTitle]}>
<Trans>Set new password</Trans>
</Text>
<Text type="lg" style={[pal.text, styles.instructions]}>
<Trans>
You will receive an email with a "reset code." Enter that code here,
then enter your new password.
</Trans>
</Text>
<View
testID="newPasswordView"
style={[pal.view, pal.borderDark, styles.group]}>
<View
style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
<FontAwesomeIcon
icon="ticket"
style={[pal.textLight, styles.groupContentIcon]}
/>
<TextInput
testID="resetCodeInput"
style={[pal.text, styles.textInput]}
placeholder="Reset code"
placeholderTextColor={pal.colors.textLight}
autoCapitalize="none"
autoCorrect={false}
keyboardAppearance={theme.colorScheme}
autoFocus
value={resetCode}
onChangeText={setResetCode}
editable={!isProcessing}
accessible={true}
accessibilityLabel={_(msg`Reset code`)}
accessibilityHint="Input code sent to your email for password reset"
/>
</View>
<View style={[pal.borderDark, styles.groupContent]}>
<FontAwesomeIcon
icon="lock"
style={[pal.textLight, styles.groupContentIcon]}
/>
<TextInput
testID="newPasswordInput"
style={[pal.text, styles.textInput]}
placeholder="New password"
placeholderTextColor={pal.colors.textLight}
autoCapitalize="none"
autoCorrect={false}
keyboardAppearance={theme.colorScheme}
secureTextEntry
value={password}
onChangeText={setPassword}
editable={!isProcessing}
accessible={true}
accessibilityLabel={_(msg`Password`)}
accessibilityHint="Input new password"
/>
</View>
</View>
{error ? (
<View style={styles.error}>
<View style={styles.errorIcon}>
<FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
</View>
<View style={s.flex1}>
<Text style={[s.white, s.bold]}>{error}</Text>
</View>
</View>
) : undefined}
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
<TouchableOpacity onPress={onPressBack} accessibilityRole="button">
<Text type="xl" style={[pal.link, s.pl5]}>
<Trans>Back</Trans>
</Text>
</TouchableOpacity>
<View style={s.flex1} />
{isProcessing ? (
<ActivityIndicator />
) : !resetCode || !password ? (
<Text type="xl-bold" style={[pal.link, s.pr5, styles.dimmed]}>
<Trans>Next</Trans>
</Text>
) : (
<TouchableOpacity
testID="setNewPasswordButton"
onPress={onPressNext}
accessibilityRole="button"
accessibilityLabel={_(msg`Go to next`)}
accessibilityHint="Navigates to the next screen">
<Text type="xl-bold" style={[pal.link, s.pr5]}>
<Trans>Next</Trans>
</Text>
</TouchableOpacity>
)}
{isProcessing ? (
<Text type="xl" style={[pal.textLight, s.pl10]}>
<Trans>Updating...</Trans>
</Text>
) : undefined}
</View>
</View>
</>
)
}

View file

@ -0,0 +1,118 @@
import {StyleSheet} from 'react-native'
import {colors} from 'lib/styles'
import {isWeb} from '#/platform/detection'
export const styles = StyleSheet.create({
screenTitle: {
marginBottom: 10,
marginHorizontal: 20,
},
instructions: {
marginBottom: 20,
marginHorizontal: 20,
},
group: {
borderWidth: 1,
borderRadius: 10,
marginBottom: 20,
marginHorizontal: 20,
},
groupLabel: {
paddingHorizontal: 20,
paddingBottom: 5,
},
groupContent: {
borderTopWidth: 1,
flexDirection: 'row',
alignItems: 'center',
},
noTopBorder: {
borderTopWidth: 0,
},
groupContentIcon: {
marginLeft: 10,
},
account: {
borderTopWidth: 1,
paddingHorizontal: 20,
paddingVertical: 4,
},
accountLast: {
borderBottomWidth: 1,
marginBottom: 20,
paddingVertical: 8,
},
textInput: {
flex: 1,
width: '100%',
paddingVertical: 10,
paddingHorizontal: 12,
fontSize: 17,
letterSpacing: 0.25,
fontWeight: '400',
borderRadius: 10,
},
textInputInnerBtn: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 6,
paddingHorizontal: 8,
marginHorizontal: 6,
},
textBtn: {
flexDirection: 'row',
flex: 1,
alignItems: 'center',
},
textBtnLabel: {
flex: 1,
paddingVertical: 10,
paddingHorizontal: 12,
},
textBtnFakeInnerBtn: {
flexDirection: 'row',
alignItems: 'center',
borderRadius: 6,
paddingVertical: 6,
paddingHorizontal: 8,
marginHorizontal: 6,
},
accountText: {
flex: 1,
flexDirection: 'row',
alignItems: 'baseline',
paddingVertical: 10,
},
accountTextOther: {
paddingLeft: 12,
},
error: {
backgroundColor: colors.red4,
flexDirection: 'row',
alignItems: 'center',
marginTop: -5,
marginHorizontal: 20,
marginBottom: 15,
borderRadius: 8,
paddingHorizontal: 8,
paddingVertical: 8,
},
errorIcon: {
borderWidth: 1,
borderColor: colors.white,
color: colors.white,
borderRadius: 30,
width: 16,
height: 16,
alignItems: 'center',
justifyContent: 'center',
marginRight: 5,
},
dimmed: {opacity: 0.5},
maxHeight: {
// @ts-ignore web only -prf
maxHeight: isWeb ? '100vh' : undefined,
height: !isWeb ? '100%' : undefined,
},
})

View file

@ -14,6 +14,7 @@ import {Text} from 'view/com/util/text/Text'
import Animated, {FadeInRight} from 'react-native-reanimated'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {useAnalytics} from 'lib/analytics/analytics'
import {Trans} from '@lingui/macro'
type Props = {
item: SuggestedActor
@ -115,7 +116,9 @@ export const ProfileCard = observer(function ProfileCardImpl({
{addingMoreSuggestions ? (
<View style={styles.addingMoreContainer}>
<ActivityIndicator size="small" color={pal.colors.text} />
<Text style={[pal.text]}>Finding similar accounts...</Text>
<Text style={[pal.text]}>
<Trans>Finding similar accounts...</Trans>
</Text>
</View>
) : null}
</View>

View file

@ -7,6 +7,7 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Button} from 'view/com/util/forms/Button'
import {observer} from 'mobx-react-lite'
import {ViewHeader} from 'view/com/util/ViewHeader'
import {Trans} from '@lingui/macro'
type Props = {
next: () => void
@ -32,7 +33,9 @@ export const WelcomeMobile = observer(function WelcomeMobileImpl({
accessibilityRole="button"
style={[s.flexRow, s.alignCenter]}
onPress={skip}>
<Text style={[pal.link]}>Skip</Text>
<Text style={[pal.link]}>
<Trans>Skip</Trans>
</Text>
<FontAwesomeIcon
icon={'chevron-right'}
size={14}
@ -45,17 +48,21 @@ export const WelcomeMobile = observer(function WelcomeMobileImpl({
<View>
<Text style={[pal.text, styles.title]}>
Welcome to{' '}
<Text style={[pal.text, pal.link, styles.title]}>Bluesky</Text>
<Text style={[pal.text, pal.link, styles.title]}>
<Trans>Bluesky</Trans>
</Text>
</Text>
<View style={styles.spacer} />
<View style={[styles.row]}>
<FontAwesomeIcon icon={'globe'} size={36} color={pal.colors.link} />
<View style={[styles.rowText]}>
<Text type="lg-bold" style={[pal.text]}>
Bluesky is public.
<Trans>Bluesky is public.</Trans>
</Text>
<Text type="lg-thin" 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>
</View>
</View>
@ -63,10 +70,10 @@ export const WelcomeMobile = observer(function WelcomeMobileImpl({
<FontAwesomeIcon icon={'at'} size={36} color={pal.colors.link} />
<View style={[styles.rowText]}>
<Text type="lg-bold" style={[pal.text]}>
Bluesky is open.
<Trans>Bluesky is open.</Trans>
</Text>
<Text type="lg-thin" style={[pal.text, s.pt2]}>
Never lose access to your followers and data.
<Trans>Never lose access to your followers and data.</Trans>
</Text>
</View>
</View>
@ -74,11 +81,13 @@ export const WelcomeMobile = observer(function WelcomeMobileImpl({
<FontAwesomeIcon icon={'gear'} size={36} color={pal.colors.link} />
<View style={[styles.rowText]}>
<Text type="lg-bold" style={[pal.text]}>
Bluesky is flexible.
<Trans>Bluesky is flexible.</Trans>
</Text>
<Text type="lg-thin" 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>
</View>
</View>

View file

@ -49,6 +49,8 @@ import {LabelsBtn} from './labels/LabelsBtn'
import {SelectLangBtn} from './select-language/SelectLangBtn'
import {EmojiPickerButton} from './text-input/web/EmojiPicker.web'
import {insertMentionAt} from 'lib/strings/mention-manip'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModals, useModalControls} from '#/state/modals'
import {useRequireAltTextEnabled} from '#/state/preferences'
import {
@ -70,6 +72,7 @@ export const ComposePost = observer(function ComposePost({
const pal = usePalette('default')
const {isDesktop, isMobile} = useWebMediaQueries()
const store = useStores()
const {_} = useLingui()
const requireAltTextEnabled = useRequireAltTextEnabled()
const langPrefs = useLanguagePrefs()
const setLangPrefs = useLanguagePrefsApi()
@ -273,9 +276,11 @@ export const ComposePost = observer(function ComposePost({
onPress={onPressCancel}
onAccessibilityEscape={onPressCancel}
accessibilityRole="button"
accessibilityLabel="Cancel"
accessibilityLabel={_(msg`Cancel`)}
accessibilityHint="Closes post composer and discards post draft">
<Text style={[pal.link, s.f18]}>Cancel</Text>
<Text style={[pal.link, s.f18]}>
<Trans>Cancel</Trans>
</Text>
</TouchableOpacity>
<View style={s.flex1} />
{isProcessing ? (
@ -316,7 +321,9 @@ export const ComposePost = observer(function ComposePost({
</TouchableOpacity>
) : (
<View style={[styles.postBtn, pal.btn]}>
<Text style={[pal.textLight, s.f16, s.bold]}>Post</Text>
<Text style={[pal.textLight, s.f16, s.bold]}>
<Trans>Post</Trans>
</Text>
</View>
)}
</>
@ -332,7 +339,7 @@ export const ComposePost = observer(function ComposePost({
/>
</View>
<Text style={[pal.text, s.flex1]}>
One or more images is missing alt text.
<Trans>One or more images is missing alt text.</Trans>
</Text>
</View>
)}
@ -388,7 +395,7 @@ export const ComposePost = observer(function ComposePost({
onSuggestedLinksChanged={setSuggestedLinks}
onError={setError}
accessible={true}
accessibilityLabel="Write post"
accessibilityLabel={_(msg`Write post`)}
accessibilityHint={`Compose posts up to ${MAX_GRAPHEME_LENGTH} characters in length`}
/>
</View>
@ -417,11 +424,11 @@ export const ComposePost = observer(function ComposePost({
style={[pal.borderDark, styles.addExtLinkBtn]}
onPress={() => onPressAddLinkCard(url)}
accessibilityRole="button"
accessibilityLabel="Add link card"
accessibilityLabel={_(msg`Add link card`)}
accessibilityHint={`Creates a card with a thumbnail. The card links to ${url}`}>
<Text style={pal.text}>
Add link card:{' '}
<Text style={pal.link}>{toShortUrl(url)}</Text>
<Trans>Add link card:</Trans>
<Text style={[pal.link, s.ml5]}>{toShortUrl(url)}</Text>
</Text>
</TouchableOpacity>
))}

View file

@ -11,6 +11,8 @@ import {Text} from '../util/text/Text'
import {s} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
import {ExternalEmbedDraft} from 'lib/api/index'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
export const ExternalEmbed = ({
link,
@ -21,6 +23,7 @@ export const ExternalEmbed = ({
}) => {
const pal = usePalette('default')
const palError = usePalette('error')
const {_} = useLingui()
if (!link) {
return <View />
}
@ -64,7 +67,7 @@ export const ExternalEmbed = ({
style={styles.removeBtn}
onPress={onRemove}
accessibilityRole="button"
accessibilityLabel="Remove image preview"
accessibilityLabel={_(msg`Remove image preview`)}
accessibilityHint={`Removes default thumbnail from ${link.uri}`}
onAccessibilityEscape={onRemove}>
<FontAwesomeIcon size={18} icon="xmark" style={s.white} />

View file

@ -5,10 +5,13 @@ import {Text} from '../util/text/Text'
import {usePalette} from 'lib/hooks/usePalette'
import {useStores} from 'state/index'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
export function ComposePrompt({onPressCompose}: {onPressCompose: () => void}) {
const store = useStores()
const pal = usePalette('default')
const {_} = useLingui()
const {isDesktop} = useWebMediaQueries()
return (
<TouchableOpacity
@ -16,7 +19,7 @@ export function ComposePrompt({onPressCompose}: {onPressCompose: () => void}) {
style={[pal.view, pal.border, styles.prompt]}
onPress={() => onPressCompose()}
accessibilityRole="button"
accessibilityLabel="Compose reply"
accessibilityLabel={_(msg`Compose reply`)}
accessibilityHint="Opens composer">
<UserAvatar avatar={store.me.avatar} size={38} />
<Text
@ -25,7 +28,7 @@ export function ComposePrompt({onPressCompose}: {onPressCompose: () => void}) {
pal.text,
isDesktop ? styles.labelDesktopWeb : styles.labelMobile,
]}>
Write your reply
<Trans>Write your reply</Trans>
</Text>
</TouchableOpacity>
)

View file

@ -7,6 +7,8 @@ import {ShieldExclamation} from 'lib/icons'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome'
import {isNative} from 'platform/detection'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
import {useModalControls} from '#/state/modals'
export const LabelsBtn = observer(function LabelsBtn({
@ -19,6 +21,7 @@ export const LabelsBtn = observer(function LabelsBtn({
onChange: (v: string[]) => void
}) {
const pal = usePalette('default')
const {_} = useLingui()
const {openModal} = useModalControls()
return (
@ -26,7 +29,7 @@ export const LabelsBtn = observer(function LabelsBtn({
type="default-light"
testID="labelsBtn"
style={[styles.button, !hasMedia && styles.dimmed]}
accessibilityLabel="Content warnings"
accessibilityLabel={_(msg`Content warnings`)}
accessibilityHint=""
onPress={() => {
if (isNative) {

View file

@ -10,6 +10,8 @@ import {Text} from 'view/com/util/text/Text'
import {Dimensions} from 'lib/media/types'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {isNative} from 'platform/detection'
@ -48,6 +50,7 @@ const GalleryInner = observer(function GalleryImpl({
containerInfo,
}: GalleryInnerProps) {
const pal = usePalette('default')
const {_} = useLingui()
const {isMobile} = useWebMediaQueries()
const {openModal} = useModalControls()
@ -113,7 +116,7 @@ const GalleryInner = observer(function GalleryImpl({
<TouchableOpacity
testID="altTextButton"
accessibilityRole="button"
accessibilityLabel="Add alt text"
accessibilityLabel={_(msg`Add alt text`)}
accessibilityHint=""
onPress={() => {
Keyboard.dismiss()
@ -124,7 +127,7 @@ const GalleryInner = observer(function GalleryImpl({
}}
style={[styles.altTextControl, altTextControlStyle]}>
<Text style={styles.altTextControlLabel} accessible={false}>
ALT
<Trans>ALT</Trans>
</Text>
{image.altText.length > 0 ? (
<FontAwesomeIcon
@ -138,7 +141,7 @@ const GalleryInner = observer(function GalleryImpl({
<TouchableOpacity
testID="editPhotoButton"
accessibilityRole="button"
accessibilityLabel="Edit image"
accessibilityLabel={_(msg`Edit image`)}
accessibilityHint=""
onPress={() => {
if (isNative) {
@ -161,7 +164,7 @@ const GalleryInner = observer(function GalleryImpl({
<TouchableOpacity
testID="removePhotoButton"
accessibilityRole="button"
accessibilityLabel="Remove image"
accessibilityLabel={_(msg`Remove image`)}
accessibilityHint=""
onPress={() => gallery.remove(image)}
style={styles.imageControl}>
@ -174,7 +177,7 @@ const GalleryInner = observer(function GalleryImpl({
</View>
<TouchableOpacity
accessibilityRole="button"
accessibilityLabel="Add alt text"
accessibilityLabel={_(msg`Add alt text`)}
accessibilityHint=""
onPress={() => {
Keyboard.dismiss()
@ -203,8 +206,10 @@ const GalleryInner = observer(function GalleryImpl({
<FontAwesomeIcon icon="info" size={12} color={pal.colors.text} />
</View>
<Text type="sm" style={[pal.textLight, s.flex1]}>
Alt text describes images for blind and low-vision users, and helps
give context to everyone.
<Trans>
Alt text describes images for blind and low-vision users, and helps
give context to everyone.
</Trans>
</Text>
</View>
</>

View file

@ -13,6 +13,8 @@ import {HITSLOP_10, POST_IMG_MAX} from 'lib/constants'
import {GalleryModel} from 'state/models/media/gallery'
import {isMobileWeb, isNative} from 'platform/detection'
import {logger} from '#/logger'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
type Props = {
gallery: GalleryModel
@ -22,6 +24,7 @@ export function OpenCameraBtn({gallery}: Props) {
const pal = usePalette('default')
const {track} = useAnalytics()
const store = useStores()
const {_} = useLingui()
const {requestCameraAccessIfNeeded} = useCameraPermission()
const onPressTakePicture = useCallback(async () => {
@ -56,7 +59,7 @@ export function OpenCameraBtn({gallery}: Props) {
style={styles.button}
hitSlop={HITSLOP_10}
accessibilityRole="button"
accessibilityLabel="Camera"
accessibilityLabel={_(msg`Camera`)}
accessibilityHint="Opens camera on device">
<FontAwesomeIcon
icon="camera"

View file

@ -10,6 +10,8 @@ import {usePhotoLibraryPermission} from 'lib/hooks/usePermissions'
import {GalleryModel} from 'state/models/media/gallery'
import {HITSLOP_10} from 'lib/constants'
import {isNative} from 'platform/detection'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
type Props = {
gallery: GalleryModel
@ -18,6 +20,7 @@ type Props = {
export function SelectPhotoBtn({gallery}: Props) {
const pal = usePalette('default')
const {track} = useAnalytics()
const {_} = useLingui()
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
const onPressSelectPhotos = useCallback(async () => {
@ -37,7 +40,7 @@ export function SelectPhotoBtn({gallery}: Props) {
style={styles.button}
hitSlop={HITSLOP_10}
accessibilityRole="button"
accessibilityLabel="Gallery"
accessibilityLabel={_(msg`Gallery`)}
accessibilityHint="Opens device photo gallery">
<FontAwesomeIcon
icon={['far', 'image']}

View file

@ -21,9 +21,12 @@ import {
toPostLanguages,
hasPostLanguage,
} from '#/state/preferences/languages'
import {t, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
export const SelectLangBtn = observer(function SelectLangBtn() {
const pal = usePalette('default')
const {_} = useLingui()
const {openModal} = useModalControls()
const langPrefs = useLanguagePrefs()
const setLangPrefs = useLanguagePrefsApi()
@ -82,11 +85,11 @@ export const SelectLangBtn = observer(function SelectLangBtn() {
}
return [
{heading: true, label: 'Post language'},
{heading: true, label: t`Post language`},
...arr.slice(0, 6),
{sep: true},
{
label: 'Other...',
label: t`Other...`,
onPress: onPressMore,
},
]
@ -99,7 +102,7 @@ export const SelectLangBtn = observer(function SelectLangBtn() {
items={items}
openUpwards
style={styles.button}
accessibilityLabel="Language selection"
accessibilityLabel={_(msg`Language selection`)}
accessibilityHint="">
{postLanguagesPref.length > 0 ? (
<Text type="lg-bold" style={[pal.link, styles.label]} numberOfLines={1}>

View file

@ -21,6 +21,8 @@ import {FAB} from '../util/fab/FAB'
import {LoadLatestBtn} from '../util/load-latest/LoadLatestBtn'
import useAppState from 'react-native-appstate-hook'
import {logger} from '#/logger'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
export const FeedPage = observer(function FeedPageImpl({
testID,
@ -37,6 +39,7 @@ export const FeedPage = observer(function FeedPageImpl({
}) {
const store = useStores()
const pal = usePalette('default')
const {_} = useLingui()
const {isDesktop} = useWebMediaQueries()
const [onMainScroll, isScrolledDown, resetMainScroll] = useOnMainScroll()
const {screen, track} = useAnalytics()
@ -157,7 +160,7 @@ export const FeedPage = observer(function FeedPageImpl({
type="title-lg"
href="/settings/home-feed"
style={{fontWeight: 'bold'}}
accessibilityLabel="Feed Preferences"
accessibilityLabel={_(msg`Feed Preferences`)}
accessibilityHint=""
text={
<FontAwesomeIcon
@ -170,7 +173,7 @@ export const FeedPage = observer(function FeedPageImpl({
)
}
return <></>
}, [isDesktop, pal, store, hasNew])
}, [isDesktop, pal.view, pal.text, pal.textLight, store, hasNew, _])
return (
<View testID={testID} style={s.h100pct}>
@ -188,7 +191,7 @@ export const FeedPage = observer(function FeedPageImpl({
{(isScrolledDown || hasNew) && (
<LoadLatestBtn
onPress={onPressLoadLatest}
label="Load new posts"
label={_(msg`Load new posts`)}
showIndicator={hasNew}
/>
)}
@ -197,7 +200,7 @@ export const FeedPage = observer(function FeedPageImpl({
onPress={onPressCompose}
icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
accessibilityRole="button"
accessibilityLabel="New post"
accessibilityLabel={_(msg`New post`)}
accessibilityHint=""
/>
</View>

View file

@ -5,10 +5,10 @@
* LICENSE file in the root directory of this source tree.
*
*/
import {createHitslop} from 'lib/constants'
import React from 'react'
import {createHitslop} from 'lib/constants'
import {SafeAreaView, Text, TouchableOpacity, StyleSheet} from 'react-native'
import {t} from '@lingui/macro'
type Props = {
onRequestClose: () => void
@ -23,7 +23,7 @@ const ImageDefaultHeader = ({onRequestClose}: Props) => (
onPress={onRequestClose}
hitSlop={HIT_SLOP}
accessibilityRole="button"
accessibilityLabel="Close image"
accessibilityLabel={t`Close image`}
accessibilityHint="Closes viewer for header image"
onAccessibilityEscape={onRequestClose}>
<Text style={styles.closeText}></Text>

View file

@ -14,6 +14,8 @@ import * as models from 'state/models/ui/shell'
import {colors, s} from 'lib/styles'
import ImageDefaultHeader from './ImageViewing/components/ImageDefaultHeader'
import {Text} from '../util/text/Text'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
interface Img {
uri: string
@ -62,6 +64,7 @@ function LightboxInner({
initialIndex: number
onClose: () => void
}) {
const {_} = useLingui()
const [index, setIndex] = useState<number>(initialIndex)
const [isAltExpanded, setAltExpanded] = useState(false)
@ -101,7 +104,7 @@ function LightboxInner({
<TouchableWithoutFeedback
onPress={onClose}
accessibilityRole="button"
accessibilityLabel="Close image viewer"
accessibilityLabel={_(msg`Close image viewer`)}
accessibilityHint="Exits image view"
onAccessibilityEscape={onClose}>
<View style={styles.imageCenterer}>
@ -117,7 +120,7 @@ function LightboxInner({
onPress={onPressLeft}
style={[styles.btn, styles.leftBtn]}
accessibilityRole="button"
accessibilityLabel="Previous image"
accessibilityLabel={_(msg`Previous image`)}
accessibilityHint="">
<FontAwesomeIcon
icon="angle-left"
@ -131,7 +134,7 @@ function LightboxInner({
onPress={onPressRight}
style={[styles.btn, styles.rightBtn]}
accessibilityRole="button"
accessibilityLabel="Next image"
accessibilityLabel={_(msg`Next image`)}
accessibilityHint="">
<FontAwesomeIcon
icon="angle-right"
@ -145,7 +148,7 @@ function LightboxInner({
{imgs[index].alt ? (
<View style={styles.footer}>
<Pressable
accessibilityLabel="Expand alt text"
accessibilityLabel={_(msg`Expand alt text`)}
accessibilityHint="If alt text is long, toggles alt text expanded state"
onPress={() => {
setAltExpanded(!isAltExpanded)

View file

@ -20,6 +20,7 @@ import {usePalette} from 'lib/hooks/usePalette'
import {FlatList} from '../util/Views'
import {s} from 'lib/styles'
import {logger} from '#/logger'
import {Trans} from '@lingui/macro'
const LOADING = {_reactKey: '__loading__'}
const EMPTY = {_reactKey: '__empty__'}
@ -107,7 +108,9 @@ export const ListsList = observer(function ListsListImpl({
<View
testID="listsEmpty"
style={[{padding: 18, borderTopWidth: 1}, pal.border]}>
<Text style={pal.textLight}>You have no lists.</Text>
<Text style={pal.textLight}>
<Trans>You have no lists.</Trans>
</Text>
</View>
)
} else if (item === ERROR_ITEM) {

View file

@ -13,6 +13,8 @@ import {
import Clipboard from '@react-native-clipboard/clipboard'
import * as Toast from '../util/Toast'
import {logger} from '#/logger'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
export const snapPoints = ['70%']
@ -55,6 +57,7 @@ const shadesOfBlue: string[] = [
export function Component({}: {}) {
const pal = usePalette('default')
const store = useStores()
const {_} = useLingui()
const {closeModal} = useModalControls()
const [name, setName] = useState(
shadesOfBlue[Math.floor(Math.random() * shadesOfBlue.length)],
@ -121,15 +124,19 @@ export function Component({}: {}) {
<View>
{!appPassword ? (
<Text type="lg" style={[pal.text]}>
Please enter a unique name for this App Password or use our randomly
generated one.
<Trans>
Please enter a unique name for this App Password or use our
randomly generated one.
</Trans>
</Text>
) : (
<Text type="lg" style={[pal.text]}>
<Text type="lg-bold" style={[pal.text]}>
Here is your app password.
</Text>{' '}
Use this to sign into the other app along with your handle.
<Text type="lg-bold" style={[pal.text, s.mr5]}>
<Trans>Here is your app password.</Trans>
</Text>
<Trans>
Use this to sign into the other app along with your handle.
</Trans>
</Text>
)}
{!appPassword ? (
@ -154,7 +161,7 @@ export function Component({}: {}) {
returnKeyType="done"
onEndEditing={createAppPassword}
accessible={true}
accessibilityLabel="Name"
accessibilityLabel={_(msg`Name`)}
accessibilityHint="Input name for app password"
/>
</View>
@ -163,13 +170,15 @@ export function Component({}: {}) {
style={[pal.border, styles.passwordContainer, pal.btn]}
onPress={onCopy}
accessibilityRole="button"
accessibilityLabel="Copy"
accessibilityLabel={_(msg`Copy`)}
accessibilityHint="Copies app password">
<Text type="2xl-bold" style={[pal.text]}>
{appPassword}
</Text>
{wasCopied ? (
<Text style={[pal.textLight]}>Copied</Text>
<Text style={[pal.textLight]}>
<Trans>Copied</Trans>
</Text>
) : (
<FontAwesomeIcon
icon={['far', 'clone']}
@ -182,14 +191,18 @@ export function Component({}: {}) {
</View>
{appPassword ? (
<Text type="lg" style={[pal.textLight, s.mb10]}>
For security reasons, you won't be able to view this again. If you
lose this password, you'll need to generate a new one.
<Trans>
For security reasons, you won't be able to view this again. If you
lose this password, you'll need to generate a new one.
</Trans>
</Text>
) : (
<Text type="xs" style={[pal.textLight, s.mb10, s.mt2]}>
Can only contain letters, numbers, spaces, dashes, and underscores.
Must be at least 4 characters long, but no more than 32 characters
long.
<Trans>
Can only contain letters, numbers, spaces, dashes, and underscores.
Must be at least 4 characters long, but no more than 32 characters
long.
</Trans>
</Text>
)}
<View style={styles.btnContainer}>

View file

@ -19,6 +19,8 @@ import {Text} from '../util/text/Text'
import LinearGradient from 'react-native-linear-gradient'
import {isAndroid, isWeb} from 'platform/detection'
import {ImageModel} from 'state/models/media/image'
import {useLingui} from '@lingui/react'
import {Trans, msg} from '@lingui/macro'
import {useModalControls} from '#/state/modals'
export const snapPoints = ['fullscreen']
@ -30,6 +32,7 @@ interface Props {
export function Component({image}: Props) {
const pal = usePalette('default')
const theme = useTheme()
const {_} = useLingui()
const [altText, setAltText] = useState(image.altText)
const windim = useWindowDimensions()
const {closeModal} = useModalControls()
@ -90,7 +93,7 @@ export function Component({image}: Props) {
placeholderTextColor={pal.colors.textLight}
value={altText}
onChangeText={text => setAltText(enforceLen(text, MAX_ALT_TEXT))}
accessibilityLabel="Image alt text"
accessibilityLabel={_(msg`Image alt text`)}
accessibilityHint=""
accessibilityLabelledBy="imageAltText"
autoFocus
@ -99,7 +102,7 @@ export function Component({image}: Props) {
<TouchableOpacity
testID="altTextImageSaveBtn"
onPress={onPressSave}
accessibilityLabel="Save alt text"
accessibilityLabel={_(msg`Save alt text`)}
accessibilityHint={`Saves alt text, which reads: ${altText}`}
accessibilityRole="button">
<LinearGradient
@ -108,7 +111,7 @@ export function Component({image}: Props) {
end={{x: 1, y: 1}}
style={[styles.button]}>
<Text type="button-lg" style={[s.white, s.bold]}>
Save
<Trans>Save</Trans>
</Text>
</LinearGradient>
</TouchableOpacity>
@ -116,12 +119,12 @@ export function Component({image}: Props) {
testID="altTextImageCancelBtn"
onPress={onPressCancel}
accessibilityRole="button"
accessibilityLabel="Cancel add image alt text"
accessibilityLabel={_(msg`Cancel add image alt text`)}
accessibilityHint=""
onAccessibilityEscape={onPressCancel}>
<View style={[styles.button]}>
<Text type="button-lg" style={[pal.textLight]}>
Cancel
<Trans>Cancel</Trans>
</Text>
</View>
</TouchableOpacity>

View file

@ -15,6 +15,8 @@ import {usePalette} from 'lib/hooks/usePalette'
import {isWeb} from 'platform/detection'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {cleanError} from 'lib/strings/errors'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
export const snapPoints = ['50%']
@ -22,6 +24,7 @@ export const snapPoints = ['50%']
export const Component = observer(function Component({}: {}) {
const pal = usePalette('default')
const store = useStores()
const {_} = useLingui()
const {closeModal} = useModalControls()
const [date, setDate] = useState<Date>(
store.preferences.birthDate || new Date(),
@ -49,12 +52,12 @@ export const Component = observer(function Component({}: {}) {
style={[pal.view, styles.container, isMobile && {paddingHorizontal: 18}]}>
<View style={styles.titleSection}>
<Text type="title-lg" style={[pal.text, styles.title]}>
My Birthday
<Trans>My Birthday</Trans>
</Text>
</View>
<Text type="lg" style={[pal.textLight, {marginBottom: 10}]}>
This information is not shared with other users.
<Trans>This information is not shared with other users.</Trans>
</Text>
<View>
@ -65,7 +68,7 @@ export const Component = observer(function Component({}: {}) {
buttonType="default-light"
buttonStyle={[pal.border, styles.dateInputButton]}
buttonLabelType="lg"
accessibilityLabel="Birthday"
accessibilityLabel={_(msg`Birthday`)}
accessibilityHint="Enter your birth date"
accessibilityLabelledBy="birthDate"
/>
@ -86,9 +89,11 @@ export const Component = observer(function Component({}: {}) {
onPress={onSave}
style={styles.btn}
accessibilityRole="button"
accessibilityLabel="Save"
accessibilityLabel={_(msg`Save`)}
accessibilityHint="">
<Text style={[s.white, s.bold, s.f18]}>Save</Text>
<Text style={[s.white, s.bold, s.f18]}>
<Trans>Save</Trans>
</Text>
</TouchableOpacity>
)}
</View>

View file

@ -12,6 +12,8 @@ import {usePalette} from 'lib/hooks/usePalette'
import {isWeb} from 'platform/detection'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {cleanError} from 'lib/strings/errors'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
enum Stages {
@ -25,6 +27,7 @@ export const snapPoints = ['90%']
export const Component = observer(function Component({}: {}) {
const pal = usePalette('default')
const store = useStores()
const {_} = useLingui()
const [stage, setStage] = useState<Stages>(Stages.InputEmail)
const [email, setEmail] = useState<string>(
store.session.currentSession?.email || '',
@ -62,7 +65,9 @@ export const Component = observer(function Component({}: {}) {
// you can remove this any time after Oct2023
// -prf
if (err === 'email must be confirmed (temporary)') {
err = `Please confirm your email before changing it. This is a temporary requirement while email-updating tools are added, and it will soon be removed.`
err = _(
msg`Please confirm your email before changing it. This is a temporary requirement while email-updating tools are added, and it will soon be removed.`,
)
}
setError(err)
} finally {
@ -103,26 +108,26 @@ export const Component = observer(function Component({}: {}) {
style={[s.flex1, isMobile && {paddingHorizontal: 18}]}>
<View style={styles.titleSection}>
<Text type="title-lg" style={[pal.text, styles.title]}>
{stage === Stages.InputEmail ? 'Change Your Email' : ''}
{stage === Stages.ConfirmCode ? 'Security Step Required' : ''}
{stage === Stages.Done ? 'Email Updated' : ''}
{stage === Stages.InputEmail ? _(msg`Change Your Email`) : ''}
{stage === Stages.ConfirmCode ? _(msg`Security Step Required`) : ''}
{stage === Stages.Done ? _(msg`Email Updated`) : ''}
</Text>
</View>
<Text type="lg" style={[pal.textLight, {marginBottom: 10}]}>
{stage === Stages.InputEmail ? (
<>Enter your new email address below.</>
<Trans>Enter your new email address below.</Trans>
) : stage === Stages.ConfirmCode ? (
<>
<Trans>
An email has been sent to your previous address,{' '}
{store.session.currentSession?.email || ''}. It includes a
confirmation code which you can enter below.
</>
</Trans>
) : (
<>
<Trans>
Your email has been updated but not verified. As a next step,
please verify your new email.
</>
</Trans>
)}
</Text>
@ -135,7 +140,7 @@ export const Component = observer(function Component({}: {}) {
value={email}
onChangeText={setEmail}
accessible={true}
accessibilityLabel="Email"
accessibilityLabel={_(msg`Email`)}
accessibilityHint=""
autoCapitalize="none"
autoComplete="email"
@ -151,7 +156,7 @@ export const Component = observer(function Component({}: {}) {
value={confirmationCode}
onChangeText={setConfirmationCode}
accessible={true}
accessibilityLabel="Confirmation code"
accessibilityLabel={_(msg`Confirmation code`)}
accessibilityHint=""
autoCapitalize="none"
autoComplete="off"
@ -175,9 +180,9 @@ export const Component = observer(function Component({}: {}) {
testID="requestChangeBtn"
type="primary"
onPress={onRequestChange}
accessibilityLabel="Request Change"
accessibilityLabel={_(msg`Request Change`)}
accessibilityHint=""
label="Request Change"
label={_(msg`Request Change`)}
labelContainerStyle={{justifyContent: 'center', padding: 4}}
labelStyle={[s.f18]}
/>
@ -187,9 +192,9 @@ export const Component = observer(function Component({}: {}) {
testID="confirmBtn"
type="primary"
onPress={onConfirm}
accessibilityLabel="Confirm Change"
accessibilityLabel={_(msg`Confirm Change`)}
accessibilityHint=""
label="Confirm Change"
label={_(msg`Confirm Change`)}
labelContainerStyle={{justifyContent: 'center', padding: 4}}
labelStyle={[s.f18]}
/>
@ -199,9 +204,9 @@ export const Component = observer(function Component({}: {}) {
testID="verifyBtn"
type="primary"
onPress={onVerify}
accessibilityLabel="Verify New Email"
accessibilityLabel={_(msg`Verify New Email`)}
accessibilityHint=""
label="Verify New Email"
label={_(msg`Verify New Email`)}
labelContainerStyle={{justifyContent: 'center', padding: 4}}
labelStyle={[s.f18]}
/>
@ -210,9 +215,9 @@ export const Component = observer(function Component({}: {}) {
testID="cancelBtn"
type="default"
onPress={() => closeModal()}
accessibilityLabel="Cancel"
accessibilityLabel={_(msg`Cancel`)}
accessibilityHint=""
label="Cancel"
label={_(msg`Cancel`)}
labelContainerStyle={{justifyContent: 'center', padding: 4}}
labelStyle={[s.f18]}
/>

View file

@ -22,6 +22,8 @@ import {useTheme} from 'lib/ThemeContext'
import {useAnalytics} from 'lib/analytics/analytics'
import {cleanError} from 'lib/strings/errors'
import {logger} from '#/logger'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
export const snapPoints = ['100%']
@ -31,6 +33,7 @@ export function Component({onChanged}: {onChanged: () => void}) {
const [error, setError] = useState<string>('')
const pal = usePalette('default')
const {track} = useAnalytics()
const {_} = useLingui()
const {closeModal} = useModalControls()
const [isProcessing, setProcessing] = useState<boolean>(false)
@ -141,7 +144,7 @@ export function Component({onChanged}: {onChanged: () => void}) {
<TouchableOpacity
onPress={onPressCancel}
accessibilityRole="button"
accessibilityLabel="Cancel change handle"
accessibilityLabel={_(msg`Cancel change handle`)}
accessibilityHint="Exits handle change process"
onAccessibilityEscape={onPressCancel}>
<Text type="lg" style={pal.textLight}>
@ -153,7 +156,7 @@ export function Component({onChanged}: {onChanged: () => void}) {
type="2xl-bold"
style={[styles.titleMiddle, pal.text]}
numberOfLines={1}>
Change Handle
<Trans>Change Handle</Trans>
</Text>
<View style={styles.titleRight}>
{isProcessing ? (
@ -163,7 +166,7 @@ export function Component({onChanged}: {onChanged: () => void}) {
testID="retryConnectButton"
onPress={onPressRetryConnect}
accessibilityRole="button"
accessibilityLabel="Retry change handle"
accessibilityLabel={_(msg`Retry change handle`)}
accessibilityHint={`Retries handle change to ${handle}`}>
<Text type="xl-bold" style={[pal.link, s.pr5]}>
Retry
@ -173,10 +176,10 @@ export function Component({onChanged}: {onChanged: () => void}) {
<TouchableOpacity
onPress={onPressSave}
accessibilityRole="button"
accessibilityLabel="Save handle change"
accessibilityLabel={_(msg`Save handle change`)}
accessibilityHint={`Saves handle change to ${handle}`}>
<Text type="2xl-medium" style={pal.link}>
Save
<Trans>Save</Trans>
</Text>
</TouchableOpacity>
) : undefined}
@ -234,6 +237,7 @@ function ProvidedHandleForm({
}) {
const pal = usePalette('default')
const theme = useTheme()
const {_} = useLingui()
// events
// =
@ -266,12 +270,12 @@ function ProvidedHandleForm({
onChangeText={onChangeHandle}
editable={!isProcessing}
accessible={true}
accessibilityLabel="Handle"
accessibilityLabel={_(msg`Handle`)}
accessibilityHint="Sets Bluesky username"
/>
</View>
<Text type="md" style={[pal.textLight, s.pl10, s.pt10]}>
Your full handle will be{' '}
<Trans>Your full handle will be </Trans>
<Text type="md-bold" style={pal.textLight}>
@{createFullHandle(handle, userDomain)}
</Text>
@ -280,9 +284,9 @@ function ProvidedHandleForm({
onPress={onToggleCustom}
accessibilityRole="button"
accessibilityHint="Hosting provider"
accessibilityLabel="Opens modal for using custom domain">
accessibilityLabel={_(msg`Opens modal for using custom domain`)}>
<Text type="md-medium" style={[pal.link, s.pl10, s.pt5]}>
I have my own domain
<Trans>I have my own domain</Trans>
</Text>
</TouchableOpacity>
</>
@ -314,6 +318,7 @@ function CustomHandleForm({
const palSecondary = usePalette('secondary')
const palError = usePalette('error')
const theme = useTheme()
const {_} = useLingui()
const [isVerifying, setIsVerifying] = React.useState(false)
const [error, setError] = React.useState<string>('')
const [isDNSForm, setDNSForm] = React.useState<boolean>(true)
@ -367,7 +372,7 @@ function CustomHandleForm({
return (
<>
<Text type="md" style={[pal.text, s.pb5, s.pl5]} nativeID="customDomain">
Enter the domain you want to use
<Trans>Enter the domain you want to use</Trans>
</Text>
<View style={[pal.btn, styles.textInputWrapper]}>
<FontAwesomeIcon
@ -385,7 +390,7 @@ function CustomHandleForm({
onChangeText={onChangeHandle}
editable={!isProcessing}
accessibilityLabelledBy="customDomain"
accessibilityLabel="Custom domain"
accessibilityLabel={_(msg`Custom domain`)}
accessibilityHint="Input your preferred hosting provider"
/>
</View>
@ -413,7 +418,7 @@ function CustomHandleForm({
{isDNSForm ? (
<>
<Text type="md" style={[pal.text, s.pb5, s.pl5]}>
Add the following DNS record to your domain:
<Trans>Add the following DNS record to your domain:</Trans>
</Text>
<View style={[styles.dnsTable, pal.btn]}>
<Text type="md-medium" style={[styles.dnsLabel, pal.text]}>
@ -451,7 +456,7 @@ function CustomHandleForm({
) : (
<>
<Text type="md" style={[pal.text, s.pb5, s.pl5]}>
Upload a text file to:
<Trans>Upload a text file to:</Trans>
</Text>
<View style={[styles.valueContainer, pal.btn]}>
<View style={[styles.dnsValue]}>
@ -483,7 +488,7 @@ function CustomHandleForm({
{canSave === true && (
<View style={[styles.message, palSecondary.view]}>
<Text type="md-medium" style={palSecondary.text}>
Domain verified!
<Trans>Domain verified!</Trans>
</Text>
</View>
)}
@ -511,7 +516,7 @@ function CustomHandleForm({
<View style={styles.spacer} />
<TouchableOpacity
onPress={onToggleCustom}
accessibilityLabel="Use default provider"
accessibilityLabel={_(msg`Use default provider`)}
accessibilityHint="Use bsky.social as hosting provider">
<Text type="md-medium" style={[pal.link, s.pl10, s.pt5]}>
Nevermind, create a handle for me

View file

@ -11,6 +11,8 @@ import {ErrorMessage} from '../util/error/ErrorMessage'
import {cleanError} from 'lib/strings/errors'
import {usePalette} from 'lib/hooks/usePalette'
import {isWeb} from 'platform/detection'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
import type {ConfirmModal} from '#/state/modals'
import {useModalControls} from '#/state/modals'
@ -26,6 +28,7 @@ export function Component({
cancelBtnText,
}: ConfirmModal) {
const pal = usePalette('default')
const {_} = useLingui()
const {closeModal} = useModalControls()
const [isProcessing, setIsProcessing] = useState<boolean>(false)
const [error, setError] = useState<string>('')
@ -69,7 +72,7 @@ export function Component({
onPress={onPress}
style={[styles.btn, confirmBtnStyle]}
accessibilityRole="button"
accessibilityLabel="Confirm"
accessibilityLabel={_(msg`Confirm`)}
accessibilityHint="">
<Text style={[s.white, s.bold, s.f18]}>
{confirmBtnText ?? 'Confirm'}
@ -82,7 +85,7 @@ export function Component({
onPress={onPressCancel}
style={[styles.btnCancel, s.mt10]}
accessibilityRole="button"
accessibilityLabel="Cancel"
accessibilityLabel={_(msg`Cancel`)}
accessibilityHint="">
<Text type="button-lg" style={pal.textLight}>
{cancelBtnText ?? 'Cancel'}

View file

@ -16,6 +16,8 @@ import {isIOS} from 'platform/detection'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import * as Toast from '../util/Toast'
import {logger} from '#/logger'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
export const snapPoints = ['90%']
@ -25,6 +27,7 @@ export const Component = observer(
const store = useStores()
const {isMobile} = useWebMediaQueries()
const pal = usePalette('default')
const {_} = useLingui()
const {closeModal} = useModalControls()
React.useEffect(() => {
@ -37,7 +40,9 @@ export const Component = observer(
return (
<View testID="contentFilteringModal" style={[pal.view, styles.container]}>
<Text style={[pal.text, styles.title]}>Content Filtering</Text>
<Text style={[pal.text, styles.title]}>
<Trans>Content Filtering</Trans>
</Text>
<ScrollView style={styles.scrollContainer}>
<AdultContentEnabledPref />
<ContentLabelPref
@ -71,14 +76,16 @@ export const Component = observer(
testID="sendReportBtn"
onPress={onPressDone}
accessibilityRole="button"
accessibilityLabel="Done"
accessibilityLabel={_(msg`Done`)}
accessibilityHint="">
<LinearGradient
colors={[gradients.blueLight.start, gradients.blueLight.end]}
start={{x: 0, y: 0}}
end={{x: 1, y: 1}}
style={[styles.btn]}>
<Text style={[s.white, s.bold, s.f18]}>Done</Text>
<Text style={[s.white, s.bold, s.f18]}>
<Trans>Done</Trans>
</Text>
</LinearGradient>
</Pressable>
</View>

View file

@ -24,6 +24,8 @@ import {useTheme} from 'lib/ThemeContext'
import {useAnalytics} from 'lib/analytics/analytics'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {cleanError, isNetworkError} from 'lib/strings/errors'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
const MAX_NAME = 64 // todo
@ -47,6 +49,7 @@ export function Component({
const pal = usePalette('default')
const theme = useTheme()
const {track} = useAnalytics()
const {_} = useLingui()
const activePurpose = useMemo(() => {
if (list?.data?.purpose) {
@ -164,14 +167,18 @@ export function Component({
]}
testID="createOrEditListModal">
<Text style={[styles.title, pal.text]}>
{list ? 'Edit' : 'New'} {purposeLabel} List
<Trans>
{list ? 'Edit' : 'New'} {purposeLabel} List
</Trans>
</Text>
{error !== '' && (
<View style={styles.errorContainer}>
<ErrorMessage message={error} />
</View>
)}
<Text style={[styles.label, pal.text]}>List Avatar</Text>
<Text style={[styles.label, pal.text]}>
<Trans>List Avatar</Trans>
</Text>
<View style={[styles.avi, {borderColor: pal.colors.background}]}>
<EditableUserAvatar
type="list"
@ -183,7 +190,7 @@ export function Component({
<View style={styles.form}>
<View>
<Text style={[styles.label, pal.text]} nativeID="list-name">
List Name
<Trans>List Name</Trans>
</Text>
<TextInput
testID="editNameInput"
@ -195,14 +202,14 @@ export function Component({
value={name}
onChangeText={v => setName(enforceLen(v, MAX_NAME))}
accessible={true}
accessibilityLabel="Name"
accessibilityLabel={_(msg`Name`)}
accessibilityHint=""
accessibilityLabelledBy="list-name"
/>
</View>
<View style={s.pb10}>
<Text style={[styles.label, pal.text]} nativeID="list-description">
Description
<Trans>Description</Trans>
</Text>
<TextInput
testID="editDescriptionInput"
@ -218,7 +225,7 @@ export function Component({
value={description}
onChangeText={v => setDescription(enforceLen(v, MAX_DESCRIPTION))}
accessible={true}
accessibilityLabel="Description"
accessibilityLabel={_(msg`Description`)}
accessibilityHint=""
accessibilityLabelledBy="list-description"
/>
@ -233,14 +240,16 @@ export function Component({
style={s.mt10}
onPress={onPressSave}
accessibilityRole="button"
accessibilityLabel="Save"
accessibilityLabel={_(msg`Save`)}
accessibilityHint="">
<LinearGradient
colors={[gradients.blueLight.start, gradients.blueLight.end]}
start={{x: 0, y: 0}}
end={{x: 1, y: 1}}
style={[styles.btn]}>
<Text style={[s.white, s.bold]}>Save</Text>
<Text style={[s.white, s.bold]}>
<Trans>Save</Trans>
</Text>
</LinearGradient>
</TouchableOpacity>
)}
@ -249,11 +258,13 @@ export function Component({
style={s.mt5}
onPress={onPressCancel}
accessibilityRole="button"
accessibilityLabel="Cancel"
accessibilityLabel={_(msg`Cancel`)}
accessibilityHint=""
onAccessibilityEscape={onPressCancel}>
<View style={[styles.btn]}>
<Text style={[s.black, s.bold, pal.text]}>Cancel</Text>
<Text style={[s.black, s.bold, pal.text]}>
<Trans>Cancel</Trans>
</Text>
</View>
</TouchableOpacity>
</View>

View file

@ -17,6 +17,8 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {cleanError} from 'lib/strings/errors'
import {resetToTab} from '../../../Navigation'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
export const snapPoints = ['60%']
@ -25,6 +27,7 @@ export function Component({}: {}) {
const pal = usePalette('default')
const theme = useTheme()
const store = useStores()
const {_} = useLingui()
const {closeModal} = useModalControls()
const {isMobile} = useWebMediaQueries()
const [isEmailSent, setIsEmailSent] = React.useState<boolean>(false)
@ -71,7 +74,7 @@ export function Component({}: {}) {
<View style={[styles.innerContainer, pal.view]}>
<View style={[styles.titleContainer, pal.view]}>
<Text type="title-xl" style={[s.textCenter, pal.text]}>
Delete Account
<Trans>Delete Account</Trans>
</Text>
<View style={[pal.view, s.flexRow]}>
<Text type="title-xl" style={[pal.text, s.bold]}>
@ -95,8 +98,10 @@ export function Component({}: {}) {
{!isEmailSent ? (
<>
<Text type="lg" style={[styles.description, pal.text]}>
For security reasons, we'll need to send a confirmation code to
your email address.
<Trans>
For security reasons, we'll need to send a confirmation code to
your email address.
</Trans>
</Text>
{error ? (
<View style={s.mt10}>
@ -113,7 +118,7 @@ export function Component({}: {}) {
style={styles.mt20}
onPress={onPressSendEmail}
accessibilityRole="button"
accessibilityLabel="Send email"
accessibilityLabel={_(msg`Send email`)}
accessibilityHint="Sends email with confirmation code for account deletion">
<LinearGradient
colors={[
@ -124,7 +129,7 @@ export function Component({}: {}) {
end={{x: 1, y: 1}}
style={[styles.btn]}>
<Text type="button-lg" style={[s.white, s.bold]}>
Send Email
<Trans>Send Email</Trans>
</Text>
</LinearGradient>
</TouchableOpacity>
@ -132,11 +137,11 @@ export function Component({}: {}) {
style={[styles.btn, s.mt10]}
onPress={onCancel}
accessibilityRole="button"
accessibilityLabel="Cancel account deletion"
accessibilityLabel={_(msg`Cancel account deletion`)}
accessibilityHint=""
onAccessibilityEscape={onCancel}>
<Text type="button-lg" style={pal.textLight}>
Cancel
<Trans>Cancel</Trans>
</Text>
</TouchableOpacity>
</>
@ -149,8 +154,10 @@ export function Component({}: {}) {
type="lg"
style={styles.description}
nativeID="confirmationCode">
Check your inbox for an email with the confirmation code to enter
below:
<Trans>
Check your inbox for an email with the confirmation code to
enter below:
</Trans>
</Text>
<TextInput
style={[styles.textInput, pal.borderDark, pal.text, styles.mb20]}
@ -160,11 +167,11 @@ export function Component({}: {}) {
value={confirmCode}
onChangeText={setConfirmCode}
accessibilityLabelledBy="confirmationCode"
accessibilityLabel="Confirmation code"
accessibilityLabel={_(msg`Confirmation code`)}
accessibilityHint="Input confirmation code for account deletion"
/>
<Text type="lg" style={styles.description} nativeID="password">
Please enter your password as well:
<Trans>Please enter your password as well:</Trans>
</Text>
<TextInput
style={[styles.textInput, pal.borderDark, pal.text]}
@ -175,7 +182,7 @@ export function Component({}: {}) {
value={password}
onChangeText={setPassword}
accessibilityLabelledBy="password"
accessibilityLabel="Password"
accessibilityLabel={_(msg`Password`)}
accessibilityHint="Input password for account deletion"
/>
{error ? (
@ -193,21 +200,21 @@ export function Component({}: {}) {
style={[styles.btn, styles.evilBtn, styles.mt20]}
onPress={onPressConfirmDelete}
accessibilityRole="button"
accessibilityLabel="Confirm delete account"
accessibilityLabel={_(msg`Confirm delete account`)}
accessibilityHint="">
<Text type="button-lg" style={[s.white, s.bold]}>
Delete my account
<Trans>Delete my account</Trans>
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.btn, s.mt10]}
onPress={onCancel}
accessibilityRole="button"
accessibilityLabel="Cancel account deletion"
accessibilityLabel={_(msg`Cancel account deletion`)}
accessibilityHint="Exits account deletion process"
onAccessibilityEscape={onCancel}>
<Text type="button-lg" style={pal.textLight}>
Cancel
<Trans>Cancel</Trans>
</Text>
</TouchableOpacity>
</>

View file

@ -18,6 +18,8 @@ import {Slider} from '@miblanchard/react-native-slider'
import {MaterialIcons} from '@expo/vector-icons'
import {observer} from 'mobx-react-lite'
import {getKeys} from 'lib/type-assertions'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
export const snapPoints = ['80%']
@ -52,6 +54,7 @@ export const Component = observer(function EditImageImpl({
}: Props) {
const pal = usePalette('default')
const theme = useTheme()
const {_} = useLingui()
const windowDimensions = useWindowDimensions()
const {isMobile} = useWebMediaQueries()
const {closeModal} = useModalControls()
@ -200,7 +203,9 @@ export const Component = observer(function EditImageImpl({
paddingHorizontal: isMobile ? 16 : undefined,
},
]}>
<Text style={[styles.title, pal.text]}>Edit image</Text>
<Text style={[styles.title, pal.text]}>
<Trans>Edit image</Trans>
</Text>
<View style={[styles.gap18, s.flexRow]}>
<View>
<View
@ -228,7 +233,7 @@ export const Component = observer(function EditImageImpl({
<View>
{!isMobile ? (
<Text type="sm-bold" style={pal.text}>
Ratios
<Trans>Ratios</Trans>
</Text>
) : null}
<View style={imgControlStyles}>
@ -263,7 +268,7 @@ export const Component = observer(function EditImageImpl({
</View>
{!isMobile ? (
<Text type="sm-bold" style={[pal.text, styles.subsection]}>
Transformations
<Trans>Transformations</Trans>
</Text>
) : null}
<View style={imgControlStyles}>
@ -291,7 +296,7 @@ export const Component = observer(function EditImageImpl({
</View>
<View style={[styles.gap18, styles.bottomSection, pal.border]}>
<Text type="sm-bold" style={pal.text} nativeID="alt-text">
Accessibility
<Trans>Accessibility</Trans>
</Text>
<TextInput
testID="altTextImageInput"
@ -307,7 +312,7 @@ export const Component = observer(function EditImageImpl({
multiline
value={altText}
onChangeText={text => setAltText(enforceLen(text, MAX_ALT_TEXT))}
accessibilityLabel="Alt text"
accessibilityLabel={_(msg`Alt text`)}
accessibilityHint=""
accessibilityLabelledBy="alt-text"
/>
@ -315,7 +320,7 @@ export const Component = observer(function EditImageImpl({
<View style={styles.btns}>
<Pressable onPress={onPressCancel} accessibilityRole="button">
<Text type="xl" style={pal.link}>
Cancel
<Trans>Cancel</Trans>
</Text>
</Pressable>
<Pressable onPress={onPressSave} accessibilityRole="button">
@ -325,7 +330,7 @@ export const Component = observer(function EditImageImpl({
end={{x: 1, y: 1}}
style={[styles.btn]}>
<Text type="xl-medium" style={s.white}>
Done
<Trans>Done</Trans>
</Text>
</LinearGradient>
</Pressable>

View file

@ -26,6 +26,8 @@ import {useAnalytics} from 'lib/analytics/analytics'
import {cleanError, isNetworkError} from 'lib/strings/errors'
import Animated, {FadeOut} from 'react-native-reanimated'
import {isWeb} from 'platform/detection'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
const AnimatedTouchableOpacity =
@ -44,6 +46,7 @@ export function Component({
const pal = usePalette('default')
const theme = useTheme()
const {track} = useAnalytics()
const {_} = useLingui()
const {closeModal} = useModalControls()
const [isProcessing, setProcessing] = useState<boolean>(false)
@ -151,7 +154,9 @@ export function Component({
return (
<KeyboardAvoidingView style={s.flex1} behavior="height">
<ScrollView style={[pal.view]} testID="editProfileModal">
<Text style={[styles.title, pal.text]}>Edit my profile</Text>
<Text style={[styles.title, pal.text]}>
<Trans>Edit my profile</Trans>
</Text>
<View style={styles.photos}>
<UserBanner
banner={userBanner}
@ -172,7 +177,9 @@ export function Component({
)}
<View style={styles.form}>
<View>
<Text style={[styles.label, pal.text]}>Display Name</Text>
<Text style={[styles.label, pal.text]}>
<Trans>Display Name</Trans>
</Text>
<TextInput
testID="editProfileDisplayNameInput"
style={[styles.textInput, pal.border, pal.text]}
@ -183,12 +190,14 @@ export function Component({
setDisplayName(enforceLen(v, MAX_DISPLAY_NAME))
}
accessible={true}
accessibilityLabel="Display name"
accessibilityLabel={_(msg`Display name`)}
accessibilityHint="Edit your display name"
/>
</View>
<View style={s.pb10}>
<Text style={[styles.label, pal.text]}>Description</Text>
<Text style={[styles.label, pal.text]}>
<Trans>Description</Trans>
</Text>
<TextInput
testID="editProfileDescriptionInput"
style={[styles.textArea, pal.border, pal.text]}
@ -199,7 +208,7 @@ export function Component({
value={description}
onChangeText={v => setDescription(enforceLen(v, MAX_DESCRIPTION))}
accessible={true}
accessibilityLabel="Description"
accessibilityLabel={_(msg`Description`)}
accessibilityHint="Edit your profile description"
/>
</View>
@ -213,14 +222,16 @@ export function Component({
style={s.mt10}
onPress={onPressSave}
accessibilityRole="button"
accessibilityLabel="Save"
accessibilityLabel={_(msg`Save`)}
accessibilityHint="Saves any changes to your profile">
<LinearGradient
colors={[gradients.blueLight.start, gradients.blueLight.end]}
start={{x: 0, y: 0}}
end={{x: 1, y: 1}}
style={[styles.btn]}>
<Text style={[s.white, s.bold]}>Save Changes</Text>
<Text style={[s.white, s.bold]}>
<Trans>Save Changes</Trans>
</Text>
</LinearGradient>
</TouchableOpacity>
)}
@ -231,11 +242,13 @@ export function Component({
style={s.mt5}
onPress={onPressCancel}
accessibilityRole="button"
accessibilityLabel="Cancel profile editing"
accessibilityLabel={_(msg`Cancel profile editing`)}
accessibilityHint=""
onAccessibilityEscape={onPressCancel}>
<View style={[styles.btn]}>
<Text style={[s.black, s.bold, pal.text]}>Cancel</Text>
<Text style={[s.black, s.bold, pal.text]}>
<Trans>Cancel</Trans>
</Text>
</View>
</AnimatedTouchableOpacity>
)}

View file

@ -15,6 +15,7 @@ import {ScrollView} from './util'
import {usePalette} from 'lib/hooks/usePalette'
import {isWeb} from 'platform/detection'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {Trans} from '@lingui/macro'
import {useModalControls} from '#/state/modals'
import {useInvitesState, useInvitesAPI} from '#/state/invites'
import {UserInfoText} from '../util/UserInfoText'
@ -38,8 +39,10 @@ export function Component({}: {}) {
<View style={[styles.container, pal.view]} testID="inviteCodesModal">
<View style={[styles.empty, pal.viewLight]}>
<Text type="lg" style={[pal.text, styles.emptyText]}>
You don't have any invite codes yet! We'll send you some when you've
been on Bluesky for a little longer.
<Trans>
You don't have any invite codes yet! We'll send you some when
you've been on Bluesky for a little longer.
</Trans>
</Text>
</View>
<View style={styles.flex1} />
@ -63,10 +66,12 @@ export function Component({}: {}) {
return (
<View style={[styles.container, pal.view]} testID="inviteCodesModal">
<Text type="title-xl" style={[styles.title, pal.text]}>
Invite a Friend
<Trans>Invite a Friend</Trans>
</Text>
<Text type="lg" style={[styles.description, pal.text]}>
Each code works once. You'll receive more invite codes periodically.
<Trans>
Each code works once. You'll receive more invite codes periodically.
</Trans>
</Text>
<ScrollView style={[styles.scrollContainer, pal.border]}>
{store.me.invites.map((invite, i) => (
@ -138,7 +143,9 @@ const InviteCode = observer(function InviteCodeImpl({
</Text>
<View style={styles.flex1} />
{!used && invitesState.copiedInvites.includes(invite.code) && (
<Text style={[pal.textLight, styles.codeCopied]}>Copied</Text>
<Text style={[pal.textLight, styles.codeCopied]}>
<Trans>Copied</Trans>
</Text>
)}
{!used && (
<FontAwesomeIcon
@ -154,7 +161,9 @@ const InviteCode = observer(function InviteCodeImpl({
gap: 8,
paddingTop: 6,
}}>
<Text style={pal.text}>Used by:</Text>
<Text style={pal.text}>
<Trans>Used by:</Trans>
</Text>
{invite.uses.map(use => (
<Link
key={use.usedBy}

View file

@ -10,6 +10,8 @@ import {usePalette} from 'lib/hooks/usePalette'
import {isWeb} from 'platform/detection'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {isPossiblyAUrl, splitApexDomain} from 'lib/strings/url-helpers'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
export const snapPoints = ['50%']
@ -24,6 +26,7 @@ export const Component = observer(function Component({
const pal = usePalette('default')
const {closeModal} = useModalControls()
const {isMobile} = useWebMediaQueries()
const {_} = useLingui()
const potentiallyMisleading = isPossiblyAUrl(text)
const onPressVisit = () => {
@ -45,26 +48,26 @@ export const Component = observer(function Component({
size={18}
/>
<Text type="title-lg" style={[pal.text, styles.title]}>
Potentially Misleading Link
<Trans>Potentially Misleading Link</Trans>
</Text>
</>
) : (
<Text type="title-lg" style={[pal.text, styles.title]}>
Leaving Bluesky
<Trans>Leaving Bluesky</Trans>
</Text>
)}
</View>
<View style={{gap: 10}}>
<Text type="lg" style={pal.text}>
This link is taking you to the following website:
<Trans>This link is taking you to the following website:</Trans>
</Text>
<LinkBox href={href} />
{potentiallyMisleading && (
<Text type="lg" style={pal.text}>
Make sure this is where you intend to go!
<Trans>Make sure this is where you intend to go!</Trans>
</Text>
)}
</View>
@ -74,7 +77,7 @@ export const Component = observer(function Component({
testID="confirmBtn"
type="primary"
onPress={onPressVisit}
accessibilityLabel="Visit Site"
accessibilityLabel={_(msg`Visit Site`)}
accessibilityHint=""
label="Visit Site"
labelContainerStyle={{justifyContent: 'center', padding: 4}}
@ -84,7 +87,7 @@ export const Component = observer(function Component({
testID="cancelBtn"
type="default"
onPress={() => closeModal()}
accessibilityLabel="Cancel"
accessibilityLabel={_(msg`Cancel`)}
accessibilityHint=""
label="Cancel"
labelContainerStyle={{justifyContent: 'center', padding: 4}}

View file

@ -26,6 +26,8 @@ import {cleanError} from 'lib/strings/errors'
import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles'
import {HITSLOP_20} from '#/lib/constants'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
export const snapPoints = ['90%']
@ -39,6 +41,7 @@ export const Component = observer(function Component({
}) {
const pal = usePalette('default')
const store = useStores()
const {_} = useLingui()
const {closeModal} = useModalControls()
const {isMobile} = useWebMediaQueries()
const [query, setQuery] = useState('')
@ -85,7 +88,7 @@ export const Component = observer(function Component({
value={query}
onChangeText={onChangeQuery}
accessible={true}
accessibilityLabel="Search"
accessibilityLabel={_(msg`Search`)}
accessibilityHint=""
autoFocus
autoCapitalize="none"
@ -97,7 +100,7 @@ export const Component = observer(function Component({
<Pressable
onPress={onPressCancelSearch}
accessibilityRole="button"
accessibilityLabel="Cancel search"
accessibilityLabel={_(msg`Cancel search`)}
accessibilityHint="Exits inputting search query"
onAccessibilityEscape={onPressCancelSearch}
hitSlop={HITSLOP_20}>
@ -136,7 +139,7 @@ export const Component = observer(function Component({
pal.textLight,
{paddingHorizontal: 12, paddingVertical: 16},
]}>
No results found for {autocompleteView.prefix}
<Trans>No results found for {autocompleteView.prefix}</Trans>
</Text>
)}
</ScrollView>
@ -149,7 +152,7 @@ export const Component = observer(function Component({
testID="doneBtn"
type="default"
onPress={() => closeModal()}
accessibilityLabel="Done"
accessibilityLabel={_(msg`Done`)}
accessibilityHint=""
label="Done"
labelContainerStyle={{justifyContent: 'center', padding: 4}}

View file

@ -6,6 +6,8 @@ import {Text} from '../util/text/Text'
import {usePalette} from 'lib/hooks/usePalette'
import {RepostIcon} from 'lib/icons'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
export const snapPoints = [250]
@ -21,6 +23,7 @@ export function Component({
// TODO: Add author into component
}) {
const pal = usePalette('default')
const {_} = useLingui()
const {closeModal} = useModalControls()
const onPress = async () => {
closeModal()
@ -38,7 +41,7 @@ export function Component({
accessibilityHint={isReposted ? 'Remove repost' : 'Repost '}>
<RepostIcon strokeWidth={2} size={24} style={s.blue3} />
<Text type="title-lg" style={[styles.actionBtnLabel, pal.text]}>
{!isReposted ? 'Repost' : 'Undo repost'}
<Trans>{!isReposted ? 'Repost' : 'Undo repost'}</Trans>
</Text>
</TouchableOpacity>
<TouchableOpacity
@ -46,11 +49,11 @@ export function Component({
style={[styles.actionBtn]}
onPress={onQuote}
accessibilityRole="button"
accessibilityLabel="Quote post"
accessibilityLabel={_(msg`Quote post`)}
accessibilityHint="">
<FontAwesomeIcon icon="quote-left" size={24} style={s.blue3} />
<Text type="title-lg" style={[styles.actionBtnLabel, pal.text]}>
Quote Post
<Trans>Quote Post</Trans>
</Text>
</TouchableOpacity>
</View>
@ -58,7 +61,7 @@ export function Component({
testID="cancelBtn"
onPress={onPress}
accessibilityRole="button"
accessibilityLabel="Cancel quote post"
accessibilityLabel={_(msg`Cancel quote post`)}
accessibilityHint=""
onAccessibilityEscape={onPress}>
<LinearGradient
@ -66,7 +69,9 @@ export function Component({
start={{x: 0, y: 0}}
end={{x: 1, y: 1}}
style={[styles.btn]}>
<Text style={[s.white, s.bold, s.f18]}>Cancel</Text>
<Text style={[s.white, s.bold, s.f18]}>
<Trans>Cancel</Trans>
</Text>
</LinearGradient>
</TouchableOpacity>
</View>

View file

@ -9,6 +9,8 @@ import {isWeb} from 'platform/detection'
import {Button} from '../util/forms/Button'
import {SelectableBtn} from '../util/forms/SelectableBtn'
import {ScrollView} from 'view/com/modals/util'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
const ADULT_CONTENT_LABELS = ['sexual', 'nudity', 'porn']
@ -28,6 +30,7 @@ export const Component = observer(function Component({
const {closeModal} = useModalControls()
const {isMobile} = useWebMediaQueries()
const [selected, setSelected] = useState(labels)
const {_} = useLingui()
const toggleAdultLabel = (label: string) => {
const hadLabel = selected.includes(label)
@ -51,7 +54,7 @@ export const Component = observer(function Component({
<View testID="selfLabelModal" style={[pal.view, styles.container]}>
<View style={styles.titleSection}>
<Text type="title-lg" style={[pal.text, styles.title]}>
Add a content warning
<Trans>Add a content warning</Trans>
</Text>
</View>
@ -70,7 +73,7 @@ export const Component = observer(function Component({
paddingBottom: 8,
}}>
<Text type="title" style={pal.text}>
Adult Content
<Trans>Adult Content</Trans>
</Text>
{hasAdultSelection ? (
<Button
@ -78,7 +81,7 @@ export const Component = observer(function Component({
onPress={removeAdultLabel}
style={{paddingTop: 0, paddingBottom: 0, paddingRight: 0}}>
<Text type="md" style={pal.link}>
Remove
<Trans>Remove</Trans>
</Text>
</Button>
) : null}
@ -116,23 +119,25 @@ export const Component = observer(function Component({
<Text style={[pal.text, styles.adultExplainer]}>
{selected.includes('sexual') ? (
<>Pictures meant for adults.</>
<Trans>Pictures meant for adults.</Trans>
) : selected.includes('nudity') ? (
<>Artistic or non-erotic nudity.</>
<Trans>Artistic or non-erotic nudity.</Trans>
) : selected.includes('porn') ? (
<>Sexual activity or erotic nudity.</>
<Trans>Sexual activity or erotic nudity.</Trans>
) : (
<>If none are selected, suitable for all ages.</>
<Trans>If none are selected, suitable for all ages.</Trans>
)}
</Text>
</>
) : (
<View>
<Text style={[pal.textLight]}>
<Text type="md-bold" style={[pal.textLight]}>
Not Applicable
<Text type="md-bold" style={[pal.textLight, s.mr5]}>
<Trans>Not Applicable.</Trans>
</Text>
. This warning is only available for posts with media attached.
<Trans>
This warning is only available for posts with media attached.
</Trans>
</Text>
</View>
)}
@ -147,9 +152,11 @@ export const Component = observer(function Component({
}}
style={styles.btn}
accessibilityRole="button"
accessibilityLabel="Confirm"
accessibilityLabel={_(msg`Confirm`)}
accessibilityHint="">
<Text style={[s.white, s.bold, s.f18]}>Done</Text>
<Text style={[s.white, s.bold, s.f18]}>
<Trans>Done</Trans>
</Text>
</TouchableOpacity>
</View>
</View>

View file

@ -11,6 +11,8 @@ import {usePalette} from 'lib/hooks/usePalette'
import {useTheme} from 'lib/ThemeContext'
import {LOCAL_DEV_SERVICE, STAGING_SERVICE, PROD_SERVICE} from 'state/index'
import {LOGIN_INCLUDE_DEV_SERVERS} from 'lib/build-flags'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
export const snapPoints = ['80%']
@ -19,6 +21,7 @@ export function Component({onSelect}: {onSelect: (url: string) => void}) {
const theme = useTheme()
const pal = usePalette('default')
const [customUrl, setCustomUrl] = useState<string>('')
const {_} = useLingui()
const {closeModal} = useModalControls()
const doSelect = (url: string) => {
@ -32,7 +35,7 @@ export function Component({onSelect}: {onSelect: (url: string) => void}) {
return (
<View style={[pal.view, s.flex1]} testID="serverInputModal">
<Text type="2xl-bold" style={[pal.text, s.textCenter]}>
Choose Service
<Trans>Choose Service</Trans>
</Text>
<ScrollView style={styles.inner}>
<View style={styles.group}>
@ -43,7 +46,9 @@ export function Component({onSelect}: {onSelect: (url: string) => void}) {
style={styles.btn}
onPress={() => doSelect(LOCAL_DEV_SERVICE)}
accessibilityRole="button">
<Text style={styles.btnText}>Local dev server</Text>
<Text style={styles.btnText}>
<Trans>Local dev server</Trans>
</Text>
<FontAwesomeIcon
icon="arrow-right"
style={s.white as FontAwesomeIconStyle}
@ -53,7 +58,9 @@ export function Component({onSelect}: {onSelect: (url: string) => void}) {
style={styles.btn}
onPress={() => doSelect(STAGING_SERVICE)}
accessibilityRole="button">
<Text style={styles.btnText}>Staging</Text>
<Text style={styles.btnText}>
<Trans>Staging</Trans>
</Text>
<FontAwesomeIcon
icon="arrow-right"
style={s.white as FontAwesomeIconStyle}
@ -65,9 +72,11 @@ export function Component({onSelect}: {onSelect: (url: string) => void}) {
style={styles.btn}
onPress={() => doSelect(PROD_SERVICE)}
accessibilityRole="button"
accessibilityLabel="Select Bluesky Social"
accessibilityLabel={_(msg`Select Bluesky Social`)}
accessibilityHint="Sets Bluesky Social as your service provider">
<Text style={styles.btnText}>Bluesky.Social</Text>
<Text style={styles.btnText}>
<Trans>Bluesky.Social</Trans>
</Text>
<FontAwesomeIcon
icon="arrow-right"
style={s.white as FontAwesomeIconStyle}
@ -75,7 +84,9 @@ export function Component({onSelect}: {onSelect: (url: string) => void}) {
</TouchableOpacity>
</View>
<View style={styles.group}>
<Text style={[pal.text, styles.label]}>Other service</Text>
<Text style={[pal.text, styles.label]}>
<Trans>Other service</Trans>
</Text>
<View style={s.flexRow}>
<TextInput
testID="customServerTextInput"
@ -88,7 +99,7 @@ export function Component({onSelect}: {onSelect: (url: string) => void}) {
keyboardAppearance={theme.colorScheme}
value={customUrl}
onChangeText={setCustomUrl}
accessibilityLabel="Custom domain"
accessibilityLabel={_(msg`Custom domain`)}
// TODO: Simplify this wording further to be understandable by everyone
accessibilityHint="Use your domain as your Bluesky client service provider"
/>

View file

@ -17,12 +17,15 @@ import {Link} from '../util/Link'
import {makeProfileLink} from 'lib/routes/links'
import {BottomSheetScrollView} from '@gorhom/bottom-sheet'
import {Haptics} from 'lib/haptics'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
export const snapPoints = ['40%', '90%']
export function Component({}: {}) {
const pal = usePalette('default')
const {track} = useAnalytics()
const {_: _lingui} = useLingui()
const store = useStores()
const [isSwitching, _, onPressSwitchAccount] = useAccountSwitcher()
@ -41,7 +44,7 @@ export function Component({}: {}) {
style={[styles.container, pal.view]}
contentContainerStyle={[styles.innerContainer, pal.view]}>
<Text type="title-xl" style={[styles.title, pal.text]}>
Switch Account
<Trans>Switch Account</Trans>
</Text>
{isSwitching ? (
<View style={[pal.view, styles.linkCard]}>
@ -65,10 +68,10 @@ export function Component({}: {}) {
testID="signOutBtn"
onPress={isSwitching ? undefined : onPressSignout}
accessibilityRole="button"
accessibilityLabel="Sign out"
accessibilityLabel={_lingui(msg`Sign out`)}
accessibilityHint={`Signs ${store.me.displayName} out of Bluesky`}>
<Text type="lg" style={pal.link}>
Sign out
<Trans>Sign out</Trans>
</Text>
</TouchableOpacity>
</View>

View file

@ -21,6 +21,8 @@ import {usePalette} from 'lib/hooks/usePalette'
import {isWeb, isAndroid} from 'platform/detection'
import isEqual from 'lodash.isequal'
import {logger} from '#/logger'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
export const snapPoints = ['fullscreen']
@ -39,6 +41,7 @@ export const Component = observer(function UserAddRemoveListsImpl({
const store = useStores()
const {closeModal} = useModalControls()
const pal = usePalette('default')
const {_} = useLingui()
const palPrimary = usePalette('primary')
const palInverted = usePalette('inverted')
const [originalSelections, setOriginalSelections] = React.useState<string[]>(
@ -181,7 +184,7 @@ export const Component = observer(function UserAddRemoveListsImpl({
return (
<View testID="userAddRemoveListsModal" style={s.hContentRegion}>
<Text style={[styles.title, pal.text]}>
Update {displayName} in Lists
<Trans>Update {displayName} in Lists</Trans>
</Text>
<ListsList
listsList={listsList}
@ -195,7 +198,7 @@ export const Component = observer(function UserAddRemoveListsImpl({
type="default"
onPress={onPressCancel}
style={styles.footerBtn}
accessibilityLabel="Cancel"
accessibilityLabel={_(msg`Cancel`)}
accessibilityHint=""
onAccessibilityEscape={onPressCancel}
label="Cancel"
@ -206,7 +209,7 @@ export const Component = observer(function UserAddRemoveListsImpl({
type="primary"
onPress={onPressSave}
style={styles.footerBtn}
accessibilityLabel="Save changes"
accessibilityLabel={_(msg`Save changes`)}
accessibilityHint=""
onAccessibilityEscape={onPressSave}
label="Save Changes"

View file

@ -20,6 +20,8 @@ import {usePalette} from 'lib/hooks/usePalette'
import {isWeb} from 'platform/detection'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {cleanError} from 'lib/strings/errors'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
export const snapPoints = ['90%']
@ -37,6 +39,7 @@ export const Component = observer(function Component({
}) {
const pal = usePalette('default')
const store = useStores()
const {_} = useLingui()
const [stage, setStage] = useState<Stages>(
showReminder ? Stages.Reminder : Stages.Email,
)
@ -98,21 +101,21 @@ export const Component = observer(function Component({
<Text type="lg" style={[pal.textLight, {marginBottom: 10}]}>
{stage === Stages.Reminder ? (
<>
<Trans>
Your email has not yet been verified. This is an important
security step which we recommend.
</>
</Trans>
) : stage === Stages.Email ? (
<>
<Trans>
This is important in case you ever need to change your email or
reset your password.
</>
</Trans>
) : stage === Stages.ConfirmCode ? (
<>
<Trans>
An email has been sent to{' '}
{store.session.currentSession?.email || ''}. It includes a
confirmation code which you can enter below.
</>
</Trans>
) : (
''
)}
@ -132,7 +135,7 @@ export const Component = observer(function Component({
</View>
<Pressable
accessibilityRole="link"
accessibilityLabel="Change my email"
accessibilityLabel={_(msg`Change my email`)}
accessibilityHint=""
onPress={onEmailIncorrect}
style={styles.changeEmailLink}>
@ -150,7 +153,7 @@ export const Component = observer(function Component({
value={confirmationCode}
onChangeText={setConfirmationCode}
accessible={true}
accessibilityLabel="Confirmation code"
accessibilityLabel={_(msg`Confirmation code`)}
accessibilityHint=""
autoCapitalize="none"
autoComplete="off"
@ -174,7 +177,7 @@ export const Component = observer(function Component({
testID="getStartedBtn"
type="primary"
onPress={() => setStage(Stages.Email)}
accessibilityLabel="Get Started"
accessibilityLabel={_(msg`Get Started`)}
accessibilityHint=""
label="Get Started"
labelContainerStyle={{justifyContent: 'center', padding: 4}}
@ -187,7 +190,7 @@ export const Component = observer(function Component({
testID="sendEmailBtn"
type="primary"
onPress={onSendEmail}
accessibilityLabel="Send Confirmation Email"
accessibilityLabel={_(msg`Send Confirmation Email`)}
accessibilityHint=""
label="Send Confirmation Email"
labelContainerStyle={{
@ -199,7 +202,7 @@ export const Component = observer(function Component({
<Button
testID="haveCodeBtn"
type="default"
accessibilityLabel="I have a code"
accessibilityLabel={_(msg`I have a code`)}
accessibilityHint=""
label="I have a confirmation code"
labelContainerStyle={{
@ -216,7 +219,7 @@ export const Component = observer(function Component({
testID="confirmBtn"
type="primary"
onPress={onConfirm}
accessibilityLabel="Confirm"
accessibilityLabel={_(msg`Confirm`)}
accessibilityHint=""
label="Confirm"
labelContainerStyle={{justifyContent: 'center', padding: 4}}

View file

@ -17,6 +17,8 @@ import {usePalette} from 'lib/hooks/usePalette'
import {useTheme} from 'lib/ThemeContext'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {cleanError} from 'lib/strings/errors'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
export const snapPoints = ['80%']
@ -24,6 +26,7 @@ export const snapPoints = ['80%']
export function Component({}: {}) {
const pal = usePalette('default')
const theme = useTheme()
const {_} = useLingui()
const {closeModal} = useModalControls()
const [email, setEmail] = React.useState<string>('')
const [isEmailSent, setIsEmailSent] = React.useState<boolean>(false)
@ -61,12 +64,14 @@ export function Component({}: {}) {
<View style={[styles.container, pal.view]}>
<View style={[styles.innerContainer, pal.view]}>
<Text type="title-xl" style={[styles.title, pal.text]}>
Join the waitlist
<Trans>Join the waitlist</Trans>
</Text>
<Text type="lg" style={[styles.description, pal.text]}>
Bluesky uses invites to build a healthier community. If you don't know
anybody with an invite, you can sign up for the waitlist and we'll
send one soon.
<Trans>
Bluesky uses invites to build a healthier community. If you don't
know anybody with an invite, you can sign up for the waitlist and
we'll send one soon.
</Trans>
</Text>
<TextInput
style={[styles.textInput, pal.borderDark, pal.text, s.mb10, s.mt10]}
@ -80,7 +85,7 @@ export function Component({}: {}) {
onSubmitEditing={onPressSignup}
enterKeyHint="done"
accessible={true}
accessibilityLabel="Email"
accessibilityLabel={_(msg`Email`)}
accessibilityHint="Input your email to get on the Bluesky waitlist"
/>
{error ? (
@ -99,7 +104,9 @@ export function Component({}: {}) {
style={pal.text as FontAwesomeIconStyle}
/>
<Text style={[s.ml10, pal.text]}>
Your email has been saved! We&apos;ll be in touch soon.
<Trans>
Your email has been saved! We&apos;ll be in touch soon.
</Trans>
</Text>
</View>
) : (
@ -114,7 +121,7 @@ export function Component({}: {}) {
end={{x: 1, y: 1}}
style={[styles.btn]}>
<Text type="button-lg" style={[s.white, s.bold]}>
Join Waitlist
<Trans>Join Waitlist</Trans>
</Text>
</LinearGradient>
</TouchableOpacity>
@ -122,11 +129,11 @@ export function Component({}: {}) {
style={[styles.btn, s.mt10]}
onPress={onCancel}
accessibilityRole="button"
accessibilityLabel="Cancel waitlist signup"
accessibilityLabel={_(msg`Cancel waitlist signup`)}
accessibilityHint={`Exits signing up for waitlist with ${email}`}
onAccessibilityEscape={onCancel}>
<Text type="button-lg" style={pal.textLight}>
Cancel
<Trans>Cancel</Trans>
</Text>
</TouchableOpacity>
</>

View file

@ -10,6 +10,8 @@ import {s, gradients} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
import {SquareIcon, RectWideIcon, RectTallIcon} from 'lib/icons'
import {Image as RNImage} from 'react-native-image-crop-picker'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
enum AspectRatio {
@ -35,6 +37,7 @@ export function Component({
}) {
const {closeModal} = useModalControls()
const pal = usePalette('default')
const {_} = useLingui()
const [as, setAs] = React.useState<AspectRatio>(AspectRatio.Square)
const [scale, setScale] = React.useState<number>(1)
const editorRef = React.useRef<ImageEditor>(null)
@ -96,7 +99,7 @@ export function Component({
<TouchableOpacity
onPress={doSetAs(AspectRatio.Wide)}
accessibilityRole="button"
accessibilityLabel="Wide"
accessibilityLabel={_(msg`Wide`)}
accessibilityHint="Sets image aspect ratio to wide">
<RectWideIcon
size={24}
@ -106,7 +109,7 @@ export function Component({
<TouchableOpacity
onPress={doSetAs(AspectRatio.Tall)}
accessibilityRole="button"
accessibilityLabel="Tall"
accessibilityLabel={_(msg`Tall`)}
accessibilityHint="Sets image aspect ratio to tall">
<RectTallIcon
size={24}
@ -116,7 +119,7 @@ export function Component({
<TouchableOpacity
onPress={doSetAs(AspectRatio.Square)}
accessibilityRole="button"
accessibilityLabel="Square"
accessibilityLabel={_(msg`Square`)}
accessibilityHint="Sets image aspect ratio to square">
<SquareIcon
size={24}
@ -128,7 +131,7 @@ export function Component({
<TouchableOpacity
onPress={onPressCancel}
accessibilityRole="button"
accessibilityLabel="Cancel image crop"
accessibilityLabel={_(msg`Cancel image crop`)}
accessibilityHint="Exits image cropping process">
<Text type="xl" style={pal.link}>
Cancel
@ -138,7 +141,7 @@ export function Component({
<TouchableOpacity
onPress={onPressDone}
accessibilityRole="button"
accessibilityLabel="Save image crop"
accessibilityLabel={_(msg`Save image crop`)}
accessibilityHint="Saves image crop settings">
<LinearGradient
colors={[gradients.blueLight.start, gradients.blueLight.end]}
@ -146,7 +149,7 @@ export function Component({
end={{x: 1, y: 1}}
style={[styles.btn]}>
<Text type="xl-medium" style={s.white}>
Done
<Trans>Done</Trans>
</Text>
</LinearGradient>
</TouchableOpacity>

View file

@ -4,6 +4,8 @@ import LinearGradient from 'react-native-linear-gradient'
import {s, colors, gradients} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
export const ConfirmLanguagesButton = ({
onPress,
@ -13,6 +15,7 @@ export const ConfirmLanguagesButton = ({
extraText?: string
}) => {
const pal = usePalette('default')
const {_} = useLingui()
const {isMobile} = useWebMediaQueries()
return (
<View
@ -28,14 +31,16 @@ export const ConfirmLanguagesButton = ({
testID="confirmContentLanguagesBtn"
onPress={onPress}
accessibilityRole="button"
accessibilityLabel="Confirm content language settings"
accessibilityLabel={_(msg`Confirm content language settings`)}
accessibilityHint="">
<LinearGradient
colors={[gradients.blueLight.start, gradients.blueLight.end]}
start={{x: 0, y: 0}}
end={{x: 1, y: 1}}
style={[styles.btn]}>
<Text style={[s.white, s.bold, s.f18]}>Done{extraText}</Text>
<Text style={[s.white, s.bold, s.f18]}>
<Trans>Done{extraText}</Trans>
</Text>
</LinearGradient>
</Pressable>
</View>

View file

@ -8,6 +8,7 @@ import {deviceLocales} from 'platform/detection'
import {LANGUAGES, LANGUAGES_MAP_CODE2} from '../../../../locale/languages'
import {LanguageToggle} from './LanguageToggle'
import {ConfirmLanguagesButton} from './ConfirmLanguagesButton'
import {Trans} from '@lingui/macro'
import {useModalControls} from '#/state/modals'
import {
useLanguagePrefs,
@ -69,12 +70,16 @@ export function Component({}: {}) {
maxHeight: '90vh',
},
]}>
<Text style={[pal.text, styles.title]}>Content Languages</Text>
<Text style={[pal.text, styles.title]}>
<Trans>Content Languages</Trans>
</Text>
<Text style={[pal.text, styles.description]}>
Which languages would you like to see in your algorithmic feeds?
<Trans>
Which languages would you like to see in your algorithmic feeds?
</Trans>
</Text>
<Text style={[pal.textLight, styles.description]}>
Leave them all unchecked to see any language.
<Trans>Leave them all unchecked to see any language.</Trans>
</Text>
<ScrollView style={styles.scrollContainer}>
{languages.map(lang => (

View file

@ -9,6 +9,7 @@ import {deviceLocales} from 'platform/detection'
import {LANGUAGES, LANGUAGES_MAP_CODE2} from '../../../../locale/languages'
import {ConfirmLanguagesButton} from './ConfirmLanguagesButton'
import {ToggleButton} from 'view/com/util/forms/ToggleButton'
import {Trans} from '@lingui/macro'
import {useModalControls} from '#/state/modals'
import {
useLanguagePrefs,
@ -71,9 +72,11 @@ export const Component = observer(function PostLanguagesSettingsImpl() {
maxHeight: '90vh',
},
]}>
<Text style={[pal.text, styles.title]}>Post Languages</Text>
<Text style={[pal.text, styles.title]}>
<Trans>Post Languages</Trans>
</Text>
<Text style={[pal.text, styles.description]}>
Which languages are used in this post?
<Trans>Which languages are used in this post?</Trans>
</Text>
<ScrollView style={styles.scrollContainer}>
{languages.map(lang => {

View file

@ -8,6 +8,8 @@ import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {s} from 'lib/styles'
import {SendReportButton} from './SendReportButton'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
export function InputIssueDetails({
details,
@ -23,6 +25,7 @@ export function InputIssueDetails({
isProcessing: boolean
}) {
const pal = usePalette('default')
const {_} = useLingui()
const {isMobile} = useWebMediaQueries()
return (
@ -35,14 +38,16 @@ export function InputIssueDetails({
style={[s.mb10, styles.backBtn]}
onPress={goBack}
accessibilityRole="button"
accessibilityLabel="Add details"
accessibilityLabel={_(msg`Add details`)}
accessibilityHint="Add more details to your report">
<FontAwesomeIcon size={18} icon="angle-left" style={[pal.link]} />
<Text style={[pal.text, s.f18, pal.link]}> Back</Text>
<Text style={[pal.text, s.f18, pal.link]}>
<Trans> Back</Trans>
</Text>
</TouchableOpacity>
<View style={[pal.btn, styles.detailsInputContainer]}>
<TextInput
accessibilityLabel="Text input field"
accessibilityLabel={_(msg`Text input field`)}
accessibilityHint="Enter a reason for reporting this post."
placeholder="Enter a reason or any other details here."
placeholderTextColor={pal.textLight.color}

View file

@ -14,6 +14,8 @@ import {SendReportButton} from './SendReportButton'
import {InputIssueDetails} from './InputIssueDetails'
import {ReportReasonOptions} from './ReasonOptions'
import {CollectionId} from './types'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
const DMCA_LINK = 'https://blueskyweb.xyz/support/copyright'
@ -148,6 +150,7 @@ const SelectIssue = ({
atUri: AtUri | null
}) => {
const pal = usePalette('default')
const {_} = useLingui()
const collectionName = getCollectionNameForReport(atUri)
const onSelectIssue = (v: string) => setIssue(v)
const goToDetails = () => {
@ -160,9 +163,11 @@ const SelectIssue = ({
return (
<>
<Text style={[pal.text, styles.title]}>Report {collectionName}</Text>
<Text style={[pal.text, styles.title]}>
<Trans>Report {collectionName}</Trans>
</Text>
<Text style={[pal.textLight, styles.description]}>
What is the issue with this {collectionName}?
<Trans>What is the issue with this {collectionName}?</Trans>
</Text>
<View style={{marginBottom: 10}}>
<ReportReasonOptions
@ -184,9 +189,11 @@ const SelectIssue = ({
style={styles.addDetailsBtn}
onPress={goToDetails}
accessibilityRole="button"
accessibilityLabel="Add details"
accessibilityLabel={_(msg`Add details`)}
accessibilityHint="Add more details to your report">
<Text style={[s.f18, pal.link]}>Add details to report</Text>
<Text style={[s.f18, pal.link]}>
<Trans>Add details to report</Trans>
</Text>
</TouchableOpacity>
</>
) : undefined}

View file

@ -8,6 +8,8 @@ import {
} from 'react-native'
import {Text} from '../../util/text/Text'
import {s, gradients, colors} from 'lib/styles'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
export function SendReportButton({
onPress,
@ -16,6 +18,7 @@ export function SendReportButton({
onPress: () => void
isProcessing: boolean
}) {
const {_} = useLingui()
// loading state
// =
if (isProcessing) {
@ -31,14 +34,16 @@ export function SendReportButton({
style={s.mt10}
onPress={onPress}
accessibilityRole="button"
accessibilityLabel="Report post"
accessibilityLabel={_(msg`Report post`)}
accessibilityHint={`Reports post with reason and details`}>
<LinearGradient
colors={[gradients.blueLight.start, gradients.blueLight.end]}
start={{x: 0, y: 0}}
end={{x: 1, y: 1}}
style={[styles.btn]}>
<Text style={[s.white, s.bold, s.f18]}>Send Report</Text>
<Text style={[s.white, s.bold, s.f18]}>
<Trans>Send Report</Trans>
</Text>
</LinearGradient>
</TouchableOpacity>
)

View file

@ -40,6 +40,8 @@ import {formatCount} from '../util/numeric/format'
import {makeProfileLink} from 'lib/routes/links'
import {TimeElapsed} from '../util/TimeElapsed'
import {isWeb} from 'platform/detection'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
const MAX_AUTHORS = 5
@ -232,7 +234,9 @@ export const FeedItem = observer(function FeedItemImpl({
/>
{authors.length > 1 ? (
<>
<Text style={[pal.text]}> and </Text>
<Text style={[pal.text, s.mr5, s.ml5]}>
<Trans>and</Trans>
</Text>
<Text style={[pal.text, s.bold]}>
{formatCount(authors.length - 1)}{' '}
{pluralize(authors.length - 1, 'other')}
@ -292,6 +296,8 @@ function CondensedAuthorsList({
onToggleAuthorsExpanded: () => void
}) {
const pal = usePalette('default')
const {_} = useLingui()
if (!visible) {
return (
<View style={styles.avis}>
@ -299,7 +305,7 @@ function CondensedAuthorsList({
style={styles.expandedAuthorsCloseBtn}
onPress={onToggleAuthorsExpanded}
accessibilityRole="button"
accessibilityLabel="Hide user list"
accessibilityLabel={_(msg`Hide user list`)}
accessibilityHint="Collapses list of users for a given notification">
<FontAwesomeIcon
icon="angle-up"
@ -307,7 +313,7 @@ function CondensedAuthorsList({
style={[styles.expandedAuthorsCloseBtnIcon, pal.text]}
/>
<Text type="sm-medium" style={pal.text}>
Hide
<Trans>Hide</Trans>
</Text>
</TouchableOpacity>
</View>
@ -328,7 +334,7 @@ function CondensedAuthorsList({
}
return (
<TouchableOpacity
accessibilityLabel="Show users"
accessibilityLabel={_(msg`Show users`)}
accessibilityHint="Opens an expanded list of users in this notification"
onPress={onToggleAuthorsExpanded}>
<View style={styles.avis}>

View file

@ -14,6 +14,8 @@ import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome'
import {s} from 'lib/styles'
import {HITSLOP_10} from 'lib/constants'
import Animated from 'react-native-reanimated'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
import {useSetDrawerOpen} from '#/state/shell/drawer-open'
@ -22,6 +24,7 @@ export const FeedsTabBar = observer(function FeedsTabBarImpl(
) {
const pal = usePalette('default')
const store = useStores()
const {_} = useLingui()
const setDrawerOpen = useSetDrawerOpen()
const items = useHomeTabs(store.preferences.pinnedFeeds)
const brandBlue = useColorSchemeStyle(s.brandBlue, s.blue3)
@ -45,7 +48,7 @@ export const FeedsTabBar = observer(function FeedsTabBarImpl(
testID="viewHeaderDrawerBtn"
onPress={onPressAvi}
accessibilityRole="button"
accessibilityLabel="Open navigation"
accessibilityLabel={_(msg`Open navigation`)}
accessibilityHint="Access profile and other navigation links"
hitSlop={HITSLOP_10}>
<FontAwesomeIcon
@ -64,7 +67,7 @@ export const FeedsTabBar = observer(function FeedsTabBarImpl(
href="/settings/home-feed"
hitSlop={HITSLOP_10}
accessibilityRole="button"
accessibilityLabel="Home Feed Preferences"
accessibilityLabel={_(msg`Home Feed Preferences`)}
accessibilityHint="">
<FontAwesomeIcon
icon="sliders"

View file

@ -31,6 +31,8 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {NavigationProp} from 'lib/routes/types'
import {sanitizeDisplayName} from 'lib/strings/display-names'
import {logger} from '#/logger'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
const MAINTAIN_VISIBLE_CONTENT_POSITION = {minIndexForVisible: 2}
@ -79,6 +81,7 @@ export const PostThread = observer(function PostThread({
treeView: boolean
}) {
const pal = usePalette('default')
const {_} = useLingui()
const {isTablet, isDesktop} = useWebMediaQueries()
const ref = useRef<FlatList>(null)
const hasScrolledIntoView = useRef<boolean>(false)
@ -197,7 +200,7 @@ export const PostThread = observer(function PostThread({
return (
<View style={[pal.border, pal.viewLight, styles.itemContainer]}>
<Text type="lg-bold" style={pal.textLight}>
Deleted post.
<Trans>Deleted post.</Trans>
</Text>
</View>
)
@ -205,7 +208,7 @@ export const PostThread = observer(function PostThread({
return (
<View style={[pal.border, pal.viewLight, styles.itemContainer]}>
<Text type="lg-bold" style={pal.textLight}>
Blocked post.
<Trans>Blocked post.</Trans>
</Text>
</View>
)
@ -214,7 +217,7 @@ export const PostThread = observer(function PostThread({
<Pressable
onPress={() => setMaxVisible(n => n + 50)}
style={[pal.border, pal.view, styles.itemContainer]}
accessibilityLabel="Load more posts"
accessibilityLabel={_(msg`Load more posts`)}
accessibilityHint="">
<View
style={[
@ -222,7 +225,7 @@ export const PostThread = observer(function PostThread({
{paddingHorizontal: 18, paddingVertical: 14, borderRadius: 6},
]}>
<Text type="lg-medium" style={pal.text}>
Load more posts
<Trans>Load more posts</Trans>
</Text>
</View>
</Pressable>
@ -275,6 +278,7 @@ export const PostThread = observer(function PostThread({
posts,
onRefresh,
treeView,
_,
],
)
@ -302,15 +306,15 @@ export const PostThread = observer(function PostThread({
<CenteredView>
<View style={[pal.view, pal.border, styles.notFoundContainer]}>
<Text type="title-lg" style={[pal.text, s.mb5]}>
Post not found
<Trans>Post not found</Trans>
</Text>
<Text type="md" style={[pal.text, s.mb10]}>
The post may have been deleted.
<Trans>The post may have been deleted.</Trans>
</Text>
<TouchableOpacity
onPress={onPressBack}
accessibilityRole="button"
accessibilityLabel="Back"
accessibilityLabel={_(msg`Back`)}
accessibilityHint="">
<Text type="2xl" style={pal.link}>
<FontAwesomeIcon
@ -318,7 +322,7 @@ export const PostThread = observer(function PostThread({
style={[pal.link as FontAwesomeIconStyle, s.mr5]}
size={14}
/>
Back
<Trans>Back</Trans>
</Text>
</TouchableOpacity>
</View>
@ -336,15 +340,18 @@ export const PostThread = observer(function PostThread({
<CenteredView>
<View style={[pal.view, pal.border, styles.notFoundContainer]}>
<Text type="title-lg" style={[pal.text, s.mb5]}>
Post hidden
<Trans>Post hidden</Trans>
</Text>
<Text type="md" style={[pal.text, s.mb10]}>
You have blocked the author or you have been blocked by the author.
<Trans>
You have blocked the author or you have been blocked by the
author.
</Trans>
</Text>
<TouchableOpacity
onPress={onPressBack}
accessibilityRole="button"
accessibilityLabel="Back"
accessibilityLabel={_(msg`Back`)}
accessibilityHint="">
<Text type="2xl" style={pal.link}>
<FontAwesomeIcon
@ -352,7 +359,7 @@ export const PostThread = observer(function PostThread({
style={[pal.link as FontAwesomeIconStyle, s.mr5]}
size={14}
/>
Back
<Trans>Back</Trans>
</Text>
</TouchableOpacity>
</View>

View file

@ -37,6 +37,7 @@ import {makeProfileLink} from 'lib/routes/links'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {MAX_POST_LINES} from 'lib/constants'
import {logger} from '#/logger'
import {Trans} from '@lingui/macro'
import {useMutedThreads, useToggleThreadMute} from '#/state/muted-threads'
import {useLanguagePrefs} from '#/state/preferences'
@ -176,7 +177,9 @@ export const PostThreadItem = observer(function PostThreadItem({
icon={['far', 'trash-can']}
style={pal.icon as FontAwesomeIconStyle}
/>
<Text style={[pal.textLight, s.ml10]}>This post has been deleted.</Text>
<Text style={[pal.textLight, s.ml10]}>
<Trans>This post has been deleted.</Trans>
</Text>
</View>
)
}
@ -650,9 +653,11 @@ function ExpandedPostDetails({
<Text style={pal.textLight}>{niceDate(post.indexedAt)}</Text>
{needsTranslation && (
<>
<Text style={pal.textLight}> </Text>
<Text style={[pal.textLight, s.ml5, s.mr5]}></Text>
<Link href={translatorUrl} title="Translate">
<Text style={pal.link}>Translate</Text>
<Text style={pal.link}>
<Trans>Translate</Trans>
</Text>
</Link>
</>
)}

View file

@ -40,6 +40,8 @@ import {makeProfileLink} from 'lib/routes/links'
import {Link} from '../util/Link'
import {ProfileHeaderSuggestedFollows} from './ProfileHeaderSuggestedFollows'
import {logger} from '#/logger'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
interface Props {
@ -114,6 +116,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
const pal = usePalette('default')
const palInverted = usePalette('inverted')
const store = useStores()
const {_} = useLingui()
const {openModal} = useModalControls()
const navigation = useNavigation<NavigationProp>()
const {track} = useAnalytics()
@ -369,10 +372,10 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
onPress={onPressEditProfile}
style={[styles.btn, styles.mainBtn, pal.btn]}
accessibilityRole="button"
accessibilityLabel="Edit profile"
accessibilityLabel={_(msg`Edit profile`)}
accessibilityHint="Opens editor for profile display name, avatar, background image, and description">
<Text type="button" style={pal.text}>
Edit Profile
<Trans>Edit Profile</Trans>
</Text>
</TouchableOpacity>
) : view.viewer.blocking ? (
@ -382,10 +385,10 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
onPress={onPressUnblockAccount}
style={[styles.btn, styles.mainBtn, pal.btn]}
accessibilityRole="button"
accessibilityLabel="Unblock"
accessibilityLabel={_(msg`Unblock`)}
accessibilityHint="">
<Text type="button" style={[pal.text, s.bold]}>
Unblock
<Trans>Unblock</Trans>
</Text>
</TouchableOpacity>
)
@ -439,7 +442,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
size={14}
/>
<Text type="button" style={pal.text}>
Following
<Trans>Following</Trans>
</Text>
</TouchableOpacity>
) : (
@ -455,7 +458,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
style={[palInverted.text, s.mr5]}
/>
<Text type="button" style={[palInverted.text, s.bold]}>
Follow
<Trans>Follow</Trans>
</Text>
</TouchableOpacity>
)}
@ -465,7 +468,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
<NativeDropdown
testID="profileHeaderDropdownBtn"
items={dropdownItems}
accessibilityLabel="More options"
accessibilityLabel={_(msg`More options`)}
accessibilityHint="">
<View style={[styles.btn, styles.secondaryBtn, pal.btn]}>
<FontAwesomeIcon icon="ellipsis" size={20} style={[pal.text]} />
@ -488,7 +491,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
{view.viewer.followedBy && !blockHide ? (
<View style={[styles.pill, pal.btn, s.mr5]}>
<Text type="xs" style={[pal.text]}>
Follows you
<Trans>Follows you</Trans>
</Text>
</View>
) : undefined}
@ -533,7 +536,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
{following}{' '}
</Text>
<Text type="md" style={[pal.textLight]}>
following
<Trans>following</Trans>
</Text>
</Link>
<Text type="md" style={[s.bold, pal.text]}>
@ -572,7 +575,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
onPress={onPressBack}
hitSlop={BACK_HITSLOP}
accessibilityRole="button"
accessibilityLabel="Back"
accessibilityLabel={_(msg`Back`)}
accessibilityHint="">
<View style={styles.backBtnWrapper}>
<BlurView style={styles.backBtn} blurType="dark">

View file

@ -17,6 +17,8 @@ import {NavigationProp} from 'lib/routes/types'
import {BACK_HITSLOP} from 'lib/constants'
import {isNative} from 'platform/detection'
import {ImagesLightbox} from 'state/models/ui/shell'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
import {useSetDrawerOpen} from '#/state/shell'
export const ProfileSubpageHeader = observer(function HeaderImpl({
@ -45,6 +47,7 @@ export const ProfileSubpageHeader = observer(function HeaderImpl({
const store = useStores()
const setDrawerOpen = useSetDrawerOpen()
const navigation = useNavigation<NavigationProp>()
const {_} = useLingui()
const {isMobile} = useWebMediaQueries()
const pal = usePalette('default')
const canGoBack = navigation.canGoBack()
@ -123,7 +126,7 @@ export const ProfileSubpageHeader = observer(function HeaderImpl({
testID="headerAviButton"
onPress={onPressAvi}
accessibilityRole="image"
accessibilityLabel="View the avatar"
accessibilityLabel={_(msg`View the avatar`)}
accessibilityHint=""
style={{width: 58}}>
<UserAvatar type={avatarType} size={58} avatar={avatar} />

View file

@ -11,6 +11,8 @@ import {usePalette} from 'lib/hooks/usePalette'
import {useAnalytics} from 'lib/analytics/analytics'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {HITSLOP_10} from 'lib/constants'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useSetDrawerOpen} from '#/state/shell'
interface Props {
@ -36,6 +38,7 @@ export function HeaderWithInput({
const setDrawerOpen = useSetDrawerOpen()
const theme = useTheme()
const pal = usePalette('default')
const {_} = useLingui()
const {track} = useAnalytics()
const textInput = React.useRef<TextInput>(null)
const {isMobile} = useWebMediaQueries()
@ -65,7 +68,7 @@ export function HeaderWithInput({
hitSlop={HITSLOP_10}
style={styles.headerMenuBtn}
accessibilityRole="button"
accessibilityLabel="Menu"
accessibilityLabel={_(msg`Menu`)}
accessibilityHint="Access navigation links and settings">
<FontAwesomeIcon icon="bars" size={18} color={pal.colors.textLight} />
</TouchableOpacity>
@ -95,7 +98,7 @@ export function HeaderWithInput({
onSubmitEditing={onSubmitQuery}
autoFocus={false}
accessibilityRole="search"
accessibilityLabel="Search"
accessibilityLabel={_(msg`Search`)}
accessibilityHint=""
autoCorrect={false}
autoCapitalize="none"
@ -105,7 +108,7 @@ export function HeaderWithInput({
testID="searchTextInputClearBtn"
onPress={onPressClearQuery}
accessibilityRole="button"
accessibilityLabel="Clear search query"
accessibilityLabel={_(msg`Clear search query`)}
accessibilityHint="">
<FontAwesomeIcon
icon="xmark"
@ -120,7 +123,9 @@ export function HeaderWithInput({
<TouchableOpacity
onPress={onPressCancelSearchInner}
accessibilityRole="button">
<Text style={pal.text}>Cancel</Text>
<Text style={pal.text}>
<Trans>Cancel</Trans>
</Text>
</TouchableOpacity>
</View>
) : undefined}

View file

@ -9,10 +9,14 @@ import {useStores} from 'state/index'
import {usePalette} from 'lib/hooks/usePalette'
import {DropdownItem, NativeDropdown} from './forms/NativeDropdown'
import * as Toast from '../../com/util/Toast'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
export function AccountDropdownBtn({handle}: {handle: string}) {
const store = useStores()
const pal = usePalette('default')
const {_} = useLingui()
const items: DropdownItem[] = [
{
label: 'Remove account',
@ -34,7 +38,7 @@ export function AccountDropdownBtn({handle}: {handle: string}) {
<NativeDropdown
testID="accountSettingsDropdownBtn"
items={items}
accessibilityLabel="Account options"
accessibilityLabel={_(msg`Account options`)}
accessibilityHint="">
<FontAwesomeIcon
icon="ellipsis-h"

View file

@ -6,6 +6,7 @@ import Animated, {
interpolate,
useAnimatedStyle,
} from 'react-native-reanimated'
import {t} from '@lingui/macro'
export function createCustomBackdrop(
onClose?: (() => void) | undefined,
@ -29,7 +30,7 @@ export function createCustomBackdrop(
return (
<TouchableWithoutFeedback
onPress={onClose}
accessibilityLabel="Close bottom drawer"
accessibilityLabel={t`Close bottom drawer`}
accessibilityHint=""
onAccessibilityEscape={() => {
if (onClose !== undefined) {

View file

@ -16,6 +16,8 @@ import {isWeb, isAndroid} from 'platform/detection'
import {Image as RNImage} from 'react-native-image-crop-picker'
import {UserPreviewLink} from './UserPreviewLink'
import {DropdownItem, NativeDropdown} from './forms/NativeDropdown'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
export type UserAvatarType = 'user' | 'algo' | 'list'
@ -184,6 +186,7 @@ export function EditableUserAvatar({
}: EditableUserAvatarProps) {
const store = useStores()
const pal = usePalette('default')
const {_} = useLingui()
const {requestCameraAccessIfNeeded} = useCameraPermission()
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
@ -294,7 +297,7 @@ export function EditableUserAvatar({
<NativeDropdown
testID="changeAvatarBtn"
items={dropdownItems}
accessibilityLabel="Image options"
accessibilityLabel={_(msg`Image options`)}
accessibilityHint="">
{avatar ? (
<HighPriorityImage

View file

@ -14,6 +14,8 @@ import {usePalette} from 'lib/hooks/usePalette'
import {isWeb, isAndroid} from 'platform/detection'
import {Image as RNImage} from 'react-native-image-crop-picker'
import {NativeDropdown, DropdownItem} from './forms/NativeDropdown'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
export function UserBanner({
banner,
@ -26,6 +28,7 @@ export function UserBanner({
}) {
const store = useStores()
const pal = usePalette('default')
const {_} = useLingui()
const {requestCameraAccessIfNeeded} = useCameraPermission()
const {requestPhotoAccessIfNeeded} = usePhotoLibraryPermission()
@ -112,7 +115,7 @@ export function UserBanner({
<NativeDropdown
testID="changeBannerBtn"
items={dropdownItems}
accessibilityLabel="Image options"
accessibilityLabel={_(msg`Image options`)}
accessibilityHint="">
{banner ? (
<Image

View file

@ -13,6 +13,8 @@ import {
import {Text} from '../text/Text'
import {useTheme} from 'lib/ThemeContext'
import {usePalette} from 'lib/hooks/usePalette'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
export function ErrorMessage({
message,
@ -27,6 +29,7 @@ export function ErrorMessage({
}) {
const theme = useTheme()
const pal = usePalette('error')
const {_} = useLingui()
return (
<View testID="errorMessageView" style={[styles.outer, pal.view, style]}>
<View
@ -49,7 +52,7 @@ export function ErrorMessage({
style={styles.btn}
onPress={onPressTryAgain}
accessibilityRole="button"
accessibilityLabel="Retry"
accessibilityLabel={_(msg`Retry`)}
accessibilityHint="Retries the last action, which errored out">
<FontAwesomeIcon
icon="arrows-rotate"

View file

@ -9,6 +9,8 @@ import {useTheme} from 'lib/ThemeContext'
import {usePalette} from 'lib/hooks/usePalette'
import {Button} from '../forms/Button'
import {CenteredView} from '../Views'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
export function ErrorScreen({
title,
@ -25,6 +27,8 @@ export function ErrorScreen({
}) {
const theme = useTheme()
const pal = usePalette('default')
const {_} = useLingui()
return (
<CenteredView testID={testID} style={[styles.outer, pal.view]}>
<View style={styles.errorIconContainer}>
@ -58,7 +62,7 @@ export function ErrorScreen({
type="default"
style={[styles.btn]}
onPress={onPressTryAgain}
accessibilityLabel="Retry"
accessibilityLabel={_(msg`Retry`)}
accessibilityHint="Retries the last action, which errored out">
<FontAwesomeIcon
icon="arrows-rotate"
@ -66,7 +70,7 @@ export function ErrorScreen({
size={16}
/>
<Text type="button" style={[styles.btnText, pal.link]}>
Try again
<Trans>Try again</Trans>
</Text>
</Button>
</View>

View file

@ -17,6 +17,8 @@ import {colors} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
import {useTheme} from 'lib/ThemeContext'
import {HITSLOP_10} from 'lib/constants'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
const ESTIMATED_BTN_HEIGHT = 50
const ESTIMATED_SEP_HEIGHT = 16
@ -207,6 +209,7 @@ const DropdownItems = ({
}: DropDownItemProps) => {
const pal = usePalette('default')
const theme = useTheme()
const {_} = useLingui()
const dropDownBackgroundColor =
theme.colorScheme === 'dark' ? pal.btn : pal.view
const separatorColor =
@ -224,7 +227,7 @@ const DropdownItems = ({
{/* This TouchableWithoutFeedback renders the background so if the user clicks outside, the dropdown closes */}
<TouchableWithoutFeedback
onPress={onOuterPress}
accessibilityLabel="Toggle dropdown"
accessibilityLabel={_(msg`Toggle dropdown`)}
accessibilityHint="">
<View style={[styles.bg]} />
</TouchableWithoutFeedback>

View file

@ -9,6 +9,8 @@ import {
DropdownItem as NativeDropdownItem,
} from './NativeDropdown'
import {EventStopper} from '../EventStopper'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
import {useModalControls} from '#/state/modals'
export function PostDropdownBtn({
@ -38,6 +40,7 @@ export function PostDropdownBtn({
style?: StyleProp<ViewStyle>
}) {
const theme = useTheme()
const {_} = useLingui()
const defaultCtrlColor = theme.palette.default.postCtrl
const {openModal} = useModalControls()
@ -152,7 +155,7 @@ export function PostDropdownBtn({
<NativeDropdown
testID={testID}
items={dropdownItems}
accessibilityLabel="More post options"
accessibilityLabel={_(msg`More post options`)}
accessibilityHint="">
<View style={style}>
<FontAwesomeIcon icon="ellipsis" size={20} color={defaultCtrlColor} />

View file

@ -14,6 +14,8 @@ import {
import {MagnifyingGlassIcon} from 'lib/icons'
import {useTheme} from 'lib/ThemeContext'
import {usePalette} from 'lib/hooks/usePalette'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
interface Props {
query: string
@ -33,6 +35,7 @@ export function SearchInput({
}: Props) {
const theme = useTheme()
const pal = usePalette('default')
const {_} = useLingui()
const textInput = React.useRef<TextInput>(null)
const onPressCancelSearchInner = React.useCallback(() => {
@ -58,7 +61,7 @@ export function SearchInput({
onChangeText={onChangeQuery}
onSubmitEditing={onSubmitQuery}
accessibilityRole="search"
accessibilityLabel="Search"
accessibilityLabel={_(msg`Search`)}
accessibilityHint=""
autoCorrect={false}
autoCapitalize="none"
@ -67,7 +70,7 @@ export function SearchInput({
<TouchableOpacity
onPress={onPressCancelSearchInner}
accessibilityRole="button"
accessibilityLabel="Clear search query"
accessibilityLabel={_(msg`Clear search query`)}
accessibilityHint="">
<FontAwesomeIcon
icon="xmark"

View file

@ -6,6 +6,8 @@ import {ModerationUI} from '@atproto/api'
import {Text} from '../text/Text'
import {ShieldExclamation} from 'lib/icons'
import {describeModerationCause} from 'lib/moderation'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
import {useModalControls} from '#/state/modals'
export function ContentHider({
@ -23,6 +25,7 @@ export function ContentHider({
childContainerStyle?: StyleProp<ViewStyle>
}>) {
const pal = usePalette('default')
const {_} = useLingui()
const {isMobile} = useWebMediaQueries()
const [override, setOverride] = React.useState(false)
const {openModal} = useModalControls()
@ -69,7 +72,7 @@ export function ContentHider({
})
}}
accessibilityRole="button"
accessibilityLabel="Learn more about this warning"
accessibilityLabel={_(msg`Learn more about this warning`)}
accessibilityHint="">
<ShieldExclamation size={18} style={pal.text} />
</Pressable>

View file

@ -5,6 +5,8 @@ import {Text} from '../text/Text'
import {usePalette} from 'lib/hooks/usePalette'
import {ShieldExclamation} from 'lib/icons'
import {describeModerationCause} from 'lib/moderation'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
export function PostAlerts({
@ -16,6 +18,7 @@ export function PostAlerts({
style?: StyleProp<ViewStyle>
}) {
const pal = usePalette('default')
const {_} = useLingui()
const {openModal} = useModalControls()
const shouldAlert = !!moderation.cause && moderation.alert
@ -34,14 +37,14 @@ export function PostAlerts({
})
}}
accessibilityRole="button"
accessibilityLabel="Learn more about this warning"
accessibilityLabel={_(msg`Learn more about this warning`)}
accessibilityHint=""
style={[styles.container, pal.viewLight, style]}>
<ShieldExclamation style={pal.text} size={16} />
<Text type="lg" style={[pal.text]}>
{desc.name}{' '}
<Text type="lg" style={[pal.link, styles.learnMoreBtn]}>
Learn More
<Trans>Learn More</Trans>
</Text>
</Text>
</Pressable>

View file

@ -8,6 +8,8 @@ import {Text} from '../text/Text'
import {addStyle} from 'lib/styles'
import {describeModerationCause} from 'lib/moderation'
import {ShieldExclamation} from 'lib/icons'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
import {useModalControls} from '#/state/modals'
interface Props extends ComponentProps<typeof Link> {
@ -26,6 +28,7 @@ export function PostHider({
...props
}: Props) {
const pal = usePalette('default')
const {_} = useLingui()
const {isMobile} = useWebMediaQueries()
const [override, setOverride] = React.useState(false)
const {openModal} = useModalControls()
@ -70,7 +73,7 @@ export function PostHider({
})
}}
accessibilityRole="button"
accessibilityLabel="Learn more about this warning"
accessibilityLabel={_(msg`Learn more about this warning`)}
accessibilityHint="">
<ShieldExclamation size={18} style={pal.text} />
</Pressable>

View file

@ -8,6 +8,8 @@ import {
describeModerationCause,
getProfileModerationCauses,
} from 'lib/moderation'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
export function ProfileHeaderAlerts({
@ -18,6 +20,7 @@ export function ProfileHeaderAlerts({
style?: StyleProp<ViewStyle>
}) {
const pal = usePalette('default')
const {_} = useLingui()
const {openModal} = useModalControls()
const causes = getProfileModerationCauses(moderation)
@ -41,7 +44,7 @@ export function ProfileHeaderAlerts({
})
}}
accessibilityRole="button"
accessibilityLabel="Learn more about this warning"
accessibilityLabel={_(msg`Learn more about this warning`)}
accessibilityHint=""
style={[styles.container, pal.viewLight, style]}>
<ShieldExclamation style={pal.text} size={24} />
@ -49,7 +52,7 @@ export function ProfileHeaderAlerts({
{desc.name}
</Text>
<Text type="lg" style={[pal.link, styles.learnMoreBtn]}>
Learn More
<Trans>Learn More</Trans>
</Text>
</Pressable>
)

View file

@ -18,7 +18,10 @@ import {NavigationProp} from 'lib/routes/types'
import {Text} from '../text/Text'
import {Button} from '../forms/Button'
import {describeModerationCause} from 'lib/moderation'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {s} from '#/lib/styles'
export function ScreenHider({
testID,
@ -36,6 +39,7 @@ export function ScreenHider({
}>) {
const pal = usePalette('default')
const palInverted = usePalette('inverted')
const {_} = useLingui()
const [override, setOverride] = React.useState(false)
const navigation = useNavigation<NavigationProp>()
const {isMobile} = useWebMediaQueries()
@ -62,14 +66,13 @@ export function ScreenHider({
</View>
</View>
<Text type="title-2xl" style={[styles.title, pal.text]}>
Content Warning
<Trans>Content Warning</Trans>
</Text>
<Text type="2xl" style={[styles.description, pal.textLight]}>
This {screenDescription} has been flagged:{' '}
<Text type="2xl-medium" style={pal.text}>
{desc.name}
<Trans>This {screenDescription} has been flagged:</Trans>
<Text type="2xl-medium" style={[pal.text, s.ml5]}>
{desc.name}.
</Text>
.{' '}
<TouchableWithoutFeedback
onPress={() => {
openModal({
@ -79,10 +82,10 @@ export function ScreenHider({
})
}}
accessibilityRole="button"
accessibilityLabel="Learn more about this warning"
accessibilityLabel={_(msg`Learn more about this warning`)}
accessibilityHint="">
<Text type="2xl" style={pal.link}>
Learn More
<Trans>Learn More</Trans>
</Text>
</TouchableWithoutFeedback>
</Text>
@ -99,7 +102,7 @@ export function ScreenHider({
}}
style={styles.btn}>
<Text type="button-lg" style={pal.textInverted}>
Go back
<Trans>Go back</Trans>
</Text>
</Button>
{!moderation.noOverride && (
@ -108,7 +111,7 @@ export function ScreenHider({
onPress={() => setOverride(v => !v)}
style={styles.btn}>
<Text type="button-lg" style={pal.text}>
Show anyway
<Trans>Show anyway</Trans>
</Text>
</Button>
)}

View file

@ -10,6 +10,8 @@ import {
DropdownItem as NativeDropdownItem,
} from '../forms/NativeDropdown'
import {EventStopper} from '../EventStopper'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
interface Props {
isReposted: boolean
@ -28,6 +30,7 @@ export const RepostButton = ({
onQuote,
}: Props) => {
const theme = useTheme()
const {_} = useLingui()
const defaultControlColor = React.useMemo(
() => ({
@ -63,7 +66,7 @@ export const RepostButton = ({
<EventStopper>
<NativeDropdown
items={dropdownItems}
accessibilityLabel="Repost or quote post"
accessibilityLabel={_(msg`Repost or quote post`)}
accessibilityHint="">
<View
style={[

View file

@ -16,6 +16,8 @@ import {useAnalytics} from 'lib/analytics/analytics'
import {useFocusEffect} from '@react-navigation/native'
import {ViewHeader} from '../com/util/ViewHeader'
import {CenteredView} from 'view/com/util/Views'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useSetMinimalShellMode} from '#/state/shell'
import {useModalControls} from '#/state/modals'
import {useLanguagePrefs} from '#/state/preferences'
@ -55,8 +57,10 @@ export const AppPasswords = withAuthRequired(
<AppPasswordsHeader />
<View style={[styles.empty, pal.viewLight]}>
<Text type="lg" style={[pal.text, styles.emptyText]}>
You have not created any app passwords yet. You can create one by
pressing the button below.
<Trans>
You have not created any app passwords yet. You can create one
by pressing the button below.
</Trans>
</Text>
</View>
{!isTabletOrDesktop && <View style={styles.flex1} />}
@ -146,8 +150,10 @@ function AppPasswordsHeader() {
pal.text,
isTabletOrDesktop && styles.descriptionDesktop,
]}>
Use app passwords to login to other Bluesky clients without giving full
access to your account or password.
<Trans>
Use app passwords to login to other Bluesky clients without giving
full access to your account or password.
</Trans>
</Text>
</>
)
@ -164,6 +170,7 @@ function AppPassword({
}) {
const pal = usePalette('default')
const store = useStores()
const {_} = useLingui()
const {openModal} = useModalControls()
const {contentLanguages} = useLanguagePrefs()
@ -188,7 +195,7 @@ function AppPassword({
style={[styles.item, pal.border]}
onPress={onDelete}
accessibilityRole="button"
accessibilityLabel="Delete app password"
accessibilityLabel={_(msg`Delete app password`)}
accessibilityHint="">
<View>
<Text type="md-bold" style={pal.text}>

View file

@ -27,6 +27,8 @@ import {FeedSourceModel} from 'state/models/content/feed-source'
import {FlatList} from 'view/com/util/Views'
import {useFocusEffect} from '@react-navigation/native'
import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useSetMinimalShellMode} from '#/state/shell'
type Props = NativeStackScreenProps<FeedsTabNavigatorParams, 'Feeds'>
@ -34,6 +36,7 @@ export const FeedsScreen = withAuthRequired(
observer<Props>(function FeedsScreenImpl({}: Props) {
const pal = usePalette('default')
const store = useStores()
const {_} = useLingui()
const setMinimalShellMode = useSetMinimalShellMode()
const {isMobile, isTabletOrDesktop} = useWebMediaQueries()
const myFeeds = store.me.myFeeds
@ -88,12 +91,12 @@ export const FeedsScreen = withAuthRequired(
href="/settings/saved-feeds"
hitSlop={10}
accessibilityRole="button"
accessibilityLabel="Edit Saved Feeds"
accessibilityLabel={_(msg`Edit Saved Feeds`)}
accessibilityHint="Opens screen to edit Saved Feeds">
<CogIcon size={22} strokeWidth={2} style={pal.textLight} />
</Link>
)
}, [pal])
}, [pal, _])
const onRefresh = React.useCallback(() => {
myFeeds.refresh()
@ -124,11 +127,11 @@ export const FeedsScreen = withAuthRequired(
},
]}>
<Text type="title-lg" style={[pal.text, s.bold]}>
My Feeds
<Trans>My Feeds</Trans>
</Text>
<Link
href="/settings/saved-feeds"
accessibilityLabel="Edit My Feeds"
accessibilityLabel={_(msg`Edit My Feeds`)}
accessibilityHint="">
<CogIcon strokeWidth={1.5} style={pal.icon} size={28} />
</Link>
@ -139,7 +142,7 @@ export const FeedsScreen = withAuthRequired(
} else if (item.type === 'saved-feeds-loading') {
return (
<>
{Array.from(Array(item.numItems)).map((_, i) => (
{Array.from(Array(item.numItems)).map((_i, i) => (
<SavedFeedLoadingPlaceholder key={`placeholder-${i}`} />
))}
</>
@ -161,7 +164,7 @@ export const FeedsScreen = withAuthRequired(
},
]}>
<Text type="title-lg" style={[pal.text, s.bold]}>
Discover new feeds
<Trans>Discover new feeds</Trans>
</Text>
{!isMobile && (
<SearchInput
@ -203,14 +206,22 @@ export const FeedsScreen = withAuthRequired(
paddingBottom: '150%',
}}>
<Text type="lg" style={pal.textLight}>
No results found for "{query}"
<Trans>No results found for "{query}"</Trans>
</Text>
</View>
)
}
return null
},
[isMobile, pal, query, onChangeQuery, onPressCancelSearch, onSubmitQuery],
[
isMobile,
pal,
query,
onChangeQuery,
onPressCancelSearch,
onSubmitQuery,
_,
],
)
return (
@ -249,7 +260,7 @@ export const FeedsScreen = withAuthRequired(
onPress={onPressCompose}
icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
accessibilityRole="button"
accessibilityLabel="New post"
accessibilityLabel={_(msg`New post`)}
accessibilityHint=""
/>
</View>
@ -289,7 +300,7 @@ function SavedFeed({feed}: {feed: FeedSourceModel}) {
{feed.error ? (
<View style={[styles.offlineSlug, pal.borderDark]}>
<Text type="xs" style={pal.textLight}>
Feed offline
<Trans>Feed offline</Trans>
</Text>
</View>
) : null}

View file

@ -11,6 +11,8 @@ import {Text} from '../com/util/text/Text'
import {usePalette} from 'lib/hooks/usePalette'
import {getEntries} from '#/logger/logDump'
import {ago} from 'lib/strings/time'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
import {useSetMinimalShellMode} from '#/state/shell'
export const LogScreen = observer(function Log({}: NativeStackScreenProps<
@ -18,6 +20,7 @@ export const LogScreen = observer(function Log({}: NativeStackScreenProps<
'Log'
>) {
const pal = usePalette('default')
const {_} = useLingui()
const setMinimalShellMode = useSetMinimalShellMode()
const [expanded, setExpanded] = React.useState<string[]>([])
@ -47,7 +50,7 @@ export const LogScreen = observer(function Log({}: NativeStackScreenProps<
<TouchableOpacity
style={[styles.entry, pal.border, pal.view]}
onPress={toggler(entry.id)}
accessibilityLabel="View debug entry"
accessibilityLabel={_(msg`View debug entry`)}
accessibilityHint="Opens additional details for a debug entry">
{entry.level === 'debug' ? (
<FontAwesomeIcon icon="info" />

View file

@ -14,6 +14,8 @@ import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
import {ViewHeader} from 'view/com/util/ViewHeader'
import {CenteredView} from 'view/com/util/Views'
import debounce from 'lodash.debounce'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
function RepliesThresholdInput({enabled}: {enabled: boolean}) {
const store = useStores()
@ -66,6 +68,7 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
}: Props) {
const pal = usePalette('default')
const store = useStores()
const {_} = useLingui()
const {isTabletOrDesktop} = useWebMediaQueries()
return (
@ -84,7 +87,7 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
isTabletOrDesktop && {paddingTop: 20, paddingBottom: 20},
]}>
<Text type="xl" style={[pal.textLight, styles.description]}>
Fine-tune the content you see on your home screen.
<Trans>Fine-tune the content you see on your home screen.</Trans>
</Text>
</View>
@ -92,10 +95,12 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
<View style={styles.cardsContainer}>
<View style={[pal.viewLight, styles.card]}>
<Text type="title-sm" style={[pal.text, s.pb5]}>
Show Replies
<Trans>Show Replies</Trans>
</Text>
<Text style={[pal.text, s.pb10]}>
Set this setting to "No" to hide all replies from your feed.
<Trans>
Set this setting to "No" to hide all replies from your feed.
</Trans>
</Text>
<ToggleButton
testID="toggleRepliesBtn"
@ -112,10 +117,13 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
store.preferences.homeFeed.hideReplies && styles.dimmed,
]}>
<Text type="title-sm" style={[pal.text, s.pb5]}>
Reply Filters
<Trans>Reply Filters</Trans>
</Text>
<Text style={[pal.text, s.pb10]}>
Enable this setting to only see replies between people you follow.
<Trans>
Enable this setting to only see replies between people you
follow.
</Trans>
</Text>
<ToggleButton
type="default-light"
@ -129,8 +137,10 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
style={[s.mb10]}
/>
<Text style={[pal.text]}>
Adjust the number of likes a reply must have to be shown in your
feed.
<Trans>
Adjust the number of likes a reply must have to be shown in your
feed.
</Trans>
</Text>
<RepliesThresholdInput
enabled={!store.preferences.homeFeed.hideReplies}
@ -139,10 +149,12 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
<View style={[pal.viewLight, styles.card]}>
<Text type="title-sm" style={[pal.text, s.pb5]}>
Show Reposts
<Trans>Show Reposts</Trans>
</Text>
<Text style={[pal.text, s.pb10]}>
Set this setting to "No" to hide all reposts from your feed.
<Trans>
Set this setting to "No" to hide all reposts from your feed.
</Trans>
</Text>
<ToggleButton
type="default-light"
@ -154,11 +166,13 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
<View style={[pal.viewLight, styles.card]}>
<Text type="title-sm" style={[pal.text, s.pb5]}>
Show Quote Posts
<Trans>Show Quote Posts</Trans>
</Text>
<Text style={[pal.text, s.pb10]}>
Set this setting to "No" to hide all quote posts from your feed.
Reposts will still be visible.
<Trans>
Set this setting to "No" to hide all quote posts from your feed.
Reposts will still be visible.
</Trans>
</Text>
<ToggleButton
type="default-light"
@ -170,12 +184,14 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
<View style={[pal.viewLight, styles.card]}>
<Text type="title-sm" style={[pal.text, s.pb5]}>
<FontAwesomeIcon icon="flask" color={pal.colors.text} /> Show
Posts from My Feeds
<FontAwesomeIcon icon="flask" color={pal.colors.text} />
<Trans>Show Posts from My Feeds</Trans>
</Text>
<Text style={[pal.text, s.pb10]}>
Set this setting to "Yes" to show samples of your saved feeds in
your following feed. This is an experimental feature.
<Trans>
Set this setting to "Yes" to show samples of your saved feeds in
your following feed. This is an experimental feature.
</Trans>
</Text>
<ToggleButton
type="default-light"
@ -204,9 +220,11 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
}}
style={[styles.btn, isTabletOrDesktop && styles.btnDesktop]}
accessibilityRole="button"
accessibilityLabel="Confirm"
accessibilityLabel={_(msg`Confirm`)}
accessibilityHint="">
<Text style={[s.white, s.bold, s.f18]}>Done</Text>
<Text style={[s.white, s.bold, s.f18]}>
<Trans>Done</Trans>
</Text>
</TouchableOpacity>
</View>
</CenteredView>

View file

@ -12,6 +12,8 @@ import {RadioGroup} from 'view/com/util/forms/RadioGroup'
import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
import {ViewHeader} from 'view/com/util/ViewHeader'
import {CenteredView} from 'view/com/util/Views'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PreferencesThreads'>
export const PreferencesThreads = observer(function PreferencesThreadsImpl({
@ -19,6 +21,7 @@ export const PreferencesThreads = observer(function PreferencesThreadsImpl({
}: Props) {
const pal = usePalette('default')
const store = useStores()
const {_} = useLingui()
const {isTabletOrDesktop} = useWebMediaQueries()
return (
@ -37,7 +40,7 @@ export const PreferencesThreads = observer(function PreferencesThreadsImpl({
isTabletOrDesktop && {paddingTop: 20, paddingBottom: 20},
]}>
<Text type="xl" style={[pal.textLight, styles.description]}>
Fine-tune the discussion threads.
<Trans>Fine-tune the discussion threads.</Trans>
</Text>
</View>
@ -45,10 +48,10 @@ export const PreferencesThreads = observer(function PreferencesThreadsImpl({
<View style={styles.cardsContainer}>
<View style={[pal.viewLight, styles.card]}>
<Text type="title-sm" style={[pal.text, s.pb5]}>
Sort Replies
<Trans>Sort Replies</Trans>
</Text>
<Text style={[pal.text, s.pb10]}>
Sort replies to the same post by:
<Trans>Sort replies to the same post by:</Trans>
</Text>
<View style={[pal.view, {borderRadius: 8, paddingVertical: 6}]}>
<RadioGroup
@ -67,10 +70,12 @@ export const PreferencesThreads = observer(function PreferencesThreadsImpl({
<View style={[pal.viewLight, styles.card]}>
<Text type="title-sm" style={[pal.text, s.pb5]}>
Prioritize Your Follows
<Trans>Prioritize Your Follows</Trans>
</Text>
<Text style={[pal.text, s.pb10]}>
Show replies by people you follow before all other replies.
<Trans>
Show replies by people you follow before all other replies.
</Trans>
</Text>
<ToggleButton
type="default-light"
@ -84,12 +89,14 @@ export const PreferencesThreads = observer(function PreferencesThreadsImpl({
<View style={[pal.viewLight, styles.card]}>
<Text type="title-sm" style={[pal.text, s.pb5]}>
<FontAwesomeIcon icon="flask" color={pal.colors.text} /> Threaded
Mode
<FontAwesomeIcon icon="flask" color={pal.colors.text} />{' '}
<Trans>Threaded Mode</Trans>
</Text>
<Text style={[pal.text, s.pb10]}>
Set this setting to "Yes" to show replies in a threaded view. This
is an experimental feature.
<Trans>
Set this setting to "Yes" to show replies in a threaded view.
This is an experimental feature.
</Trans>
</Text>
<ToggleButton
type="default-light"
@ -118,9 +125,11 @@ export const PreferencesThreads = observer(function PreferencesThreadsImpl({
}}
style={[styles.btn, isTabletOrDesktop && styles.btnDesktop]}
accessibilityRole="button"
accessibilityLabel="Confirm"
accessibilityLabel={_(msg`Confirm`)}
accessibilityHint="">
<Text style={[s.white, s.bold, s.f18]}>Done</Text>
<Text style={[s.white, s.bold, s.f18]}>
<Trans>Done</Trans>
</Text>
</TouchableOpacity>
</View>
</CenteredView>

View file

@ -30,6 +30,8 @@ import {FeedSourceModel} from 'state/models/content/feed-source'
import {useSetTitle} from 'lib/hooks/useSetTitle'
import {combinedDisplayName} from 'lib/strings/display-names'
import {logger} from '#/logger'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useSetMinimalShellMode} from '#/state/shell'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Profile'>
@ -38,6 +40,7 @@ export const ProfileScreen = withAuthRequired(
const store = useStores()
const setMinimalShellMode = useSetMinimalShellMode()
const {screen, track} = useAnalytics()
const {_} = useLingui()
const viewSelectorRef = React.useRef<ViewSelectorHandle>(null)
const name = route.params.name === 'me' ? store.me.did : route.params.name
@ -206,7 +209,11 @@ export const ProfileScreen = withAuthRequired(
// if section is posts or posts & replies
} else {
if (item === ProfileUiModel.END_ITEM) {
return <Text style={styles.endItem}>- end of feed -</Text>
return (
<Text style={styles.endItem}>
<Trans>- end of feed -</Trans>
</Text>
)
} else if (item === ProfileUiModel.LOADING_ITEM) {
return <PostFeedLoadingPlaceholder />
} else if (item._reactKey === '__error__') {
@ -296,7 +303,7 @@ export const ProfileScreen = withAuthRequired(
onPress={onPressCompose}
icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />}
accessibilityRole="button"
accessibilityLabel="New post"
accessibilityLabel={_(msg`New post`)}
accessibilityHint=""
/>
</ScreenHider>

View file

@ -47,6 +47,8 @@ import {sanitizeHandle} from 'lib/strings/handles'
import {makeProfileLink} from 'lib/routes/links'
import {ComposeIcon2} from 'lib/icons'
import {logger} from '#/logger'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
const SECTION_TITLES = ['Posts', 'About']
@ -60,6 +62,7 @@ export const ProfileFeedScreen = withAuthRequired(
observer(function ProfileFeedScreenImpl(props: Props) {
const pal = usePalette('default')
const store = useStores()
const {_} = useLingui()
const navigation = useNavigation<NavigationProp>()
const {name: handleOrDid} = props.route.params
@ -98,7 +101,7 @@ export const ProfileFeedScreen = withAuthRequired(
<CenteredView>
<View style={[pal.view, pal.border, styles.notFoundContainer]}>
<Text type="title-lg" style={[pal.text, s.mb10]}>
Could not load feed
<Trans>Could not load feed</Trans>
</Text>
<Text type="md" style={[pal.text, s.mb20]}>
{error}
@ -107,12 +110,12 @@ export const ProfileFeedScreen = withAuthRequired(
<View style={{flexDirection: 'row'}}>
<Button
type="default"
accessibilityLabel="Go Back"
accessibilityLabel={_(msg`Go Back`)}
accessibilityHint="Return to previous page"
onPress={onPressBack}
style={{flexShrink: 1}}>
<Text type="button" style={pal.text}>
Go Back
<Trans>Go Back</Trans>
</Text>
</Button>
</View>
@ -142,6 +145,7 @@ export const ProfileFeedScreenInner = observer(
const pal = usePalette('default')
const store = useStores()
const {track} = useAnalytics()
const {_} = useLingui()
const feedSectionRef = React.useRef<SectionRef>(null)
const {rkey, name: handleOrDid} = route.params
const uri = useMemo(
@ -313,7 +317,7 @@ export const ProfileFeedScreenInner = observer(
<NativeDropdown
testID="headerDropdownBtn"
items={dropdownItems}
accessibilityLabel="More options"
accessibilityLabel={_(msg`More options`)}
accessibilityHint="">
<View style={[pal.viewLight, styles.btn]}>
<FontAwesomeIcon
@ -334,6 +338,7 @@ export const ProfileFeedScreenInner = observer(
onTogglePinned,
onToggleSaved,
dropdownItems,
_,
])
return (
@ -374,7 +379,7 @@ export const ProfileFeedScreenInner = observer(
/>
}
accessibilityRole="button"
accessibilityLabel="New post"
accessibilityLabel={_(msg`New post`)}
accessibilityHint=""
/>
</View>
@ -448,6 +453,7 @@ const AboutSection = observer(function AboutPageImpl({
onScroll: (e: NativeScrollEvent) => void
}) {
const pal = usePalette('default')
const {_} = useLingui()
const scrollHandler = useAnimatedScrollHandler({onScroll})
if (!feedInfo) {
@ -478,14 +484,14 @@ const AboutSection = observer(function AboutPageImpl({
/>
) : (
<Text type="lg" style={[{fontStyle: 'italic'}, pal.textLight]}>
No description
<Trans>No description</Trans>
</Text>
)}
<View style={{flexDirection: 'row', alignItems: 'center', gap: 10}}>
<Button
type="default"
testID="toggleLikeBtn"
accessibilityLabel="Like this feed"
accessibilityLabel={_(msg`Like this feed`)}
accessibilityHint=""
onPress={onToggleLiked}
style={{paddingHorizontal: 10}}>

View file

@ -45,6 +45,8 @@ import {makeProfileLink, makeListLink} from 'lib/routes/links'
import {ComposeIcon2} from 'lib/icons'
import {ListItems} from 'view/com/lists/ListItems'
import {logger} from '#/logger'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useSetMinimalShellMode} from '#/state/shell'
import {useModalControls} from '#/state/modals'
@ -107,6 +109,7 @@ export const ProfileListScreenInner = observer(
listOwnerDid,
}: Props & {listOwnerDid: string}) {
const store = useStores()
const {_} = useLingui()
const setMinimalShellMode = useSetMinimalShellMode()
const {rkey} = route.params
const feedSectionRef = React.useRef<SectionRef>(null)
@ -208,7 +211,7 @@ export const ProfileListScreenInner = observer(
/>
}
accessibilityRole="button"
accessibilityLabel="New post"
accessibilityLabel={_(msg`New post`)}
accessibilityHint=""
/>
</View>
@ -246,7 +249,7 @@ export const ProfileListScreenInner = observer(
/>
}
accessibilityRole="button"
accessibilityLabel="New post"
accessibilityLabel={_(msg`New post`)}
accessibilityHint=""
/>
</View>
@ -270,6 +273,7 @@ const Header = observer(function HeaderImpl({
}) {
const pal = usePalette('default')
const palInverted = usePalette('inverted')
const {_} = useLingui()
const navigation = useNavigation<NavigationProp>()
const {openModal, closeModal} = useModalControls()
@ -526,10 +530,12 @@ const Header = observer(function HeaderImpl({
<NativeDropdown
testID="subscribeBtn"
items={subscribeDropdownItems}
accessibilityLabel="Subscribe to this list"
accessibilityLabel={_(msg`Subscribe to this list`)}
accessibilityHint="">
<View style={[palInverted.view, styles.btn]}>
<Text style={palInverted.text}>Subscribe</Text>
<Text style={palInverted.text}>
<Trans>Subscribe</Trans>
</Text>
</View>
</NativeDropdown>
)
@ -537,7 +543,7 @@ const Header = observer(function HeaderImpl({
<NativeDropdown
testID="headerDropdownBtn"
items={dropdownItems}
accessibilityLabel="More options"
accessibilityLabel={_(msg`More options`)}
accessibilityHint="">
<View style={[pal.viewLight, styles.btn]}>
<FontAwesomeIcon icon="ellipsis" size={20} color={pal.colors.text} />
@ -624,6 +630,7 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
ref,
) {
const pal = usePalette('default')
const {_} = useLingui()
const {isMobile} = useWebMediaQueries()
const scrollElRef = React.useRef<FlatList>(null)
@ -662,7 +669,7 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
testID="listDescriptionEmpty"
type="lg"
style={[{fontStyle: 'italic'}, pal.textLight]}>
No description
<Trans>No description</Trans>
</Text>
)}
<Text type="md" style={[pal.textLight]} numberOfLines={1}>
@ -688,12 +695,14 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
paddingBottom: isMobile ? 14 : 18,
},
]}>
<Text type="lg-bold">Users</Text>
<Text type="lg-bold">
<Trans>Users</Trans>
</Text>
{isOwner && (
<Pressable
testID="addUserBtn"
accessibilityRole="button"
accessibilityLabel="Add a user to this list"
accessibilityLabel={_(msg`Add a user to this list`)}
accessibilityHint=""
onPress={onPressAddUser}
style={{flexDirection: 'row', alignItems: 'center', gap: 6}}>
@ -702,7 +711,9 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
color={pal.colors.link}
size={16}
/>
<Text style={pal.link}>Add</Text>
<Text style={pal.link}>
<Trans>Add</Trans>
</Text>
</Pressable>
)}
</View>
@ -717,6 +728,7 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
isCurateList,
isOwner,
onPressAddUser,
_,
])
const renderEmptyState = useCallback(() => {
@ -757,6 +769,7 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
function ErrorScreen({error}: {error: string}) {
const pal = usePalette('default')
const navigation = useNavigation<NavigationProp>()
const {_} = useLingui()
const onPressBack = useCallback(() => {
if (navigation.canGoBack()) {
navigation.goBack()
@ -778,7 +791,7 @@ function ErrorScreen({error}: {error: string}) {
},
]}>
<Text type="title-lg" style={[pal.text, s.mb10]}>
Could not load list
<Trans>Could not load list</Trans>
</Text>
<Text type="md" style={[pal.text, s.mb20]}>
{error}
@ -787,12 +800,12 @@ function ErrorScreen({error}: {error: string}) {
<View style={{flexDirection: 'row'}}>
<Button
type="default"
accessibilityLabel="Go Back"
accessibilityLabel={_(msg`Go Back`)}
accessibilityHint="Return to previous page"
onPress={onPressBack}
style={{flexShrink: 1}}>
<Text type="button" style={pal.text}>
Go Back
<Trans>Go Back</Trans>
</Text>
</Button>
</View>

View file

@ -63,6 +63,8 @@ import {
// -prf
import {useDebugHeaderSetting} from 'lib/api/debug-appview-proxy-header'
import {STATUS_PAGE_URL} from 'lib/constants'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'>
export const SettingsScreen = withAuthRequired(
@ -71,6 +73,7 @@ export const SettingsScreen = withAuthRequired(
const setColorMode = useSetColorMode()
const pal = usePalette('default')
const store = useStores()
const {_} = useLingui()
const setMinimalShellMode = useSetMinimalShellMode()
const requireAltTextEnabled = useRequireAltTextEnabled()
const setRequireAltTextEnabled = useSetRequireAltTextEnabled()
@ -213,7 +216,7 @@ export const SettingsScreen = withAuthRequired(
{store.session.currentSession !== undefined ? (
<>
<Text type="xl-bold" style={[pal.text, styles.heading]}>
Account
<Trans>Account</Trans>
</Text>
<View style={[styles.infoLine]}>
<Text type="lg-medium" style={pal.text}>
@ -233,17 +236,17 @@ export const SettingsScreen = withAuthRequired(
</Text>
<Link onPress={() => openModal({name: 'change-email'})}>
<Text type="lg" style={pal.link}>
Change
<Trans>Change</Trans>
</Text>
</Link>
</View>
<View style={[styles.infoLine]}>
<Text type="lg-medium" style={pal.text}>
Birthday:{' '}
<Trans>Birthday: </Trans>
</Text>
<Link onPress={() => openModal({name: 'birth-date-settings'})}>
<Text type="lg" style={pal.link}>
Show
<Trans>Show</Trans>
</Text>
</Link>
</View>
@ -253,7 +256,7 @@ export const SettingsScreen = withAuthRequired(
) : null}
<View style={[s.flexRow, styles.heading]}>
<Text type="xl-bold" style={pal.text}>
Signed in as
<Trans>Signed in as</Trans>
</Text>
<View style={s.flex1} />
</View>
@ -282,10 +285,10 @@ export const SettingsScreen = withAuthRequired(
testID="signOutBtn"
onPress={isSwitching ? undefined : onPressSignout}
accessibilityRole="button"
accessibilityLabel="Sign out"
accessibilityLabel={_(msg`Sign out`)}
accessibilityHint={`Signs ${store.me.displayName} out of Bluesky`}>
<Text type="lg" style={pal.link}>
Sign out
<Trans>Sign out</Trans>
</Text>
</TouchableOpacity>
</View>
@ -321,7 +324,7 @@ export const SettingsScreen = withAuthRequired(
style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
onPress={isSwitching ? undefined : onPressAddAccount}
accessibilityRole="button"
accessibilityLabel="Add account"
accessibilityLabel={_(msg`Add account`)}
accessibilityHint="Create a new Bluesky account">
<View style={[styles.iconContainer, pal.btn]}>
<FontAwesomeIcon
@ -330,21 +333,21 @@ export const SettingsScreen = withAuthRequired(
/>
</View>
<Text type="lg" style={pal.text}>
Add account
<Trans>Add account</Trans>
</Text>
</TouchableOpacity>
<View style={styles.spacer20} />
<Text type="xl-bold" style={[pal.text, styles.heading]}>
Invite a Friend
<Trans>Invite a Friend</Trans>
</Text>
<TouchableOpacity
testID="inviteFriendBtn"
style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
onPress={isSwitching ? undefined : onPressInviteCodes}
accessibilityRole="button"
accessibilityLabel="Invite"
accessibilityLabel={_(msg`Invite`)}
accessibilityHint="Opens invite code list">
<View
style={[
@ -371,7 +374,7 @@ export const SettingsScreen = withAuthRequired(
<View style={styles.spacer20} />
<Text type="xl-bold" style={[pal.text, styles.heading]}>
Accessibility
<Trans>Accessibility</Trans>
</Text>
<View style={[pal.view, styles.toggleCard]}>
<ToggleButton
@ -386,7 +389,7 @@ export const SettingsScreen = withAuthRequired(
<View style={styles.spacer20} />
<Text type="xl-bold" style={[pal.text, styles.heading]}>
Appearance
<Trans>Appearance</Trans>
</Text>
<View>
<View style={[styles.linkCard, pal.view, styles.selectableBtns]}>
@ -415,7 +418,7 @@ export const SettingsScreen = withAuthRequired(
<View style={styles.spacer20} />
<Text type="xl-bold" style={[pal.text, styles.heading]}>
Basics
<Trans>Basics</Trans>
</Text>
<TouchableOpacity
testID="preferencesHomeFeedButton"
@ -423,7 +426,7 @@ export const SettingsScreen = withAuthRequired(
onPress={openHomeFeedPreferences}
accessibilityRole="button"
accessibilityHint=""
accessibilityLabel="Opens the home feed preferences">
accessibilityLabel={_(msg`Opens the home feed preferences`)}>
<View style={[styles.iconContainer, pal.btn]}>
<FontAwesomeIcon
icon="sliders"
@ -431,7 +434,7 @@ export const SettingsScreen = withAuthRequired(
/>
</View>
<Text type="lg" style={pal.text}>
Home Feed Preferences
<Trans>Home Feed Preferences</Trans>
</Text>
</TouchableOpacity>
<TouchableOpacity
@ -440,7 +443,7 @@ export const SettingsScreen = withAuthRequired(
onPress={openThreadsPreferences}
accessibilityRole="button"
accessibilityHint=""
accessibilityLabel="Opens the threads preferences">
accessibilityLabel={_(msg`Opens the threads preferences`)}>
<View style={[styles.iconContainer, pal.btn]}>
<FontAwesomeIcon
icon={['far', 'comments']}
@ -449,20 +452,20 @@ export const SettingsScreen = withAuthRequired(
/>
</View>
<Text type="lg" style={pal.text}>
Thread Preferences
<Trans>Thread Preferences</Trans>
</Text>
</TouchableOpacity>
<TouchableOpacity
testID="savedFeedsBtn"
style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
accessibilityHint="My Saved Feeds"
accessibilityLabel="Opens screen with all saved feeds"
accessibilityLabel={_(msg`Opens screen with all saved feeds`)}
onPress={onPressSavedFeeds}>
<View style={[styles.iconContainer, pal.btn]}>
<HashtagIcon style={pal.text} size={18} strokeWidth={3} />
</View>
<Text type="lg" style={pal.text}>
My Saved Feeds
<Trans>My Saved Feeds</Trans>
</Text>
</TouchableOpacity>
<TouchableOpacity
@ -471,7 +474,7 @@ export const SettingsScreen = withAuthRequired(
onPress={isSwitching ? undefined : onPressLanguageSettings}
accessibilityRole="button"
accessibilityHint="Language settings"
accessibilityLabel="Opens configurable language settings">
accessibilityLabel={_(msg`Opens configurable language settings`)}>
<View style={[styles.iconContainer, pal.btn]}>
<FontAwesomeIcon
icon="language"
@ -479,7 +482,7 @@ export const SettingsScreen = withAuthRequired(
/>
</View>
<Text type="lg" style={pal.text}>
Languages
<Trans>Languages</Trans>
</Text>
</TouchableOpacity>
<TouchableOpacity
@ -490,18 +493,18 @@ export const SettingsScreen = withAuthRequired(
}
accessibilityRole="button"
accessibilityHint=""
accessibilityLabel="Opens moderation settings">
accessibilityLabel={_(msg`Opens moderation settings`)}>
<View style={[styles.iconContainer, pal.btn]}>
<HandIcon style={pal.text} size={18} strokeWidth={6} />
</View>
<Text type="lg" style={pal.text}>
Moderation
<Trans>Moderation</Trans>
</Text>
</TouchableOpacity>
<View style={styles.spacer20} />
<Text type="xl-bold" style={[pal.text, styles.heading]}>
Advanced
<Trans>Advanced</Trans>
</Text>
<TouchableOpacity
testID="appPasswordBtn"
@ -509,7 +512,7 @@ export const SettingsScreen = withAuthRequired(
onPress={onPressAppPasswords}
accessibilityRole="button"
accessibilityHint="Open app password settings"
accessibilityLabel="Opens the app password settings page">
accessibilityLabel={_(msg`Opens the app password settings page`)}>
<View style={[styles.iconContainer, pal.btn]}>
<FontAwesomeIcon
icon="lock"
@ -517,7 +520,7 @@ export const SettingsScreen = withAuthRequired(
/>
</View>
<Text type="lg" style={pal.text}>
App passwords
<Trans>App passwords</Trans>
</Text>
</TouchableOpacity>
<TouchableOpacity
@ -525,7 +528,7 @@ export const SettingsScreen = withAuthRequired(
style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
onPress={isSwitching ? undefined : onPressChangeHandle}
accessibilityRole="button"
accessibilityLabel="Change handle"
accessibilityLabel={_(msg`Change handle`)}
accessibilityHint="Choose a new Bluesky username or create">
<View style={[styles.iconContainer, pal.btn]}>
<FontAwesomeIcon
@ -534,19 +537,19 @@ export const SettingsScreen = withAuthRequired(
/>
</View>
<Text type="lg" style={pal.text} numberOfLines={1}>
Change handle
<Trans>Change handle</Trans>
</Text>
</TouchableOpacity>
<View style={styles.spacer20} />
<Text type="xl-bold" style={[pal.text, styles.heading]}>
Danger Zone
<Trans>Danger Zone</Trans>
</Text>
<TouchableOpacity
style={[pal.view, styles.linkCard]}
onPress={onPressDeleteAccount}
accessible={true}
accessibilityRole="button"
accessibilityLabel="Delete account"
accessibilityLabel={_(msg`Delete account`)}
accessibilityHint="Opens modal for account deletion confirmation. Requires email code.">
<View style={[styles.iconContainer, dangerBg]}>
<FontAwesomeIcon
@ -556,21 +559,21 @@ export const SettingsScreen = withAuthRequired(
/>
</View>
<Text type="lg" style={dangerText}>
Delete my account
<Trans>Delete my account</Trans>
</Text>
</TouchableOpacity>
<View style={styles.spacer20} />
<Text type="xl-bold" style={[pal.text, styles.heading]}>
Developer Tools
<Trans>Developer Tools</Trans>
</Text>
<TouchableOpacity
style={[pal.view, styles.linkCardNoIcon]}
onPress={onPressSystemLog}
accessibilityRole="button"
accessibilityHint="Open system log"
accessibilityLabel="Opens the system log page">
accessibilityLabel={_(msg`Opens the system log page`)}>
<Text type="lg" style={pal.text}>
System log
<Trans>System log</Trans>
</Text>
</TouchableOpacity>
{__DEV__ ? (
@ -588,9 +591,9 @@ export const SettingsScreen = withAuthRequired(
onPress={onPressStorybook}
accessibilityRole="button"
accessibilityHint="Open storybook page"
accessibilityLabel="Opens the storybook page">
accessibilityLabel={_(msg`Opens the storybook page`)}>
<Text type="lg" style={pal.text}>
Storybook
<Trans>Storybook</Trans>
</Text>
</TouchableOpacity>
<TouchableOpacity
@ -598,9 +601,9 @@ export const SettingsScreen = withAuthRequired(
onPress={onPressResetPreferences}
accessibilityRole="button"
accessibilityHint="Reset preferences"
accessibilityLabel="Resets the preferences state">
accessibilityLabel={_(msg`Resets the preferences state`)}>
<Text type="lg" style={pal.text}>
Reset preferences state
<Trans>Reset preferences state</Trans>
</Text>
</TouchableOpacity>
<TouchableOpacity
@ -608,9 +611,9 @@ export const SettingsScreen = withAuthRequired(
onPress={onPressResetOnboarding}
accessibilityRole="button"
accessibilityHint="Reset onboarding"
accessibilityLabel="Resets the onboarding state">
accessibilityLabel={_(msg`Resets the onboarding state`)}>
<Text type="lg" style={pal.text}>
Reset onboarding state
<Trans>Reset onboarding state</Trans>
</Text>
</TouchableOpacity>
</>
@ -620,7 +623,9 @@ export const SettingsScreen = withAuthRequired(
accessibilityRole="button"
onPress={onPressBuildInfo}>
<Text type="sm" style={[styles.buildInfo, pal.textLight]}>
Build version {AppInfo.appVersion} {AppInfo.updateChannel}
<Trans>
Build version {AppInfo.appVersion} {AppInfo.updateChannel}
</Trans>
</Text>
</TouchableOpacity>
<Text type="sm" style={[pal.textLight]}>
@ -630,7 +635,7 @@ export const SettingsScreen = withAuthRequired(
accessibilityRole="button"
onPress={onPressStatusPage}>
<Text type="sm" style={[styles.buildInfo, pal.textLight]}>
Status page
<Trans>Status page</Trans>
</Text>
</TouchableOpacity>
</View>
@ -646,6 +651,7 @@ const EmailConfirmationNotice = observer(
const pal = usePalette('default')
const palInverted = usePalette('inverted')
const store = useStores()
const {_} = useLingui()
const {isMobile} = useWebMediaQueries()
const {openModal} = useModalControls()
@ -656,7 +662,7 @@ const EmailConfirmationNotice = observer(
return (
<View style={{marginBottom: 20}}>
<Text type="xl-bold" style={[pal.text, styles.heading]}>
Verify email
<Trans>Verify email</Trans>
</Text>
<View
style={[
@ -681,7 +687,7 @@ const EmailConfirmationNotice = observer(
isMobile && {flex: 1},
]}
accessibilityRole="button"
accessibilityLabel="Verify my email"
accessibilityLabel={_(msg`Verify my email`)}
accessibilityHint=""
onPress={() => openModal({name: 'verify-email'})}>
<FontAwesomeIcon
@ -690,12 +696,12 @@ const EmailConfirmationNotice = observer(
size={16}
/>
<Text type="button" style={palInverted.text}>
Verify My Email
<Trans>Verify My Email</Trans>
</Text>
</Pressable>
</View>
<Text style={pal.textLight}>
Protect your account by verifying your email.
<Trans>Protect your account by verifying your email.</Trans>
</Text>
</View>
</View>

View file

@ -43,6 +43,8 @@ import {NavigationProp} from 'lib/routes/types'
import {useNavigationTabState} from 'lib/hooks/useNavigationTabState'
import {isWeb} from 'platform/detection'
import {formatCount, formatCountShortOnly} from 'view/com/util/numeric/format'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useSetDrawerOpen} from '#/state/shell'
import {useModalControls} from '#/state/modals'
@ -50,6 +52,7 @@ export const DrawerContent = observer(function DrawerContentImpl() {
const theme = useTheme()
const pal = usePalette('default')
const store = useStores()
const {_} = useLingui()
const setDrawerOpen = useSetDrawerOpen()
const navigation = useNavigation<NavigationProp>()
const {track} = useAnalytics()
@ -158,7 +161,7 @@ export const DrawerContent = observer(function DrawerContentImpl() {
<View style={{}}>
<TouchableOpacity
testID="profileCardButton"
accessibilityLabel="Profile"
accessibilityLabel={_(msg`Profile`)}
accessibilityHint="Navigates to your profile"
onPress={onPressProfile}>
<UserAvatar
@ -215,7 +218,7 @@ export const DrawerContent = observer(function DrawerContentImpl() {
)
}
label="Search"
accessibilityLabel="Search"
accessibilityLabel={_(msg`Search`)}
accessibilityHint=""
bold={isAtSearch}
onPress={onPressSearch}
@ -237,7 +240,7 @@ export const DrawerContent = observer(function DrawerContentImpl() {
)
}
label="Home"
accessibilityLabel="Home"
accessibilityLabel={_(msg`Home`)}
accessibilityHint=""
bold={isAtHome}
onPress={onPressHome}
@ -259,7 +262,7 @@ export const DrawerContent = observer(function DrawerContentImpl() {
)
}
label="Notifications"
accessibilityLabel="Notifications"
accessibilityLabel={_(msg`Notifications`)}
accessibilityHint={
notifications.unreadCountLabel === ''
? ''
@ -286,7 +289,7 @@ export const DrawerContent = observer(function DrawerContentImpl() {
)
}
label="Feeds"
accessibilityLabel="Feeds"
accessibilityLabel={_(msg`Feeds`)}
accessibilityHint=""
bold={isAtFeeds}
onPress={onPressMyFeeds}
@ -294,14 +297,14 @@ export const DrawerContent = observer(function DrawerContentImpl() {
<MenuItem
icon={<ListIcon strokeWidth={2} style={pal.text} size={26} />}
label="Lists"
accessibilityLabel="Lists"
accessibilityLabel={_(msg`Lists`)}
accessibilityHint=""
onPress={onPressLists}
/>
<MenuItem
icon={<HandIcon strokeWidth={5} style={pal.text} size={24} />}
label="Moderation"
accessibilityLabel="Moderation"
accessibilityLabel={_(msg`Moderation`)}
accessibilityHint=""
onPress={onPressModeration}
/>
@ -322,7 +325,7 @@ export const DrawerContent = observer(function DrawerContentImpl() {
)
}
label="Profile"
accessibilityLabel="Profile"
accessibilityLabel={_(msg`Profile`)}
accessibilityHint=""
onPress={onPressProfile}
/>
@ -335,7 +338,7 @@ export const DrawerContent = observer(function DrawerContentImpl() {
/>
}
label="Settings"
accessibilityLabel="Settings"
accessibilityLabel={_(msg`Settings`)}
accessibilityHint=""
onPress={onPressSettings}
/>
@ -346,7 +349,7 @@ export const DrawerContent = observer(function DrawerContentImpl() {
<View style={styles.footer}>
<TouchableOpacity
accessibilityRole="link"
accessibilityLabel="Send feedback"
accessibilityLabel={_(msg`Send feedback`)}
accessibilityHint=""
onPress={onPressFeedback}
style={[
@ -362,17 +365,17 @@ export const DrawerContent = observer(function DrawerContentImpl() {
icon={['far', 'message']}
/>
<Text type="lg-medium" style={[pal.link, s.pl10]}>
Feedback
<Trans>Feedback</Trans>
</Text>
</TouchableOpacity>
<TouchableOpacity
accessibilityRole="link"
accessibilityLabel="Send feedback"
accessibilityLabel={_(msg`Send feedback`)}
accessibilityHint=""
onPress={onPressHelp}
style={[styles.footerBtn]}>
<Text type="lg-medium" style={[pal.link, s.pl10]}>
Help
<Trans>Help</Trans>
</Text>
</TouchableOpacity>
</View>

View file

@ -24,6 +24,8 @@ import {styles} from './BottomBarStyles'
import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
import {useNavigationTabState} from 'lib/hooks/useNavigationTabState'
import {UserAvatar} from 'view/com/util/UserAvatar'
import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro'
import {useModalControls} from '#/state/modals'
type TabOptions = 'Home' | 'Search' | 'Notifications' | 'MyProfile' | 'Feeds'
@ -34,6 +36,7 @@ export const BottomBar = observer(function BottomBarImpl({
const {openModal} = useModalControls()
const store = useStores()
const pal = usePalette('default')
const {_} = useLingui()
const safeAreaInsets = useSafeAreaInsets()
const {track} = useAnalytics()
const {isAtHome, isAtSearch, isAtFeeds, isAtNotifications, isAtMyProfile} =
@ -105,7 +108,7 @@ export const BottomBar = observer(function BottomBarImpl({
}
onPress={onPressHome}
accessibilityRole="tab"
accessibilityLabel="Home"
accessibilityLabel={_(msg`Home`)}
accessibilityHint=""
/>
<Btn
@ -127,7 +130,7 @@ export const BottomBar = observer(function BottomBarImpl({
}
onPress={onPressSearch}
accessibilityRole="search"
accessibilityLabel="Search"
accessibilityLabel={_(msg`Search`)}
accessibilityHint=""
/>
<Btn
@ -149,7 +152,7 @@ export const BottomBar = observer(function BottomBarImpl({
}
onPress={onPressFeeds}
accessibilityRole="tab"
accessibilityLabel="Feeds"
accessibilityLabel={_(msg`Feeds`)}
accessibilityHint=""
/>
<Btn
@ -173,7 +176,7 @@ export const BottomBar = observer(function BottomBarImpl({
notificationCount={notifications.unreadCountLabel}
accessible={true}
accessibilityRole="tab"
accessibilityLabel="Notifications"
accessibilityLabel={_(msg`Notifications`)}
accessibilityHint={
notifications.unreadCountLabel === ''
? ''
@ -215,7 +218,7 @@ export const BottomBar = observer(function BottomBarImpl({
onPress={onPressProfile}
onLongPress={onLongPressProfile}
accessibilityRole="tab"
accessibilityLabel="Profile"
accessibilityLabel={_(msg`Profile`)}
accessibilityHint=""
/>
</Animated.View>

View file

@ -39,6 +39,8 @@ import {getCurrentRoute, isTab, isStateAtTabRoot} from 'lib/routes/helpers'
import {NavigationProp, CommonNavigatorParams} from 'lib/routes/types'
import {router} from '../../../routes'
import {makeProfileLink} from 'lib/routes/links'
import {useLingui} from '@lingui/react'
import {Trans, msg} from '@lingui/macro'
const ProfileCard = observer(function ProfileCardImpl() {
const store = useStores()
@ -67,6 +69,7 @@ function BackBtn() {
const {isTablet} = useWebMediaQueries()
const pal = usePalette('default')
const navigation = useNavigation<NavigationProp>()
const {_} = useLingui()
const shouldShow = useNavigationState(state => !isStateAtTabRoot(state))
const onPressBack = React.useCallback(() => {
@ -86,7 +89,7 @@ function BackBtn() {
onPress={onPressBack}
style={styles.backBtn}
accessibilityRole="button"
accessibilityLabel="Go back"
accessibilityLabel={_(msg`Go back`)}
accessibilityHint="">
<FontAwesomeIcon
size={24}
@ -184,6 +187,7 @@ const NavItem = observer(function NavItemImpl({
function ComposeBtn() {
const store = useStores()
const {getState} = useNavigation()
const {_} = useLingui()
const {isTablet} = useWebMediaQueries()
const getProfileHandle = async () => {
@ -222,7 +226,7 @@ function ComposeBtn() {
style={[styles.newPostBtn]}
onPress={onPressCompose}
accessibilityRole="button"
accessibilityLabel="New post"
accessibilityLabel={_(msg`New post`)}
accessibilityHint="">
<View style={styles.newPostBtnIconWrapper}>
<ComposeIcon2
@ -232,7 +236,7 @@ function ComposeBtn() {
/>
</View>
<Text type="button" style={styles.newPostBtnLabel}>
New Post
<Trans>New Post</Trans>
</Text>
</TouchableOpacity>
)

View file

@ -9,10 +9,13 @@ import {MagnifyingGlassIcon2} from 'lib/icons'
import {NavigationProp} from 'lib/routes/types'
import {ProfileCard} from 'view/com/profile/ProfileCard'
import {Text} from 'view/com/util/text/Text'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
export const DesktopSearch = observer(function DesktopSearch() {
const store = useStores()
const pal = usePalette('default')
const {_} = useLingui()
const textInput = React.useRef<TextInput>(null)
const [isInputFocused, setIsInputFocused] = React.useState<boolean>(false)
const [query, setQuery] = React.useState<string>('')
@ -75,7 +78,7 @@ export const DesktopSearch = observer(function DesktopSearch() {
onChangeText={onChangeQuery}
onSubmitEditing={onSubmit}
accessibilityRole="search"
accessibilityLabel="Search"
accessibilityLabel={_(msg`Search`)}
accessibilityHint=""
/>
{query ? (
@ -83,11 +86,11 @@ export const DesktopSearch = observer(function DesktopSearch() {
<TouchableOpacity
onPress={onPressCancelSearch}
accessibilityRole="button"
accessibilityLabel="Cancel search"
accessibilityLabel={_(msg`Cancel search`)}
accessibilityHint="Exits inputting search query"
onAccessibilityEscape={onPressCancelSearch}>
<Text type="lg" style={[pal.link]}>
Cancel
<Trans>Cancel</Trans>
</Text>
</TouchableOpacity>
</View>
@ -106,7 +109,7 @@ export const DesktopSearch = observer(function DesktopSearch() {
) : (
<View>
<Text style={[pal.textLight, styles.noResults]}>
No results found for {autocompleteView.prefix}
<Trans>No results found for {autocompleteView.prefix}</Trans>
</Text>
</View>
)}

View file

@ -17,6 +17,7 @@ import {BottomBarWeb} from './bottom-bar/BottomBarWeb'
import {useNavigation} from '@react-navigation/native'
import {NavigationProp} from 'lib/routes/types'
import {useAuxClick} from 'lib/hooks/useAuxClick'
import {t} from '@lingui/macro'
import {
useIsDrawerOpen,
useSetDrawerOpen,
@ -73,7 +74,7 @@ const ShellInner = observer(function ShellInnerImpl() {
<TouchableOpacity
onPress={() => setDrawerOpen(false)}
style={styles.drawerMask}
accessibilityLabel="Close navigation footer"
accessibilityLabel={t`Close navigation footer`}
accessibilityHint="Closes bottom navigation bar">
<View style={styles.drawerContainer}>
<DrawerContent />