add on-device translation to post feed item

This commit is contained in:
Samuel Newman
2026-03-03 16:14:56 +02:00
parent e97e32d7b6
commit d92627205c
3 changed files with 61 additions and 36 deletions
+33 -13
View File
@@ -1,13 +1,14 @@
import {useMemo} from 'react' import {useMemo} from 'react'
import {Platform, View} from 'react-native' import {Platform, View} from 'react-native'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {Trans, useLingui} from '@lingui/react/macro'
import {Trans} from '@lingui/react/macro'
import {HITSLOP_30} from '#/lib/constants'
import {codeToLanguageName, languageName} from '#/locale/helpers' import {codeToLanguageName, languageName} from '#/locale/helpers'
import {LANGUAGES} from '#/locale/languages' import {LANGUAGES} from '#/locale/languages'
import {useLanguagePrefs} from '#/state/preferences' import {useLanguagePrefs} from '#/state/preferences'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, native, useTheme} from '#/alf'
import {Button} from '#/components/Button'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import * as Select from '#/components/Select' import * as Select from '#/components/Select'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
@@ -16,24 +17,43 @@ import {useTranslateOnDevice} from '#/translation'
export function TranslatedPost({ export function TranslatedPost({
postText, postText,
hideLoading = false, standalone = false,
}: { }: {
postText: string postText: string
hideLoading: boolean /**
* Hides the loading state, and adds its own "hide translation button"
*/
standalone?: boolean
}) { }) {
const {translationState} = useTranslateOnDevice() const {translationState, clearTranslation} = useTranslateOnDevice()
const {t: l} = useLingui()
const t = useTheme()
if (translationState.status === 'loading' && !hideLoading) { if (translationState.status === 'loading' && standalone) {
return <TranslationLoading /> return <TranslationLoading />
} }
if (translationState.status === 'success') { if (translationState.status === 'success') {
return ( return (
<TranslationResult <>
postText={postText} <TranslationResult
sourceLanguage={translationState.sourceLanguage} postText={postText}
translatedText={translationState.translatedText} sourceLanguage={translationState.sourceLanguage}
/> translatedText={translationState.translatedText}
/>
{standalone && (
<Button
label={l`Hide translation`}
onPress={clearTranslation}
style={[a.self_start, a.mt_sm]}
hoverStyle={native({opacity: 0.5})}
hitSlop={HITSLOP_30}>
<Text style={[a.text_sm, {color: t.palette.primary_500}]}>
<Trans>Hide translation</Trans>
</Text>
</Button>
)}
</>
) )
} }
@@ -45,7 +65,7 @@ function TranslationLoading() {
return ( return (
<View style={[a.flex_row, a.align_center, a.gap_sm, a.py_xs]}> <View style={[a.flex_row, a.align_center, a.gap_sm, a.py_xs]}>
<Loader size="sm" /> <Loader size="xs" />
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}> <Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
<Trans>Translating…</Trans> <Trans>Translating…</Trans>
</Text> </Text>
@@ -420,7 +420,7 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
shouldProxyLinks={true} shouldProxyLinks={true}
/> />
) : undefined} ) : undefined}
<TranslatedPost postText={record.text} hideLoading /> <TranslatedPost postText={record.text} />
<TranslateLink post={item.value.post} /> <TranslateLink post={item.value.post} />
{post.embed && ( {post.embed && (
<View style={[a.py_xs]}> <View style={[a.py_xs]}>
@@ -617,7 +617,7 @@ function TranslateLink({
) )
return ( return (
needsTranslation && ( (needsTranslation || translationState.status !== 'idle') && (
<View style={[a.gap_md, a.pt_md, a.align_start]}> <View style={[a.gap_md, a.pt_md, a.align_start]}>
{translationState.status === 'loading' ? ( {translationState.status === 'loading' ? (
<View style={[a.flex_row, a.align_center, a.gap_xs]}> <View style={[a.flex_row, a.align_center, a.gap_xs]}>
+26 -21
View File
@@ -42,12 +42,14 @@ import {Embed} from '#/components/Post/Embed'
import {PostEmbedViewContext} from '#/components/Post/Embed/types' import {PostEmbedViewContext} from '#/components/Post/Embed/types'
import {PostRepliedTo} from '#/components/Post/PostRepliedTo' import {PostRepliedTo} from '#/components/Post/PostRepliedTo'
import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton' import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton'
import {TranslatedPost} from '#/components/Post/Translated'
import {PostControls} from '#/components/PostControls' import {PostControls} from '#/components/PostControls'
import {DiscoverDebug} from '#/components/PostControls/DiscoverDebug' import {DiscoverDebug} from '#/components/PostControls/DiscoverDebug'
import {RichText} from '#/components/RichText' import {RichText} from '#/components/RichText'
import {SubtleHover} from '#/components/SubtleHover' import {SubtleHover} from '#/components/SubtleHover'
import {useAnalytics} from '#/analytics' import {useAnalytics} from '#/analytics'
import {useActorStatus} from '#/features/liveNow' import {useActorStatus} from '#/features/liveNow'
import {Provider as OnDeviceTranslationProvider} from '#/translation'
import * as bsky from '#/types/bsky' import * as bsky from '#/types/bsky'
import {PostFeedReason} from './PostFeedReason' import {PostFeedReason} from './PostFeedReason'
@@ -108,27 +110,29 @@ export function PostFeedItem({
} }
if (richText && moderation) { if (richText && moderation) {
return ( return (
<FeedItemInner <OnDeviceTranslationProvider>
// Safeguard from clobbering per-post state below: <FeedItemInner
key={postShadowed.uri} // Safeguard from clobbering per-post state below:
post={postShadowed} key={postShadowed.uri}
record={record} post={postShadowed}
reason={reason} record={record}
feedContext={feedContext} reason={reason}
reqId={reqId} feedContext={feedContext}
richText={richText} reqId={reqId}
parentAuthor={parentAuthor} richText={richText}
showReplyTo={showReplyTo} parentAuthor={parentAuthor}
moderation={moderation} showReplyTo={showReplyTo}
isThreadChild={isThreadChild} moderation={moderation}
isThreadLastChild={isThreadLastChild} isThreadChild={isThreadChild}
isThreadParent={isThreadParent} isThreadLastChild={isThreadLastChild}
hideTopBorder={hideTopBorder} isThreadParent={isThreadParent}
isParentBlocked={isParentBlocked} hideTopBorder={hideTopBorder}
isParentNotFound={isParentNotFound} isParentBlocked={isParentBlocked}
rootPost={rootPost} isParentNotFound={isParentNotFound}
onShowLess={onShowLess} rootPost={rootPost}
/> onShowLess={onShowLess}
/>
</OnDeviceTranslationProvider>
) )
} }
return null return null
@@ -479,6 +483,7 @@ let PostContent = ({
{limitLines && ( {limitLines && (
<ShowMoreTextButton style={[a.text_md]} onPress={onPressShowMore} /> <ShowMoreTextButton style={[a.text_md]} onPress={onPressShowMore} />
)} )}
<TranslatedPost postText={richText.text} standalone />
</View> </View>
) : undefined} ) : undefined}
{postEmbed ? ( {postEmbed ? (