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 {Platform, View} from 'react-native'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
import {Trans, useLingui} from '@lingui/react/macro'
import {HITSLOP_30} from '#/lib/constants'
import {codeToLanguageName, languageName} from '#/locale/helpers'
import {LANGUAGES} from '#/locale/languages'
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 * as Select from '#/components/Select'
import {Text} from '#/components/Typography'
@@ -16,24 +17,43 @@ import {useTranslateOnDevice} from '#/translation'
export function TranslatedPost({
postText,
hideLoading = false,
standalone = false,
}: {
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 />
}
if (translationState.status === 'success') {
return (
<TranslationResult
postText={postText}
sourceLanguage={translationState.sourceLanguage}
translatedText={translationState.translatedText}
/>
<>
<TranslationResult
postText={postText}
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 (
<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]}>
<Trans>Translating…</Trans>
</Text>
@@ -420,7 +420,7 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
shouldProxyLinks={true}
/>
) : undefined}
<TranslatedPost postText={record.text} hideLoading />
<TranslatedPost postText={record.text} />
<TranslateLink post={item.value.post} />
{post.embed && (
<View style={[a.py_xs]}>
@@ -617,7 +617,7 @@ function TranslateLink({
)
return (
needsTranslation && (
(needsTranslation || translationState.status !== 'idle') && (
<View style={[a.gap_md, a.pt_md, a.align_start]}>
{translationState.status === 'loading' ? (
<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 {PostRepliedTo} from '#/components/Post/PostRepliedTo'
import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton'
import {TranslatedPost} from '#/components/Post/Translated'
import {PostControls} from '#/components/PostControls'
import {DiscoverDebug} from '#/components/PostControls/DiscoverDebug'
import {RichText} from '#/components/RichText'
import {SubtleHover} from '#/components/SubtleHover'
import {useAnalytics} from '#/analytics'
import {useActorStatus} from '#/features/liveNow'
import {Provider as OnDeviceTranslationProvider} from '#/translation'
import * as bsky from '#/types/bsky'
import {PostFeedReason} from './PostFeedReason'
@@ -108,27 +110,29 @@ export function PostFeedItem({
}
if (richText && moderation) {
return (
<FeedItemInner
// Safeguard from clobbering per-post state below:
key={postShadowed.uri}
post={postShadowed}
record={record}
reason={reason}
feedContext={feedContext}
reqId={reqId}
richText={richText}
parentAuthor={parentAuthor}
showReplyTo={showReplyTo}
moderation={moderation}
isThreadChild={isThreadChild}
isThreadLastChild={isThreadLastChild}
isThreadParent={isThreadParent}
hideTopBorder={hideTopBorder}
isParentBlocked={isParentBlocked}
isParentNotFound={isParentNotFound}
rootPost={rootPost}
onShowLess={onShowLess}
/>
<OnDeviceTranslationProvider>
<FeedItemInner
// Safeguard from clobbering per-post state below:
key={postShadowed.uri}
post={postShadowed}
record={record}
reason={reason}
feedContext={feedContext}
reqId={reqId}
richText={richText}
parentAuthor={parentAuthor}
showReplyTo={showReplyTo}
moderation={moderation}
isThreadChild={isThreadChild}
isThreadLastChild={isThreadLastChild}
isThreadParent={isThreadParent}
hideTopBorder={hideTopBorder}
isParentBlocked={isParentBlocked}
isParentNotFound={isParentNotFound}
rootPost={rootPost}
onShowLess={onShowLess}
/>
</OnDeviceTranslationProvider>
)
}
return null
@@ -479,6 +483,7 @@ let PostContent = ({
{limitLines && (
<ShowMoreTextButton style={[a.text_md]} onPress={onPressShowMore} />
)}
<TranslatedPost postText={richText.text} standalone />
</View>
) : undefined}
{postEmbed ? (