···158159/**
160 * These "common screens" are reused across stacks.
000000161 */
162function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) {
163 const title = (page: MessageDescriptor) =>
···158159/**
160 * These "common screens" are reused across stacks.
161+ *
162+ * Note: Navigation titles use i18n._() which evaluates at setup time, not render time.
163+ * This means they cannot dynamically respond to terminology preference changes.
164+ * All in-app content (buttons, dialogs, messages) uses the terminology system,
165+ * but these navigation bar titles remain static. This is a low-impact limitation
166+ * as navigation titles are rarely noticed by users.
167 */
168function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) {
169 const title = (page: MessageDescriptor) =>
···8import {makeProfileLink} from '#/lib/routes/links'
9import {type NavigationProp} from '#/lib/routes/types'
10import {shareText, shareUrl} from '#/lib/sharing'
11+import {getTerminology, TERMINOLOGY} from '#/lib/strings/terminology'
12import {toShareUrl, toShareUrlBsky} from '#/lib/strings/url-helpers'
13import {logger} from '#/logger'
14import {isWeb} from '#/platform/detection'
15import {useProfileShadow} from '#/state/cache/profile-shadow'
16+import {useTerminologyPreference} from '#/state/preferences'
17import {useShowExternalShareButtons} from '#/state/preferences/external-share-buttons'
18import {useSession} from '#/state/session'
19import {useBreakpoints} from '#/alf'
···39 const {hasSession} = useSession()
40 const {gtMobile} = useBreakpoints()
41 const {_} = useLingui()
42+ const terminologyPreference = useTerminologyPreference()
43 const navigation = useNavigation<NavigationProp>()
44 const embedPostControl = useDialogControl()
45 const sendViaChatControl = useDialogControl()
···112 <Menu.Group>
113 <Menu.Item
114 testID="postDropdownShareBtn"
115+ label={_(getTerminology(terminologyPreference, {
116+ skeet: msg`Copy link to skeet`,
117+ post: msg`Copy link to post`,
118+ spell: msg`Copy link to spell`,
119+ }))}
120 onPress={onCopyLink}>
121 <Menu.ItemText>
122+ <Trans>{_(getTerminology(terminologyPreference, {
123+ skeet: msg`Copy link to skeet`,
124+ post: msg`Copy link to post`,
125+ spell: msg`Copy link to spell`,
126+ }))}</Trans>
127 </Menu.ItemText>
128 <Menu.ItemIcon icon={ChainLinkIcon} position="right" />
129 </Menu.Item>
···147 {showExternalShareButtons && isBridgedPost && (
148 <Menu.Item
149 testID="postDropdownOpenOriginalPost"
150+ label={_(getTerminology(terminologyPreference, {
151+ skeet: msg`Open original skeet`,
152+ post: msg`Open original post`,
153+ spell: msg`Open original spell`,
154+ }))}
155 onPress={onOpenOriginalPost}>
156 <Menu.ItemText>
157+ <Trans>{_(getTerminology(terminologyPreference, {
158+ skeet: msg`Open original skeet`,
159+ post: msg`Open original post`,
160+ spell: msg`Open original spell`,
161+ }))}</Trans>
162 </Menu.ItemText>
163 <Menu.ItemIcon icon={ExternalIcon} position="right" />
164 </Menu.Item>
···167 {showExternalShareButtons && (
168 <Menu.Item
169 testID="postDropdownOpenInPdsls"
170+ label={_(getTerminology(terminologyPreference, {
171+ skeet: msg`Open skeet in PDSls`,
172+ post: msg`Open post in PDSls`,
173+ spell: msg`Open spell in PDSls`,
174+ }))}
175 onPress={onOpenPostInPdsls}>
176 <Menu.ItemText>
177+ <Trans>{_(getTerminology(terminologyPreference, {
178+ skeet: msg`Open skeet in PDSls`,
179+ post: msg`Open post in PDSls`,
180+ spell: msg`Open spell in PDSls`,
181+ }))}</Trans>
182 </Menu.ItemText>
183 <Menu.ItemIcon icon={ExternalIcon} position="right" />
184 </Menu.Item>
···202 {canEmbed && (
203 <Menu.Item
204 testID="postDropdownEmbedBtn"
205+ label={_(getTerminology(terminologyPreference, {
206+ skeet: msg`Embed skeet`,
207+ post: msg`Embed post`,
208+ spell: msg`Embed spell`,
209+ }))}
210 onPress={() => {
211 logger.metric('share:press:embed', {}, {statsig: true})
212 embedPostControl.open()
213 }}>
214+ <Menu.ItemText>{_(getTerminology(terminologyPreference, {
215+ skeet: msg`Embed skeet`,
216+ post: msg`Embed post`,
217+ spell: msg`Embed spell`,
218+ }))}</Menu.ItemText>
219 <Menu.ItemIcon icon={CodeBracketsIcon} position="right" />
220 </Menu.Item>
221 )}
···225 {hasSession && <Menu.Divider />}
226 {copyLinkItem}
227 <Menu.LabelText style={{maxWidth: 220}}>
228+ <Trans>{_(getTerminology(terminologyPreference, {
229+ skeet: msg`Note: This skeet is only visible to logged-in users.`,
230+ post: msg`Note: This post is only visible to logged-in users.`,
231+ spell: msg`Note: This spell is only visible to logged-in users.`,
232+ }))}</Trans>
233 </Menu.LabelText>
234 </>
235 )}
···239 <Menu.Divider />
240 <Menu.Item
241 testID="postAtUriShareBtn"
242+ label={_(getTerminology(terminologyPreference, {
243+ skeet: msg`Copy skeet at:// URI`,
244+ post: msg`Copy post at:// URI`,
245+ spell: msg`Copy spell at:// URI`,
246+ }))}
247 onPress={onShareATURI}>
248 <Menu.ItemText>
249+ <Trans>{_(getTerminology(terminologyPreference, {
250+ skeet: msg`Copy skeet at:// URI`,
251+ post: msg`Copy post at:// URI`,
252+ spell: msg`Copy spell at:// URI`,
253+ }))}</Trans>
254 </Menu.ItemText>
255 <Menu.ItemIcon icon={ClipboardIcon} position="right" />
256 </Menu.Item>
+30-8
src/components/WhoCanReply.tsx
···1718import {HITSLOP_10} from '#/lib/constants'
19import {makeListLink, makeProfileLink} from '#/lib/routes/links'
0020import {logger} from '#/logger'
21import {isNative} from '#/platform/detection'
22import {
···212 embeddingDisabled: boolean
213}) {
214 const {_} = useLingui()
0215216 return (
217 <Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
···221 style={web({maxWidth: 400})}>
222 <View style={[a.gap_sm]}>
223 <Text style={[a.font_semi_bold, a.text_xl, a.pb_sm]}>
224- <Trans>Who can interact with this skeet?</Trans>
0000225 </Text>
226 <Rules
227 post={post}
···258 embeddingDisabled: boolean
259}) {
260 const t = useTheme()
00261262 return (
263 <>
···269 t.atoms.text_contrast_medium,
270 ]}>
271 {settings.length === 0 ? (
272- <Trans>
273- This skeet has an unknown type of threadgate on it. Your app may be
274- out of date.
275- </Trans>
0276 ) : settings[0].type === 'everybody' ? (
277- <Trans>Everybody can reply to this skeet.</Trans>
0000278 ) : settings[0].type === 'nobody' ? (
279- <Trans>Replies to this skeet are disabled.</Trans>
0000280 ) : (
281 <Trans>
282 Only{' '}
···298 a.flex_wrap,
299 t.atoms.text_contrast_medium,
300 ]}>
301- <Trans>No one but the author can quote this skeet.</Trans>
0000302 </Text>
303 )}
304 </>
···1718import {HITSLOP_10} from '#/lib/constants'
19import {makeListLink, makeProfileLink} from '#/lib/routes/links'
20+import {getTerminology} from '#/lib/strings/terminology'
21+import {useTerminologyPreference} from '#/state/preferences'
22import {logger} from '#/logger'
23import {isNative} from '#/platform/detection'
24import {
···214 embeddingDisabled: boolean
215}) {
216 const {_} = useLingui()
217+ const terminologyPreference = useTerminologyPreference()
218219 return (
220 <Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
···224 style={web({maxWidth: 400})}>
225 <View style={[a.gap_sm]}>
226 <Text style={[a.font_semi_bold, a.text_xl, a.pb_sm]}>
227+ <Trans>{_(getTerminology(terminologyPreference, {
228+ skeet: msg`Who can interact with this skeet?`,
229+ post: msg`Who can interact with this post?`,
230+ spell: msg`Who can interact with this spell?`,
231+ }))}</Trans>
232 </Text>
233 <Rules
234 post={post}
···265 embeddingDisabled: boolean
266}) {
267 const t = useTheme()
268+ const {_} = useLingui()
269+ const terminologyPreference = useTerminologyPreference()
270271 return (
272 <>
···278 t.atoms.text_contrast_medium,
279 ]}>
280 {settings.length === 0 ? (
281+ <Trans>{_(getTerminology(terminologyPreference, {
282+ skeet: msg`This skeet has an unknown type of threadgate on it. Your app may be out of date.`,
283+ post: msg`This post has an unknown type of threadgate on it. Your app may be out of date.`,
284+ spell: msg`This spell has an unknown type of threadgate on it. Your app may be out of date.`,
285+ }))}</Trans>
286 ) : settings[0].type === 'everybody' ? (
287+ <Trans>{_(getTerminology(terminologyPreference, {
288+ skeet: msg`Everybody can reply to this skeet.`,
289+ post: msg`Everybody can reply to this post.`,
290+ spell: msg`Everybody can reply to this spell.`,
291+ }))}</Trans>
292 ) : settings[0].type === 'nobody' ? (
293+ <Trans>{_(getTerminology(terminologyPreference, {
294+ skeet: msg`Replies to this skeet are disabled.`,
295+ post: msg`Replies to this post are disabled.`,
296+ spell: msg`Replies to this spell are disabled.`,
297+ }))}</Trans>
298 ) : (
299 <Trans>
300 Only{' '}
···316 a.flex_wrap,
317 t.atoms.text_contrast_medium,
318 ]}>
319+ <Trans>{_(getTerminology(terminologyPreference, {
320+ skeet: msg`No one but the author can quote this skeet.`,
321+ post: msg`No one but the author can quote this post.`,
322+ spell: msg`No one but the author can quote this spell.`,
323+ }))}</Trans>
324 </Text>
325 )}
326 </>
+18-6
src/components/dialogs/Embed.tsx
···67import {EMBED_SCRIPT} from '#/lib/constants'
8import {niceDate} from '#/lib/strings/time'
009import {toShareUrl} from '#/lib/strings/url-helpers'
10import {atoms as a, useTheme} from '#/alf'
11import {Button, ButtonIcon, ButtonText} from '#/components/Button'
···51}: Omit<EmbedDialogProps, 'control'>) {
52 const t = useTheme()
53 const {_, i18n} = useLingui()
054 const [copied, setCopied] = useState(false)
55 const [showCustomisation, setShowCustomisation] = useState(false)
56 const [colorMode, setColorMode] = useState<ColorModeValues>('system')
···101 }, [i18n, postUri, postCid, record, timestamp, postAuthor, colorMode])
102103 return (
104- <Dialog.Inner label={_(msg`Embed post`)} style={[{maxWidth: 500}]}>
0000105 <View style={[a.gap_lg]}>
106 <View style={[a.gap_sm]}>
107 <Text style={[a.text_2xl, a.font_bold]}>
108- <Trans>Embed skeet</Trans>
0000109 </Text>
110 <Text
111 style={[a.text_md, t.atoms.text_contrast_medium, a.leading_normal]}>
112- <Trans>
113- Embed this skeet in your website. Simply copy the following snippet
114- and paste it into the HTML code of your website.
115- </Trans>
0116 </Text>
117 </View>
118 <View
···67import {EMBED_SCRIPT} from '#/lib/constants'
8import {niceDate} from '#/lib/strings/time'
9+import {getTerminology} from '#/lib/strings/terminology'
10+import {useTerminologyPreference} from '#/state/preferences'
11import {toShareUrl} from '#/lib/strings/url-helpers'
12import {atoms as a, useTheme} from '#/alf'
13import {Button, ButtonIcon, ButtonText} from '#/components/Button'
···53}: Omit<EmbedDialogProps, 'control'>) {
54 const t = useTheme()
55 const {_, i18n} = useLingui()
56+ const terminologyPreference = useTerminologyPreference()
57 const [copied, setCopied] = useState(false)
58 const [showCustomisation, setShowCustomisation] = useState(false)
59 const [colorMode, setColorMode] = useState<ColorModeValues>('system')
···104 }, [i18n, postUri, postCid, record, timestamp, postAuthor, colorMode])
105106 return (
107+ <Dialog.Inner label={_(getTerminology(terminologyPreference, {
108+ skeet: msg`Embed skeet`,
109+ post: msg`Embed post`,
110+ spell: msg`Embed spell`,
111+ }))} style={[{maxWidth: 500}]}>
112 <View style={[a.gap_lg]}>
113 <View style={[a.gap_sm]}>
114 <Text style={[a.text_2xl, a.font_bold]}>
115+ <Trans>{_(getTerminology(terminologyPreference, {
116+ skeet: msg`Embed skeet`,
117+ post: msg`Embed post`,
118+ spell: msg`Embed spell`,
119+ }))}</Trans>
120 </Text>
121 <Text
122 style={[a.text_md, t.atoms.text_contrast_medium, a.leading_normal]}>
123+ <Trans>{_(getTerminology(terminologyPreference, {
124+ skeet: msg`Embed this skeet in your website. Simply copy the following snippet and paste it into the HTML code of your website.`,
125+ post: msg`Embed this post in your website. Simply copy the following snippet and paste it into the HTML code of your website.`,
126+ spell: msg`Embed this spell in your website. Simply copy the following snippet and paste it into the HTML code of your website.`,
127+ }))}</Trans>
128 </Text>
129 </View>
130 <View
+3-1
src/lib/api/index.ts
···2425import {isNetworkError} from '#/lib/strings/errors'
26import {parseMarkdownLinks,shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip'
027import {logger} from '#/logger'
28import {compressImage} from '#/state/gallery'
29import {
···49 replyTo?: string
50 onStateChange?: (state: string) => void
51 langs?: string[]
052}
5354export async function post(
···183 })
184 if (isNetworkError(e)) {
185 throw new Error(
186- t`Skeet failed to upload. Please check your Internet connection and try again.`,
187 )
188 } else {
189 throw e
···2425import {isNetworkError} from '#/lib/strings/errors'
26import {parseMarkdownLinks,shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip'
27+import {getTerminology, TERMINOLOGY} from '#/lib/strings/terminology'
28import {logger} from '#/logger'
29import {compressImage} from '#/state/gallery'
30import {
···50 replyTo?: string
51 onStateChange?: (state: string) => void
52 langs?: string[]
53+ terminologyPreference?: string
54}
5556export async function post(
···185 })
186 if (isNetworkError(e)) {
187 throw new Error(
188+ getTerminology(opts.terminologyPreference || 'skeet', TERMINOLOGY.uploadFailed),
189 )
190 } else {
191 throw e
···1+import {msg} from '@lingui/macro'
2+3+export type TerminologyPreference = 'skeet' | 'post' | 'spell'
4+5+/**
6+ * Returns the appropriate terminology based on the user's preference
7+ * @param preference - The user's terminology preference ('skeet', 'post', or 'spell')
8+ * @param variants - An object with message descriptors for each terminology option
9+ * @returns The appropriate message descriptor based on the preference
10+ */
11+export function getTerminology<T extends {skeet: any; post: any; spell: any}>(
12+ preference: TerminologyPreference | undefined,
13+ variants: T,
14+): T[keyof T] {
15+ const pref = preference ?? 'skeet'
16+ return variants[pref]
17+}
18+19+/**
20+ * Common terminology variants used throughout the app
21+ */
22+export const TERMINOLOGY = {
23+ // Single post
24+ singular: {
25+ skeet: msg`skeet`,
26+ post: msg`post`,
27+ spell: msg`spell`,
28+ },
29+ // Multiple posts
30+ plural: {
31+ skeet: msg`skeets`,
32+ post: msg`posts`,
33+ spell: msg`spells`,
34+ },
35+ // "Skeet by @handle"
36+ byHandle: (handle: string) => ({
37+ skeet: msg`Skeet by @${handle}`,
38+ post: msg`Post by @${handle}`,
39+ spell: msg`Spell by @${handle}`,
40+ }),
41+ // Repost terminology
42+ repost: {
43+ singular: {
44+ skeet: msg`reskeet`,
45+ post: msg`repost`,
46+ spell: msg`respell`,
47+ },
48+ plural: {
49+ skeet: msg`reskeets`,
50+ post: msg`reposts`,
51+ spell: msg`respells`,
52+ },
53+ pastTense: {
54+ skeet: msg`reskeeted`,
55+ post: msg`reposted`,
56+ spell: msg`respelled`,
57+ },
58+ byLine: {
59+ skeet: msg`Reskeeted By`,
60+ post: msg`Reposted By`,
61+ spell: msg`Respelled By`,
62+ },
63+ action: {
64+ skeet: msg`reskeet`,
65+ post: msg`repost`,
66+ spell: msg`respell`,
67+ },
68+ },
69+ // For metrics/counts
70+ metrics: {
71+ skeet: msg`skeets metrics`,
72+ post: msg`posts metrics`,
73+ spell: msg`spells metrics`,
74+ },
75+ // For carousel
76+ carousel: {
77+ skeet: msg`Combine reskeets into a horizontal carousel`,
78+ post: msg`Combine reposts into a horizontal carousel`,
79+ spell: msg`Combine respells into a horizontal carousel`,
80+ },
81+ // For notifications
82+ viaRepostNotification: {
83+ skeet: msg`Disable "via reskeet" notifications`,
84+ post: msg`Disable "via repost" notifications`,
85+ spell: msg`Disable "via respell" notifications`,
86+ },
87+ viaRepostPrivacy: {
88+ skeet: msg`Forcefully disables the notifications other people receive when you like/reskeet a skeet someone else has reskeeted for privacy.`,
89+ post: msg`Forcefully disables the notifications other people receive when you like/repost a post someone else has reposted for privacy.`,
90+ spell: msg`Forcefully disables the notifications other people receive when you like/respell a spell someone else has respelled for privacy.`,
91+ },
92+ // For external share buttons
93+ externalShareButtons: {
94+ skeet: msg`Show "Open original skeet" and "Open skeet in PDSls" buttons`,
95+ post: msg`Show "Open original post" and "Open post in PDSls" buttons`,
96+ spell: msg`Show "Open original spell" and "Open spell in PDSls" buttons`,
97+ },
98+ // For metrics labels
99+ repostMetrics: {
100+ skeet: msg`Disable reskeets metrics`,
101+ post: msg`Disable reposts metrics`,
102+ spell: msg`Disable respells metrics`,
103+ },
104+ // For deletion messages
105+ deleted: {
106+ skeet: msg`This skeet was deleted by its author`,
107+ post: msg`This post was deleted by its author`,
108+ spell: msg`This spell was deleted by its author`,
109+ },
110+ deletedShort: {
111+ skeet: msg`Skeet has been deleted`,
112+ post: msg`Post has been deleted`,
113+ spell: msg`Spell has been deleted`,
114+ },
115+ // For error messages
116+ notFound: {
117+ skeet: msg`Skeet not found`,
118+ post: msg`Post not found`,
119+ spell: msg`Spell not found`,
120+ },
121+ blocked: {
122+ skeet: msg`Skeet blocked`,
123+ post: msg`Post blocked`,
124+ spell: msg`Spell blocked`,
125+ },
126+ errorLoading: {
127+ skeet: msg`Error loading skeet`,
128+ post: msg`Error loading post`,
129+ spell: msg`Error loading spell`,
130+ },
131+ // For success messages
132+ sent: {
133+ skeet: msg`Your skeet was sent`,
134+ post: msg`Your post was sent`,
135+ spell: msg`Your spell was sent`,
136+ },
137+ sentPlural: {
138+ skeet: msg`Your skeets were sent`,
139+ post: msg`Your posts were sent`,
140+ spell: msg`Your spells were sent`,
141+ },
142+ pinned: {
143+ skeet: msg`Skeet pinned`,
144+ post: msg`Post pinned`,
145+ spell: msg`Spell pinned`,
146+ },
147+ unpinned: {
148+ skeet: msg`Skeet unpinned`,
149+ post: msg`Post unpinned`,
150+ spell: msg`Spell unpinned`,
151+ },
152+ failedToPin: {
153+ skeet: msg`Failed to pin skeet`,
154+ post: msg`Failed to pin post`,
155+ spell: msg`Failed to pin spell`,
156+ },
157+ // For composer
158+ addAnother: {
159+ skeet: msg`Add another skeet`,
160+ post: msg`Add another post`,
161+ spell: msg`Add another spell`,
162+ },
163+ anythingBut: {
164+ skeet: msg`Anything but skeet`,
165+ post: msg`Anything but post`,
166+ spell: msg`Anything but spell`,
167+ },
168+ delete: {
169+ skeet: msg`Delete skeet`,
170+ post: msg`Delete post`,
171+ spell: msg`Delete spell`,
172+ },
173+ discard: {
174+ skeet: msg`Discard skeet?`,
175+ post: msg`Discard post?`,
176+ spell: msg`Discard spell?`,
177+ },
178+ discardConfirm: {
179+ skeet: msg`Are you sure you'd like to discard this skeet?`,
180+ post: msg`Are you sure you'd like to discard this post?`,
181+ spell: msg`Are you sure you'd like to discard this spell?`,
182+ },
183+ view: {
184+ skeet: msg`View skeet`,
185+ post: msg`View post`,
186+ spell: msg`View spell`,
187+ },
188+ // For composer actions
189+ composeNew: {
190+ skeet: msg`Compose new skeet`,
191+ post: msg`Compose new post`,
192+ spell: msg`Compose new spell`,
193+ },
194+ newAction: {
195+ skeet: msg`New Skeet`,
196+ post: msg`New Post`,
197+ spell: msg`New Spell`,
198+ },
199+ postAll: {
200+ skeet: msg`Skeet All`,
201+ post: msg`Post All`,
202+ spell: msg`Spell All`,
203+ },
204+ postSingle: {
205+ skeet: msg`Skeet`,
206+ post: msg`Post`,
207+ spell: msg`Spell`,
208+ },
209+ // For language selection
210+ selectLanguage: {
211+ skeet: msg`Select skeet language`,
212+ post: msg`Select post language`,
213+ spell: msg`Select spell language`,
214+ },
215+ chooseLanguages: {
216+ skeet: msg`Choose Skeet Languages`,
217+ post: msg`Choose Post Languages`,
218+ spell: msg`Choose Spell Languages`,
219+ },
220+ languageDescription: {
221+ skeet: msg`Select up to 3 languages used in this skeet`,
222+ post: msg`Select up to 3 languages used in this post`,
223+ spell: msg`Select up to 3 languages used in this spell`,
224+ },
225+ replyingLanguage: (langs: string) => ({
226+ skeet: msg`The skeet you're replying to was marked as being written in ${langs}`,
227+ post: msg`The post you're replying to was marked as being written in ${langs}`,
228+ spell: msg`The spell you're replying to was marked as being written in ${langs}`,
229+ }),
230+ // For interaction settings
231+ interactionSettings: {
232+ skeet: msg`Skeet interaction settings`,
233+ post: msg`Post interaction settings`,
234+ spell: msg`Spell interaction settings`,
235+ },
236+ editInteractionSettings: {
237+ skeet: msg`Edit skeet interaction settings`,
238+ post: msg`Edit post interaction settings`,
239+ spell: msg`Edit spell interaction settings`,
240+ },
241+ whoCanInteract: {
242+ skeet: msg`Who can interact with this skeet?`,
243+ post: msg`Who can interact with this post?`,
244+ spell: msg`Who can interact with this spell?`,
245+ },
246+ // For embedding
247+ embed: {
248+ skeet: msg`Embed skeet`,
249+ post: msg`Embed post`,
250+ spell: msg`Embed spell`,
251+ },
252+ embedDescription: {
253+ skeet: msg`Embed this skeet in your website. Simply copy the following snippet and paste it into the HTML code of your website.`,
254+ post: msg`Embed this post in your website. Simply copy the following snippet and paste it into the HTML code of your website.`,
255+ spell: msg`Embed this spell in your website. Simply copy the following snippet and paste it into the HTML code of your website.`,
256+ },
257+ // For sharing
258+ copyLink: {
259+ skeet: msg`Copy link to skeet`,
260+ post: msg`Copy link to post`,
261+ spell: msg`Copy link to spell`,
262+ },
263+ shareUri: {
264+ skeet: msg`Share skeet at:// URI`,
265+ post: msg`Share post at:// URI`,
266+ spell: msg`Share spell at:// URI`,
267+ },
268+ copyUri: {
269+ skeet: msg`Copy skeet at:// URI`,
270+ post: msg`Copy post at:// URI`,
271+ spell: msg`Copy spell at:// URI`,
272+ },
273+ sendTo: {
274+ skeet: msg`Send skeet to...`,
275+ post: msg`Send post to...`,
276+ spell: msg`Send spell to...`,
277+ },
278+ openOriginal: {
279+ skeet: msg`Open original skeet`,
280+ post: msg`Open original post`,
281+ spell: msg`Open original spell`,
282+ },
283+ openInPDSls: {
284+ skeet: msg`Open skeet in PDSls`,
285+ post: msg`Open post in PDSls`,
286+ spell: msg`Open spell in PDSls`,
287+ },
288+ // For reporting
289+ report: {
290+ skeet: msg`Report this skeet`,
291+ post: msg`Report this post`,
292+ spell: msg`Report this spell`,
293+ },
294+ reportWhy: {
295+ skeet: msg`Why should this skeet be reviewed?`,
296+ post: msg`Why should this post be reviewed?`,
297+ spell: msg`Why should this spell be reviewed?`,
298+ },
299+ // For search and browse
300+ search: {
301+ skeet: msg`Search skeets`,
302+ post: msg`Search posts`,
303+ spell: msg`Search spells`,
304+ },
305+ searchProfile: (handle: string) => ({
306+ skeet: msg`Search @${handle}'s skeets`,
307+ post: msg`Search @${handle}'s posts`,
308+ spell: msg`Search @${handle}'s spells`,
309+ }),
310+ searchMy: {
311+ skeet: msg`Search my skeets`,
312+ post: msg`Search my posts`,
313+ spell: msg`Search my spells`,
314+ },
315+ browse: (tag: string) => ({
316+ skeet: msg`Browse skeets about ${tag}`,
317+ post: msg`Browse posts about ${tag}`,
318+ spell: msg`Browse spells about ${tag}`,
319+ }),
320+ browseTagged: (tag: string) => ({
321+ skeet: msg`Browse skeets tagged with ${tag}`,
322+ post: msg`Browse posts tagged with ${tag}`,
323+ spell: msg`Browse spells tagged with ${tag}`,
324+ }),
325+ seeTag: (tag: string) => ({
326+ skeet: msg`See ${tag} skeets`,
327+ post: msg`See ${tag} posts`,
328+ spell: msg`See ${tag} spells`,
329+ }),
330+ seeTagByUser: (tag: string) => ({
331+ skeet: msg`See ${tag} skeets by user`,
332+ post: msg`See ${tag} posts by user`,
333+ spell: msg`See ${tag} spells by user`,
334+ }),
335+ // For loading and errors
336+ loadNew: {
337+ skeet: msg`Load new skeets`,
338+ post: msg`Load new posts`,
339+ spell: msg`Load new spells`,
340+ },
341+ fetchError: {
342+ skeet: msg`There was an issue fetching skeets. Tap here to try again.`,
343+ post: msg`There was an issue fetching posts. Tap here to try again.`,
344+ spell: msg`There was an issue fetching spells. Tap here to try again.`,
345+ },
346+ uploadFailed: {
347+ skeet: msg`Skeet failed to upload. Please check your Internet connection and try again.`,
348+ post: msg`Post failed to upload. Please check your Internet connection and try again.`,
349+ spell: msg`Spell failed to upload. Please check your Internet connection and try again.`,
350+ },
351+ // For feeds and filtering
352+ ranOut: {
353+ skeet: msg`We ran out of skeets from your follows. Here's the latest from`,
354+ post: msg`We ran out of posts from your follows. Here's the latest from`,
355+ spell: msg`We ran out of spells from your follows. Here's the latest from`,
356+ },
357+ // For quote posts
358+ quoteAction: {
359+ skeet: msg`Quote skeet`,
360+ post: msg`Quote post`,
361+ spell: msg`Quote spell`,
362+ },
363+ quoteDisabled: {
364+ skeet: msg`Quote skeets disabled`,
365+ post: msg`Quote posts disabled`,
366+ spell: msg`Quote spells disabled`,
367+ },
368+ allowQuote: {
369+ skeet: msg`Allow quote skeets`,
370+ post: msg`Allow quote posts`,
371+ spell: msg`Allow quote spells`,
372+ },
373+ quoteAuthorDisabled: {
374+ skeet: msg`This skeet's author has disabled quote skeets.`,
375+ post: msg`This post's author has disabled quote posts.`,
376+ spell: msg`This spell's author has disabled quote spells.`,
377+ },
378+ cancelQuote: {
379+ skeet: msg`Cancel quote skeet`,
380+ post: msg`Cancel quote post`,
381+ spell: msg`Cancel quote spell`,
382+ },
383+ noQuoteYet: {
384+ skeet: msg`No one but the author can quote this skeet.`,
385+ post: msg`No one but the author can quote this post.`,
386+ spell: msg`No one but the author can quote this spell.`,
387+ },
388+ // For reply settings
389+ repliesDisabled: {
390+ skeet: msg`Replies to this skeet are disabled.`,
391+ post: msg`Replies to this post are disabled.`,
392+ spell: msg`Replies to this spell are disabled.`,
393+ },
394+ everyoneCanReply: {
395+ skeet: msg`Everybody can reply to this skeet.`,
396+ post: msg`Everybody can reply to this post.`,
397+ spell: msg`Everybody can reply to this spell.`,
398+ },
399+ unknownThreadgate: {
400+ skeet: msg`This skeet has an unknown type of threadgate on it. Your app may be out of date.`,
401+ post: msg`This post has an unknown type of threadgate on it. Your app may be out of date.`,
402+ spell: msg`This spell has an unknown type of threadgate on it. Your app may be out of date.`,
403+ },
404+ // For replies
405+ repliedTo: {
406+ skeet: msg`Replied to a skeet`,
407+ post: msg`Replied to a post`,
408+ spell: msg`Replied to a spell`,
409+ },
410+ repliedToBlocked: {
411+ skeet: msg`Replied to a blocked skeet`,
412+ post: msg`Replied to a blocked post`,
413+ spell: msg`Replied to a blocked spell`,
414+ },
415+ replyWasDeleted: {
416+ skeet: msg`We're sorry! The skeet you are replying to has been deleted.`,
417+ post: msg`We're sorry! The post you are replying to has been deleted.`,
418+ spell: msg`We're sorry! The spell you are replying to has been deleted.`,
419+ },
420+ sortReplies: {
421+ skeet: msg`Sort replies to the same skeet by:`,
422+ post: msg`Sort replies to the same post by:`,
423+ spell: msg`Sort replies to the same spell by:`,
424+ },
425+ showRepliesTree: {
426+ skeet: msg`Show skeet replies in a threaded tree view`,
427+ post: msg`Show post replies in a threaded tree view`,
428+ spell: msg`Show spell replies in a threaded tree view`,
429+ },
430+ // For visibility
431+ onlyLoggedIn: {
432+ skeet: msg`This skeet is only visible to logged-in users.`,
433+ post: msg`This post is only visible to logged-in users.`,
434+ spell: msg`This spell is only visible to logged-in users.`,
435+ },
436+ noteOnlyLoggedIn: {
437+ skeet: msg`Note: This skeet is only visible to logged-in users.`,
438+ post: msg`Note: This post is only visible to logged-in users.`,
439+ spell: msg`Note: This spell is only visible to logged-in users.`,
440+ },
441+}
+25-4
src/screens/Bookmarks/index.tsx
···16import {useCleanError} from '#/lib/hooks/useCleanError'
17import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
18import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking'
019import {
20 type CommonNavigatorParams,
21 type NativeStackScreenProps,
22} from '#/lib/routes/types'
23import {logger} from '#/logger'
24import {isIOS} from '#/platform/detection'
025import {useBookmarkMutation} from '#/state/queries/bookmarks/useBookmarkMutation'
26import {useBookmarksQuery} from '#/state/queries/bookmarks/useBookmarksQuery'
27import {useSetMinimalShellMode} from '#/state/shell'
···42type Props = NativeStackScreenProps<CommonNavigatorParams, 'Bookmarks'>
4344export function BookmarksScreen({}: Props) {
0045 const setMinimalShellMode = useSetMinimalShellMode()
4647 useFocusEffect(
···57 <Layout.Header.BackButton />
58 <Layout.Header.Content>
59 <Layout.Header.TitleText>
60- <Trans>Saved Skeets</Trans>
000061 </Layout.Header.TitleText>
62 </Layout.Header.Content>
63 <Layout.Header.Slot />
···209}) {
210 const t = useTheme()
211 const {_} = useLingui()
0212 const {mutateAsync: bookmark} = useBookmarkMutation()
213 const cleanError = useCleanError()
214215 const remove = async () => {
216 try {
217 await bookmark({action: 'delete', uri: post.uri})
218- toast.show(_(msg`Removed from saved skeets`), {
0000219 type: 'info',
220 })
221 } catch (e: any) {
···253 a.italic,
254 t.atoms.text_contrast_medium,
255 ]}>
256- <Trans>This skeet was deleted by its author</Trans>
0000257 </Text>
258 </View>
259 <Button
260- label={_(msg`Remove from saved skeets`)}
0000261 size="tiny"
262 color="secondary"
263 onPress={remove}>
···16import {useCleanError} from '#/lib/hooks/useCleanError'
17import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
18import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking'
19+import {getTerminology} from '#/lib/strings/terminology'
20import {
21 type CommonNavigatorParams,
22 type NativeStackScreenProps,
23} from '#/lib/routes/types'
24import {logger} from '#/logger'
25import {isIOS} from '#/platform/detection'
26+import {useTerminologyPreference} from '#/state/preferences'
27import {useBookmarkMutation} from '#/state/queries/bookmarks/useBookmarkMutation'
28import {useBookmarksQuery} from '#/state/queries/bookmarks/useBookmarksQuery'
29import {useSetMinimalShellMode} from '#/state/shell'
···44type Props = NativeStackScreenProps<CommonNavigatorParams, 'Bookmarks'>
4546export function BookmarksScreen({}: Props) {
47+ const {_} = useLingui()
48+ const terminologyPreference = useTerminologyPreference()
49 const setMinimalShellMode = useSetMinimalShellMode()
5051 useFocusEffect(
···61 <Layout.Header.BackButton />
62 <Layout.Header.Content>
63 <Layout.Header.TitleText>
64+ <Trans>{_(getTerminology(terminologyPreference, {
65+ skeet: msg`Saved Skeets`,
66+ post: msg`Saved Posts`,
67+ spell: msg`Saved Spells`,
68+ }))}</Trans>
69 </Layout.Header.TitleText>
70 </Layout.Header.Content>
71 <Layout.Header.Slot />
···217}) {
218 const t = useTheme()
219 const {_} = useLingui()
220+ const terminologyPreference = useTerminologyPreference()
221 const {mutateAsync: bookmark} = useBookmarkMutation()
222 const cleanError = useCleanError()
223224 const remove = async () => {
225 try {
226 await bookmark({action: 'delete', uri: post.uri})
227+ toast.show(_(getTerminology(terminologyPreference, {
228+ skeet: msg`Removed from saved skeets`,
229+ post: msg`Removed from saved posts`,
230+ spell: msg`Removed from saved spells`,
231+ })), {
232 type: 'info',
233 })
234 } catch (e: any) {
···266 a.italic,
267 t.atoms.text_contrast_medium,
268 ]}>
269+ <Trans>{_(getTerminology(terminologyPreference, {
270+ skeet: msg`This skeet was deleted by its author`,
271+ post: msg`This post was deleted by its author`,
272+ spell: msg`This spell was deleted by its author`,
273+ }))}</Trans>
274 </Text>
275 </View>
276 <Button
277+ label={_(getTerminology(terminologyPreference, {
278+ skeet: msg`Remove from saved skeets`,
279+ post: msg`Remove from saved posts`,
280+ spell: msg`Remove from saved spells`,
281+ }))}
282 size="tiny"
283 color="secondary"
284 onPress={remove}>
···1import {View} from 'react-native'
2-import {Trans} from '@lingui/macro'
034import {
5 type AllNavigatorParams,
6 type NativeStackScreenProps,
7} from '#/lib/routes/types'
008import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings'
9import {atoms as a} from '#/alf'
10import {Admonition} from '#/components/Admonition'
···19 'LikesOnRepostsNotificationSettings'
20>
21export function LikesOnRepostsNotificationSettingsScreen({}: Props) {
0022 const {data: preferences, isError} = useNotificationSettingsQuery()
2324 return (
···38 <SettingsList.ItemIcon icon={LikeRepostIcon} />
39 <ItemTextWithSubtitle
40 bold
41- titleText={<Trans>Likes of your reskeets</Trans>}
000042 subtitleText={
43- <Trans>
44- Get notifications when people like skeets that you've reskeeted.
45- </Trans>
0046 }
47 />
48 </SettingsList.Item>
···1import {View} from 'react-native'
2+import {msg, Trans} from '@lingui/macro'
3+import {useLingui} from '@lingui/react'
45import {
6 type AllNavigatorParams,
7 type NativeStackScreenProps,
8} from '#/lib/routes/types'
9+import {getTerminology} from '#/lib/strings/terminology'
10+import {useTerminologyPreference} from '#/state/preferences'
11import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings'
12import {atoms as a} from '#/alf'
13import {Admonition} from '#/components/Admonition'
···22 'LikesOnRepostsNotificationSettings'
23>
24export function LikesOnRepostsNotificationSettingsScreen({}: Props) {
25+ const {_} = useLingui()
26+ const terminologyPreference = useTerminologyPreference()
27 const {data: preferences, isError} = useNotificationSettingsQuery()
2829 return (
···43 <SettingsList.ItemIcon icon={LikeRepostIcon} />
44 <ItemTextWithSubtitle
45 bold
46+ titleText={<Trans>{_(getTerminology(terminologyPreference, {
47+ skeet: msg`Likes of your reskeets`,
48+ post: msg`Likes of your reposts`,
49+ spell: msg`Likes of your respells`,
50+ }))}</Trans>}
51 subtitleText={
52+ <Trans>{_(getTerminology(terminologyPreference, {
53+ skeet: msg`Get notifications when people like skeets that you've reskeeted.`,
54+ post: msg`Get notifications when people like posts that you've reposted.`,
55+ spell: msg`Get notifications when people like spells that you've respelled.`,
56+ }))}</Trans>
57 }
58 />
59 </SettingsList.Item>