···158158159159/**
160160 * These "common screens" are reused across stacks.
161161+ *
162162+ * Note: Navigation titles use i18n._() which evaluates at setup time, not render time.
163163+ * This means they cannot dynamically respond to terminology preference changes.
164164+ * All in-app content (buttons, dialogs, messages) uses the terminology system,
165165+ * but these navigation bar titles remain static. This is a low-impact limitation
166166+ * as navigation titles are rarely noticed by users.
161167 */
162168function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) {
163169 const title = (page: MessageDescriptor) =>
···88import {makeProfileLink} from '#/lib/routes/links'
99import {type NavigationProp} from '#/lib/routes/types'
1010import {shareText, shareUrl} from '#/lib/sharing'
1111+import {getTerminology, TERMINOLOGY} from '#/lib/strings/terminology'
1112import {toShareUrl, toShareUrlBsky} from '#/lib/strings/url-helpers'
1213import {logger} from '#/logger'
1314import {isWeb} from '#/platform/detection'
1415import {useProfileShadow} from '#/state/cache/profile-shadow'
1616+import {useTerminologyPreference} from '#/state/preferences'
1517import {useShowExternalShareButtons} from '#/state/preferences/external-share-buttons'
1618import {useSession} from '#/state/session'
1719import {useBreakpoints} from '#/alf'
···3739 const {hasSession} = useSession()
3840 const {gtMobile} = useBreakpoints()
3941 const {_} = useLingui()
4242+ const terminologyPreference = useTerminologyPreference()
4043 const navigation = useNavigation<NavigationProp>()
4144 const embedPostControl = useDialogControl()
4245 const sendViaChatControl = useDialogControl()
···109112 <Menu.Group>
110113 <Menu.Item
111114 testID="postDropdownShareBtn"
112112- label={_(msg`Copy link to post`)}
115115+ label={_(getTerminology(terminologyPreference, {
116116+ skeet: msg`Copy link to skeet`,
117117+ post: msg`Copy link to post`,
118118+ spell: msg`Copy link to spell`,
119119+ }))}
113120 onPress={onCopyLink}>
114121 <Menu.ItemText>
115115- <Trans>Copy link to skeet</Trans>
122122+ <Trans>{_(getTerminology(terminologyPreference, {
123123+ skeet: msg`Copy link to skeet`,
124124+ post: msg`Copy link to post`,
125125+ spell: msg`Copy link to spell`,
126126+ }))}</Trans>
116127 </Menu.ItemText>
117128 <Menu.ItemIcon icon={ChainLinkIcon} position="right" />
118129 </Menu.Item>
···136147 {showExternalShareButtons && isBridgedPost && (
137148 <Menu.Item
138149 testID="postDropdownOpenOriginalPost"
139139- label={_(msg`Open original post`)}
150150+ label={_(getTerminology(terminologyPreference, {
151151+ skeet: msg`Open original skeet`,
152152+ post: msg`Open original post`,
153153+ spell: msg`Open original spell`,
154154+ }))}
140155 onPress={onOpenOriginalPost}>
141156 <Menu.ItemText>
142142- <Trans>Open original skeet</Trans>
157157+ <Trans>{_(getTerminology(terminologyPreference, {
158158+ skeet: msg`Open original skeet`,
159159+ post: msg`Open original post`,
160160+ spell: msg`Open original spell`,
161161+ }))}</Trans>
143162 </Menu.ItemText>
144163 <Menu.ItemIcon icon={ExternalIcon} position="right" />
145164 </Menu.Item>
···148167 {showExternalShareButtons && (
149168 <Menu.Item
150169 testID="postDropdownOpenInPdsls"
151151- label={_(msg`Open post in PDSls`)}
170170+ label={_(getTerminology(terminologyPreference, {
171171+ skeet: msg`Open skeet in PDSls`,
172172+ post: msg`Open post in PDSls`,
173173+ spell: msg`Open spell in PDSls`,
174174+ }))}
152175 onPress={onOpenPostInPdsls}>
153176 <Menu.ItemText>
154154- <Trans>Open skeet in PDSls</Trans>
177177+ <Trans>{_(getTerminology(terminologyPreference, {
178178+ skeet: msg`Open skeet in PDSls`,
179179+ post: msg`Open post in PDSls`,
180180+ spell: msg`Open spell in PDSls`,
181181+ }))}</Trans>
155182 </Menu.ItemText>
156183 <Menu.ItemIcon icon={ExternalIcon} position="right" />
157184 </Menu.Item>
···175202 {canEmbed && (
176203 <Menu.Item
177204 testID="postDropdownEmbedBtn"
178178- label={_(msg`Embed post`)}
205205+ label={_(getTerminology(terminologyPreference, {
206206+ skeet: msg`Embed skeet`,
207207+ post: msg`Embed post`,
208208+ spell: msg`Embed spell`,
209209+ }))}
179210 onPress={() => {
180211 logger.metric('share:press:embed', {}, {statsig: true})
181212 embedPostControl.open()
182213 }}>
183183- <Menu.ItemText>{_(msg`Embed skeet`)}</Menu.ItemText>
214214+ <Menu.ItemText>{_(getTerminology(terminologyPreference, {
215215+ skeet: msg`Embed skeet`,
216216+ post: msg`Embed post`,
217217+ spell: msg`Embed spell`,
218218+ }))}</Menu.ItemText>
184219 <Menu.ItemIcon icon={CodeBracketsIcon} position="right" />
185220 </Menu.Item>
186221 )}
···190225 {hasSession && <Menu.Divider />}
191226 {copyLinkItem}
192227 <Menu.LabelText style={{maxWidth: 220}}>
193193- <Trans>
194194- Note: This skeet is only visible to logged-in users.
195195- </Trans>
228228+ <Trans>{_(getTerminology(terminologyPreference, {
229229+ skeet: msg`Note: This skeet is only visible to logged-in users.`,
230230+ post: msg`Note: This post is only visible to logged-in users.`,
231231+ spell: msg`Note: This spell is only visible to logged-in users.`,
232232+ }))}</Trans>
196233 </Menu.LabelText>
197234 </>
198235 )}
···202239 <Menu.Divider />
203240 <Menu.Item
204241 testID="postAtUriShareBtn"
205205- label={_(msg`Copy post at:// URI`)}
242242+ label={_(getTerminology(terminologyPreference, {
243243+ skeet: msg`Copy skeet at:// URI`,
244244+ post: msg`Copy post at:// URI`,
245245+ spell: msg`Copy spell at:// URI`,
246246+ }))}
206247 onPress={onShareATURI}>
207248 <Menu.ItemText>
208208- <Trans>Copy skeet at:// URI</Trans>
249249+ <Trans>{_(getTerminology(terminologyPreference, {
250250+ skeet: msg`Copy skeet at:// URI`,
251251+ post: msg`Copy post at:// URI`,
252252+ spell: msg`Copy spell at:// URI`,
253253+ }))}</Trans>
209254 </Menu.ItemText>
210255 <Menu.ItemIcon icon={ClipboardIcon} position="right" />
211256 </Menu.Item>
+30-8
src/components/WhoCanReply.tsx
···17171818import {HITSLOP_10} from '#/lib/constants'
1919import {makeListLink, makeProfileLink} from '#/lib/routes/links'
2020+import {getTerminology} from '#/lib/strings/terminology'
2121+import {useTerminologyPreference} from '#/state/preferences'
2022import {logger} from '#/logger'
2123import {isNative} from '#/platform/detection'
2224import {
···212214 embeddingDisabled: boolean
213215}) {
214216 const {_} = useLingui()
217217+ const terminologyPreference = useTerminologyPreference()
215218216219 return (
217220 <Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
···221224 style={web({maxWidth: 400})}>
222225 <View style={[a.gap_sm]}>
223226 <Text style={[a.font_semi_bold, a.text_xl, a.pb_sm]}>
224224- <Trans>Who can interact with this skeet?</Trans>
227227+ <Trans>{_(getTerminology(terminologyPreference, {
228228+ skeet: msg`Who can interact with this skeet?`,
229229+ post: msg`Who can interact with this post?`,
230230+ spell: msg`Who can interact with this spell?`,
231231+ }))}</Trans>
225232 </Text>
226233 <Rules
227234 post={post}
···258265 embeddingDisabled: boolean
259266}) {
260267 const t = useTheme()
268268+ const {_} = useLingui()
269269+ const terminologyPreference = useTerminologyPreference()
261270262271 return (
263272 <>
···269278 t.atoms.text_contrast_medium,
270279 ]}>
271280 {settings.length === 0 ? (
272272- <Trans>
273273- This skeet has an unknown type of threadgate on it. Your app may be
274274- out of date.
275275- </Trans>
281281+ <Trans>{_(getTerminology(terminologyPreference, {
282282+ skeet: msg`This skeet has an unknown type of threadgate on it. Your app may be out of date.`,
283283+ post: msg`This post has an unknown type of threadgate on it. Your app may be out of date.`,
284284+ spell: msg`This spell has an unknown type of threadgate on it. Your app may be out of date.`,
285285+ }))}</Trans>
276286 ) : settings[0].type === 'everybody' ? (
277277- <Trans>Everybody can reply to this skeet.</Trans>
287287+ <Trans>{_(getTerminology(terminologyPreference, {
288288+ skeet: msg`Everybody can reply to this skeet.`,
289289+ post: msg`Everybody can reply to this post.`,
290290+ spell: msg`Everybody can reply to this spell.`,
291291+ }))}</Trans>
278292 ) : settings[0].type === 'nobody' ? (
279279- <Trans>Replies to this skeet are disabled.</Trans>
293293+ <Trans>{_(getTerminology(terminologyPreference, {
294294+ skeet: msg`Replies to this skeet are disabled.`,
295295+ post: msg`Replies to this post are disabled.`,
296296+ spell: msg`Replies to this spell are disabled.`,
297297+ }))}</Trans>
280298 ) : (
281299 <Trans>
282300 Only{' '}
···298316 a.flex_wrap,
299317 t.atoms.text_contrast_medium,
300318 ]}>
301301- <Trans>No one but the author can quote this skeet.</Trans>
319319+ <Trans>{_(getTerminology(terminologyPreference, {
320320+ skeet: msg`No one but the author can quote this skeet.`,
321321+ post: msg`No one but the author can quote this post.`,
322322+ spell: msg`No one but the author can quote this spell.`,
323323+ }))}</Trans>
302324 </Text>
303325 )}
304326 </>
+18-6
src/components/dialogs/Embed.tsx
···6677import {EMBED_SCRIPT} from '#/lib/constants'
88import {niceDate} from '#/lib/strings/time'
99+import {getTerminology} from '#/lib/strings/terminology'
1010+import {useTerminologyPreference} from '#/state/preferences'
911import {toShareUrl} from '#/lib/strings/url-helpers'
1012import {atoms as a, useTheme} from '#/alf'
1113import {Button, ButtonIcon, ButtonText} from '#/components/Button'
···5153}: Omit<EmbedDialogProps, 'control'>) {
5254 const t = useTheme()
5355 const {_, i18n} = useLingui()
5656+ const terminologyPreference = useTerminologyPreference()
5457 const [copied, setCopied] = useState(false)
5558 const [showCustomisation, setShowCustomisation] = useState(false)
5659 const [colorMode, setColorMode] = useState<ColorModeValues>('system')
···101104 }, [i18n, postUri, postCid, record, timestamp, postAuthor, colorMode])
102105103106 return (
104104- <Dialog.Inner label={_(msg`Embed post`)} style={[{maxWidth: 500}]}>
107107+ <Dialog.Inner label={_(getTerminology(terminologyPreference, {
108108+ skeet: msg`Embed skeet`,
109109+ post: msg`Embed post`,
110110+ spell: msg`Embed spell`,
111111+ }))} style={[{maxWidth: 500}]}>
105112 <View style={[a.gap_lg]}>
106113 <View style={[a.gap_sm]}>
107114 <Text style={[a.text_2xl, a.font_bold]}>
108108- <Trans>Embed skeet</Trans>
115115+ <Trans>{_(getTerminology(terminologyPreference, {
116116+ skeet: msg`Embed skeet`,
117117+ post: msg`Embed post`,
118118+ spell: msg`Embed spell`,
119119+ }))}</Trans>
109120 </Text>
110121 <Text
111122 style={[a.text_md, t.atoms.text_contrast_medium, a.leading_normal]}>
112112- <Trans>
113113- Embed this skeet in your website. Simply copy the following snippet
114114- and paste it into the HTML code of your website.
115115- </Trans>
123123+ <Trans>{_(getTerminology(terminologyPreference, {
124124+ skeet: msg`Embed this skeet in your website. Simply copy the following snippet and paste it into the HTML code of your website.`,
125125+ post: msg`Embed this post in your website. Simply copy the following snippet and paste it into the HTML code of your website.`,
126126+ spell: msg`Embed this spell in your website. Simply copy the following snippet and paste it into the HTML code of your website.`,
127127+ }))}</Trans>
116128 </Text>
117129 </View>
118130 <View
+3-1
src/lib/api/index.ts
···24242525import {isNetworkError} from '#/lib/strings/errors'
2626import {parseMarkdownLinks,shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip'
2727+import {getTerminology, TERMINOLOGY} from '#/lib/strings/terminology'
2728import {logger} from '#/logger'
2829import {compressImage} from '#/state/gallery'
2930import {
···4950 replyTo?: string
5051 onStateChange?: (state: string) => void
5152 langs?: string[]
5353+ terminologyPreference?: string
5254}
53555456export async function post(
···183185 })
184186 if (isNetworkError(e)) {
185187 throw new Error(
186186- t`Skeet failed to upload. Please check your Internet connection and try again.`,
188188+ getTerminology(opts.terminologyPreference || 'skeet', TERMINOLOGY.uploadFailed),
187189 )
188190 } else {
189191 throw e
+62
src/lib/hooks/useTerminology.ts
···11+import {useMemo} from 'react'
22+import {defineMessage} from '@lingui/macro'
33+44+import {useTerminologyPreference} from '#/state/preferences'
55+66+export type TerminologyKey =
77+ | 'post'
88+ | 'posts'
99+ | 'repost'
1010+ | 'reposts'
1111+ | 'reposted'
1212+ | 'reposting'
1313+1414+const terminologyMap = {
1515+ skeet: {
1616+ post: defineMessage({message: 'skeet'}),
1717+ posts: defineMessage({message: 'skeets'}),
1818+ repost: defineMessage({message: 'reskeet'}),
1919+ reposts: defineMessage({message: 'reskeets'}),
2020+ reposted: defineMessage({message: 'reskeeted'}),
2121+ reposting: defineMessage({message: 'reskeeting'}),
2222+ },
2323+ post: {
2424+ post: defineMessage({message: 'post'}),
2525+ posts: defineMessage({message: 'posts'}),
2626+ repost: defineMessage({message: 'repost'}),
2727+ reposts: defineMessage({message: 'reposts'}),
2828+ reposted: defineMessage({message: 'reposted'}),
2929+ reposting: defineMessage({message: 'reposting'}),
3030+ },
3131+ spell: {
3232+ post: defineMessage({message: 'spell'}),
3333+ posts: defineMessage({message: 'spells'}),
3434+ repost: defineMessage({message: 'respell'}),
3535+ reposts: defineMessage({message: 'respells'}),
3636+ reposted: defineMessage({message: 'respelled'}),
3737+ reposting: defineMessage({message: 'respelling'}),
3838+ },
3939+}
4040+4141+/**
4242+ * Hook to get the correct terminology based on user preference
4343+ * @returns An object with all terminology variants
4444+ */
4545+export function useTerminology() {
4646+ const preference = useTerminologyPreference()
4747+4848+ return useMemo(() => {
4949+ const selectedTerminology = preference || 'skeet'
5050+ return terminologyMap[selectedTerminology]
5151+ }, [preference])
5252+}
5353+5454+/**
5555+ * Hook to get a specific terminology term
5656+ * @param key - The terminology key to retrieve
5757+ * @returns The localized terminology message
5858+ */
5959+export function useTerm(key: TerminologyKey) {
6060+ const terminology = useTerminology()
6161+ return terminology[key]
6262+}
+441
src/lib/strings/terminology.ts
···11+import {msg} from '@lingui/macro'
22+33+export type TerminologyPreference = 'skeet' | 'post' | 'spell'
44+55+/**
66+ * Returns the appropriate terminology based on the user's preference
77+ * @param preference - The user's terminology preference ('skeet', 'post', or 'spell')
88+ * @param variants - An object with message descriptors for each terminology option
99+ * @returns The appropriate message descriptor based on the preference
1010+ */
1111+export function getTerminology<T extends {skeet: any; post: any; spell: any}>(
1212+ preference: TerminologyPreference | undefined,
1313+ variants: T,
1414+): T[keyof T] {
1515+ const pref = preference ?? 'skeet'
1616+ return variants[pref]
1717+}
1818+1919+/**
2020+ * Common terminology variants used throughout the app
2121+ */
2222+export const TERMINOLOGY = {
2323+ // Single post
2424+ singular: {
2525+ skeet: msg`skeet`,
2626+ post: msg`post`,
2727+ spell: msg`spell`,
2828+ },
2929+ // Multiple posts
3030+ plural: {
3131+ skeet: msg`skeets`,
3232+ post: msg`posts`,
3333+ spell: msg`spells`,
3434+ },
3535+ // "Skeet by @handle"
3636+ byHandle: (handle: string) => ({
3737+ skeet: msg`Skeet by @${handle}`,
3838+ post: msg`Post by @${handle}`,
3939+ spell: msg`Spell by @${handle}`,
4040+ }),
4141+ // Repost terminology
4242+ repost: {
4343+ singular: {
4444+ skeet: msg`reskeet`,
4545+ post: msg`repost`,
4646+ spell: msg`respell`,
4747+ },
4848+ plural: {
4949+ skeet: msg`reskeets`,
5050+ post: msg`reposts`,
5151+ spell: msg`respells`,
5252+ },
5353+ pastTense: {
5454+ skeet: msg`reskeeted`,
5555+ post: msg`reposted`,
5656+ spell: msg`respelled`,
5757+ },
5858+ byLine: {
5959+ skeet: msg`Reskeeted By`,
6060+ post: msg`Reposted By`,
6161+ spell: msg`Respelled By`,
6262+ },
6363+ action: {
6464+ skeet: msg`reskeet`,
6565+ post: msg`repost`,
6666+ spell: msg`respell`,
6767+ },
6868+ },
6969+ // For metrics/counts
7070+ metrics: {
7171+ skeet: msg`skeets metrics`,
7272+ post: msg`posts metrics`,
7373+ spell: msg`spells metrics`,
7474+ },
7575+ // For carousel
7676+ carousel: {
7777+ skeet: msg`Combine reskeets into a horizontal carousel`,
7878+ post: msg`Combine reposts into a horizontal carousel`,
7979+ spell: msg`Combine respells into a horizontal carousel`,
8080+ },
8181+ // For notifications
8282+ viaRepostNotification: {
8383+ skeet: msg`Disable "via reskeet" notifications`,
8484+ post: msg`Disable "via repost" notifications`,
8585+ spell: msg`Disable "via respell" notifications`,
8686+ },
8787+ viaRepostPrivacy: {
8888+ skeet: msg`Forcefully disables the notifications other people receive when you like/reskeet a skeet someone else has reskeeted for privacy.`,
8989+ post: msg`Forcefully disables the notifications other people receive when you like/repost a post someone else has reposted for privacy.`,
9090+ spell: msg`Forcefully disables the notifications other people receive when you like/respell a spell someone else has respelled for privacy.`,
9191+ },
9292+ // For external share buttons
9393+ externalShareButtons: {
9494+ skeet: msg`Show "Open original skeet" and "Open skeet in PDSls" buttons`,
9595+ post: msg`Show "Open original post" and "Open post in PDSls" buttons`,
9696+ spell: msg`Show "Open original spell" and "Open spell in PDSls" buttons`,
9797+ },
9898+ // For metrics labels
9999+ repostMetrics: {
100100+ skeet: msg`Disable reskeets metrics`,
101101+ post: msg`Disable reposts metrics`,
102102+ spell: msg`Disable respells metrics`,
103103+ },
104104+ // For deletion messages
105105+ deleted: {
106106+ skeet: msg`This skeet was deleted by its author`,
107107+ post: msg`This post was deleted by its author`,
108108+ spell: msg`This spell was deleted by its author`,
109109+ },
110110+ deletedShort: {
111111+ skeet: msg`Skeet has been deleted`,
112112+ post: msg`Post has been deleted`,
113113+ spell: msg`Spell has been deleted`,
114114+ },
115115+ // For error messages
116116+ notFound: {
117117+ skeet: msg`Skeet not found`,
118118+ post: msg`Post not found`,
119119+ spell: msg`Spell not found`,
120120+ },
121121+ blocked: {
122122+ skeet: msg`Skeet blocked`,
123123+ post: msg`Post blocked`,
124124+ spell: msg`Spell blocked`,
125125+ },
126126+ errorLoading: {
127127+ skeet: msg`Error loading skeet`,
128128+ post: msg`Error loading post`,
129129+ spell: msg`Error loading spell`,
130130+ },
131131+ // For success messages
132132+ sent: {
133133+ skeet: msg`Your skeet was sent`,
134134+ post: msg`Your post was sent`,
135135+ spell: msg`Your spell was sent`,
136136+ },
137137+ sentPlural: {
138138+ skeet: msg`Your skeets were sent`,
139139+ post: msg`Your posts were sent`,
140140+ spell: msg`Your spells were sent`,
141141+ },
142142+ pinned: {
143143+ skeet: msg`Skeet pinned`,
144144+ post: msg`Post pinned`,
145145+ spell: msg`Spell pinned`,
146146+ },
147147+ unpinned: {
148148+ skeet: msg`Skeet unpinned`,
149149+ post: msg`Post unpinned`,
150150+ spell: msg`Spell unpinned`,
151151+ },
152152+ failedToPin: {
153153+ skeet: msg`Failed to pin skeet`,
154154+ post: msg`Failed to pin post`,
155155+ spell: msg`Failed to pin spell`,
156156+ },
157157+ // For composer
158158+ addAnother: {
159159+ skeet: msg`Add another skeet`,
160160+ post: msg`Add another post`,
161161+ spell: msg`Add another spell`,
162162+ },
163163+ anythingBut: {
164164+ skeet: msg`Anything but skeet`,
165165+ post: msg`Anything but post`,
166166+ spell: msg`Anything but spell`,
167167+ },
168168+ delete: {
169169+ skeet: msg`Delete skeet`,
170170+ post: msg`Delete post`,
171171+ spell: msg`Delete spell`,
172172+ },
173173+ discard: {
174174+ skeet: msg`Discard skeet?`,
175175+ post: msg`Discard post?`,
176176+ spell: msg`Discard spell?`,
177177+ },
178178+ discardConfirm: {
179179+ skeet: msg`Are you sure you'd like to discard this skeet?`,
180180+ post: msg`Are you sure you'd like to discard this post?`,
181181+ spell: msg`Are you sure you'd like to discard this spell?`,
182182+ },
183183+ view: {
184184+ skeet: msg`View skeet`,
185185+ post: msg`View post`,
186186+ spell: msg`View spell`,
187187+ },
188188+ // For composer actions
189189+ composeNew: {
190190+ skeet: msg`Compose new skeet`,
191191+ post: msg`Compose new post`,
192192+ spell: msg`Compose new spell`,
193193+ },
194194+ newAction: {
195195+ skeet: msg`New Skeet`,
196196+ post: msg`New Post`,
197197+ spell: msg`New Spell`,
198198+ },
199199+ postAll: {
200200+ skeet: msg`Skeet All`,
201201+ post: msg`Post All`,
202202+ spell: msg`Spell All`,
203203+ },
204204+ postSingle: {
205205+ skeet: msg`Skeet`,
206206+ post: msg`Post`,
207207+ spell: msg`Spell`,
208208+ },
209209+ // For language selection
210210+ selectLanguage: {
211211+ skeet: msg`Select skeet language`,
212212+ post: msg`Select post language`,
213213+ spell: msg`Select spell language`,
214214+ },
215215+ chooseLanguages: {
216216+ skeet: msg`Choose Skeet Languages`,
217217+ post: msg`Choose Post Languages`,
218218+ spell: msg`Choose Spell Languages`,
219219+ },
220220+ languageDescription: {
221221+ skeet: msg`Select up to 3 languages used in this skeet`,
222222+ post: msg`Select up to 3 languages used in this post`,
223223+ spell: msg`Select up to 3 languages used in this spell`,
224224+ },
225225+ replyingLanguage: (langs: string) => ({
226226+ skeet: msg`The skeet you're replying to was marked as being written in ${langs}`,
227227+ post: msg`The post you're replying to was marked as being written in ${langs}`,
228228+ spell: msg`The spell you're replying to was marked as being written in ${langs}`,
229229+ }),
230230+ // For interaction settings
231231+ interactionSettings: {
232232+ skeet: msg`Skeet interaction settings`,
233233+ post: msg`Post interaction settings`,
234234+ spell: msg`Spell interaction settings`,
235235+ },
236236+ editInteractionSettings: {
237237+ skeet: msg`Edit skeet interaction settings`,
238238+ post: msg`Edit post interaction settings`,
239239+ spell: msg`Edit spell interaction settings`,
240240+ },
241241+ whoCanInteract: {
242242+ skeet: msg`Who can interact with this skeet?`,
243243+ post: msg`Who can interact with this post?`,
244244+ spell: msg`Who can interact with this spell?`,
245245+ },
246246+ // For embedding
247247+ embed: {
248248+ skeet: msg`Embed skeet`,
249249+ post: msg`Embed post`,
250250+ spell: msg`Embed spell`,
251251+ },
252252+ embedDescription: {
253253+ skeet: msg`Embed this skeet in your website. Simply copy the following snippet and paste it into the HTML code of your website.`,
254254+ post: msg`Embed this post in your website. Simply copy the following snippet and paste it into the HTML code of your website.`,
255255+ spell: msg`Embed this spell in your website. Simply copy the following snippet and paste it into the HTML code of your website.`,
256256+ },
257257+ // For sharing
258258+ copyLink: {
259259+ skeet: msg`Copy link to skeet`,
260260+ post: msg`Copy link to post`,
261261+ spell: msg`Copy link to spell`,
262262+ },
263263+ shareUri: {
264264+ skeet: msg`Share skeet at:// URI`,
265265+ post: msg`Share post at:// URI`,
266266+ spell: msg`Share spell at:// URI`,
267267+ },
268268+ copyUri: {
269269+ skeet: msg`Copy skeet at:// URI`,
270270+ post: msg`Copy post at:// URI`,
271271+ spell: msg`Copy spell at:// URI`,
272272+ },
273273+ sendTo: {
274274+ skeet: msg`Send skeet to...`,
275275+ post: msg`Send post to...`,
276276+ spell: msg`Send spell to...`,
277277+ },
278278+ openOriginal: {
279279+ skeet: msg`Open original skeet`,
280280+ post: msg`Open original post`,
281281+ spell: msg`Open original spell`,
282282+ },
283283+ openInPDSls: {
284284+ skeet: msg`Open skeet in PDSls`,
285285+ post: msg`Open post in PDSls`,
286286+ spell: msg`Open spell in PDSls`,
287287+ },
288288+ // For reporting
289289+ report: {
290290+ skeet: msg`Report this skeet`,
291291+ post: msg`Report this post`,
292292+ spell: msg`Report this spell`,
293293+ },
294294+ reportWhy: {
295295+ skeet: msg`Why should this skeet be reviewed?`,
296296+ post: msg`Why should this post be reviewed?`,
297297+ spell: msg`Why should this spell be reviewed?`,
298298+ },
299299+ // For search and browse
300300+ search: {
301301+ skeet: msg`Search skeets`,
302302+ post: msg`Search posts`,
303303+ spell: msg`Search spells`,
304304+ },
305305+ searchProfile: (handle: string) => ({
306306+ skeet: msg`Search @${handle}'s skeets`,
307307+ post: msg`Search @${handle}'s posts`,
308308+ spell: msg`Search @${handle}'s spells`,
309309+ }),
310310+ searchMy: {
311311+ skeet: msg`Search my skeets`,
312312+ post: msg`Search my posts`,
313313+ spell: msg`Search my spells`,
314314+ },
315315+ browse: (tag: string) => ({
316316+ skeet: msg`Browse skeets about ${tag}`,
317317+ post: msg`Browse posts about ${tag}`,
318318+ spell: msg`Browse spells about ${tag}`,
319319+ }),
320320+ browseTagged: (tag: string) => ({
321321+ skeet: msg`Browse skeets tagged with ${tag}`,
322322+ post: msg`Browse posts tagged with ${tag}`,
323323+ spell: msg`Browse spells tagged with ${tag}`,
324324+ }),
325325+ seeTag: (tag: string) => ({
326326+ skeet: msg`See ${tag} skeets`,
327327+ post: msg`See ${tag} posts`,
328328+ spell: msg`See ${tag} spells`,
329329+ }),
330330+ seeTagByUser: (tag: string) => ({
331331+ skeet: msg`See ${tag} skeets by user`,
332332+ post: msg`See ${tag} posts by user`,
333333+ spell: msg`See ${tag} spells by user`,
334334+ }),
335335+ // For loading and errors
336336+ loadNew: {
337337+ skeet: msg`Load new skeets`,
338338+ post: msg`Load new posts`,
339339+ spell: msg`Load new spells`,
340340+ },
341341+ fetchError: {
342342+ skeet: msg`There was an issue fetching skeets. Tap here to try again.`,
343343+ post: msg`There was an issue fetching posts. Tap here to try again.`,
344344+ spell: msg`There was an issue fetching spells. Tap here to try again.`,
345345+ },
346346+ uploadFailed: {
347347+ skeet: msg`Skeet failed to upload. Please check your Internet connection and try again.`,
348348+ post: msg`Post failed to upload. Please check your Internet connection and try again.`,
349349+ spell: msg`Spell failed to upload. Please check your Internet connection and try again.`,
350350+ },
351351+ // For feeds and filtering
352352+ ranOut: {
353353+ skeet: msg`We ran out of skeets from your follows. Here's the latest from`,
354354+ post: msg`We ran out of posts from your follows. Here's the latest from`,
355355+ spell: msg`We ran out of spells from your follows. Here's the latest from`,
356356+ },
357357+ // For quote posts
358358+ quoteAction: {
359359+ skeet: msg`Quote skeet`,
360360+ post: msg`Quote post`,
361361+ spell: msg`Quote spell`,
362362+ },
363363+ quoteDisabled: {
364364+ skeet: msg`Quote skeets disabled`,
365365+ post: msg`Quote posts disabled`,
366366+ spell: msg`Quote spells disabled`,
367367+ },
368368+ allowQuote: {
369369+ skeet: msg`Allow quote skeets`,
370370+ post: msg`Allow quote posts`,
371371+ spell: msg`Allow quote spells`,
372372+ },
373373+ quoteAuthorDisabled: {
374374+ skeet: msg`This skeet's author has disabled quote skeets.`,
375375+ post: msg`This post's author has disabled quote posts.`,
376376+ spell: msg`This spell's author has disabled quote spells.`,
377377+ },
378378+ cancelQuote: {
379379+ skeet: msg`Cancel quote skeet`,
380380+ post: msg`Cancel quote post`,
381381+ spell: msg`Cancel quote spell`,
382382+ },
383383+ noQuoteYet: {
384384+ skeet: msg`No one but the author can quote this skeet.`,
385385+ post: msg`No one but the author can quote this post.`,
386386+ spell: msg`No one but the author can quote this spell.`,
387387+ },
388388+ // For reply settings
389389+ repliesDisabled: {
390390+ skeet: msg`Replies to this skeet are disabled.`,
391391+ post: msg`Replies to this post are disabled.`,
392392+ spell: msg`Replies to this spell are disabled.`,
393393+ },
394394+ everyoneCanReply: {
395395+ skeet: msg`Everybody can reply to this skeet.`,
396396+ post: msg`Everybody can reply to this post.`,
397397+ spell: msg`Everybody can reply to this spell.`,
398398+ },
399399+ unknownThreadgate: {
400400+ skeet: msg`This skeet has an unknown type of threadgate on it. Your app may be out of date.`,
401401+ post: msg`This post has an unknown type of threadgate on it. Your app may be out of date.`,
402402+ spell: msg`This spell has an unknown type of threadgate on it. Your app may be out of date.`,
403403+ },
404404+ // For replies
405405+ repliedTo: {
406406+ skeet: msg`Replied to a skeet`,
407407+ post: msg`Replied to a post`,
408408+ spell: msg`Replied to a spell`,
409409+ },
410410+ repliedToBlocked: {
411411+ skeet: msg`Replied to a blocked skeet`,
412412+ post: msg`Replied to a blocked post`,
413413+ spell: msg`Replied to a blocked spell`,
414414+ },
415415+ replyWasDeleted: {
416416+ skeet: msg`We're sorry! The skeet you are replying to has been deleted.`,
417417+ post: msg`We're sorry! The post you are replying to has been deleted.`,
418418+ spell: msg`We're sorry! The spell you are replying to has been deleted.`,
419419+ },
420420+ sortReplies: {
421421+ skeet: msg`Sort replies to the same skeet by:`,
422422+ post: msg`Sort replies to the same post by:`,
423423+ spell: msg`Sort replies to the same spell by:`,
424424+ },
425425+ showRepliesTree: {
426426+ skeet: msg`Show skeet replies in a threaded tree view`,
427427+ post: msg`Show post replies in a threaded tree view`,
428428+ spell: msg`Show spell replies in a threaded tree view`,
429429+ },
430430+ // For visibility
431431+ onlyLoggedIn: {
432432+ skeet: msg`This skeet is only visible to logged-in users.`,
433433+ post: msg`This post is only visible to logged-in users.`,
434434+ spell: msg`This spell is only visible to logged-in users.`,
435435+ },
436436+ noteOnlyLoggedIn: {
437437+ skeet: msg`Note: This skeet is only visible to logged-in users.`,
438438+ post: msg`Note: This post is only visible to logged-in users.`,
439439+ spell: msg`Note: This spell is only visible to logged-in users.`,
440440+ },
441441+}
+25-4
src/screens/Bookmarks/index.tsx
···1616import {useCleanError} from '#/lib/hooks/useCleanError'
1717import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
1818import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking'
1919+import {getTerminology} from '#/lib/strings/terminology'
1920import {
2021 type CommonNavigatorParams,
2122 type NativeStackScreenProps,
2223} from '#/lib/routes/types'
2324import {logger} from '#/logger'
2425import {isIOS} from '#/platform/detection'
2626+import {useTerminologyPreference} from '#/state/preferences'
2527import {useBookmarkMutation} from '#/state/queries/bookmarks/useBookmarkMutation'
2628import {useBookmarksQuery} from '#/state/queries/bookmarks/useBookmarksQuery'
2729import {useSetMinimalShellMode} from '#/state/shell'
···4244type Props = NativeStackScreenProps<CommonNavigatorParams, 'Bookmarks'>
43454446export function BookmarksScreen({}: Props) {
4747+ const {_} = useLingui()
4848+ const terminologyPreference = useTerminologyPreference()
4549 const setMinimalShellMode = useSetMinimalShellMode()
46504751 useFocusEffect(
···5761 <Layout.Header.BackButton />
5862 <Layout.Header.Content>
5963 <Layout.Header.TitleText>
6060- <Trans>Saved Skeets</Trans>
6464+ <Trans>{_(getTerminology(terminologyPreference, {
6565+ skeet: msg`Saved Skeets`,
6666+ post: msg`Saved Posts`,
6767+ spell: msg`Saved Spells`,
6868+ }))}</Trans>
6169 </Layout.Header.TitleText>
6270 </Layout.Header.Content>
6371 <Layout.Header.Slot />
···209217}) {
210218 const t = useTheme()
211219 const {_} = useLingui()
220220+ const terminologyPreference = useTerminologyPreference()
212221 const {mutateAsync: bookmark} = useBookmarkMutation()
213222 const cleanError = useCleanError()
214223215224 const remove = async () => {
216225 try {
217226 await bookmark({action: 'delete', uri: post.uri})
218218- toast.show(_(msg`Removed from saved skeets`), {
227227+ toast.show(_(getTerminology(terminologyPreference, {
228228+ skeet: msg`Removed from saved skeets`,
229229+ post: msg`Removed from saved posts`,
230230+ spell: msg`Removed from saved spells`,
231231+ })), {
219232 type: 'info',
220233 })
221234 } catch (e: any) {
···253266 a.italic,
254267 t.atoms.text_contrast_medium,
255268 ]}>
256256- <Trans>This skeet was deleted by its author</Trans>
269269+ <Trans>{_(getTerminology(terminologyPreference, {
270270+ skeet: msg`This skeet was deleted by its author`,
271271+ post: msg`This post was deleted by its author`,
272272+ spell: msg`This spell was deleted by its author`,
273273+ }))}</Trans>
257274 </Text>
258275 </View>
259276 <Button
260260- label={_(msg`Remove from saved skeets`)}
277277+ label={_(getTerminology(terminologyPreference, {
278278+ skeet: msg`Remove from saved skeets`,
279279+ post: msg`Remove from saved posts`,
280280+ spell: msg`Remove from saved spells`,
281281+ }))}
261282 size="tiny"
262283 color="secondary"
263284 onPress={remove}>
···11import {View} from 'react-native'
22-import {Trans} from '@lingui/macro'
22+import {msg, Trans} from '@lingui/macro'
33+import {useLingui} from '@lingui/react'
3445import {
56 type AllNavigatorParams,
67 type NativeStackScreenProps,
78} from '#/lib/routes/types'
99+import {getTerminology} from '#/lib/strings/terminology'
1010+import {useTerminologyPreference} from '#/state/preferences'
811import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings'
912import {atoms as a} from '#/alf'
1013import {Admonition} from '#/components/Admonition'
···1922 'LikesOnRepostsNotificationSettings'
2023>
2124export function LikesOnRepostsNotificationSettingsScreen({}: Props) {
2525+ const {_} = useLingui()
2626+ const terminologyPreference = useTerminologyPreference()
2227 const {data: preferences, isError} = useNotificationSettingsQuery()
23282429 return (
···3843 <SettingsList.ItemIcon icon={LikeRepostIcon} />
3944 <ItemTextWithSubtitle
4045 bold
4141- titleText={<Trans>Likes of your reskeets</Trans>}
4646+ titleText={<Trans>{_(getTerminology(terminologyPreference, {
4747+ skeet: msg`Likes of your reskeets`,
4848+ post: msg`Likes of your reposts`,
4949+ spell: msg`Likes of your respells`,
5050+ }))}</Trans>}
4251 subtitleText={
4343- <Trans>
4444- Get notifications when people like skeets that you've reskeeted.
4545- </Trans>
5252+ <Trans>{_(getTerminology(terminologyPreference, {
5353+ skeet: msg`Get notifications when people like skeets that you've reskeeted.`,
5454+ post: msg`Get notifications when people like posts that you've reposted.`,
5555+ spell: msg`Get notifications when people like spells that you've respelled.`,
5656+ }))}</Trans>
4657 }
4758 />
4859 </SettingsList.Item>
···11import {View} from 'react-native'
22-import {Trans} from '@lingui/macro'
22+import {msg, Trans} from '@lingui/macro'
33+import {useLingui} from '@lingui/react'
3445import {
56 type AllNavigatorParams,
67 type NativeStackScreenProps,
78} from '#/lib/routes/types'
99+import {getTerminology} from '#/lib/strings/terminology'
1010+import {useTerminologyPreference} from '#/state/preferences'
811import {useNotificationSettingsQuery} from '#/state/queries/notifications/settings'
912import {atoms as a} from '#/alf'
1013import {Admonition} from '#/components/Admonition'
···1922 'RepostsOnRepostsNotificationSettings'
2023>
2124export function RepostsOnRepostsNotificationSettingsScreen({}: Props) {
2525+ const {_} = useLingui()
2626+ const terminologyPreference = useTerminologyPreference()
2227 const {data: preferences, isError} = useNotificationSettingsQuery()
23282429 return (
···3843 <SettingsList.ItemIcon icon={RepostRepostIcon} />
3944 <ItemTextWithSubtitle
4045 bold
4141- titleText={<Trans>Reskeets of your reskeets</Trans>}
4646+ titleText={<Trans>{_(getTerminology(terminologyPreference, {
4747+ skeet: msg`Reskeets of your reskeets`,
4848+ post: msg`Reposts of your reposts`,
4949+ spell: msg`Respells of your respells`,
5050+ }))}</Trans>}
4251 subtitleText={
4343- <Trans>
4444- Get notifications when people reskeet skeets that you've
4545- reskeeted.
4646- </Trans>
5252+ <Trans>{_(getTerminology(terminologyPreference, {
5353+ skeet: msg`Get notifications when people reskeet skeets that you've reskeeted.`,
5454+ post: msg`Get notifications when people repost posts that you've reposted.`,
5555+ spell: msg`Get notifications when people respell spells that you've respelled.`,
5656+ }))}</Trans>
4757 }
4858 />
4959 </SettingsList.Item>
+6-3
src/state/queries/pinned-post.ts
···33import {useMutation, useQueryClient} from '@tanstack/react-query'
4455import {logger} from '#/logger'
66+import {getTerminology, TERMINOLOGY} from '#/lib/strings/terminology'
77+import {useTerminologyPreference} from '#/state/preferences'
68import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
79import * as Toast from '#/view/com/util/Toast'
810import {updatePostShadow} from '../cache/post-shadow'
···11131214export function usePinnedPostMutation() {
1315 const {_} = useLingui()
1616+ const terminologyPreference = useTerminologyPreference()
1417 const {currentAccount} = useSession()
1518 const agent = useAgent()
1619 const queryClient = useQueryClient()
···5659 })
57605861 if (pinCurrentPost) {
5959- Toast.show(_(msg({message: 'Skeet pinned', context: 'toast'})))
6262+ Toast.show(_(getTerminology(terminologyPreference, TERMINOLOGY.pinned)))
6063 } else {
6161- Toast.show(_(msg({message: 'Skeet unpinned', context: 'toast'})))
6464+ Toast.show(_(getTerminology(terminologyPreference, TERMINOLOGY.unpinned)))
6265 }
63666467 queryClient.invalidateQueries({
···7275 ),
7376 })
7477 } catch (e: any) {
7575- Toast.show(_(msg`Failed to pin skeet`))
7878+ Toast.show(_(getTerminology(terminologyPreference, TERMINOLOGY.failedToPin)))
7679 logger.error('Failed to pin post', {message: String(e)})
7780 // revert optimistic update
7881 updatePostShadow(queryClient, postUri, {
···77import {languageName} from '#/locale/helpers'
88import {type Language, LANGUAGES, LANGUAGES_MAP_CODE2} from '#/locale/languages'
99import {isNative, isWeb} from '#/platform/detection'
1010+import {getTerminology, TERMINOLOGY} from '#/lib/strings/terminology'
1111+import {useTerminologyPreference} from '#/state/preferences'
1012import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
1113import {
1214 toPostLanguages,
···9395 const setLangPrefs = useLanguagePrefsApi()
9496 const t = useTheme()
9597 const {_} = useLingui()
9898+ const terminologyPreference = useTerminologyPreference()
969997100 const enableSquareButtons = useEnableSquareButtons()
98101···184187 a.text_xl,
185188 a.mb_sm,
186189 ]}>
187187- <Trans>Choose Skeet Languages</Trans>
190190+ <Trans>{_(getTerminology(terminologyPreference, TERMINOLOGY.chooseLanguages))}</Trans>
188191 </Text>
189192 <Text
190193 nativeID="dialog-description"
···194197 a.text_md,
195198 a.mb_lg,
196199 ]}>
197197- <Trans>Select up to 3 languages used in this skeet</Trans>
200200+ <Trans>{_(getTerminology(terminologyPreference, TERMINOLOGY.languageDescription))}</Trans>
198201 </Text>
199202 </View>
200203
+13-4
src/view/com/post-thread/PostRepostedBy.tsx
···5566import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
77import {cleanError} from '#/lib/strings/errors'
88+import {getTerminology, TERMINOLOGY} from '#/lib/strings/terminology'
99+import {useTerminologyPreference} from '#/state/preferences'
810import {logger} from '#/logger'
911import {usePostRepostedByQuery} from '#/state/queries/post-reposted-by'
1012import {useResolveUriQuery} from '#/state/queries/resolve-uri'
···34363537export function PostRepostedBy({uri}: {uri: string}) {
3638 const {_} = useLingui()
3939+ const terminologyPreference = useTerminologyPreference()
3740 const initialNumToRender = useInitialNumToRender()
38413942 const [isPTRing, setIsPTRing] = useState(false)
···8790 isLoading={isLoadingUri || isLoadingRepostedBy}
8891 isError={isError}
8992 emptyType="results"
9090- emptyTitle={_(msg`No reskeets yet`)}
9191- emptyMessage={_(
9292- msg`Nobody has reskeeted this yet. Maybe you should be the first!`,
9393- )}
9393+ emptyTitle={_(getTerminology(terminologyPreference, {
9494+ skeet: msg`No reskeets yet`,
9595+ post: msg`No reposts yet`,
9696+ spell: msg`No respells yet`,
9797+ }))}
9898+ emptyMessage={_(getTerminology(terminologyPreference, {
9999+ skeet: msg`Nobody has reskeeted this yet. Maybe you should be the first!`,
100100+ post: msg`Nobody has reposted this yet. Maybe you should be the first!`,
101101+ spell: msg`Nobody has respelled this yet. Maybe you should be the first!`,
102102+ }))}
94103 errorMessage={cleanError(resolveError || error)}
95104 sideBorders={false}
96105 />
+8-3
src/view/com/posts/PostFeedReason.tsx
···66import {isReasonFeedSource, type ReasonFeedSource} from '#/lib/api/feed/types'
77import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
88import {makeProfileLink} from '#/lib/routes/links'
99+import {getTerminology, TERMINOLOGY} from '#/lib/strings/terminology'
1010+import {useTerminologyPreference} from '#/state/preferences'
911import {useSession} from '#/state/session'
1012import {atoms as a, useTheme} from '#/alf'
1113import {Pin_Stroke2_Corner0_Rounded as PinIcon} from '#/components/icons/Pin'
···3032}) {
3133 const t = useTheme()
3234 const {_} = useLingui()
3535+ const terminologyPreference = useTerminologyPreference()
33363437 const {currentAccount} = useSession()
3538···7477 style={styles.includeReason}
7578 to={makeProfileLink(reason.by)}
7679 label={
7777- isOwner ? _(msg`Reskeeted by you`) : _(msg`Reskeeted by ${reposter}`)
8080+ isOwner
8181+ ? _(getTerminology(terminologyPreference, TERMINOLOGY.repost.byLine))
8282+ : `${_(getTerminology(terminologyPreference, TERMINOLOGY.repost.byLine))} ${reposter}`
7883 }
7984 onPress={onOpenReposter}>
8085 <RepostIcon
···9196 ]}
9297 numberOfLines={1}>
9398 {isOwner ? (
9494- <Trans>Reskeeted by you</Trans>
9999+ <Trans>{_(getTerminology(terminologyPreference, TERMINOLOGY.repost.byLine))}</Trans>
95100 ) : (
9696- <Trans>Reskeeted by {reposter}</Trans>
101101+ <Trans>{_(getTerminology(terminologyPreference, TERMINOLOGY.repost.byLine))} {reposter}</Trans>
97102 )}
98103 </Text>
99104 </ProfileHoverCard>