about summary refs log tree commit diff
path: root/src/view/com/modals
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/com/modals')
-rw-r--r--src/view/com/modals/AddAppPasswords.tsx28
-rw-r--r--src/view/com/modals/AltImage.tsx147
-rw-r--r--src/view/com/modals/AppealLabel.tsx4
-rw-r--r--src/view/com/modals/BirthDateSettings.tsx5
-rw-r--r--src/view/com/modals/ChangeEmail.tsx6
-rw-r--r--src/view/com/modals/ChangeHandle.tsx16
-rw-r--r--src/view/com/modals/Confirm.tsx10
-rw-r--r--src/view/com/modals/ContentFilteringSettings.tsx57
-rw-r--r--src/view/com/modals/CreateOrEditList.tsx160
-rw-r--r--src/view/com/modals/DeleteAccount.tsx25
-rw-r--r--src/view/com/modals/EditImage.tsx10
-rw-r--r--src/view/com/modals/EditProfile.tsx13
-rw-r--r--src/view/com/modals/EmbedConsent.tsx153
-rw-r--r--src/view/com/modals/InAppBrowserConsent.tsx102
-rw-r--r--src/view/com/modals/InviteCodes.tsx19
-rw-r--r--src/view/com/modals/LinkWarning.tsx6
-rw-r--r--src/view/com/modals/ListAddRemoveUsers.tsx6
-rw-r--r--src/view/com/modals/Modal.tsx8
-rw-r--r--src/view/com/modals/Modal.web.tsx14
-rw-r--r--src/view/com/modals/ModerationDetails.tsx45
-rw-r--r--src/view/com/modals/ProfilePreview.tsx11
-rw-r--r--src/view/com/modals/Repost.tsx24
-rw-r--r--src/view/com/modals/SelfLabel.tsx8
-rw-r--r--src/view/com/modals/ServerInput.tsx6
-rw-r--r--src/view/com/modals/SwitchAccount.tsx8
-rw-r--r--src/view/com/modals/Threadgate.tsx4
-rw-r--r--src/view/com/modals/UserAddRemoveLists.tsx26
-rw-r--r--src/view/com/modals/VerifyEmail.tsx34
-rw-r--r--src/view/com/modals/Waitlist.tsx16
-rw-r--r--src/view/com/modals/report/InputIssueDetails.tsx3
-rw-r--r--src/view/com/modals/report/Modal.tsx6
31 files changed, 714 insertions, 266 deletions
diff --git a/src/view/com/modals/AddAppPasswords.tsx b/src/view/com/modals/AddAppPasswords.tsx
index 812a36f45..7ec8268be 100644
--- a/src/view/com/modals/AddAppPasswords.tsx
+++ b/src/view/com/modals/AddAppPasswords.tsx
@@ -72,10 +72,10 @@ export function Component({}: {}) {
   const onCopy = React.useCallback(() => {
     if (appPassword) {
       Clipboard.setString(appPassword)
-      Toast.show('Copied to clipboard')
+      Toast.show(_(msg`Copied to clipboard`))
       setWasCopied(true)
     }
-  }, [appPassword])
+  }, [appPassword, _])
 
   const onDone = React.useCallback(() => {
     closeModal()
@@ -85,7 +85,9 @@ export function Component({}: {}) {
     // if name is all whitespace, we don't allow it
     if (!name || !name.trim()) {
       Toast.show(
-        'Please enter a name for your app password. All spaces is not allowed.',
+        _(
+          msg`Please enter a name for your app password. All spaces is not allowed.`,
+        ),
         'times',
       )
       return
@@ -93,14 +95,14 @@ export function Component({}: {}) {
     // if name is too short (under 4 chars), we don't allow it
     if (name.length < 4) {
       Toast.show(
-        'App Password names must be at least 4 characters long.',
+        _(msg`App Password names must be at least 4 characters long.`),
         'times',
       )
       return
     }
 
     if (passwords?.find(p => p.name === name)) {
-      Toast.show('This name is already in use', 'times')
+      Toast.show(_(msg`This name is already in use`), 'times')
       return
     }
 
@@ -109,11 +111,11 @@ export function Component({}: {}) {
       if (newPassword) {
         setAppPassword(newPassword.password)
       } else {
-        Toast.show('Failed to create app password.', 'times')
+        Toast.show(_(msg`Failed to create app password.`), 'times')
         // TODO: better error handling (?)
       }
     } catch (e) {
-      Toast.show('Failed to create app password.', 'times')
+      Toast.show(_(msg`Failed to create app password.`), 'times')
       logger.error('Failed to create app password', {error: e})
     }
   }
@@ -127,7 +129,9 @@ export function Component({}: {}) {
       setName(text)
     } else {
       Toast.show(
-        'App Password names can only contain letters, numbers, spaces, dashes, and underscores.',
+        _(
+          msg`App Password names can only contain letters, numbers, spaces, dashes, and underscores.`,
+        ),
       )
     }
   }
@@ -158,7 +162,7 @@ export function Component({}: {}) {
               style={[styles.input, pal.text]}
               onChangeText={_onChangeText}
               value={name}
-              placeholder="Enter a name for this App Password"
+              placeholder={_(msg`Enter a name for this App Password`)}
               placeholderTextColor={pal.colors.textLight}
               autoCorrect={false}
               autoComplete="off"
@@ -175,7 +179,7 @@ export function Component({}: {}) {
               onEndEditing={createAppPassword}
               accessible={true}
               accessibilityLabel={_(msg`Name`)}
-              accessibilityHint="Input name for app password"
+              accessibilityHint={_(msg`Input name for app password`)}
             />
           </View>
         ) : (
@@ -184,7 +188,7 @@ export function Component({}: {}) {
             onPress={onCopy}
             accessibilityRole="button"
             accessibilityLabel={_(msg`Copy`)}
-            accessibilityHint="Copies app password">
+            accessibilityHint={_(msg`Copies app password`)}>
             <Text type="2xl-bold" style={[pal.text]}>
               {appPassword}
             </Text>
@@ -221,7 +225,7 @@ export function Component({}: {}) {
       <View style={styles.btnContainer}>
         <Button
           type="primary"
-          label={!appPassword ? 'Create App Password' : 'Done'}
+          label={!appPassword ? _(msg`Create App Password`) : _(msg`Done`)}
           style={styles.btn}
           labelStyle={styles.btnLabel}
           onPress={!appPassword ? createAppPassword : onDone}
diff --git a/src/view/com/modals/AltImage.tsx b/src/view/com/modals/AltImage.tsx
index a2e918317..5156511d6 100644
--- a/src/view/com/modals/AltImage.tsx
+++ b/src/view/com/modals/AltImage.tsx
@@ -1,14 +1,12 @@
 import React, {useMemo, useCallback, useState} from 'react'
 import {
   ImageStyle,
-  KeyboardAvoidingView,
-  ScrollView,
   StyleSheet,
-  TextInput,
   TouchableOpacity,
   View,
   useWindowDimensions,
 } from 'react-native'
+import {ScrollView, TextInput} from './util'
 import {Image} from 'expo-image'
 import {usePalette} from 'lib/hooks/usePalette'
 import {gradients, s} from 'lib/styles'
@@ -17,13 +15,13 @@ import {MAX_ALT_TEXT} from 'lib/constants'
 import {useTheme} from 'lib/ThemeContext'
 import {Text} from '../util/text/Text'
 import LinearGradient from 'react-native-linear-gradient'
-import {isAndroid, isWeb} from 'platform/detection'
+import {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']
+export const snapPoints = ['100%']
 
 interface Props {
   image: ImageModel
@@ -54,102 +52,86 @@ export function Component({image}: Props) {
     }
   }, [image, windim])
 
+  const onUpdate = useCallback(
+    (v: string) => {
+      v = enforceLen(v, MAX_ALT_TEXT)
+      setAltText(v)
+      image.setAltText(v)
+    },
+    [setAltText, image],
+  )
+
   const onPressSave = useCallback(() => {
     image.setAltText(altText)
     closeModal()
   }, [closeModal, image, altText])
 
-  const onPressCancel = () => {
-    closeModal()
-  }
-
   return (
-    <KeyboardAvoidingView
-      behavior={isAndroid ? 'height' : 'padding'}
-      style={[pal.view, styles.container]}>
-      <ScrollView
-        testID="altTextImageModal"
-        style={styles.scrollContainer}
-        keyboardShouldPersistTaps="always"
-        nativeID="imageAltText">
-        <View style={styles.scrollInner}>
-          <View style={[pal.viewLight, styles.imageContainer]}>
-            <Image
-              testID="selectedPhotoImage"
-              style={imageStyles}
-              source={{
-                uri: image.cropped?.path ?? image.path,
-              }}
-              contentFit="contain"
-              accessible={true}
-              accessibilityIgnoresInvertColors
-            />
-          </View>
-          <TextInput
-            testID="altTextImageInput"
-            style={[styles.textArea, pal.border, pal.text]}
-            keyboardAppearance={theme.colorScheme}
-            multiline
-            placeholder="Add alt text"
-            placeholderTextColor={pal.colors.textLight}
-            value={altText}
-            onChangeText={text => setAltText(enforceLen(text, MAX_ALT_TEXT))}
-            accessibilityLabel={_(msg`Image alt text`)}
-            accessibilityHint=""
-            accessibilityLabelledBy="imageAltText"
-            autoFocus
+    <ScrollView
+      testID="altTextImageModal"
+      style={[pal.view, styles.scrollContainer]}
+      keyboardShouldPersistTaps="always"
+      nativeID="imageAltText">
+      <View style={styles.scrollInner}>
+        <View style={[pal.viewLight, styles.imageContainer]}>
+          <Image
+            testID="selectedPhotoImage"
+            style={imageStyles}
+            source={{
+              uri: image.cropped?.path ?? image.path,
+            }}
+            contentFit="contain"
+            accessible={true}
+            accessibilityIgnoresInvertColors
           />
-          <View style={styles.buttonControls}>
-            <TouchableOpacity
-              testID="altTextImageSaveBtn"
-              onPress={onPressSave}
-              accessibilityLabel={_(msg`Save alt text`)}
-              accessibilityHint={`Saves alt text, which reads: ${altText}`}
-              accessibilityRole="button">
-              <LinearGradient
-                colors={[gradients.blueLight.start, gradients.blueLight.end]}
-                start={{x: 0, y: 0}}
-                end={{x: 1, y: 1}}
-                style={[styles.button]}>
-                <Text type="button-lg" style={[s.white, s.bold]}>
-                  <Trans>Save</Trans>
-                </Text>
-              </LinearGradient>
-            </TouchableOpacity>
-            <TouchableOpacity
-              testID="altTextImageCancelBtn"
-              onPress={onPressCancel}
-              accessibilityRole="button"
-              accessibilityLabel={_(msg`Cancel add image alt text`)}
-              accessibilityHint=""
-              onAccessibilityEscape={onPressCancel}>
-              <View style={[styles.button]}>
-                <Text type="button-lg" style={[pal.textLight]}>
-                  <Trans>Cancel</Trans>
-                </Text>
-              </View>
-            </TouchableOpacity>
-          </View>
         </View>
-      </ScrollView>
-    </KeyboardAvoidingView>
+        <TextInput
+          testID="altTextImageInput"
+          style={[styles.textArea, pal.border, pal.text]}
+          keyboardAppearance={theme.colorScheme}
+          multiline
+          placeholder={_(msg`Add alt text`)}
+          placeholderTextColor={pal.colors.textLight}
+          value={altText}
+          onChangeText={onUpdate}
+          accessibilityLabel={_(msg`Image alt text`)}
+          accessibilityHint=""
+          accessibilityLabelledBy="imageAltText"
+          autoFocus
+        />
+        <View style={styles.buttonControls}>
+          <TouchableOpacity
+            testID="altTextImageSaveBtn"
+            onPress={onPressSave}
+            accessibilityLabel={_(msg`Save alt text`)}
+            accessibilityHint=""
+            accessibilityRole="button">
+            <LinearGradient
+              colors={[gradients.blueLight.start, gradients.blueLight.end]}
+              start={{x: 0, y: 0}}
+              end={{x: 1, y: 1}}
+              style={[styles.button]}>
+              <Text type="button-lg" style={[s.white, s.bold]}>
+                <Trans>Done</Trans>
+              </Text>
+            </LinearGradient>
+          </TouchableOpacity>
+        </View>
+      </View>
+    </ScrollView>
   )
 }
 
 const styles = StyleSheet.create({
-  container: {
-    flex: 1,
-    height: '100%',
-    width: '100%',
-    paddingVertical: isWeb ? 0 : 18,
-  },
   scrollContainer: {
     flex: 1,
     height: '100%',
     paddingHorizontal: isWeb ? 0 : 12,
+    paddingVertical: isWeb ? 0 : 24,
   },
   scrollInner: {
     gap: 12,
+    paddingTop: isWeb ? 0 : 12,
   },
   imageContainer: {
     borderRadius: 8,
@@ -173,5 +155,6 @@ const styles = StyleSheet.create({
   },
   buttonControls: {
     gap: 8,
+    paddingBottom: isWeb ? 0 : 50,
   },
 })
diff --git a/src/view/com/modals/AppealLabel.tsx b/src/view/com/modals/AppealLabel.tsx
index edc6f4cd0..b0aaaf625 100644
--- a/src/view/com/modals/AppealLabel.tsx
+++ b/src/view/com/modals/AppealLabel.tsx
@@ -38,14 +38,14 @@ export function Component(props: ReportComponentProps) {
         ? 'com.atproto.repo.strongRef'
         : 'com.atproto.admin.defs#repoRef'
       await getAgent().createModerationReport({
-        reasonType: ComAtprotoModerationDefs.REASONOTHER,
+        reasonType: ComAtprotoModerationDefs.REASONAPPEAL,
         subject: {
           $type,
           ...props,
         },
         reason: details,
       })
-      Toast.show("We'll look into your appeal promptly.")
+      Toast.show(_(msg`We'll look into your appeal promptly.`))
     } finally {
       closeModal()
     }
diff --git a/src/view/com/modals/BirthDateSettings.tsx b/src/view/com/modals/BirthDateSettings.tsx
index c78f06ed4..5ebc61137 100644
--- a/src/view/com/modals/BirthDateSettings.tsx
+++ b/src/view/com/modals/BirthDateSettings.tsx
@@ -23,7 +23,7 @@ import {
 } from '#/state/queries/preferences'
 import {logger} from '#/logger'
 
-export const snapPoints = ['50%']
+export const snapPoints = ['50%', '90%']
 
 function Inner({preferences}: {preferences: UsePreferencesQueryResponse}) {
   const pal = usePalette('default')
@@ -63,6 +63,7 @@ function Inner({preferences}: {preferences: UsePreferencesQueryResponse}) {
 
       <View>
         <DateInput
+          handleAsUTC
           testID="birthdayInput"
           value={date}
           onChange={setDate}
@@ -70,7 +71,7 @@ function Inner({preferences}: {preferences: UsePreferencesQueryResponse}) {
           buttonStyle={[pal.border, styles.dateInputButton]}
           buttonLabelType="lg"
           accessibilityLabel={_(msg`Birthday`)}
-          accessibilityHint="Enter your birth date"
+          accessibilityHint={_(msg`Enter your birth date`)}
           accessibilityLabelledBy="birthDate"
         />
       </View>
diff --git a/src/view/com/modals/ChangeEmail.tsx b/src/view/com/modals/ChangeEmail.tsx
index 44b102fa0..c5672bc81 100644
--- a/src/view/com/modals/ChangeEmail.tsx
+++ b/src/view/com/modals/ChangeEmail.tsx
@@ -38,7 +38,7 @@ export function Component() {
 
   const onRequestChange = async () => {
     if (email === currentAccount?.email) {
-      setError('Enter your new email above')
+      setError(_(msg`Enter your new email above`))
       return
     }
     setError('')
@@ -53,7 +53,7 @@ export function Component() {
           email: email.trim(),
           emailConfirmed: false,
         })
-        Toast.show('Email updated')
+        Toast.show(_(msg`Email updated`))
         setStage(Stages.Done)
       }
     } catch (e) {
@@ -85,7 +85,7 @@ export function Component() {
         email: email.trim(),
         emailConfirmed: false,
       })
-      Toast.show('Email updated')
+      Toast.show(_(msg`Email updated`))
       setStage(Stages.Done)
     } catch (e) {
       setError(cleanError(String(e)))
diff --git a/src/view/com/modals/ChangeHandle.tsx b/src/view/com/modals/ChangeHandle.tsx
index 31f6d6ea7..e578fa7da 100644
--- a/src/view/com/modals/ChangeHandle.tsx
+++ b/src/view/com/modals/ChangeHandle.tsx
@@ -147,7 +147,7 @@ export function Inner({
             onPress={onPressCancel}
             accessibilityRole="button"
             accessibilityLabel={_(msg`Cancel change handle`)}
-            accessibilityHint="Exits handle change process"
+            accessibilityHint={_(msg`Exits handle change process`)}
             onAccessibilityEscape={onPressCancel}>
             <Text type="lg" style={pal.textLight}>
               Cancel
@@ -168,7 +168,7 @@ export function Inner({
               onPress={onPressSave}
               accessibilityRole="button"
               accessibilityLabel={_(msg`Save handle change`)}
-              accessibilityHint={`Saves handle change to ${handle}`}>
+              accessibilityHint={_(msg`Saves handle change to ${handle}`)}>
               <Text type="2xl-medium" style={pal.link}>
                 <Trans>Save</Trans>
               </Text>
@@ -263,14 +263,16 @@ function ProvidedHandleForm({
           editable={!isProcessing}
           accessible={true}
           accessibilityLabel={_(msg`Handle`)}
-          accessibilityHint="Sets Bluesky username"
+          accessibilityHint={_(msg`Sets Bluesky username`)}
         />
       </View>
       <Text type="md" style={[pal.textLight, s.pl10, s.pt10]}>
-        <Trans>Your full handle will be</Trans>{' '}
-        <Text type="md-bold" style={pal.textLight}>
-          @{createFullHandle(handle, userDomain)}
-        </Text>
+        <Trans>
+          Your full handle will be{' '}
+          <Text type="md-bold" style={pal.textLight}>
+            @{createFullHandle(handle, userDomain)}
+          </Text>
+        </Trans>
       </Text>
       <TouchableOpacity
         onPress={onToggleCustom}
diff --git a/src/view/com/modals/Confirm.tsx b/src/view/com/modals/Confirm.tsx
index 5e869f396..307897fb8 100644
--- a/src/view/com/modals/Confirm.tsx
+++ b/src/view/com/modals/Confirm.tsx
@@ -12,7 +12,7 @@ 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 {Trans, msg} from '@lingui/macro'
 import type {ConfirmModal} from '#/state/modals'
 import {useModalControls} from '#/state/modals'
 
@@ -72,10 +72,10 @@ export function Component({
           onPress={onPress}
           style={[styles.btn, confirmBtnStyle]}
           accessibilityRole="button"
-          accessibilityLabel={_(msg`Confirm`)}
+          accessibilityLabel={_(msg({message: 'Confirm', context: 'action'}))}
           accessibilityHint="">
           <Text style={[s.white, s.bold, s.f18]}>
-            {confirmBtnText ?? 'Confirm'}
+            {confirmBtnText ?? <Trans context="action">Confirm</Trans>}
           </Text>
         </TouchableOpacity>
       )}
@@ -85,10 +85,10 @@ export function Component({
           onPress={onPressCancel}
           style={[styles.btnCancel, s.mt10]}
           accessibilityRole="button"
-          accessibilityLabel={_(msg`Cancel`)}
+          accessibilityLabel={_(msg({message: 'Cancel', context: 'action'}))}
           accessibilityHint="">
           <Text type="button-lg" style={pal.textLight}>
-            {cancelBtnText ?? 'Cancel'}
+            {cancelBtnText ?? <Trans context="action">Cancel</Trans>}
           </Text>
         </TouchableOpacity>
       )}
diff --git a/src/view/com/modals/ContentFilteringSettings.tsx b/src/view/com/modals/ContentFilteringSettings.tsx
index 8b42e1b1d..d681fbf0b 100644
--- a/src/view/com/modals/ContentFilteringSettings.tsx
+++ b/src/view/com/modals/ContentFilteringSettings.tsx
@@ -104,6 +104,7 @@ export function Component({}: {}) {
 
 function AdultContentEnabledPref() {
   const pal = usePalette('default')
+  const {_} = useLingui()
   const {data: preferences} = usePreferencesQuery()
   const {mutate, variables} = usePreferencesSetAdultContentMutation()
   const {openModal} = useModalControls()
@@ -121,36 +122,44 @@ function AdultContentEnabledPref() {
         enabled: !(variables?.enabled ?? preferences?.adultContentEnabled),
       })
     } catch (e) {
-      Toast.show('There was an issue syncing your preferences with the server')
+      Toast.show(
+        _(msg`There was an issue syncing your preferences with the server`),
+      )
       logger.error('Failed to update preferences with server', {error: e})
     }
-  }, [variables, preferences, mutate])
+  }, [variables, preferences, mutate, _])
 
   return (
     <View style={s.mb10}>
       {isIOS ? (
         preferences?.adultContentEnabled ? null : (
           <Text type="md" style={pal.textLight}>
-            Adult content can only be enabled via the Web at{' '}
-            <TextLink
-              style={pal.link}
-              href="https://bsky.app"
-              text="bsky.app"
-            />
-            .
+            <Trans>
+              Adult content can only be enabled via the Web at{' '}
+              <TextLink
+                style={pal.link}
+                href="https://bsky.app"
+                text="bsky.app"
+              />
+              .
+            </Trans>
           </Text>
         )
       ) : typeof preferences?.birthDate === 'undefined' ? (
         <View style={[pal.viewLight, styles.agePrompt]}>
           <Text type="md" style={[pal.text, {flex: 1}]}>
-            Confirm your age to enable adult content.
+            <Trans>Confirm your age to enable adult content.</Trans>
           </Text>
-          <Button type="primary" label="Set Age" onPress={onSetAge} />
+          <Button
+            type="primary"
+            label={_(msg({message: 'Set Age', context: 'action'}))}
+            onPress={onSetAge}
+          />
         </View>
       ) : (preferences.userAge || 0) >= 18 ? (
         <ToggleButton
           type="default-light"
-          label="Enable Adult Content"
+          label={_(msg`Enable Adult Content`)}
           isSelected={variables?.enabled ?? preferences?.adultContentEnabled}
           onPress={onToggleAdultContent}
           style={styles.toggleBtn}
@@ -158,9 +167,13 @@ function AdultContentEnabledPref() {
       ) : (
         <View style={[pal.viewLight, styles.agePrompt]}>
           <Text type="md" style={[pal.text, {flex: 1}]}>
-            You must be 18 or older to enable adult content.
+            <Trans>You must be 18 or older to enable adult content.</Trans>
           </Text>
-          <Button type="primary" label="Set Age" onPress={onSetAge} />
+          <Button
+            type="primary"
+            label={_(msg({message: 'Set Age', context: 'action'}))}
+            onPress={onSetAge}
+          />
         </View>
       )}
     </View>
@@ -203,7 +216,7 @@ function ContentLabelPref({
 
       {disabled || !visibility ? (
         <Text type="sm-bold" style={pal.textLight}>
-          Hide
+          <Trans context="action">Hide</Trans>
         </Text>
       ) : (
         <SelectGroup
@@ -223,12 +236,14 @@ interface SelectGroupProps {
 }
 
 function SelectGroup({current, onChange, labelGroup}: SelectGroupProps) {
+  const {_} = useLingui()
+
   return (
     <View style={styles.selectableBtns}>
       <SelectableBtn
         current={current}
         value="hide"
-        label="Hide"
+        label={_(msg`Hide`)}
         left
         onChange={onChange}
         labelGroup={labelGroup}
@@ -236,14 +251,14 @@ function SelectGroup({current, onChange, labelGroup}: SelectGroupProps) {
       <SelectableBtn
         current={current}
         value="warn"
-        label="Warn"
+        label={_(msg`Warn`)}
         onChange={onChange}
         labelGroup={labelGroup}
       />
       <SelectableBtn
         current={current}
         value="ignore"
-        label="Show"
+        label={_(msg`Show`)}
         right
         onChange={onChange}
         labelGroup={labelGroup}
@@ -273,6 +288,8 @@ function SelectableBtn({
 }: SelectableBtnProps) {
   const pal = usePalette('default')
   const palPrimary = usePalette('inverted')
+  const {_} = useLingui()
+
   return (
     <Pressable
       style={[
@@ -285,7 +302,9 @@ function SelectableBtn({
       onPress={() => onChange(value)}
       accessibilityRole="button"
       accessibilityLabel={value}
-      accessibilityHint={`Set ${value} for ${labelGroup} content moderation policy`}>
+      accessibilityHint={_(
+        msg`Set ${value} for ${labelGroup} content moderation policy`,
+      )}>
       <Text style={current === value ? palPrimary.text : pal.text}>
         {label}
       </Text>
diff --git a/src/view/com/modals/CreateOrEditList.tsx b/src/view/com/modals/CreateOrEditList.tsx
index 8d13cdf2f..0e11fcffd 100644
--- a/src/view/com/modals/CreateOrEditList.tsx
+++ b/src/view/com/modals/CreateOrEditList.tsx
@@ -8,7 +8,11 @@ import {
   TouchableOpacity,
   View,
 } from 'react-native'
-import {AppBskyGraphDefs} from '@atproto/api'
+import {
+  AppBskyGraphDefs,
+  AppBskyRichtextFacet,
+  RichText as RichTextAPI,
+} from '@atproto/api'
 import LinearGradient from 'react-native-linear-gradient'
 import {Image as RNImage} from 'react-native-image-crop-picker'
 import {Text} from '../util/text/Text'
@@ -30,6 +34,9 @@ import {
   useListCreateMutation,
   useListMetadataMutation,
 } from '#/state/queries/list'
+import {richTextToString} from '#/lib/strings/rich-text-helpers'
+import {shortenLinks} from '#/lib/strings/rich-text-manip'
+import {getAgent} from '#/state/session'
 
 const MAX_NAME = 64 // todo
 const MAX_DESCRIPTION = 300 // todo
@@ -65,16 +72,45 @@ export function Component({
     return 'app.bsky.graph.defs#curatelist'
   }, [list, purpose])
   const isCurateList = activePurpose === 'app.bsky.graph.defs#curatelist'
-  const purposeLabel = isCurateList ? 'User' : 'Moderation'
 
   const [isProcessing, setProcessing] = useState<boolean>(false)
   const [name, setName] = useState<string>(list?.name || '')
-  const [description, setDescription] = useState<string>(
-    list?.description || '',
-  )
+
+  const [descriptionRt, setDescriptionRt] = useState<RichTextAPI>(() => {
+    const text = list?.description
+    const facets = list?.descriptionFacets
+
+    if (!text || !facets) {
+      return new RichTextAPI({text: text || ''})
+    }
+
+    // We want to be working with a blank state here, so let's get the
+    // serialized version and turn it back into a RichText
+    const serialized = richTextToString(new RichTextAPI({text, facets}), false)
+
+    const richText = new RichTextAPI({text: serialized})
+    richText.detectFacetsWithoutResolution()
+
+    return richText
+  })
+  const graphemeLength = useMemo(() => {
+    return shortenLinks(descriptionRt).graphemeLength
+  }, [descriptionRt])
+  const isDescriptionOver = graphemeLength > MAX_DESCRIPTION
+
   const [avatar, setAvatar] = useState<string | undefined>(list?.avatar)
   const [newAvatar, setNewAvatar] = useState<RNImage | undefined | null>()
 
+  const onDescriptionChange = useCallback(
+    (newText: string) => {
+      const richText = new RichTextAPI({text: newText})
+      richText.detectFacetsWithoutResolution()
+
+      setDescriptionRt(richText)
+    },
+    [setDescriptionRt],
+  )
+
   const onPressCancel = useCallback(() => {
     closeModal()
   }, [closeModal])
@@ -106,7 +142,7 @@ export function Component({
     }
     const nameTrimmed = name.trim()
     if (!nameTrimmed) {
-      setError('Name is required')
+      setError(_(msg`Name is required`))
       return
     }
     setProcessing(true)
@@ -114,30 +150,61 @@ export function Component({
       setError('')
     }
     try {
+      let richText = new RichTextAPI(
+        {text: descriptionRt.text.trimEnd()},
+        {cleanNewlines: true},
+      )
+
+      await richText.detectFacets(getAgent())
+      richText = shortenLinks(richText)
+
+      // filter out any mention facets that didn't map to a user
+      richText.facets = richText.facets?.filter(facet => {
+        const mention = facet.features.find(feature =>
+          AppBskyRichtextFacet.isMention(feature),
+        )
+        if (mention && !mention.did) {
+          return false
+        }
+        return true
+      })
+
       if (list) {
         await listMetadataMutation.mutateAsync({
           uri: list.uri,
           name: nameTrimmed,
-          description: description.trim(),
+          description: richText.text,
+          descriptionFacets: richText.facets,
           avatar: newAvatar,
         })
-        Toast.show(`${purposeLabel} list updated`)
+        Toast.show(
+          isCurateList
+            ? _(msg`User list updated`)
+            : _(msg`Moderation list updated`),
+        )
         onSave?.(list.uri)
       } else {
         const res = await listCreateMutation.mutateAsync({
           purpose: activePurpose,
           name,
-          description,
+          description: richText.text,
+          descriptionFacets: richText.facets,
           avatar: newAvatar,
         })
-        Toast.show(`${purposeLabel} list created`)
+        Toast.show(
+          isCurateList
+            ? _(msg`User list created`)
+            : _(msg`Moderation list created`),
+        )
         onSave?.(res.uri)
       }
       closeModal()
     } catch (e: any) {
       if (isNetworkError(e)) {
         setError(
-          'Failed to create the list. Check your internet connection and try again.',
+          _(
+            msg`Failed to create the list. Check your internet connection and try again.`,
+          ),
         )
       } else {
         setError(cleanError(e))
@@ -153,13 +220,13 @@ export function Component({
     closeModal,
     activePurpose,
     isCurateList,
-    purposeLabel,
     name,
-    description,
+    descriptionRt,
     newAvatar,
     list,
     listMetadataMutation,
     listCreateMutation,
+    _,
   ])
 
   return (
@@ -173,9 +240,17 @@ export function Component({
         ]}
         testID="createOrEditListModal">
         <Text style={[styles.title, pal.text]}>
-          <Trans>
-            {list ? 'Edit' : 'New'} {purposeLabel} List
-          </Trans>
+          {isCurateList ? (
+            list ? (
+              <Trans>Edit User List</Trans>
+            ) : (
+              <Trans>New User List</Trans>
+            )
+          ) : list ? (
+            <Trans>Edit Moderation List</Trans>
+          ) : (
+            <Trans>New Moderation List</Trans>
+          )}
         </Text>
         {error !== '' && (
           <View style={styles.errorContainer}>
@@ -195,14 +270,18 @@ export function Component({
         </View>
         <View style={styles.form}>
           <View>
-            <Text style={[styles.label, pal.text]} nativeID="list-name">
-              <Trans>List Name</Trans>
-            </Text>
+            <View style={styles.labelWrapper}>
+              <Text style={[styles.label, pal.text]} nativeID="list-name">
+                <Trans>List Name</Trans>
+              </Text>
+            </View>
             <TextInput
               testID="editNameInput"
               style={[styles.textInput, pal.border, pal.text]}
               placeholder={
-                isCurateList ? 'e.g. Great Posters' : 'e.g. Spammers'
+                isCurateList
+                  ? _(msg`e.g. Great Posters`)
+                  : _(msg`e.g. Spammers`)
               }
               placeholderTextColor={colors.gray4}
               value={name}
@@ -214,22 +293,30 @@ export function Component({
             />
           </View>
           <View style={s.pb10}>
-            <Text style={[styles.label, pal.text]} nativeID="list-description">
-              <Trans>Description</Trans>
-            </Text>
+            <View style={styles.labelWrapper}>
+              <Text
+                style={[styles.label, pal.text]}
+                nativeID="list-description">
+                <Trans>Description</Trans>
+              </Text>
+              <Text
+                style={[!isDescriptionOver ? pal.textLight : s.red3, s.f13]}>
+                {graphemeLength}/{MAX_DESCRIPTION}
+              </Text>
+            </View>
             <TextInput
               testID="editDescriptionInput"
               style={[styles.textArea, pal.border, pal.text]}
               placeholder={
                 isCurateList
-                  ? 'e.g. The posters who never miss.'
-                  : 'e.g. Users that repeatedly reply with ads.'
+                  ? _(msg`e.g. The posters who never miss.`)
+                  : _(msg`e.g. Users that repeatedly reply with ads.`)
               }
               placeholderTextColor={colors.gray4}
               keyboardAppearance={theme.colorScheme}
               multiline
-              value={description}
-              onChangeText={v => setDescription(enforceLen(v, MAX_DESCRIPTION))}
+              value={descriptionRt.text}
+              onChangeText={onDescriptionChange}
               accessible={true}
               accessibilityLabel={_(msg`Description`)}
               accessibilityHint=""
@@ -243,7 +330,8 @@ export function Component({
           ) : (
             <TouchableOpacity
               testID="saveBtn"
-              style={s.mt10}
+              style={[s.mt10, isDescriptionOver && s.dimmed]}
+              disabled={isDescriptionOver}
               onPress={onPressSave}
               accessibilityRole="button"
               accessibilityLabel={_(msg`Save`)}
@@ -252,9 +340,9 @@ export function Component({
                 colors={[gradients.blueLight.start, gradients.blueLight.end]}
                 start={{x: 0, y: 0}}
                 end={{x: 1, y: 1}}
-                style={[styles.btn]}>
+                style={styles.btn}>
                 <Text style={[s.white, s.bold]}>
-                  <Trans>Save</Trans>
+                  <Trans context="action">Save</Trans>
                 </Text>
               </LinearGradient>
             </TouchableOpacity>
@@ -269,7 +357,7 @@ export function Component({
             onAccessibilityEscape={onPressCancel}>
             <View style={[styles.btn]}>
               <Text style={[s.black, s.bold, pal.text]}>
-                <Trans>Cancel</Trans>
+                <Trans context="action">Cancel</Trans>
               </Text>
             </View>
           </TouchableOpacity>
@@ -286,12 +374,18 @@ const styles = StyleSheet.create({
     fontSize: 24,
     marginBottom: 18,
   },
-  label: {
-    fontWeight: 'bold',
+  labelWrapper: {
+    flexDirection: 'row',
+    gap: 8,
+    alignItems: 'center',
+    justifyContent: 'space-between',
     paddingHorizontal: 4,
     paddingBottom: 4,
     marginTop: 20,
   },
+  label: {
+    fontWeight: 'bold',
+  },
   form: {
     paddingHorizontal: 6,
   },
diff --git a/src/view/com/modals/DeleteAccount.tsx b/src/view/com/modals/DeleteAccount.tsx
index ee16d46b3..945d7bc89 100644
--- a/src/view/com/modals/DeleteAccount.tsx
+++ b/src/view/com/modals/DeleteAccount.tsx
@@ -62,7 +62,7 @@ export function Component({}: {}) {
         password,
         token,
       })
-      Toast.show('Your account has been deleted')
+      Toast.show(_(msg`Your account has been deleted`))
       resetToTab('HomeTab')
       removeAccount(currentAccount)
       clearCurrentAccount()
@@ -125,7 +125,9 @@ export function Component({}: {}) {
                   onPress={onPressSendEmail}
                   accessibilityRole="button"
                   accessibilityLabel={_(msg`Send email`)}
-                  accessibilityHint="Sends email with confirmation code for account deletion">
+                  accessibilityHint={_(
+                    msg`Sends email with confirmation code for account deletion`,
+                  )}>
                   <LinearGradient
                     colors={[
                       gradients.blueLight.start,
@@ -135,7 +137,7 @@ export function Component({}: {}) {
                     end={{x: 1, y: 1}}
                     style={[styles.btn]}>
                     <Text type="button-lg" style={[s.white, s.bold]}>
-                      <Trans>Send Email</Trans>
+                      <Trans context="action">Send Email</Trans>
                     </Text>
                   </LinearGradient>
                 </TouchableOpacity>
@@ -147,7 +149,7 @@ export function Component({}: {}) {
                   accessibilityHint=""
                   onAccessibilityEscape={onCancel}>
                   <Text type="button-lg" style={pal.textLight}>
-                    <Trans>Cancel</Trans>
+                    <Trans context="action">Cancel</Trans>
                   </Text>
                 </TouchableOpacity>
               </>
@@ -158,7 +160,7 @@ export function Component({}: {}) {
             {/* TODO: Update this label to be more concise */}
             <Text
               type="lg"
-              style={styles.description}
+              style={[pal.text, styles.description]}
               nativeID="confirmationCode">
               <Trans>
                 Check your inbox for an email with the confirmation code to
@@ -174,9 +176,14 @@ export function Component({}: {}) {
               onChangeText={setConfirmCode}
               accessibilityLabelledBy="confirmationCode"
               accessibilityLabel={_(msg`Confirmation code`)}
-              accessibilityHint="Input confirmation code for account deletion"
+              accessibilityHint={_(
+                msg`Input confirmation code for account deletion`,
+              )}
             />
-            <Text type="lg" style={styles.description} nativeID="password">
+            <Text
+              type="lg"
+              style={[pal.text, styles.description]}
+              nativeID="password">
               <Trans>Please enter your password as well:</Trans>
             </Text>
             <TextInput
@@ -189,7 +196,7 @@ export function Component({}: {}) {
               onChangeText={setPassword}
               accessibilityLabelledBy="password"
               accessibilityLabel={_(msg`Password`)}
-              accessibilityHint="Input password for account deletion"
+              accessibilityHint={_(msg`Input password for account deletion`)}
             />
             {error ? (
               <View style={styles.mt20}>
@@ -220,7 +227,7 @@ export function Component({}: {}) {
                   accessibilityHint="Exits account deletion process"
                   onAccessibilityEscape={onCancel}>
                   <Text type="button-lg" style={pal.textLight}>
-                    <Trans>Cancel</Trans>
+                    <Trans context="action">Cancel</Trans>
                   </Text>
                 </TouchableOpacity>
               </>
diff --git a/src/view/com/modals/EditImage.tsx b/src/view/com/modals/EditImage.tsx
index 753907472..3b35ffee2 100644
--- a/src/view/com/modals/EditImage.tsx
+++ b/src/view/com/modals/EditImage.tsx
@@ -112,16 +112,16 @@ export const Component = observer(function EditImageImpl({
       // },
       {
         name: 'flip' as const,
-        label: 'Flip horizontal',
+        label: _(msg`Flip horizontal`),
         onPress: onFlipHorizontal,
       },
       {
         name: 'flip' as const,
-        label: 'Flip vertically',
+        label: _(msg`Flip vertically`),
         onPress: onFlipVertical,
       },
     ],
-    [onFlipHorizontal, onFlipVertical],
+    [onFlipHorizontal, onFlipVertical, _],
   )
 
   useEffect(() => {
@@ -284,7 +284,7 @@ export const Component = observer(function EditImageImpl({
                   size={label?.startsWith('Flip') ? 22 : 24}
                   style={[
                     pal.text,
-                    label === 'Flip vertically'
+                    label === _(msg`Flip vertically`)
                       ? styles.flipVertical
                       : undefined,
                   ]}
@@ -330,7 +330,7 @@ export const Component = observer(function EditImageImpl({
             end={{x: 1, y: 1}}
             style={[styles.btn]}>
             <Text type="xl-medium" style={s.white}>
-              <Trans>Done</Trans>
+              <Trans context="action">Done</Trans>
             </Text>
           </LinearGradient>
         </Pressable>
diff --git a/src/view/com/modals/EditProfile.tsx b/src/view/com/modals/EditProfile.tsx
index e044f8c0e..dd8ac9ae7 100644
--- a/src/view/com/modals/EditProfile.tsx
+++ b/src/view/com/modals/EditProfile.tsx
@@ -125,7 +125,7 @@ export function Component({
         newUserAvatar,
         newUserBanner,
       })
-      Toast.show('Profile updated')
+      Toast.show(_(msg`Profile updated`))
       onUpdate?.()
       closeModal()
     } catch (e: any) {
@@ -142,6 +142,7 @@ export function Component({
     newUserAvatar,
     newUserBanner,
     setImageError,
+    _,
   ])
 
   return (
@@ -181,7 +182,7 @@ export function Component({
             <TextInput
               testID="editProfileDisplayNameInput"
               style={[styles.textInput, pal.border, pal.text]}
-              placeholder="e.g. Alice Roberts"
+              placeholder={_(msg`e.g. Alice Roberts`)}
               placeholderTextColor={colors.gray4}
               value={displayName}
               onChangeText={v =>
@@ -189,7 +190,7 @@ export function Component({
               }
               accessible={true}
               accessibilityLabel={_(msg`Display name`)}
-              accessibilityHint="Edit your display name"
+              accessibilityHint={_(msg`Edit your display name`)}
             />
           </View>
           <View style={s.pb10}>
@@ -199,7 +200,7 @@ export function Component({
             <TextInput
               testID="editProfileDescriptionInput"
               style={[styles.textArea, pal.border, pal.text]}
-              placeholder="e.g. Artist, dog-lover, and avid reader."
+              placeholder={_(msg`e.g. Artist, dog-lover, and avid reader.`)}
               placeholderTextColor={colors.gray4}
               keyboardAppearance={theme.colorScheme}
               multiline
@@ -207,7 +208,7 @@ export function Component({
               onChangeText={v => setDescription(enforceLen(v, MAX_DESCRIPTION))}
               accessible={true}
               accessibilityLabel={_(msg`Description`)}
-              accessibilityHint="Edit your profile description"
+              accessibilityHint={_(msg`Edit your profile description`)}
             />
           </View>
           {updateMutation.isPending ? (
@@ -221,7 +222,7 @@ export function Component({
               onPress={onPressSave}
               accessibilityRole="button"
               accessibilityLabel={_(msg`Save`)}
-              accessibilityHint="Saves any changes to your profile">
+              accessibilityHint={_(msg`Saves any changes to your profile`)}>
               <LinearGradient
                 colors={[gradients.blueLight.start, gradients.blueLight.end]}
                 start={{x: 0, y: 0}}
diff --git a/src/view/com/modals/EmbedConsent.tsx b/src/view/com/modals/EmbedConsent.tsx
new file mode 100644
index 000000000..04104c52e
--- /dev/null
+++ b/src/view/com/modals/EmbedConsent.tsx
@@ -0,0 +1,153 @@
+import React from 'react'
+import {StyleSheet, TouchableOpacity, View} from 'react-native'
+import LinearGradient from 'react-native-linear-gradient'
+import {s, colors, gradients} from 'lib/styles'
+import {Text} from '../util/text/Text'
+import {ScrollView} from './util'
+import {usePalette} from 'lib/hooks/usePalette'
+import {
+  EmbedPlayerSource,
+  embedPlayerSources,
+  externalEmbedLabels,
+} from '#/lib/strings/embed-player'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useModalControls} from '#/state/modals'
+import {useSetExternalEmbedPref} from '#/state/preferences/external-embeds-prefs'
+import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
+
+export const snapPoints = [450]
+
+export function Component({
+  onAccept,
+  source,
+}: {
+  onAccept: () => void
+  source: EmbedPlayerSource
+}) {
+  const pal = usePalette('default')
+  const {closeModal} = useModalControls()
+  const {_} = useLingui()
+  const setExternalEmbedPref = useSetExternalEmbedPref()
+  const {isMobile} = useWebMediaQueries()
+
+  const onShowAllPress = React.useCallback(() => {
+    for (const key of embedPlayerSources) {
+      setExternalEmbedPref(key, 'show')
+    }
+    onAccept()
+    closeModal()
+  }, [closeModal, onAccept, setExternalEmbedPref])
+
+  const onShowPress = React.useCallback(() => {
+    setExternalEmbedPref(source, 'show')
+    onAccept()
+    closeModal()
+  }, [closeModal, onAccept, setExternalEmbedPref, source])
+
+  const onHidePress = React.useCallback(() => {
+    setExternalEmbedPref(source, 'hide')
+    closeModal()
+  }, [closeModal, setExternalEmbedPref, source])
+
+  return (
+    <ScrollView
+      testID="embedConsentModal"
+      style={[
+        s.flex1,
+        pal.view,
+        isMobile
+          ? {paddingHorizontal: 20, paddingTop: 10}
+          : {paddingHorizontal: 30},
+      ]}>
+      <Text style={[pal.text, styles.title]}>
+        <Trans>External Media</Trans>
+      </Text>
+
+      <Text style={pal.text}>
+        <Trans>
+          This content is hosted by {externalEmbedLabels[source]}. Do you want
+          to enable external media?
+        </Trans>
+      </Text>
+      <View style={[s.mt10]} />
+      <Text style={pal.textLight}>
+        <Trans>
+          External media may allow websites to collect information about you and
+          your device. No information is sent or requested until you press the
+          "play" button.
+        </Trans>
+      </Text>
+      <View style={[s.mt20]} />
+      <TouchableOpacity
+        testID="enableAllBtn"
+        onPress={onShowAllPress}
+        accessibilityRole="button"
+        accessibilityLabel={_(
+          msg`Show embeds from ${externalEmbedLabels[source]}`,
+        )}
+        accessibilityHint=""
+        onAccessibilityEscape={closeModal}>
+        <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]}>
+            <Trans>Enable External Media</Trans>
+          </Text>
+        </LinearGradient>
+      </TouchableOpacity>
+      <View style={[s.mt10]} />
+      <TouchableOpacity
+        testID="enableSourceBtn"
+        onPress={onShowPress}
+        accessibilityRole="button"
+        accessibilityLabel={_(
+          msg`Never load embeds from ${externalEmbedLabels[source]}`,
+        )}
+        accessibilityHint=""
+        onAccessibilityEscape={closeModal}>
+        <View style={[styles.btn, pal.btn]}>
+          <Text style={[pal.text, s.bold, s.f18]}>
+            <Trans>Enable {externalEmbedLabels[source]} only</Trans>
+          </Text>
+        </View>
+      </TouchableOpacity>
+      <View style={[s.mt10]} />
+      <TouchableOpacity
+        testID="disableSourceBtn"
+        onPress={onHidePress}
+        accessibilityRole="button"
+        accessibilityLabel={_(
+          msg`Never load embeds from ${externalEmbedLabels[source]}`,
+        )}
+        accessibilityHint=""
+        onAccessibilityEscape={closeModal}>
+        <View style={[styles.btn, pal.btn]}>
+          <Text style={[pal.text, s.bold, s.f18]}>
+            <Trans>No thanks</Trans>
+          </Text>
+        </View>
+      </TouchableOpacity>
+    </ScrollView>
+  )
+}
+
+const styles = StyleSheet.create({
+  title: {
+    textAlign: 'center',
+    fontWeight: 'bold',
+    fontSize: 24,
+    marginBottom: 12,
+  },
+  btn: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'center',
+    width: '100%',
+    borderRadius: 32,
+    padding: 14,
+    backgroundColor: colors.gray1,
+  },
+})
diff --git a/src/view/com/modals/InAppBrowserConsent.tsx b/src/view/com/modals/InAppBrowserConsent.tsx
new file mode 100644
index 000000000..86bb46ca8
--- /dev/null
+++ b/src/view/com/modals/InAppBrowserConsent.tsx
@@ -0,0 +1,102 @@
+import React from 'react'
+import {StyleSheet, View} from 'react-native'
+
+import {s} from 'lib/styles'
+import {Text} from '../util/text/Text'
+import {Button} from '../util/forms/Button'
+import {ScrollView} from './util'
+import {usePalette} from 'lib/hooks/usePalette'
+
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useModalControls} from '#/state/modals'
+import {
+  useOpenLink,
+  useSetInAppBrowser,
+} from '#/state/preferences/in-app-browser'
+
+export const snapPoints = [350]
+
+export function Component({href}: {href: string}) {
+  const pal = usePalette('default')
+  const {closeModal} = useModalControls()
+  const {_} = useLingui()
+  const setInAppBrowser = useSetInAppBrowser()
+  const openLink = useOpenLink()
+
+  const onUseIAB = React.useCallback(() => {
+    setInAppBrowser(true)
+    closeModal()
+    openLink(href, true)
+  }, [closeModal, setInAppBrowser, href, openLink])
+
+  const onUseLinking = React.useCallback(() => {
+    setInAppBrowser(false)
+    closeModal()
+    openLink(href, false)
+  }, [closeModal, setInAppBrowser, href, openLink])
+
+  return (
+    <ScrollView
+      testID="inAppBrowserConsentModal"
+      style={[s.flex1, pal.view, {paddingHorizontal: 20, paddingTop: 10}]}>
+      <Text style={[pal.text, styles.title]}>
+        <Trans>How should we open this link?</Trans>
+      </Text>
+      <Text style={pal.text}>
+        <Trans>
+          Your choice will be saved, but can be changed later in settings.
+        </Trans>
+      </Text>
+      <View style={[styles.btnContainer]}>
+        <Button
+          testID="confirmBtn"
+          type="inverted"
+          onPress={onUseIAB}
+          accessibilityLabel={_(msg`Use in-app browser`)}
+          accessibilityHint=""
+          label={_(msg`Use in-app browser`)}
+          labelContainerStyle={{justifyContent: 'center', padding: 8}}
+          labelStyle={[s.f18]}
+        />
+        <Button
+          testID="confirmBtn"
+          type="inverted"
+          onPress={onUseLinking}
+          accessibilityLabel={_(msg`Use my default browser`)}
+          accessibilityHint=""
+          label={_(msg`Use my default browser`)}
+          labelContainerStyle={{justifyContent: 'center', padding: 8}}
+          labelStyle={[s.f18]}
+        />
+        <Button
+          testID="cancelBtn"
+          type="default"
+          onPress={() => {
+            closeModal()
+          }}
+          accessibilityLabel={_(msg`Cancel`)}
+          accessibilityHint=""
+          label="Cancel"
+          labelContainerStyle={{justifyContent: 'center', padding: 8}}
+          labelStyle={[s.f18]}
+        />
+      </View>
+    </ScrollView>
+  )
+}
+
+const styles = StyleSheet.create({
+  title: {
+    textAlign: 'center',
+    fontWeight: 'bold',
+    fontSize: 24,
+    marginBottom: 12,
+  },
+  btnContainer: {
+    marginTop: 20,
+    flexDirection: 'column',
+    justifyContent: 'center',
+    rowGap: 10,
+  },
+})
diff --git a/src/view/com/modals/InviteCodes.tsx b/src/view/com/modals/InviteCodes.tsx
index 0ebb545cf..c0318df01 100644
--- a/src/view/com/modals/InviteCodes.tsx
+++ b/src/view/com/modals/InviteCodes.tsx
@@ -18,7 +18,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 {Trans, msg} from '@lingui/macro'
 import {cleanError} from 'lib/strings/errors'
 import {useModalControls} from '#/state/modals'
 import {useInvitesState, useInvitesAPI} from '#/state/invites'
@@ -30,6 +30,7 @@ import {
   useInviteCodesQuery,
   InviteCodesQueryResponse,
 } from '#/state/queries/invites'
+import {useLingui} from '@lingui/react'
 
 export const snapPoints = ['70%']
 
@@ -49,6 +50,7 @@ export function Component() {
 
 export function Inner({invites}: {invites: InviteCodesQueryResponse}) {
   const pal = usePalette('default')
+  const {_} = useLingui()
   const {closeModal} = useModalControls()
   const {isTabletOrDesktop} = useWebMediaQueries()
 
@@ -75,7 +77,7 @@ export function Inner({invites}: {invites: InviteCodesQueryResponse}) {
           ]}>
           <Button
             type="primary"
-            label="Done"
+            label={_(msg`Done`)}
             style={styles.btn}
             labelStyle={styles.btnLabel}
             onPress={onClose}
@@ -118,7 +120,7 @@ export function Inner({invites}: {invites: InviteCodesQueryResponse}) {
         <Button
           testID="closeBtn"
           type="primary"
-          label="Done"
+          label={_(msg`Done`)}
           style={styles.btn}
           labelStyle={styles.btnLabel}
           onPress={onClose}
@@ -140,15 +142,16 @@ function InviteCode({
   invites: InviteCodesQueryResponse
 }) {
   const pal = usePalette('default')
+  const {_} = useLingui()
   const invitesState = useInvitesState()
   const {setInviteCopied} = useInvitesAPI()
   const uses = invite.uses
 
   const onPress = React.useCallback(() => {
     Clipboard.setString(invite.code)
-    Toast.show('Copied to clipboard')
+    Toast.show(_(msg`Copied to clipboard`))
     setInviteCopied(invite.code)
-  }, [setInviteCopied, invite])
+  }, [setInviteCopied, invite, _])
 
   return (
     <View
@@ -163,10 +166,10 @@ function InviteCode({
         accessibilityRole="button"
         accessibilityLabel={
           invites.available.length === 1
-            ? 'Invite codes: 1 available'
-            : `Invite codes: ${invites.available.length} available`
+            ? _(msg`Invite codes: 1 available`)
+            : _(msg`Invite codes: ${invites.available.length} available`)
         }
-        accessibilityHint="Opens list of invite codes">
+        accessibilityHint={_(msg`Opens list of invite codes`)}>
         <Text
           testID={`${testID}-code`}
           type={used ? 'md' : 'md-bold'}
diff --git a/src/view/com/modals/LinkWarning.tsx b/src/view/com/modals/LinkWarning.tsx
index 39e6cc3e6..81fdc7285 100644
--- a/src/view/com/modals/LinkWarning.tsx
+++ b/src/view/com/modals/LinkWarning.tsx
@@ -1,5 +1,5 @@
 import React from 'react'
-import {Linking, SafeAreaView, StyleSheet, View} from 'react-native'
+import {SafeAreaView, StyleSheet, View} from 'react-native'
 import {ScrollView} from './util'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {Text} from '../util/text/Text'
@@ -12,6 +12,7 @@ import {isPossiblyAUrl, splitApexDomain} from 'lib/strings/url-helpers'
 import {Trans, msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useModalControls} from '#/state/modals'
+import {useOpenLink} from '#/state/preferences/in-app-browser'
 
 export const snapPoints = ['50%']
 
@@ -21,10 +22,11 @@ export function Component({text, href}: {text: string; href: string}) {
   const {isMobile} = useWebMediaQueries()
   const {_} = useLingui()
   const potentiallyMisleading = isPossiblyAUrl(text)
+  const openLink = useOpenLink()
 
   const onPressVisit = () => {
     closeModal()
-    Linking.openURL(href)
+    openLink(href)
   }
 
   return (
diff --git a/src/view/com/modals/ListAddRemoveUsers.tsx b/src/view/com/modals/ListAddRemoveUsers.tsx
index 14e16d6bf..27c33f806 100644
--- a/src/view/com/modals/ListAddRemoveUsers.tsx
+++ b/src/view/com/modals/ListAddRemoveUsers.tsx
@@ -67,7 +67,7 @@ export function Component({
           <TextInput
             testID="searchInput"
             style={[styles.searchInput, pal.border, pal.text]}
-            placeholder="Search for users"
+            placeholder={_(msg`Search for users`)}
             placeholderTextColor={pal.colors.textLight}
             value={query}
             onChangeText={setQuery}
@@ -85,7 +85,7 @@ export function Component({
               onPress={onPressCancelSearch}
               accessibilityRole="button"
               accessibilityLabel={_(msg`Cancel search`)}
-              accessibilityHint="Exits inputting search query"
+              accessibilityHint={_(msg`Exits inputting search query`)}
               onAccessibilityEscape={onPressCancelSearch}
               hitSlop={HITSLOP_20}>
               <FontAwesomeIcon
@@ -141,7 +141,7 @@ export function Component({
             }}
             accessibilityLabel={_(msg`Done`)}
             accessibilityHint=""
-            label="Done"
+            label={_(msg({message: 'Done', context: 'action'}))}
             labelContainerStyle={{justifyContent: 'center', padding: 4}}
             labelStyle={[s.f18]}
           />
diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx
index 2aac20dac..7f814d971 100644
--- a/src/view/com/modals/Modal.tsx
+++ b/src/view/com/modals/Modal.tsx
@@ -38,6 +38,8 @@ import * as VerifyEmailModal from './VerifyEmail'
 import * as ChangeEmailModal from './ChangeEmail'
 import * as SwitchAccountModal from './SwitchAccount'
 import * as LinkWarningModal from './LinkWarning'
+import * as EmbedConsentModal from './EmbedConsent'
+import * as InAppBrowserConsentModal from './InAppBrowserConsent'
 
 const DEFAULT_SNAPPOINTS = ['90%']
 const HANDLE_HEIGHT = 24
@@ -176,6 +178,12 @@ export function ModalsContainer() {
   } else if (activeModal?.name === 'link-warning') {
     snapPoints = LinkWarningModal.snapPoints
     element = <LinkWarningModal.Component {...activeModal} />
+  } else if (activeModal?.name === 'embed-consent') {
+    snapPoints = EmbedConsentModal.snapPoints
+    element = <EmbedConsentModal.Component {...activeModal} />
+  } else if (activeModal?.name === 'in-app-browser-consent') {
+    snapPoints = InAppBrowserConsentModal.snapPoints
+    element = <InAppBrowserConsentModal.Component {...activeModal} />
   } else {
     return null
   }
diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx
index 12138f54d..d79663746 100644
--- a/src/view/com/modals/Modal.web.tsx
+++ b/src/view/com/modals/Modal.web.tsx
@@ -3,6 +3,7 @@ import {TouchableWithoutFeedback, StyleSheet, View} from 'react-native'
 import Animated, {FadeIn, FadeOut} from 'react-native-reanimated'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock'
 
 import {useModals, useModalControls} from '#/state/modals'
 import type {Modal as ModalIface} from '#/state/modals'
@@ -34,9 +35,11 @@ import * as BirthDateSettingsModal from './BirthDateSettings'
 import * as VerifyEmailModal from './VerifyEmail'
 import * as ChangeEmailModal from './ChangeEmail'
 import * as LinkWarningModal from './LinkWarning'
+import * as EmbedConsentModal from './EmbedConsent'
 
 export function ModalsContainer() {
   const {isModalActive, activeModals} = useModals()
+  useWebBodyScrollLock(isModalActive)
 
   if (!isModalActive) {
     return null
@@ -62,7 +65,11 @@ function Modal({modal}: {modal: ModalIface}) {
   }
 
   const onPressMask = () => {
-    if (modal.name === 'crop-image' || modal.name === 'edit-image') {
+    if (
+      modal.name === 'crop-image' ||
+      modal.name === 'edit-image' ||
+      modal.name === 'alt-text-image'
+    ) {
       return // dont close on mask presses during crop
     }
     closeModal()
@@ -129,6 +136,8 @@ function Modal({modal}: {modal: ModalIface}) {
     element = <ChangeEmailModal.Component />
   } else if (modal.name === 'link-warning') {
     element = <LinkWarningModal.Component {...modal} />
+  } else if (modal.name === 'embed-consent') {
+    element = <EmbedConsentModal.Component {...modal} />
   } else {
     return null
   }
@@ -159,7 +168,8 @@ function Modal({modal}: {modal: ModalIface}) {
 
 const styles = StyleSheet.create({
   mask: {
-    position: 'absolute',
+    // @ts-ignore
+    position: 'fixed',
     top: 0,
     left: 0,
     width: '100%',
diff --git a/src/view/com/modals/ModerationDetails.tsx b/src/view/com/modals/ModerationDetails.tsx
index c117023d4..ba7f76db1 100644
--- a/src/view/com/modals/ModerationDetails.tsx
+++ b/src/view/com/modals/ModerationDetails.tsx
@@ -10,6 +10,8 @@ import {isWeb} from 'platform/detection'
 import {listUriToHref} from 'lib/strings/url-helpers'
 import {Button} from '../util/forms/Button'
 import {useModalControls} from '#/state/modals'
+import {useLingui} from '@lingui/react'
+import {Trans, msg} from '@lingui/macro'
 
 export const snapPoints = [300]
 
@@ -23,19 +25,21 @@ export function Component({
   const {closeModal} = useModalControls()
   const {isMobile} = useWebMediaQueries()
   const pal = usePalette('default')
+  const {_} = useLingui()
 
   let name
   let description
   if (!moderation.cause) {
-    name = 'Content Warning'
-    description =
-      'Moderator has chosen to set a general warning on the content.'
+    name = _(msg`Content Warning`)
+    description = _(
+      msg`Moderator has chosen to set a general warning on the content.`,
+    )
   } else if (moderation.cause.type === 'blocking') {
     if (moderation.cause.source.type === 'list') {
       const list = moderation.cause.source.list
-      name = 'User Blocked by List'
+      name = _(msg`User Blocked by List`)
       description = (
-        <>
+        <Trans>
           This user is included in the{' '}
           <TextLink
             type="2xl"
@@ -44,25 +48,30 @@ export function Component({
             style={pal.link}
           />{' '}
           list which you have blocked.
-        </>
+        </Trans>
       )
     } else {
-      name = 'User Blocked'
-      description = 'You have blocked this user. You cannot view their content.'
+      name = _(msg`User Blocked`)
+      description = _(
+        msg`You have blocked this user. You cannot view their content.`,
+      )
     }
   } else if (moderation.cause.type === 'blocked-by') {
-    name = 'User Blocks You'
-    description = 'This user has blocked you. You cannot view their content.'
+    name = _(msg`User Blocks You`)
+    description = _(
+      msg`This user has blocked you. You cannot view their content.`,
+    )
   } else if (moderation.cause.type === 'block-other') {
-    name = 'Content Not Available'
-    description =
-      'This content is not available because one of the users involved has blocked the other.'
+    name = _(msg`Content Not Available`)
+    description = _(
+      msg`This content is not available because one of the users involved has blocked the other.`,
+    )
   } else if (moderation.cause.type === 'muted') {
     if (moderation.cause.source.type === 'list') {
       const list = moderation.cause.source.list
-      name = <>Account Muted by List</>
+      name = _(msg`Account Muted by List`)
       description = (
-        <>
+        <Trans>
           This user is included the{' '}
           <TextLink
             type="2xl"
@@ -71,11 +80,11 @@ export function Component({
             style={pal.link}
           />{' '}
           list which you have muted.
-        </>
+        </Trans>
       )
     } else {
-      name = 'Account Muted'
-      description = 'You have muted this user.'
+      name = _(msg`Account Muted`)
+      description = _(msg`You have muted this user.`)
     }
   } else {
     name = moderation.cause.labelDef.strings[context].en.name
diff --git a/src/view/com/modals/ProfilePreview.tsx b/src/view/com/modals/ProfilePreview.tsx
index edfbf6a82..77e68db70 100644
--- a/src/view/com/modals/ProfilePreview.tsx
+++ b/src/view/com/modals/ProfilePreview.tsx
@@ -14,11 +14,14 @@ import {ErrorScreen} from '../util/error/ErrorScreen'
 import {CenteredView} from '../util/Views'
 import {cleanError} from '#/lib/strings/errors'
 import {useProfileShadow} from '#/state/cache/profile-shadow'
+import {Trans, msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
 
 export const snapPoints = [520, '100%']
 
 export function Component({did}: {did: string}) {
   const pal = usePalette('default')
+  const {_} = useLingui()
   const moderationOpts = useModerationOpts()
   const {
     data: profile,
@@ -43,7 +46,7 @@ export function Component({did}: {did: string}) {
   if (profileError) {
     return (
       <ErrorScreen
-        title="Oops!"
+        title={_(msg`Oops!`)}
         message={cleanError(profileError)}
         onPressTryAgain={refetchProfile}
       />
@@ -55,8 +58,8 @@ export function Component({did}: {did: string}) {
   // should never happen
   return (
     <ErrorScreen
-      title="Oops!"
-      message="Something went wrong and we're not sure what."
+      title={_(msg`Oops!`)}
+      message={_(msg`Something went wrong and we're not sure what.`)}
       onPressTryAgain={refetchProfile}
     />
   )
@@ -104,7 +107,7 @@ function ComponentLoaded({
             <>
               <InfoCircleIcon size={21} style={pal.textLight} />
               <ThemedText type="xl" fg="light">
-                Swipe up to see more
+                <Trans>Swipe up to see more</Trans>
               </ThemedText>
             </>
           )}
diff --git a/src/view/com/modals/Repost.tsx b/src/view/com/modals/Repost.tsx
index a72da29b4..6e4881adc 100644
--- a/src/view/com/modals/Repost.tsx
+++ b/src/view/com/modals/Repost.tsx
@@ -37,11 +37,23 @@ export function Component({
           style={[styles.actionBtn]}
           onPress={onRepost}
           accessibilityRole="button"
-          accessibilityLabel={isReposted ? 'Undo repost' : 'Repost'}
-          accessibilityHint={isReposted ? 'Remove repost' : 'Repost '}>
+          accessibilityLabel={
+            isReposted
+              ? _(msg`Undo repost`)
+              : _(msg({message: `Repost`, context: 'action'}))
+          }
+          accessibilityHint={
+            isReposted
+              ? _(msg`Remove repost`)
+              : _(msg({message: `Repost`, context: 'action'}))
+          }>
           <RepostIcon strokeWidth={2} size={24} style={s.blue3} />
           <Text type="title-lg" style={[styles.actionBtnLabel, pal.text]}>
-            <Trans>{!isReposted ? 'Repost' : 'Undo repost'}</Trans>
+            {!isReposted ? (
+              <Trans context="action">Repost</Trans>
+            ) : (
+              <Trans>Undo repost</Trans>
+            )}
           </Text>
         </TouchableOpacity>
         <TouchableOpacity
@@ -49,11 +61,13 @@ export function Component({
           style={[styles.actionBtn]}
           onPress={onQuote}
           accessibilityRole="button"
-          accessibilityLabel={_(msg`Quote post`)}
+          accessibilityLabel={_(
+            msg({message: `Quote post`, context: 'action'}),
+          )}
           accessibilityHint="">
           <FontAwesomeIcon icon="quote-left" size={24} style={s.blue3} />
           <Text type="title-lg" style={[styles.actionBtnLabel, pal.text]}>
-            <Trans>Quote Post</Trans>
+            <Trans context="action">Quote Post</Trans>
           </Text>
         </TouchableOpacity>
       </View>
diff --git a/src/view/com/modals/SelfLabel.tsx b/src/view/com/modals/SelfLabel.tsx
index 092dd2d32..779a9e71b 100644
--- a/src/view/com/modals/SelfLabel.tsx
+++ b/src/view/com/modals/SelfLabel.tsx
@@ -92,7 +92,7 @@ export function Component({
                   testID="sexualLabelBtn"
                   selected={selected.includes('sexual')}
                   left
-                  label="Suggestive"
+                  label={_(msg`Suggestive`)}
                   onSelect={() => toggleAdultLabel('sexual')}
                   accessibilityHint=""
                   style={s.flex1}
@@ -100,7 +100,7 @@ export function Component({
                 <SelectableBtn
                   testID="nudityLabelBtn"
                   selected={selected.includes('nudity')}
-                  label="Nudity"
+                  label={_(msg`Nudity`)}
                   onSelect={() => toggleAdultLabel('nudity')}
                   accessibilityHint=""
                   style={s.flex1}
@@ -108,7 +108,7 @@ export function Component({
                 <SelectableBtn
                   testID="pornLabelBtn"
                   selected={selected.includes('porn')}
-                  label="Porn"
+                  label={_(msg`Porn`)}
                   right
                   onSelect={() => toggleAdultLabel('porn')}
                   accessibilityHint=""
@@ -154,7 +154,7 @@ export function Component({
           accessibilityLabel={_(msg`Confirm`)}
           accessibilityHint="">
           <Text style={[s.white, s.bold, s.f18]}>
-            <Trans>Done</Trans>
+            <Trans context="action">Done</Trans>
           </Text>
         </TouchableOpacity>
       </View>
diff --git a/src/view/com/modals/ServerInput.tsx b/src/view/com/modals/ServerInput.tsx
index b30293859..550dffa1c 100644
--- a/src/view/com/modals/ServerInput.tsx
+++ b/src/view/com/modals/ServerInput.tsx
@@ -101,7 +101,9 @@ export function Component({onSelect}: {onSelect: (url: string) => void}) {
               onChangeText={setCustomUrl}
               accessibilityLabel={_(msg`Custom domain`)}
               // TODO: Simplify this wording further to be understandable by everyone
-              accessibilityHint="Use your domain as your Bluesky client service provider"
+              accessibilityHint={_(
+                msg`Use your domain as your Bluesky client service provider`,
+              )}
             />
             <TouchableOpacity
               testID="customServerSelectBtn"
@@ -110,7 +112,7 @@ export function Component({onSelect}: {onSelect: (url: string) => void}) {
               accessibilityRole="button"
               accessibilityLabel={`Confirm service. ${
                 customUrl === ''
-                  ? 'Button disabled. Input custom domain to proceed.'
+                  ? _(msg`Button disabled. Input custom domain to proceed.`)
                   : ''
               }`}
               accessibilityHint=""
diff --git a/src/view/com/modals/SwitchAccount.tsx b/src/view/com/modals/SwitchAccount.tsx
index 37691e717..c034c4b52 100644
--- a/src/view/com/modals/SwitchAccount.tsx
+++ b/src/view/com/modals/SwitchAccount.tsx
@@ -62,7 +62,9 @@ function SwitchAccountCard({account}: {account: SessionAccount}) {
           onPress={isSwitchingAccounts ? undefined : onPressSignout}
           accessibilityRole="button"
           accessibilityLabel={_(msg`Sign out`)}
-          accessibilityHint={`Signs ${profile?.displayName} out of Bluesky`}>
+          accessibilityHint={_(
+            msg`Signs ${profile?.displayName} out of Bluesky`,
+          )}>
           <Text type="lg" style={pal.link}>
             <Trans>Sign out</Trans>
           </Text>
@@ -92,8 +94,8 @@ function SwitchAccountCard({account}: {account: SessionAccount}) {
         isSwitchingAccounts ? undefined : () => onPressSwitchAccount(account)
       }
       accessibilityRole="button"
-      accessibilityLabel={`Switch to ${account.handle}`}
-      accessibilityHint="Switches the account you are logged in to">
+      accessibilityLabel={_(msg`Switch to ${account.handle}`)}
+      accessibilityHint={_(msg`Switches the account you are logged in to`)}>
       {contents}
     </TouchableOpacity>
   )
diff --git a/src/view/com/modals/Threadgate.tsx b/src/view/com/modals/Threadgate.tsx
index 0deef185b..0e49fc2f3 100644
--- a/src/view/com/modals/Threadgate.tsx
+++ b/src/view/com/modals/Threadgate.tsx
@@ -126,10 +126,10 @@ export function Component({
           }}
           style={styles.btn}
           accessibilityRole="button"
-          accessibilityLabel={_(msg`Done`)}
+          accessibilityLabel={_(msg({message: `Done`, context: 'action'}))}
           accessibilityHint="">
           <Text style={[s.white, s.bold, s.f18]}>
-            <Trans>Done</Trans>
+            <Trans context="action">Done</Trans>
           </Text>
         </TouchableOpacity>
       </View>
diff --git a/src/view/com/modals/UserAddRemoveLists.tsx b/src/view/com/modals/UserAddRemoveLists.tsx
index c51f862cc..23adbe1a8 100644
--- a/src/view/com/modals/UserAddRemoveLists.tsx
+++ b/src/view/com/modals/UserAddRemoveLists.tsx
@@ -76,10 +76,10 @@ export function Component({
           type="default"
           onPress={onPressDone}
           style={styles.footerBtn}
-          accessibilityLabel={_(msg`Done`)}
+          accessibilityLabel={_(msg({message: `Done`, context: 'action'}))}
           accessibilityHint=""
           onAccessibilityEscape={onPressDone}
-          label="Done"
+          label={_(msg({message: `Done`, context: 'action'}))}
         />
       </View>
     </View>
@@ -175,12 +175,22 @@ function ListItem({
           {sanitizeDisplayName(list.name)}
         </Text>
         <Text type="md" style={[pal.textLight]} numberOfLines={1}>
-          {list.purpose === 'app.bsky.graph.defs#curatelist' && 'User list '}
-          {list.purpose === 'app.bsky.graph.defs#modlist' && 'Moderation list '}
-          by{' '}
-          {list.creator.did === currentAccount?.did
-            ? 'you'
-            : sanitizeHandle(list.creator.handle, '@')}
+          {list.purpose === 'app.bsky.graph.defs#curatelist' &&
+            (list.creator.did === currentAccount?.did ? (
+              <Trans>User list by you</Trans>
+            ) : (
+              <Trans>
+                User list by {sanitizeHandle(list.creator.handle, '@')}
+              </Trans>
+            ))}
+          {list.purpose === 'app.bsky.graph.defs#modlist' &&
+            (list.creator.did === currentAccount?.did ? (
+              <Trans>Moderation list by you</Trans>
+            ) : (
+              <Trans>
+                Moderation list by {sanitizeHandle(list.creator.handle, '@')}
+              </Trans>
+            ))}
         </Text>
       </View>
       <View>
diff --git a/src/view/com/modals/VerifyEmail.tsx b/src/view/com/modals/VerifyEmail.tsx
index 4f2b1aadf..30a57afc5 100644
--- a/src/view/com/modals/VerifyEmail.tsx
+++ b/src/view/com/modals/VerifyEmail.tsx
@@ -75,7 +75,7 @@ export function Component({showReminder}: {showReminder?: boolean}) {
         token: confirmationCode.trim(),
       })
       updateCurrentAccount({emailConfirmed: true})
-      Toast.show('Email verified')
+      Toast.show(_(msg`Email verified`))
       closeModal()
     } catch (e) {
       setError(cleanError(String(e)))
@@ -97,9 +97,15 @@ export function Component({showReminder}: {showReminder?: boolean}) {
         {stage === Stages.Reminder && <ReminderIllustration />}
         <View style={styles.titleSection}>
           <Text type="title-lg" style={[pal.text, styles.title]}>
-            {stage === Stages.Reminder ? 'Please Verify Your Email' : ''}
-            {stage === Stages.ConfirmCode ? 'Enter Confirmation Code' : ''}
-            {stage === Stages.Email ? 'Verify Your Email' : ''}
+            {stage === Stages.Reminder ? (
+              <Trans>Please Verify Your Email</Trans>
+            ) : stage === Stages.Email ? (
+              <Trans>Verify Your Email</Trans>
+            ) : stage === Stages.ConfirmCode ? (
+              <Trans>Enter Confirmation Code</Trans>
+            ) : (
+              ''
+            )}
           </Text>
         </View>
 
@@ -133,7 +139,7 @@ export function Component({showReminder}: {showReminder?: boolean}) {
                 size={16}
               />
               <Text type="xl-medium" style={[pal.text, s.flex1, {minWidth: 0}]}>
-                {currentAccount?.email || '(no email)'}
+                {currentAccount?.email || _(msg`(no email)`)}
               </Text>
             </View>
             <Pressable
@@ -182,7 +188,7 @@ export function Component({showReminder}: {showReminder?: boolean}) {
                   onPress={() => setStage(Stages.Email)}
                   accessibilityLabel={_(msg`Get Started`)}
                   accessibilityHint=""
-                  label="Get Started"
+                  label={_(msg`Get Started`)}
                   labelContainerStyle={{justifyContent: 'center', padding: 4}}
                   labelStyle={[s.f18]}
                 />
@@ -195,7 +201,7 @@ export function Component({showReminder}: {showReminder?: boolean}) {
                     onPress={onSendEmail}
                     accessibilityLabel={_(msg`Send Confirmation Email`)}
                     accessibilityHint=""
-                    label="Send Confirmation Email"
+                    label={_(msg`Send Confirmation Email`)}
                     labelContainerStyle={{
                       justifyContent: 'center',
                       padding: 4,
@@ -207,7 +213,7 @@ export function Component({showReminder}: {showReminder?: boolean}) {
                     type="default"
                     accessibilityLabel={_(msg`I have a code`)}
                     accessibilityHint=""
-                    label="I have a confirmation code"
+                    label={_(msg`I have a confirmation code`)}
                     labelContainerStyle={{
                       justifyContent: 'center',
                       padding: 4,
@@ -224,7 +230,7 @@ export function Component({showReminder}: {showReminder?: boolean}) {
                   onPress={onConfirm}
                   accessibilityLabel={_(msg`Confirm`)}
                   accessibilityHint=""
-                  label="Confirm"
+                  label={_(msg`Confirm`)}
                   labelContainerStyle={{justifyContent: 'center', padding: 4}}
                   labelStyle={[s.f18]}
                 />
@@ -236,10 +242,16 @@ export function Component({showReminder}: {showReminder?: boolean}) {
                   closeModal()
                 }}
                 accessibilityLabel={
-                  stage === Stages.Reminder ? 'Not right now' : 'Cancel'
+                  stage === Stages.Reminder
+                    ? _(msg`Not right now`)
+                    : _(msg`Cancel`)
                 }
                 accessibilityHint=""
-                label={stage === Stages.Reminder ? 'Not right now' : 'Cancel'}
+                label={
+                  stage === Stages.Reminder
+                    ? _(msg`Not right now`)
+                    : _(msg`Cancel`)
+                }
                 labelContainerStyle={{justifyContent: 'center', padding: 4}}
                 labelStyle={[s.f18]}
               />
diff --git a/src/view/com/modals/Waitlist.tsx b/src/view/com/modals/Waitlist.tsx
index a31545c0a..263dd27a2 100644
--- a/src/view/com/modals/Waitlist.tsx
+++ b/src/view/com/modals/Waitlist.tsx
@@ -48,7 +48,7 @@ export function Component({}: {}) {
       } else {
         setError(
           resBody.error ||
-            'Something went wrong. Check your email and try again.',
+            _(msg`Something went wrong. Check your email and try again.`),
         )
       }
     } catch (e: any) {
@@ -75,7 +75,7 @@ export function Component({}: {}) {
         </Text>
         <TextInput
           style={[styles.textInput, pal.borderDark, pal.text, s.mb10, s.mt10]}
-          placeholder="Enter your email"
+          placeholder={_(msg`Enter your email`)}
           placeholderTextColor={pal.textLight.color}
           autoCapitalize="none"
           autoCorrect={false}
@@ -86,7 +86,9 @@ export function Component({}: {}) {
           enterKeyHint="done"
           accessible={true}
           accessibilityLabel={_(msg`Email`)}
-          accessibilityHint="Input your email to get on the Bluesky waitlist"
+          accessibilityHint={_(
+            msg`Input your email to get on the Bluesky waitlist`,
+          )}
         />
         {error ? (
           <View style={s.mt10}>
@@ -114,7 +116,9 @@ export function Component({}: {}) {
             <TouchableOpacity
               onPress={onPressSignup}
               accessibilityRole="button"
-              accessibilityHint={`Confirms signing up ${email} to the waitlist`}>
+              accessibilityHint={_(
+                msg`Confirms signing up ${email} to the waitlist`,
+              )}>
               <LinearGradient
                 colors={[gradients.blueLight.start, gradients.blueLight.end]}
                 start={{x: 0, y: 0}}
@@ -130,7 +134,9 @@ export function Component({}: {}) {
               onPress={onCancel}
               accessibilityRole="button"
               accessibilityLabel={_(msg`Cancel waitlist signup`)}
-              accessibilityHint={`Exits signing up for waitlist with ${email}`}
+              accessibilityHint={_(
+                msg`Exits signing up for waitlist with ${email}`,
+              )}
               onAccessibilityEscape={onCancel}>
               <Text type="button-lg" style={pal.textLight}>
                 <Trans>Cancel</Trans>
diff --git a/src/view/com/modals/report/InputIssueDetails.tsx b/src/view/com/modals/report/InputIssueDetails.tsx
index 2f701b799..2bc86f75e 100644
--- a/src/view/com/modals/report/InputIssueDetails.tsx
+++ b/src/view/com/modals/report/InputIssueDetails.tsx
@@ -42,7 +42,8 @@ export function InputIssueDetails({
         accessibilityHint="Add more details to your report">
         <FontAwesomeIcon size={18} icon="angle-left" style={[pal.link]} />
         <Text style={[pal.text, s.f18, pal.link]}>
-          <Trans> Back</Trans>
+          {' '}
+          <Trans>Back</Trans>
         </Text>
       </TouchableOpacity>
       <View style={[pal.btn, styles.detailsInputContainer]}>
diff --git a/src/view/com/modals/report/Modal.tsx b/src/view/com/modals/report/Modal.tsx
index 60c3f06b7..afd0d417d 100644
--- a/src/view/com/modals/report/Modal.tsx
+++ b/src/view/com/modals/report/Modal.tsx
@@ -44,9 +44,9 @@ export function Component(content: ReportComponentProps) {
   const {isMobile} = useWebMediaQueries()
   const [isProcessing, setIsProcessing] = useState(false)
   const [showDetailsInput, setShowDetailsInput] = useState(false)
-  const [error, setError] = useState<string>()
-  const [issue, setIssue] = useState<string>()
-  const [details, setDetails] = useState<string>()
+  const [error, setError] = useState<string>('')
+  const [issue, setIssue] = useState<string>('')
+  const [details, setDetails] = useState<string>('')
   const isAccountReport = 'did' in content
   const subjectKey = isAccountReport ? content.did : content.uri
   const atUri = useMemo(