diff --git a/src/analytics/metrics/types.ts b/src/analytics/metrics/types.ts index 8ad54fbcac..8c43b1d333 100644 --- a/src/analytics/metrics/types.ts +++ b/src/analytics/metrics/types.ts @@ -708,8 +708,19 @@ 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 } 'translate:result': { @@ -752,8 +763,27 @@ export type Events = { } '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 444bdb446d..4e137d0fc5 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 || '', + targetLangCode: 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,25 +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.targetLangCode, + resultSourceLanguage, }) void translate({ - text: postText, - targetLangCode: langPrefs.primaryLanguage, + text: initialTranslationParams.text, + targetLangCode: initialTranslationParams.targetLangCode, sourceLangCode, + possibleSourceLanguages: initialTranslationParams.possibleSourceLanguages, }) } return ( {({props}) => { diff --git a/src/components/PostControls/PostMenu/PostMenuItems.tsx b/src/components/PostControls/PostMenu/PostMenuItems.tsx index 4f0fbbb2a5..fd5e09546e 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() @@ -280,21 +278,8 @@ let PostMenuItems = ({ void translate({ text: record.text, targetLangCode: 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..6dc0dbabb2 100644 --- a/src/components/dms/MessageContextMenu.tsx +++ b/src/components/dms/MessageContextMenu.tsx @@ -68,8 +68,8 @@ 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, }) }, [ax, langPrefs.primaryLanguage, message.text, translate]) diff --git a/src/lib/translation/index.tsx b/src/lib/translation/index.tsx index 5d8e1989f2..d6a78707de 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) { @@ -125,10 +124,9 @@ export function useTranslate({ ...params, key, forceGoogleTranslate, - postLangCodes, }) }, - [context, forceGoogleTranslate, key, postLangCodes], + [context, forceGoogleTranslate, key], ) const clearTranslation = useCallback( @@ -213,9 +211,16 @@ export function Provider({children}: React.PropsWithChildren) { text, targetLangCode, sourceLangCode: expectedSourceLanguage, - postLangCodes, + possibleSourceLanguages, ...options }) => { + ax.metric('translate', { + os: Platform.OS, + possibleSourceLanguages, + expectedTargetLanguage: targetLangCode, + textLength: text.length, + }) + if (options?.forceGoogleTranslate || !HAS_ON_DEVICE_TRANSLATION) { await googleTranslate(text, targetLangCode, expectedSourceLanguage) return @@ -237,7 +242,7 @@ export function Provider({children}: React.PropsWithChildren) { ax.metric('translate:result', { success: true, os: Platform.OS, - possibleSourceLanguages: postLangCodes, + possibleSourceLanguages, expectedSourceLanguage: expectedSourceLanguage ?? null, expectedTargetLanguage: targetLangCode, resultSourceLanguage: result.sourceLanguage, @@ -254,7 +259,7 @@ export function Provider({children}: React.PropsWithChildren) { translatedText: result.translatedText, sourceLanguage: result.sourceLanguage, targetLanguage: result.targetLanguage, - postLanguages: postLangCodes, + postLanguages: possibleSourceLanguages, }, })) } catch (e) { @@ -264,7 +269,7 @@ export function Provider({children}: React.PropsWithChildren) { ax.metric('translate:result', { success: false, os: Platform.OS, - possibleSourceLanguages: postLangCodes, + possibleSourceLanguages, expectedSourceLanguage: expectedSourceLanguage ?? null, expectedTargetLanguage: targetLangCode, resultSourceLanguage: null, diff --git a/src/lib/translation/index.web.tsx b/src/lib/translation/index.web.tsx index 034a38f9b6..4935159e01 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( @@ -37,10 +37,9 @@ export function useTranslate({key, postLangCodes}: TranslationOptions) { ...params, key, forceGoogleTranslate: true, - postLangCodes, }) }, - [key, context, postLangCodes], + [key, context], ) const clearTranslation = useCallback(() => { @@ -61,7 +60,13 @@ export function Provider({children}: React.PropsWithChildren) { const googleTranslate = useGoogleTranslate() const translate = useCallback( - async ({text, targetLangCode, sourceLangCode}) => { + async ({text, targetLangCode, sourceLangCode, possibleSourceLanguages}) => { + ax.metric('translate', { + os: 'web', + possibleSourceLanguages, + expectedTargetLanguage: targetLangCode, + textLength: text.length, + }) await googleTranslate(text, targetLangCode, sourceLangCode) }, [ax, googleTranslate], diff --git a/src/lib/translation/types.ts b/src/lib/translation/types.ts index c2ff9f0c35..8882d86505 100644 --- a/src/lib/translation/types.ts +++ b/src/lib/translation/types.ts @@ -30,19 +30,26 @@ export type TranslationFunctionParams = { * source language and want to specify it explicitly. */ sourceLangCode?: string + /** + * The languages the content might be in, such as the user-supplied + * language codes on posts. Currently only available on posts. + */ + 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 = ( 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 ? (