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
zio/stable
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

@ -45,6 +45,7 @@ module.exports = function (api) {
},
},
],
'macros',
'react-native-reanimated/plugin', // NOTE: this plugin MUST be last
],
env: {

View File

@ -0,0 +1,113 @@
# Internationalization
We want the official Bluesky app to be supported in as many languages as possible. If you want to help us translate the app, please open a PR or issue on the [Bluesky app repo on GitHub](https://github.com/bluesky-social/social-app)
## Tools
We are using Lingui to manage translations. You can find the documentation [here](https://lingui.dev/).
### Adding new strings
When adding a new string, do it as follows:
```jsx
// Before
import { Text } from "react-native";
<Text>Hello World</Text>
```
```jsx
// After
import { Text } from "react-native";
import { Trans } from "@lingui/macro";
<Text><Trans>Hello World</Trans></Text>
```
The `<Trans>` macro will extract the string and add it to the catalog. It is not really a component, but a macro. Further reading [here](https://lingui.dev/ref/macro.html)
However sometimes you will run into this case:
```jsx
// Before
import { Text } from "react-native";
const text = "Hello World";
<Text accessibilityLabel="Label is here">{text}</Text>
```
In this case, you cannot use the `useLingui()` hook:
```jsx
import { msg } from "@lingui/macro";
import { useLingui } from "@lingui/react";
const { _ } = useLingui();
return <Text accessibilityLabel={_(msg`Label is here`)}>{text}</Text>
```
If you want to do this outside of a React component, you can use the `t` macro instead (note: this won't react to changes if the locale is switched dynamically within the app):
```jsx
import { t } from "@lingui/macro";
const text = t`Hello World`;
```
We can then run `yarn intl:extract` to update the catalog in `src/locale/locales/{locale}/messages.po`. This will add the new string to the catalog.
We can then run `yarn intl:compile` to update the translation files in `src/locale/locales/{locale}/messages.js`. This will add the new string to the translation files.
The configuration for translations is defined in `lingui.config.js`
So the workflow is as follows:
1. Wrap messages in Trans macro
2. Run `yarn intl:extract` command to generate message catalogs
3. Translate message catalogs (send them to translators usually)
4. Run `yarn intl:compile` to create runtime catalogs
5. Load runtime catalog
6. Enjoy translated app!
### Common pitfalls
These pitfalls are memoization pitfalls that will cause the components to not re-render when the locale is changed -- causing stale translations to be shown.
```jsx
import { msg } from "@lingui/macro";
import { i18n } from "@lingui/core";
const welcomeMessage = msg`Welcome!`;
// ❌ Bad! This code won't work
export function Welcome() {
const buggyWelcome = useMemo(() => {
return i18n._(welcomeMessage);
}, []);
return <div>{buggyWelcome}</div>;
}
// ❌ Bad! This code won't work either because the reference to i18n does not change
export function Welcome() {
const { i18n } = useLingui();
const buggyWelcome = useMemo(() => {
return i18n._(welcomeMessage);
}, [i18n]);
return <div>{buggyWelcome}</div>;
}
// ✅ Good! `useMemo` has i18n context in the dependency
export function Welcome() {
const linguiCtx = useLingui();
const welcome = useMemo(() => {
return linguiCtx.i18n._(welcomeMessage);
}, [linguiCtx]);
return <div>{welcome}</div>;
}
// 🤩 Better! `useMemo` consumes the `_` function from the Lingui context
export function Welcome() {
const { _ } = useLingui();
const welcome = useMemo(() => {
return _(welcomeMessage);
}, [_]);
return <div>{welcome}</div>;
}
```

11
lingui.config.js 100644
View File

@ -0,0 +1,11 @@
/** @type {import('@lingui/conf').LinguiConfig} */
module.exports = {
locales: ['en', 'cs', 'fr', 'hi', 'es'],
catalogs: [
{
path: '<rootDir>/src/locale/locales/{locale}/messages',
include: ['src'],
},
],
format: 'po',
}

View File

@ -28,7 +28,9 @@
"perf:test:measure": "NODE_ENV=test flashlight test --bundleId xyz.blueskyweb.app --testCommand 'yarn perf:test' --duration 150000 --resultsFilePath .perf/results.json",
"perf:test:results": "NODE_ENV=test flashlight report .perf/results.json",
"perf:measure": "NODE_ENV=test flashlight measure",
"build:apk": "eas build -p android --profile dev-android-apk"
"build:apk": "eas build -p android --profile dev-android-apk",
"intl:extract": "lingui extract",
"intl:compile": "lingui compile"
},
"dependencies": {
"@atproto/api": "^0.6.23",
@ -42,6 +44,7 @@
"@fortawesome/free-solid-svg-icons": "^6.1.1",
"@fortawesome/react-native-fontawesome": "^0.3.0",
"@gorhom/bottom-sheet": "^4.5.1",
"@lingui/react": "^4.5.0",
"@mattermost/react-native-paste-input": "^0.6.4",
"@miblanchard/react-native-slider": "^2.3.1",
"@react-native-async-storage/async-storage": "1.18.2",
@ -164,10 +167,12 @@
},
"devDependencies": {
"@atproto/dev-env": "^0.2.5",
"@babel/core": "^7.20.0",
"@babel/core": "^7.23.2",
"@babel/preset-env": "^7.20.0",
"@babel/runtime": "^7.20.0",
"@did-plc/server": "^0.0.1",
"@lingui/cli": "^4.5.0",
"@lingui/macro": "^4.5.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.11",
"@react-native-community/eslint-config": "^3.0.0",
"@testing-library/jest-native": "^5.4.1",
@ -192,6 +197,7 @@
"@typescript-eslint/parser": "^5.48.2",
"babel-jest": "^29.4.2",
"babel-loader": "^9.1.2",
"babel-plugin-macros": "^3.1.0",
"babel-plugin-module-resolver": "^5.0.0",
"babel-plugin-react-native-web": "^0.18.12",
"detox": "^20.13.0",

View File

@ -16,6 +16,9 @@ import {Shell} from 'view/shell/index'
import {ToastContainer} from 'view/com/util/Toast.web'
import {ThemeProvider} from 'lib/ThemeContext'
import {queryClient} from 'lib/react-query'
import {i18n} from '@lingui/core'
import {I18nProvider} from '@lingui/react'
import {defaultLocale, dynamicActivate} from './locale/i18n'
import {Provider as ShellStateProvider} from 'state/shell'
import {Provider as ModalStateProvider} from 'state/modals'
import {Provider as MutedThreadsProvider} from 'state/muted-threads'
@ -34,6 +37,7 @@ const InnerApp = observer(function AppImpl() {
setRootStore(store)
analytics.init(store)
})
dynamicActivate(defaultLocale) // async import of locale data
}, [])
// show nothing prior to init
@ -47,9 +51,11 @@ const InnerApp = observer(function AppImpl() {
<RootSiblingParent>
<analytics.Provider>
<RootStoreProvider value={rootStore}>
<I18nProvider i18n={i18n}>
<SafeAreaProvider>
<Shell />
</SafeAreaProvider>
</I18nProvider>
<ToastContainer />
</RootStoreProvider>
</analytics.Provider>

20
src/locale/i18n.ts 100644
View File

@ -0,0 +1,20 @@
import {i18n} from '@lingui/core'
export const locales = {
en: 'English',
cs: 'Česky',
fr: 'Français',
hi: 'हिंदी',
es: 'Español',
}
export const defaultLocale = 'en'
/**
* We do a dynamic import of just the catalog that we need
* @param locale any locale string
*/
export async function dynamicActivate(locale: string) {
const {messages} = await import(`./locales/${locale}/messages`)
i18n.load(locale, messages)
i18n.activate(locale)
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

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]}>
<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]}>
<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]}>
<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>{' '}
<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]}>
<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]}>
<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]}>
<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]}>
<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]}>
<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]}>
<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]}>
<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]}>
<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]}>
<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]}>
<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]}>
<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]}>
<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]}>
<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}}>

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