diff --git a/bskylink/src/html/linkRedirectContents.ts b/bskylink/src/html/linkRedirectContents.ts
index f1bcdbb91..bc87aef9d 100644
--- a/bskylink/src/html/linkRedirectContents.ts
+++ b/bskylink/src/html/linkRedirectContents.ts
@@ -1,10 +1,20 @@
import escapeHTML from 'escape-html'
export function linkRedirectContents(link: string): string {
+ // Encode characters that could break out of the single-quoted URL in meta refresh.
+ // HTML entity escaping (') is insufficient because the browser decodes entities
+ // before the meta refresh parser processes the URL, allowing apostrophes to
+ // prematurely terminate the URL string.
+ //
+ // Example: "They're" with HTML escaping becomes "They're" in HTML, but after
+ // the browser decodes the content attribute, the meta refresh parser sees "They're"
+ // and interprets the apostrophe as the closing quote, truncating the URL to "They".
+ const safeLink = link.replace(/'/g, '%27')
+
return `
-
+
--
2.51.2
From 5e8ef6aa9b57baa6d83b869a037a4520158ec1d5 Mon Sep 17 00:00:00 2001
From: Samuel Newman
Date: Tue, 17 Mar 2026 09:05:58 -0700
Subject: [PATCH 02/43] Adjust VTT cue line to avoid occlusion by video
controls (#10017)
Co-authored-by: Claude Opus 4.6
---
.../VideoEmbedInner/VideoEmbedInnerWeb.tsx | 51 ++++++++++++++++++-
.../web-controls/VideoControls.tsx | 9 ++++
2 files changed, 58 insertions(+), 2 deletions(-)
diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx
index de3c130c2..849241fd7 100644
--- a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx
+++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb.tsx
@@ -1,4 +1,4 @@
-import {useEffect, useId, useRef, useState} from 'react'
+import {useCallback, useEffect, useId, useRef, useState} from 'react'
import {View} from 'react-native'
import {type AppBskyEmbedVideo} from '@atproto/api'
import {msg} from '@lingui/core/macro'
@@ -37,7 +37,7 @@ export function VideoEmbedInnerWeb({
throw error
}
- const {hlsRef, loop} = useHLS({
+ const {hlsRef, loop, updateCuePositions} = useHLS({
playlist: embed.playlist,
setHasSubtitleTrack,
setError,
@@ -90,6 +90,7 @@ export function VideoEmbedInnerWeb({
hasSubtitleTrack={hasSubtitleTrack}
isGif={embed.presentation === 'gif'}
altText={embed.alt}
+ updateCuePositions={updateCuePositions}
/>
@@ -145,6 +146,47 @@ function useHLS({
}, [Hls, setHlsLoading])
const hlsRef = useRef(undefined)
+ const controlsVisibleRef = useRef(false)
+
+ /**
+ * Repositions VTT subtitle cues using percentage-based line values
+ * (snapToLines=false) so that multi-line/wrapped cues grow upward
+ * instead of extending offscreen. Moves cues higher when controls
+ * are visible to avoid occlusion by the scrub bar.
+ *
+ * Called from two sites:
+ * - SUBTITLE_FRAG_PROCESSED: applies positioning to newly loaded cues
+ * - VideoControls effect: updates positioning when controls show/hide
+ */
+ const updateCuePositions = useCallback(
+ (controlsVisible?: boolean) => {
+ if (controlsVisible != null) {
+ // save controlsVisible state so that when it's called from SUBTITLE_FRAG_PROCESSED,
+ // the most recent value is used (as we won't know the control state there)
+ controlsVisibleRef.current = controlsVisible
+ }
+ // magic numbers: cue position, % from top of video
+ const line = controlsVisibleRef.current ? 70 : 85
+ const video = videoRef.current
+ if (!video) return
+ for (let i = 0; i < video.textTracks.length; i++) {
+ const track = video.textTracks[i]
+ if (track.cues) {
+ for (let j = 0; j < track.cues.length; j++) {
+ const cue = track.cues[j] as VTTCue
+ cue.snapToLines = false
+ cue.line = line
+ }
+ }
+ // toggle track mode to force the browser to re-render active cues
+ if (track.mode === 'showing') {
+ track.mode = 'hidden'
+ track.mode = 'showing'
+ }
+ }
+ },
+ [videoRef],
+ )
const [lowQualityFragments, setLowQualityFragments] = useState<
HlsTypes.Fragment[]
>([])
@@ -220,6 +262,10 @@ function useHLS({
}
})
+ hls.on(Hls.Events.SUBTITLE_FRAG_PROCESSED, () => {
+ updateCuePositions()
+ })
+
hls.on(Hls.Events.FRAG_BUFFERED, (_event, {frag}) => {
if (frag.level === 0) {
setLowQualityFragments(prev => [...prev, frag])
@@ -307,5 +353,6 @@ function useHLS({
return {
hlsRef,
loop: !hasLowQualityFragmentAtStart,
+ updateCuePositions,
}
}
diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx
index d6a5628bb..2e5822589 100644
--- a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx
+++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/web-controls/VideoControls.tsx
@@ -48,6 +48,7 @@ export function Controls({
hasSubtitleTrack,
isGif,
altText,
+ updateCuePositions,
}: {
videoRef: React.RefObject
hlsRef: React.RefObject
@@ -61,6 +62,7 @@ export function Controls({
hasSubtitleTrack: boolean
isGif: boolean
altText?: string
+ updateCuePositions: (controlsVisible?: boolean) => void
}) {
const {
play,
@@ -294,6 +296,13 @@ export function Controls({
((focused || autoplayDisabled) && !playing) ||
(interactingViaKeypress ? hasFocus : hovered)
+ // adjust subtitle cue positioning to avoid occlusion by controls
+ // uses percentage-based positioning (snapToLines=false) so wrapped
+ // multi-line cues grow upward instead of extending offscreen
+ useEffect(() => {
+ updateCuePositions(showControls)
+ }, [showControls, updateCuePositions])
+
if (isGif) {
return (
Date: Tue, 17 Mar 2026 17:04:04 +0000
Subject: [PATCH 03/43] Use proper constants.ts exports for Edit Profile Dialog
limits (#10054)
---
src/screens/Profile/Header/EditProfileDialog.tsx | 13 +++++--------
1 file changed, 5 insertions(+), 8 deletions(-)
diff --git a/src/screens/Profile/Header/EditProfileDialog.tsx b/src/screens/Profile/Header/EditProfileDialog.tsx
index 3b948f237..1715328f5 100644
--- a/src/screens/Profile/Header/EditProfileDialog.tsx
+++ b/src/screens/Profile/Header/EditProfileDialog.tsx
@@ -5,7 +5,7 @@ import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Plural, Trans} from '@lingui/react/macro'
-import {urls} from '#/lib/constants'
+import {MAX_DESCRIPTION, MAX_DISPLAY_NAME, urls} from '#/lib/constants'
import {cleanError} from '#/lib/strings/errors'
import {isOverMaxGraphemeCount} from '#/lib/strings/helpers'
import {logger} from '#/logger'
@@ -26,9 +26,6 @@ import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography'
import {useSimpleVerificationState} from '#/components/verification'
-const DISPLAY_NAME_MAX_GRAPHEMES = 64
-const DESCRIPTION_MAX_GRAPHEMES = 256
-
export function EditProfileDialog({
profile,
control,
@@ -205,11 +202,11 @@ function DialogInner({
const displayNameTooLong = isOverMaxGraphemeCount({
text: displayName,
- maxCount: DISPLAY_NAME_MAX_GRAPHEMES,
+ maxCount: MAX_DISPLAY_NAME,
})
const descriptionTooLong = isOverMaxGraphemeCount({
text: description,
- maxCount: DESCRIPTION_MAX_GRAPHEMES,
+ maxCount: MAX_DESCRIPTION,
})
const cancelButton = useCallback(
@@ -330,7 +327,7 @@ function DialogInner({
{color: t.palette.negative_400},
]}>
@@ -381,7 +378,7 @@ function DialogInner({
{color: t.palette.negative_400},
]}>
--
2.51.2
From be0d00de17b1e0b65044fcbb456ec91492b89a68 Mon Sep 17 00:00:00 2001
From: RetroSunstar <57507616+RetroSunstar@users.noreply.github.com>
Date: Tue, 17 Mar 2026 17:04:27 +0000
Subject: [PATCH 04/43] Set displayName and description to undefined if no
values are provided during a profile edit (#10016)
---
src/state/queries/profile.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/state/queries/profile.ts b/src/state/queries/profile.ts
index a42807405..b792fecb4 100644
--- a/src/state/queries/profile.ts
+++ b/src/state/queries/profile.ts
@@ -176,8 +176,8 @@ export function useProfileUpdateMutation() {
if (typeof updates === 'function') {
next = updates(next)
} else {
- next.displayName = updates.displayName
- next.description = updates.description
+ next.displayName = updates.displayName || undefined
+ next.description = updates.description || undefined
if ('pinnedPost' in updates) {
next.pinnedPost = updates.pinnedPost
}
--
2.51.2
From 1386a559b75531769ac525ce464e1e03cd7c00e6 Mon Sep 17 00:00:00 2001
From: DS Boyce <260543580+ds-boyce@users.noreply.github.com>
Date: Tue, 17 Mar 2026 10:31:23 -0700
Subject: [PATCH 05/43] Log languages a post is tagged with after translating
(#10014)
Co-authored-by: Eric Bailey
---
package.json | 2 +-
src/analytics/metrics/types.ts | 7 ++-
src/components/Post/Translated/index.tsx | 4 ++
.../PostControls/PostMenu/PostMenuItems.tsx | 3 ++
src/components/dms/MessageContextMenu.tsx | 3 +-
src/lib/translation/context.ts | 14 +-----
src/lib/translation/index.tsx | 47 ++++++++++---------
src/lib/translation/index.web.tsx | 42 +++++++----------
src/lib/translation/types.ts | 26 ++++++++++
src/locale/helpers.ts | 27 +++++------
yarn.lock | 8 ++--
11 files changed, 102 insertions(+), 81 deletions(-)
diff --git a/package.json b/package.json
index 3066a0421..5a5415bd7 100644
--- a/package.json
+++ b/package.json
@@ -85,7 +85,7 @@
"@braintree/sanitize-url": "^6.0.2",
"@bsky.app/alf": "^0.1.7",
"@bsky.app/expo-image-crop-tool": "^0.5.0",
- "@bsky.app/expo-translate-text": "^0.2.7",
+ "@bsky.app/expo-translate-text": "^0.2.9",
"@bsky.app/react-native-mmkv": "2.12.5",
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
"@emoji-mart/data": "^1.2.1",
diff --git a/src/analytics/metrics/types.ts b/src/analytics/metrics/types.ts
index 4946213f4..494c7ee1d 100644
--- a/src/analytics/metrics/types.ts
+++ b/src/analytics/metrics/types.ts
@@ -707,15 +707,20 @@ export type Events = {
'reportDialog:failure': {}
translate: {
+ os: Platform['OS']
sourceLanguages: string[]
targetLanguage: string
textLength: number
}
'translate:result': {
- method: 'on-device' | 'google-translate' | 'fallback-alert'
+ method: 'on-device' | 'fallback-alert'
os: Platform['OS']
+ sourceSelection: 'automatic' | 'manual'
sourceLanguage: string | null
targetLanguage: string
+
+ /* Only relevant to posts */
+ postLanguages?: string[]
}
'translate:override': {
os: Platform['OS']
diff --git a/src/components/Post/Translated/index.tsx b/src/components/Post/Translated/index.tsx
index c6402978a..1100c81bf 100644
--- a/src/components/Post/Translated/index.tsx
+++ b/src/components/Post/Translated/index.tsx
@@ -9,6 +9,7 @@ import {useTranslate} from '#/lib/translation'
import {type TranslationFunction} from '#/lib/translation'
import {
codeToLanguageName,
+ getPostLanguageTags,
isPostInLanguage,
languageName,
} from '#/locale/helpers'
@@ -42,6 +43,7 @@ export function TranslatedPost({
const langPrefs = useLanguagePrefs()
const {clearTranslation, translate, translationState} = useTranslate({
key: post.uri,
+ postLangCodes: getPostLanguageTags(post),
})
const needsTranslation = useMemo(() => {
@@ -122,6 +124,7 @@ function TranslationLink({
})
ax.metric('translate', {
+ os: Platform.OS,
sourceLanguages: [], // todo: get from post maybe?
targetLanguage: primaryLanguage,
textLength: postText.length,
@@ -405,6 +408,7 @@ function TranslationLanguageSelect({
text: postText,
targetLangCode: langPrefs.primaryLanguage,
sourceLangCode,
+ sourceSelection: 'manual',
})
}
diff --git a/src/components/PostControls/PostMenu/PostMenuItems.tsx b/src/components/PostControls/PostMenu/PostMenuItems.tsx
index 6fe8692d2..4f0fbbb2a 100644
--- a/src/components/PostControls/PostMenu/PostMenuItems.tsx
+++ b/src/components/PostControls/PostMenu/PostMenuItems.tsx
@@ -28,6 +28,7 @@ import {
import {richTextToString} from '#/lib/strings/rich-text-helpers'
import {toShareUrl} from '#/lib/strings/url-helpers'
import {useTranslate} from '#/lib/translation'
+import {getPostLanguageTags} from '#/locale/helpers'
import {logger} from '#/logger'
import {type Shadow} from '#/state/cache/post-shadow'
import {useProfileShadow} from '#/state/cache/profile-shadow'
@@ -137,6 +138,7 @@ let PostMenuItems = ({
const openLink = useOpenLink()
const {clearTranslation, translate, translationState} = useTranslate({
key: post.uri,
+ postLangCodes: getPostLanguageTags(post),
forceGoogleTranslate,
})
const navigation = useNavigation()
@@ -287,6 +289,7 @@ let PostMenuItems = ({
)
) {
ax.metric('translate', {
+ os: Platform.OS,
sourceLanguages: post.record.langs ?? [],
targetLanguage: langPrefs.primaryLanguage,
textLength: post.record.text.length,
diff --git a/src/components/dms/MessageContextMenu.tsx b/src/components/dms/MessageContextMenu.tsx
index dd48eda12..1c6a72177 100644
--- a/src/components/dms/MessageContextMenu.tsx
+++ b/src/components/dms/MessageContextMenu.tsx
@@ -1,5 +1,5 @@
import {memo, useCallback} from 'react'
-import {LayoutAnimation} from 'react-native'
+import {LayoutAnimation, Platform} from 'react-native'
import * as Clipboard from 'expo-clipboard'
import {type ChatBskyConvoDefs, RichText} from '@atproto/api'
import {msg} from '@lingui/core/macro'
@@ -67,6 +67,7 @@ export let MessageContextMenu = ({
void translate(message.text, langPrefs.primaryLanguage)
ax.metric('translate', {
+ os: Platform.OS,
sourceLanguages: [],
targetLanguage: langPrefs.primaryLanguage,
textLength: message.text.length,
diff --git a/src/lib/translation/context.ts b/src/lib/translation/context.ts
index 423efc4f1..aa671cf98 100644
--- a/src/lib/translation/context.ts
+++ b/src/lib/translation/context.ts
@@ -1,16 +1,6 @@
import {createContext} from 'react'
-import {type TranslationFunctionParams, type TranslationState} from './types'
+import {type ContextType} from './types'
-export const Context = createContext<{
- translationState: Record
- translate: (
- parameters: TranslationFunctionParams & {
- key: string
- forceGoogleTranslate: boolean
- },
- ) => Promise
- clearTranslation: (key: string) => void
- acquireTranslation: (key: string) => () => void
-} | null>(null)
+export const Context = createContext(null)
Context.displayName = 'TranslationContext'
diff --git a/src/lib/translation/index.tsx b/src/lib/translation/index.tsx
index 50445767c..9f41a4e89 100644
--- a/src/lib/translation/index.tsx
+++ b/src/lib/translation/index.tsx
@@ -11,7 +11,12 @@ import {logger} from '#/logger'
import {useAnalytics} from '#/analytics'
import {HAS_ON_DEVICE_TRANSLATION, IS_ANDROID, IS_IOS} from '#/env'
import {Context} from './context'
-import {type TranslationFunctionParams, type TranslationState} from './types'
+import {
+ type ContextType,
+ type TranslationFunctionParams,
+ type TranslationOptions,
+ type TranslationState,
+} from './types'
import {guessLanguage} from './utils'
export * from './types'
@@ -98,10 +103,8 @@ async function attemptTranslation(
export function useTranslate({
key,
forceGoogleTranslate = false,
-}: {
- key: string
- forceGoogleTranslate?: boolean
-}) {
+ postLangCodes,
+}: TranslationOptions) {
const context = useContext(Context)
if (!context) {
throw new Error(
@@ -118,9 +121,14 @@ export function useTranslate({
const translate = useCallback(
async (params: TranslationFunctionParams) => {
- return context.translate({...params, key, forceGoogleTranslate})
+ return context.translate({
+ ...params,
+ key,
+ forceGoogleTranslate,
+ postLangCodes,
+ })
},
- [context, forceGoogleTranslate, key],
+ [context, forceGoogleTranslate, key, postLangCodes],
)
const clearTranslation = useCallback(
@@ -199,27 +207,17 @@ export function Provider({children}: React.PropsWithChildren) {
})
}, [])
- const translate = useCallback(
+ const translate = useCallback(
async ({
key,
text,
targetLangCode,
sourceLangCode,
+ sourceSelection = 'automatic',
+ postLangCodes,
...options
- }: {
- key: string
- text: string
- targetLangCode: string
- sourceLangCode?: string
- forceGoogleTranslate?: boolean
}) => {
if (options?.forceGoogleTranslate || !HAS_ON_DEVICE_TRANSLATION) {
- ax.metric('translate:result', {
- method: 'google-translate',
- os: Platform.OS,
- sourceLanguage: sourceLangCode ?? null,
- targetLanguage: targetLangCode,
- })
await googleTranslate(text, targetLangCode, sourceLangCode)
return
}
@@ -240,8 +238,10 @@ export function Provider({children}: React.PropsWithChildren) {
ax.metric('translate:result', {
method: 'on-device',
os: Platform.OS,
+ sourceSelection,
sourceLanguage: result.sourceLanguage,
targetLanguage: result.targetLanguage,
+ postLanguages: postLangCodes,
})
if (!IS_ANDROID) {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
@@ -253,17 +253,20 @@ export function Provider({children}: React.PropsWithChildren) {
translatedText: result.translatedText,
sourceLanguage: result.sourceLanguage,
targetLanguage: result.targetLanguage,
+ postLanguages: postLangCodes,
},
}))
} catch (e) {
- logger.error('Failed to translate post on device', {safeMessage: e})
+ logger.error('Failed to translate text on device', {safeMessage: e})
// On-device translation failed (language pack missing or user
- // dismissed the download prompt). Fall back to Google Translate.
+ // dismissed the download prompt).
ax.metric('translate:result', {
method: 'fallback-alert',
os: Platform.OS,
+ sourceSelection,
sourceLanguage: sourceLangCode ?? null,
targetLanguage: targetLangCode,
+ postLanguages: postLangCodes,
})
let errorMessage = l`Device failed to translate :(`
if (!IS_ANDROID) {
diff --git a/src/lib/translation/index.web.tsx b/src/lib/translation/index.web.tsx
index 8fc46175f..034a38f9b 100644
--- a/src/lib/translation/index.web.tsx
+++ b/src/lib/translation/index.web.tsx
@@ -3,7 +3,12 @@ import {useCallback, useContext, useMemo} from 'react'
import {useGoogleTranslate} from '#/lib/hooks/useGoogleTranslate'
import {useAnalytics} from '#/analytics'
import {Context} from './context'
-import {type TranslationFunctionParams, type TranslationState} from './types'
+import {
+ type ContextType,
+ type TranslationFunctionParams,
+ type TranslationOptions,
+ type TranslationState,
+} from './types'
export * from './types'
export * from './utils'
@@ -17,12 +22,7 @@ const clearTranslation = (_key: string) => {}
/**
* Web always opens Google Translate.
*/
-export function useTranslate({
- key,
-}: {
- key: string
- forceGoogleTranslate?: boolean
-}) {
+export function useTranslate({key, postLangCodes}: TranslationOptions) {
const context = useContext(Context)
if (!context) {
throw new Error(
@@ -33,9 +33,14 @@ export function useTranslate({
// Always call hooks in consistent order
const translate = useCallback(
async (params: TranslationFunctionParams) => {
- return context.translate({...params, key, forceGoogleTranslate: true})
+ return context.translate({
+ ...params,
+ key,
+ forceGoogleTranslate: true,
+ postLangCodes,
+ })
},
- [key, context],
+ [key, context, postLangCodes],
)
const clearTranslation = useCallback(() => {
@@ -55,23 +60,8 @@ export function Provider({children}: React.PropsWithChildren) {
const ax = useAnalytics()
const googleTranslate = useGoogleTranslate()
- const translate = useCallback(
- async ({
- text,
- targetLangCode,
- sourceLangCode,
- }: {
- key: string
- text: string
- targetLangCode: string
- sourceLangCode?: string
- }) => {
- ax.metric('translate:result', {
- method: 'google-translate',
- os: 'web',
- sourceLanguage: sourceLangCode ?? null,
- targetLanguage: targetLangCode,
- })
+ const translate = useCallback(
+ async ({text, targetLangCode, sourceLangCode}) => {
await googleTranslate(text, targetLangCode, sourceLangCode)
},
[ax, googleTranslate],
diff --git a/src/lib/translation/types.ts b/src/lib/translation/types.ts
index 4bf2d021e..352df445f 100644
--- a/src/lib/translation/types.ts
+++ b/src/lib/translation/types.ts
@@ -27,8 +27,34 @@ export type TranslationFunctionParams = {
* The source language of the text. Will auto-detect if not provided.
*/
sourceLangCode?: string
+ /**
+ * Whether we auto-detected the language or it was selected manually. Defaults to 'automatic'.
+ */
+ sourceSelection?: 'automatic' | 'manual'
+}
+
+export type TranslationOptions = {
+ key: string
+ forceGoogleTranslate?: boolean
+ /**
+ * The language(s) of the post being translated. Used for analytics purposes
+ * to understand translation usage patterns better. Optional because it may
+ * not always be available (e.g. if the post text is empty or if the
+ * translation is triggered from a non-post
+ * context).
+ */
+ postLangCodes?: string[]
}
export type TranslationFunction = (
parameters: TranslationFunctionParams,
) => Promise
+
+export type ContextType = {
+ translationState: Record
+ translate: (
+ parameters: TranslationFunctionParams & TranslationOptions,
+ ) => Promise
+ clearTranslation: (key: string) => void
+ acquireTranslation: (key: string) => () => void
+}
diff --git a/src/locale/helpers.ts b/src/locale/helpers.ts
index eb384dfec..18b378c26 100644
--- a/src/locale/helpers.ts
+++ b/src/locale/helpers.ts
@@ -61,6 +61,14 @@ function getLocalizedLanguage(
}
}
+export function getPostLanguageTags(post: AppBskyFeedDefs.PostView) {
+ return AppBskyFeedPost.isRecord(post.record) &&
+ hasProp(post.record, 'langs') &&
+ Array.isArray(post.record.langs)
+ ? post.record.langs
+ : []
+}
+
export function languageName(language: Language, appLang: string): string {
// if Intl.DisplayNames is unavailable on the target, display the English name
if (!Intl.DisplayNames) {
@@ -80,22 +88,14 @@ export function codeToLanguageName(lang2or3: string, appLang: string): string {
export function getPostLanguage(
post: AppBskyFeedDefs.PostView,
): string | undefined {
- let candidates: string[] = []
+ let candidates: string[] = getPostLanguageTags(post)
let postText: string = ''
if (hasProp(post.record, 'text') && typeof post.record.text === 'string') {
postText = post.record.text
}
- if (
- AppBskyFeedPost.isRecord(post.record) &&
- hasProp(post.record, 'langs') &&
- Array.isArray(post.record.langs)
- ) {
- candidates = post.record.langs
- }
-
// if there's only one declared language, use that
- if (candidates?.length === 1) {
+ if (candidates.length === 1) {
return candidates[0]
}
@@ -108,11 +108,10 @@ export function getPostLanguage(
let langsProbabilityMap = lande(postText)
// filter down using declared languages
- if (candidates?.length) {
+ if (candidates.length) {
langsProbabilityMap = langsProbabilityMap.filter(
- ([lang, _probability]: [string, number]) => {
- return candidates.includes(code3ToCode2(lang))
- },
+ ([lang, _probability]: [string, number]) =>
+ candidates.includes(code3ToCode2(lang)),
)
}
diff --git a/yarn.lock b/yarn.lock
index af7fd4c64..5d9f53db1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2403,10 +2403,10 @@
resolved "https://registry.yarnpkg.com/@bsky.app/expo-image-crop-tool/-/expo-image-crop-tool-0.5.0.tgz#4308fbde5c15e6be9122601797bc3d9549c95e31"
integrity sha512-gmhQr2HWTRFyPO00fn5OmtiEVtikXusHMrN5Zoq26pu1VZX3zVE+aoc668etTqrvsQcm2Qu8fo96k5F3Wu+6wg==
-"@bsky.app/expo-translate-text@^0.2.7":
- version "0.2.7"
- resolved "https://registry.yarnpkg.com/@bsky.app/expo-translate-text/-/expo-translate-text-0.2.7.tgz#e34811d0f0300f8808762e5676aa50790ca5d5e8"
- integrity sha512-J9zctP9hLxX0eustTKk5CBnCkk6cEdlu1s7GzUnpT65qkCSNbYqbbUCpcU2Z2S2dN/1+w6L/iHb+vmCEbZMOaQ==
+"@bsky.app/expo-translate-text@^0.2.9":
+ version "0.2.9"
+ resolved "https://registry.yarnpkg.com/@bsky.app/expo-translate-text/-/expo-translate-text-0.2.9.tgz#4ed4552cd50bca7d02d14e706e419bd728d4ab51"
+ integrity sha512-VmqMhc/YavjgkGhxT/fB8mGSi+VZHJET1tsbpTg8peqKRXFSju2F294NsRxH/4aaMQFlt5oRfPCRnLm1H5o3lA==
"@bsky.app/react-native-mmkv@2.12.5":
version "2.12.5"
--
2.51.2
From b9a3256e5127d37f32b88b8818e858093c30abcf Mon Sep 17 00:00:00 2001
From: surfdude29 <149612116+surfdude29@users.noreply.github.com>
Date: Tue, 17 Mar 2026 18:43:37 +0000
Subject: [PATCH 06/43] Don't strip query parameters on in-app navigation
(#9948)
---
src/lib/strings/url-helpers.ts | 7 +------
1 file changed, 1 insertion(+), 6 deletions(-)
diff --git a/src/lib/strings/url-helpers.ts b/src/lib/strings/url-helpers.ts
index 6088e2806..cd2ae29af 100644
--- a/src/lib/strings/url-helpers.ts
+++ b/src/lib/strings/url-helpers.ts
@@ -194,12 +194,7 @@ export function convertBskyAppUrlIfNeeded(url: string): string {
return startUriToStarterPackUri(urlp.pathname)
}
- // special-case search links
- if (urlp.pathname === '/search') {
- return `/search?q=${urlp.searchParams.get('q')}`
- }
-
- return urlp.pathname
+ return urlp.pathname + urlp.search
} catch (e) {
console.error('Unexpected error in convertBskyAppUrlIfNeeded()', e)
}
--
2.51.2
From 45df50ec195c7b0f1b5a10aca10c23bfeee644dd Mon Sep 17 00:00:00 2001
From: Eric Bailey
Date: Tue, 17 Mar 2026 13:59:28 -0500
Subject: [PATCH 07/43] Rename translation properties for clarity (#10061)
---
src/analytics/metrics/types.ts | 81 ++++++++++--
src/components/Post/Translated/index.tsx | 123 +++++++++---------
.../PostControls/PostMenu/PostMenuItems.tsx | 21 +--
src/components/dms/MessageContextMenu.tsx | 5 +-
src/lib/translation/index.tsx | 87 ++++++++-----
src/lib/translation/index.web.tsx | 39 ++++--
src/lib/translation/types.ts | 35 +++--
.../components/ThreadItemAnchor.tsx | 6 +-
.../PostThread/components/ThreadItemPost.tsx | 6 +-
.../components/ThreadItemTreePost.tsx | 6 +-
src/view/com/post/Post.tsx | 6 +-
src/view/com/posts/PostFeedItem.tsx | 8 +-
12 files changed, 250 insertions(+), 173 deletions(-)
diff --git a/src/analytics/metrics/types.ts b/src/analytics/metrics/types.ts
index 494c7ee1d..248965fd6 100644
--- a/src/analytics/metrics/types.ts
+++ b/src/analytics/metrics/types.ts
@@ -708,24 +708,83 @@ export type Events = {
translate: {
os: Platform['OS']
- sourceLanguages: string[]
- targetLanguage: string
+ /**
+ * The languages the content might be in, such as the user-supplied
+ * language codes on posts. Currently only available on posts.
+ */
+ possibleSourceLanguages: string[] | undefined
+ /**
+ * This is the user's configured primary language, which is always defined.
+ */
+ expectedTargetLanguage: string
+ /**
+ * The length of the text being translated. We assume shorter texts are
+ * more likely to have inaccurate translations.
+ */
textLength: number
+ googleTranslate: boolean
}
'translate:result': {
- method: 'on-device' | 'fallback-alert'
+ success: boolean
os: Platform['OS']
- sourceSelection: 'automatic' | 'manual'
- sourceLanguage: string | null
- targetLanguage: string
-
- /* Only relevant to posts */
- postLanguages?: string[]
+ /**
+ * The languages the content might be in, such as the user-supplied
+ * language codes on posts. Currently only available on posts.
+ */
+ possibleSourceLanguages: string[] | undefined
+ /**
+ * The language we expected the content to be in. This could be based on
+ * user selection or on our confidence in the detected language. This is
+ * nullable because we may not always have an expected source language.
+ */
+ expectedSourceLanguage: string | null
+ /**
+ * This is the user's configured primary language, which is always defined.
+ */
+ expectedTargetLanguage: string
+ /**
+ * The language the translation result was actually in. This is nullable
+ * because the translation could have failed, in which case we won't have a
+ * result source language.
+ */
+ resultSourceLanguage: string | null
+ /**
+ * The language the translation result was translated into. This should be
+ * the same as `expectedTargetLanguage`, but we include it for completeness
+ * and in case there are any edge cases where they differ. This is nullable
+ * because if the translation failed, we won't have a result target
+ * language.
+ */
+ resultTargetLanguage: string | null
+ /**
+ * The length of the text being translated. We assume shorter texts are
+ * more likely to have inaccurate translations.
+ */
+ textLength: number
}
'translate:override': {
os: Platform['OS']
- sourceLanguage: string
- targetLanguage: string
+ /**
+ * The languages the content might be in, such as the user-supplied
+ * language codes on posts. Currently only available on posts.
+ */
+ possibleSourceLanguages: string[] | undefined
+ /**
+ * The language the user has indicated the content is actually in, which
+ * may be different from the expected source language if the user is
+ * overriding the auto-detected language. This is the language the user
+ * wants to translate from after overriding.
+ */
+ expectedSourceLanguage: string
+ /**
+ * This is the user's configured primary language, which is always defined.
+ */
+ expectedTargetLanguage: string
+ /**
+ * The language the translation result was actually in, which the user now
+ * wishes to override.
+ */
+ resultSourceLanguage: string
}
'postMenu:openMuteWordsDialog': {
diff --git a/src/components/Post/Translated/index.tsx b/src/components/Post/Translated/index.tsx
index 1100c81bf..9aff552bb 100644
--- a/src/components/Post/Translated/index.tsx
+++ b/src/components/Post/Translated/index.tsx
@@ -1,12 +1,14 @@
import {useCallback, useMemo} from 'react'
import {Platform, type StyleProp, type TextStyle, View} from 'react-native'
-import {type AppBskyFeedDefs} from '@atproto/api'
+import {type AppBskyFeedDefs, AppBskyFeedPost} from '@atproto/api'
import {Trans, useLingui} from '@lingui/react/macro'
import {HITSLOP_30} from '#/lib/constants'
-import {useGoogleTranslate} from '#/lib/hooks/useGoogleTranslate'
import {useTranslate} from '#/lib/translation'
-import {type TranslationFunction} from '#/lib/translation'
+import {
+ type TranslationFunction,
+ type TranslationFunctionParams,
+} from '#/lib/translation'
import {
codeToLanguageName,
getPostLanguageTags,
@@ -26,26 +28,39 @@ import * as Select from '#/components/Select'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import {IS_WEB} from '#/env'
+import * as bsky from '#/types/bsky'
const X_ICON_OFFSET = 16
export function TranslatedPost({
hideTranslateLink = false,
post,
- postText,
postTextStyle = a.text_md,
}: {
hideTranslateLink?: boolean
post: AppBskyFeedDefs.PostView
- postText: string
postTextStyle?: StyleProp
}) {
const langPrefs = useLanguagePrefs()
const {clearTranslation, translate, translationState} = useTranslate({
key: post.uri,
- postLangCodes: getPostLanguageTags(post),
})
+ const record = useMemo(() => {
+ return bsky.dangerousIsType(
+ post.record,
+ AppBskyFeedPost.isRecord,
+ )
+ ? post.record
+ : undefined
+ }, [post])
+ const initialTranslationParams = useMemo(() => {
+ return {
+ text: record?.text || '',
+ expectedTargetLanguage: langPrefs.primaryLanguage,
+ possibleSourceLanguages: getPostLanguageTags(post),
+ }
+ }, [post, record, langPrefs])
const needsTranslation = useMemo(() => {
if (hideTranslateLink) return false
return !isPostInLanguage(post, [langPrefs.primaryLanguage])
@@ -57,11 +72,11 @@ export function TranslatedPost({
case 'success':
return (
)
default:
return (
needsTranslation && (
)
)
@@ -105,31 +119,18 @@ function TranslationLoading() {
}
function TranslationLink({
- postText,
- primaryLanguage,
translate,
+ initialTranslationParams,
}: {
- postText: string
- primaryLanguage: string
translate: TranslationFunction
+ initialTranslationParams: TranslationFunctionParams
}) {
const t = useTheme()
const {t: l} = useLingui()
- const ax = useAnalytics()
const handleTranslate = useCallback(() => {
- void translate({
- text: postText,
- targetLangCode: primaryLanguage,
- })
-
- ax.metric('translate', {
- os: Platform.OS,
- sourceLanguages: [], // todo: get from post maybe?
- targetLanguage: primaryLanguage,
- textLength: postText.length,
- })
- }, [ax, postText, primaryLanguage, translate])
+ void translate(initialTranslationParams)
+ }, [initialTranslationParams, translate])
return (
void
message: string
- postText: string
- primaryLanguage: string
+ initialTranslationParams: TranslationFunctionParams
}) {
const t = useTheme()
const {t: l} = useLingui()
- const translate = useGoogleTranslate()
const handleFallback = () => {
- void translate(postText, primaryLanguage)
+ void translate({
+ ...initialTranslationParams,
+ forceGoogleTranslate: true,
+ })
}
return (
@@ -247,24 +250,24 @@ function TranslationError({
function TranslationResult({
clearTranslation,
translate,
- postText,
postTextStyle,
- sourceLanguage,
+ resultSourceLanguage,
translatedText,
+ initialTranslationParams,
}: {
clearTranslation: () => void
translate: TranslationFunction
- postText: string
postTextStyle?: StyleProp
- sourceLanguage: string | null
+ resultSourceLanguage: string | null
translatedText: string
+ initialTranslationParams: TranslationFunctionParams
}) {
const t = useTheme()
const langPrefs = useLanguagePrefs()
const {i18n, t: l} = useLingui()
- const langName = sourceLanguage
- ? codeToLanguageName(sourceLanguage, i18n.locale)
+ const langName = resultSourceLanguage
+ ? codeToLanguageName(resultSourceLanguage, i18n.locale)
: undefined
const flattenedStyle = flatten(postTextStyle) ?? {}
@@ -323,7 +326,7 @@ function TranslationResult({
Translated
)}
- {sourceLanguage != null && (
+ {resultSourceLanguage != null && (
<>
>
)}
@@ -362,12 +365,12 @@ function TranslationResult({
function TranslationLanguageSelect({
translate,
- postText,
- sourceLanguage,
+ resultSourceLanguage,
+ initialTranslationParams,
}: {
translate: TranslationFunction
- postText: string
- sourceLanguage: string
+ resultSourceLanguage: string
+ initialTranslationParams: TranslationFunctionParams
}) {
const t = useTheme()
const ax = useAnalytics()
@@ -383,8 +386,8 @@ function TranslationLanguageSelect({
)
.sort((a, b) => {
// Prioritize sourceLanguage at the top
- if (a.code2 === sourceLanguage) return -1
- if (b.code2 === sourceLanguage) return 1
+ if (a.code2 === resultSourceLanguage) return -1
+ if (b.code2 === resultSourceLanguage) return 1
// Localized sort
return languageName(a, langPrefs.appLanguage).localeCompare(
languageName(b, langPrefs.appLanguage),
@@ -395,26 +398,28 @@ function TranslationLanguageSelect({
label: languageName(l, langPrefs.appLanguage), // The viewer may not be familiar with the source language, so localize the name
value: l.code2,
})),
- [langPrefs, sourceLanguage],
+ [langPrefs, resultSourceLanguage],
)
const handleChangeTranslationLanguage = (sourceLangCode: string) => {
ax.metric('translate:override', {
os: Platform.OS,
- sourceLanguage: sourceLangCode,
- targetLanguage: langPrefs.primaryLanguage,
+ possibleSourceLanguages: initialTranslationParams.possibleSourceLanguages,
+ expectedSourceLanguage: sourceLangCode,
+ expectedTargetLanguage: initialTranslationParams.expectedTargetLanguage,
+ resultSourceLanguage,
})
void translate({
- text: postText,
- targetLangCode: langPrefs.primaryLanguage,
- sourceLangCode,
- sourceSelection: 'manual',
+ text: initialTranslationParams.text,
+ expectedTargetLanguage: initialTranslationParams.expectedTargetLanguage,
+ expectedSourceLanguage: sourceLangCode,
+ possibleSourceLanguages: initialTranslationParams.possibleSourceLanguages,
})
}
return (
{({props}) => {
diff --git a/src/components/PostControls/PostMenu/PostMenuItems.tsx b/src/components/PostControls/PostMenu/PostMenuItems.tsx
index 4f0fbbb2a..257c1dcb4 100644
--- a/src/components/PostControls/PostMenu/PostMenuItems.tsx
+++ b/src/components/PostControls/PostMenu/PostMenuItems.tsx
@@ -8,7 +8,7 @@ import {
import * as Clipboard from 'expo-clipboard'
import {
type AppBskyFeedDefs,
- AppBskyFeedPost,
+ type AppBskyFeedPost,
type AppBskyFeedThreadgate,
AtUri,
type RichText as RichTextAPI,
@@ -96,7 +96,6 @@ import * as Prompt from '#/components/Prompt'
import * as Toast from '#/components/Toast'
import {useAnalytics} from '#/analytics'
import {IS_INTERNAL} from '#/env'
-import * as bsky from '#/types/bsky'
let PostMenuItems = ({
post,
@@ -138,7 +137,6 @@ let PostMenuItems = ({
const openLink = useOpenLink()
const {clearTranslation, translate, translationState} = useTranslate({
key: post.uri,
- postLangCodes: getPostLanguageTags(post),
forceGoogleTranslate,
})
const navigation = useNavigation()
@@ -279,22 +277,9 @@ let PostMenuItems = ({
const onPressTranslate = () => {
void translate({
text: record.text,
- targetLangCode: langPrefs.primaryLanguage,
+ expectedTargetLanguage: langPrefs.primaryLanguage,
+ possibleSourceLanguages: getPostLanguageTags(post),
})
-
- if (
- bsky.dangerousIsType(
- post.record,
- AppBskyFeedPost.isRecord,
- )
- ) {
- ax.metric('translate', {
- os: Platform.OS,
- sourceLanguages: post.record.langs ?? [],
- targetLanguage: langPrefs.primaryLanguage,
- textLength: post.record.text.length,
- })
- }
}
const onHidePost = () => {
diff --git a/src/components/dms/MessageContextMenu.tsx b/src/components/dms/MessageContextMenu.tsx
index 1c6a72177..dda99c77e 100644
--- a/src/components/dms/MessageContextMenu.tsx
+++ b/src/components/dms/MessageContextMenu.tsx
@@ -68,9 +68,10 @@ export let MessageContextMenu = ({
ax.metric('translate', {
os: Platform.OS,
- sourceLanguages: [],
- targetLanguage: langPrefs.primaryLanguage,
+ possibleSourceLanguages: [], // N/A for chats
+ expectedTargetLanguage: langPrefs.primaryLanguage,
textLength: message.text.length,
+ googleTranslate: true,
})
}, [ax, langPrefs.primaryLanguage, message.text, translate])
diff --git a/src/lib/translation/index.tsx b/src/lib/translation/index.tsx
index 9f41a4e89..a8f187519 100644
--- a/src/lib/translation/index.tsx
+++ b/src/lib/translation/index.tsx
@@ -103,7 +103,6 @@ async function attemptTranslation(
export function useTranslate({
key,
forceGoogleTranslate = false,
- postLangCodes,
}: TranslationOptions) {
const context = useContext(Context)
if (!context) {
@@ -121,14 +120,17 @@ export function useTranslate({
const translate = useCallback(
async (params: TranslationFunctionParams) => {
- return context.translate({
- ...params,
- key,
- forceGoogleTranslate,
- postLangCodes,
- })
+ return context.translate(
+ {
+ ...params,
+ },
+ {
+ key,
+ forceGoogleTranslate,
+ },
+ )
},
- [context, forceGoogleTranslate, key, postLangCodes],
+ [context, forceGoogleTranslate, key],
)
const clearTranslation = useCallback(
@@ -208,17 +210,34 @@ export function Provider({children}: React.PropsWithChildren) {
}, [])
const translate = useCallback(
- async ({
- key,
- text,
- targetLangCode,
- sourceLangCode,
- sourceSelection = 'automatic',
- postLangCodes,
- ...options
- }) => {
- if (options?.forceGoogleTranslate || !HAS_ON_DEVICE_TRANSLATION) {
- await googleTranslate(text, targetLangCode, sourceLangCode)
+ async (
+ {
+ text,
+ expectedTargetLanguage,
+ expectedSourceLanguage,
+ possibleSourceLanguages,
+ forceGoogleTranslate: forceGoogleTranslateOverride,
+ },
+ {key, forceGoogleTranslate},
+ ) => {
+ const shouldForceGoogleTranslate = Boolean(
+ forceGoogleTranslateOverride ?? forceGoogleTranslate,
+ )
+
+ ax.metric('translate', {
+ os: Platform.OS,
+ possibleSourceLanguages,
+ expectedTargetLanguage: expectedTargetLanguage,
+ textLength: text.length,
+ googleTranslate: shouldForceGoogleTranslate,
+ })
+
+ if (shouldForceGoogleTranslate || !HAS_ON_DEVICE_TRANSLATION) {
+ await googleTranslate(
+ text,
+ expectedTargetLanguage,
+ expectedSourceLanguage,
+ )
return
}
@@ -232,16 +251,18 @@ export function Provider({children}: React.PropsWithChildren) {
try {
const result = await attemptTranslation(
text,
- targetLangCode,
- sourceLangCode,
+ expectedTargetLanguage,
+ expectedSourceLanguage,
)
ax.metric('translate:result', {
- method: 'on-device',
+ success: true,
os: Platform.OS,
- sourceSelection,
- sourceLanguage: result.sourceLanguage,
- targetLanguage: result.targetLanguage,
- postLanguages: postLangCodes,
+ possibleSourceLanguages,
+ expectedSourceLanguage: expectedSourceLanguage ?? null,
+ expectedTargetLanguage,
+ resultSourceLanguage: result.sourceLanguage,
+ resultTargetLanguage: result.targetLanguage,
+ textLength: text.length,
})
if (!IS_ANDROID) {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
@@ -253,7 +274,7 @@ export function Provider({children}: React.PropsWithChildren) {
translatedText: result.translatedText,
sourceLanguage: result.sourceLanguage,
targetLanguage: result.targetLanguage,
- postLanguages: postLangCodes,
+ postLanguages: possibleSourceLanguages,
},
}))
} catch (e) {
@@ -261,12 +282,14 @@ export function Provider({children}: React.PropsWithChildren) {
// On-device translation failed (language pack missing or user
// dismissed the download prompt).
ax.metric('translate:result', {
- method: 'fallback-alert',
+ success: false,
os: Platform.OS,
- sourceSelection,
- sourceLanguage: sourceLangCode ?? null,
- targetLanguage: targetLangCode,
- postLanguages: postLangCodes,
+ possibleSourceLanguages,
+ expectedSourceLanguage: expectedSourceLanguage ?? null,
+ expectedTargetLanguage,
+ resultSourceLanguage: null,
+ resultTargetLanguage: null,
+ textLength: text.length,
})
let errorMessage = l`Device failed to translate :(`
if (!IS_ANDROID) {
diff --git a/src/lib/translation/index.web.tsx b/src/lib/translation/index.web.tsx
index 034a38f9b..05802016f 100644
--- a/src/lib/translation/index.web.tsx
+++ b/src/lib/translation/index.web.tsx
@@ -22,7 +22,7 @@ const clearTranslation = (_key: string) => {}
/**
* Web always opens Google Translate.
*/
-export function useTranslate({key, postLangCodes}: TranslationOptions) {
+export function useTranslate({key}: TranslationOptions) {
const context = useContext(Context)
if (!context) {
throw new Error(
@@ -33,14 +33,17 @@ export function useTranslate({key, postLangCodes}: TranslationOptions) {
// Always call hooks in consistent order
const translate = useCallback(
async (params: TranslationFunctionParams) => {
- return context.translate({
- ...params,
- key,
- forceGoogleTranslate: true,
- postLangCodes,
- })
+ return context.translate(
+ {
+ ...params,
+ },
+ {
+ key,
+ forceGoogleTranslate: true,
+ },
+ )
},
- [key, context, postLangCodes],
+ [key, context],
)
const clearTranslation = useCallback(() => {
@@ -61,8 +64,24 @@ export function Provider({children}: React.PropsWithChildren) {
const googleTranslate = useGoogleTranslate()
const translate = useCallback(
- async ({text, targetLangCode, sourceLangCode}) => {
- await googleTranslate(text, targetLangCode, sourceLangCode)
+ async ({
+ text,
+ expectedTargetLanguage,
+ expectedSourceLanguage,
+ possibleSourceLanguages,
+ }) => {
+ ax.metric('translate', {
+ os: 'web',
+ possibleSourceLanguages,
+ expectedTargetLanguage,
+ textLength: text.length,
+ googleTranslate: true,
+ })
+ await googleTranslate(
+ text,
+ expectedTargetLanguage,
+ expectedSourceLanguage,
+ )
},
[ax, googleTranslate],
)
diff --git a/src/lib/translation/types.ts b/src/lib/translation/types.ts
index 352df445f..c859148b0 100644
--- a/src/lib/translation/types.ts
+++ b/src/lib/translation/types.ts
@@ -22,38 +22,45 @@ export type TranslationFunctionParams = {
/**
* The language to translate the text into.
*/
- targetLangCode: string
+ expectedTargetLanguage: string
/**
- * The source language of the text. Will auto-detect if not provided.
+ * We auto-detect the source language by default, but the user has the option
+ * to specify a source language if they want to. If this value is present, it
+ * means the user selected a source language, or we were certain of the
+ * source language and want to specify it explicitly.
*/
- sourceLangCode?: string
+ expectedSourceLanguage?: string
/**
- * Whether we auto-detected the language or it was selected manually. Defaults to 'automatic'.
+ * The languages the content might be in, such as the user-supplied
+ * language codes on posts. Currently only available on posts.
*/
- sourceSelection?: 'automatic' | 'manual'
+ possibleSourceLanguages?: string[]
+ /**
+ * Override the default behavior and always use Google Translate.
+ */
+ forceGoogleTranslate?: boolean
}
export type TranslationOptions = {
+ /**
+ * A unique key to identify this translation instance e.g. the post URI
+ */
key: string
- forceGoogleTranslate?: boolean
/**
- * The language(s) of the post being translated. Used for analytics purposes
- * to understand translation usage patterns better. Optional because it may
- * not always be available (e.g. if the post text is empty or if the
- * translation is triggered from a non-post
- * context).
+ * Override the default behavior and always use Google Translate.
*/
- postLangCodes?: string[]
+ forceGoogleTranslate?: boolean
}
export type TranslationFunction = (
- parameters: TranslationFunctionParams,
+ params: TranslationFunctionParams,
) => Promise
export type ContextType = {
translationState: Record
translate: (
- parameters: TranslationFunctionParams & TranslationOptions,
+ params: TranslationFunctionParams,
+ options: TranslationOptions,
) => Promise
clearTranslation: (key: string) => void
acquireTranslation: (key: string) => () => void
diff --git a/src/screens/PostThread/components/ThreadItemAnchor.tsx b/src/screens/PostThread/components/ThreadItemAnchor.tsx
index f17a83246..f0eca2122 100644
--- a/src/screens/PostThread/components/ThreadItemAnchor.tsx
+++ b/src/screens/PostThread/components/ThreadItemAnchor.tsx
@@ -411,11 +411,7 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
shouldProxyLinks={true}
/>
) : undefined}
-
+
{post.embed && (