From 45df50ec195c7b0f1b5a10aca10c23bfeee644dd Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 17 Mar 2026 13:59:28 -0500 Subject: [PATCH] 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 | 37 +++--- .../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, 251 insertions(+), 174 deletions(-) diff --git a/src/analytics/metrics/types.ts b/src/analytics/metrics/types.ts index 494c7ee1d9..248965fd6d 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 1100c81bfa..9aff552bbf 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 4f0fbbb2a5..257c1dcb41 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 1c6a721775..dda99c77e2 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 9f41a4e894..a8f1875192 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 034a38f9b6..05802016ff 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 352df445f6..c859148b0e 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 = { - 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). + * A unique key to identify this translation instance e.g. the post URI */ - postLangCodes?: string[] + key: string + /** + * Override the default behavior and always use Google Translate. + */ + 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 f17a832461..f0eca21224 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 && ( ) : undefined} - + {post.embed && ( ) : null} - + {post.embed && ( ) : undefined} - + {post.embed ? ( ) : undefined} - {record && ( - - )} + {record && } {postEmbed ? (