Make metrics really really clear

This commit is contained in:
Eric Bailey
2026-03-17 13:11:58 -05:00
parent c81214aeee
commit 389bea7c93
12 changed files with 142 additions and 126 deletions
+34 -4
View File
@@ -708,8 +708,19 @@ export type Events = {
translate: { translate: {
os: Platform['OS'] 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 textLength: number
} }
'translate:result': { 'translate:result': {
@@ -752,8 +763,27 @@ export type Events = {
} }
'translate:override': { 'translate:override': {
os: Platform['OS'] 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': { 'postMenu:openMuteWordsDialog': {
+63 -57
View File
@@ -1,12 +1,14 @@
import {useCallback, useMemo} from 'react' import {useCallback, useMemo} from 'react'
import {Platform, type StyleProp, type TextStyle, View} from 'react-native' 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 {Trans, useLingui} from '@lingui/react/macro'
import {HITSLOP_30} from '#/lib/constants' import {HITSLOP_30} from '#/lib/constants'
import {useGoogleTranslate} from '#/lib/hooks/useGoogleTranslate'
import {useTranslate} from '#/lib/translation' import {useTranslate} from '#/lib/translation'
import {type TranslationFunction} from '#/lib/translation' import {
type TranslationFunction,
type TranslationFunctionParams,
} from '#/lib/translation'
import { import {
codeToLanguageName, codeToLanguageName,
getPostLanguageTags, getPostLanguageTags,
@@ -26,26 +28,39 @@ import * as Select from '#/components/Select'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics' import {useAnalytics} from '#/analytics'
import {IS_WEB} from '#/env' import {IS_WEB} from '#/env'
import * as bsky from '#/types/bsky'
const X_ICON_OFFSET = 16 const X_ICON_OFFSET = 16
export function TranslatedPost({ export function TranslatedPost({
hideTranslateLink = false, hideTranslateLink = false,
post, post,
postText,
postTextStyle = a.text_md, postTextStyle = a.text_md,
}: { }: {
hideTranslateLink?: boolean hideTranslateLink?: boolean
post: AppBskyFeedDefs.PostView post: AppBskyFeedDefs.PostView
postText: string
postTextStyle?: StyleProp<TextStyle> postTextStyle?: StyleProp<TextStyle>
}) { }) {
const langPrefs = useLanguagePrefs() const langPrefs = useLanguagePrefs()
const {clearTranslation, translate, translationState} = useTranslate({ const {clearTranslation, translate, translationState} = useTranslate({
key: post.uri, key: post.uri,
postLangCodes: getPostLanguageTags(post),
}) })
const record = useMemo<AppBskyFeedPost.Record | undefined>(() => {
return bsky.dangerousIsType<AppBskyFeedPost.Record>(
post.record,
AppBskyFeedPost.isRecord,
)
? post.record
: undefined
}, [post])
const initialTranslationParams = useMemo<TranslationFunctionParams>(() => {
return {
text: record?.text || '',
targetLangCode: langPrefs.primaryLanguage,
possibleSourceLanguages: getPostLanguageTags(post),
}
}, [post, record, langPrefs])
const needsTranslation = useMemo(() => { const needsTranslation = useMemo(() => {
if (hideTranslateLink) return false if (hideTranslateLink) return false
return !isPostInLanguage(post, [langPrefs.primaryLanguage]) return !isPostInLanguage(post, [langPrefs.primaryLanguage])
@@ -57,11 +72,11 @@ export function TranslatedPost({
case 'success': case 'success':
return ( return (
<TranslationResult <TranslationResult
clearTranslation={clearTranslation}
translate={translate} translate={translate}
postText={postText} clearTranslation={clearTranslation}
initialTranslationParams={initialTranslationParams}
postTextStyle={postTextStyle} postTextStyle={postTextStyle}
sourceLanguage={ resultSourceLanguage={
translationState.sourceLanguage ?? null // Fallback primarily for iOS translationState.sourceLanguage ?? null // Fallback primarily for iOS
} }
translatedText={translationState.translatedText} translatedText={translationState.translatedText}
@@ -70,19 +85,18 @@ export function TranslatedPost({
case 'error': case 'error':
return ( return (
<TranslationError <TranslationError
translate={translate}
clearTranslation={clearTranslation} clearTranslation={clearTranslation}
message={translationState.message} message={translationState.message}
postText={postText} initialTranslationParams={initialTranslationParams}
primaryLanguage={langPrefs.primaryLanguage}
/> />
) )
default: default:
return ( return (
needsTranslation && ( needsTranslation && (
<TranslationLink <TranslationLink
postText={postText}
primaryLanguage={langPrefs.primaryLanguage}
translate={translate} translate={translate}
initialTranslationParams={initialTranslationParams}
/> />
) )
) )
@@ -105,31 +119,18 @@ function TranslationLoading() {
} }
function TranslationLink({ function TranslationLink({
postText,
primaryLanguage,
translate, translate,
initialTranslationParams,
}: { }: {
postText: string
primaryLanguage: string
translate: TranslationFunction translate: TranslationFunction
initialTranslationParams: TranslationFunctionParams
}) { }) {
const t = useTheme() const t = useTheme()
const {t: l} = useLingui() const {t: l} = useLingui()
const ax = useAnalytics()
const handleTranslate = useCallback(() => { const handleTranslate = useCallback(() => {
void translate({ void translate(initialTranslationParams)
text: postText, }, [initialTranslationParams, translate])
targetLangCode: primaryLanguage,
})
ax.metric('translate', {
os: Platform.OS,
sourceLanguages: [], // todo: get from post maybe?
targetLanguage: primaryLanguage,
textLength: postText.length,
})
}, [ax, postText, primaryLanguage, translate])
return ( return (
<View <View
@@ -161,22 +162,24 @@ function TranslationLink({
} }
function TranslationError({ function TranslationError({
translate,
clearTranslation, clearTranslation,
message, message,
postText, initialTranslationParams,
primaryLanguage,
}: { }: {
translate: TranslationFunction
clearTranslation: () => void clearTranslation: () => void
message: string message: string
postText: string initialTranslationParams: TranslationFunctionParams
primaryLanguage: string
}) { }) {
const t = useTheme() const t = useTheme()
const {t: l} = useLingui() const {t: l} = useLingui()
const translate = useGoogleTranslate()
const handleFallback = () => { const handleFallback = () => {
void translate(postText, primaryLanguage) void translate({
...initialTranslationParams,
forceGoogleTranslate: true,
})
} }
return ( return (
@@ -247,24 +250,24 @@ function TranslationError({
function TranslationResult({ function TranslationResult({
clearTranslation, clearTranslation,
translate, translate,
postText,
postTextStyle, postTextStyle,
sourceLanguage, resultSourceLanguage,
translatedText, translatedText,
initialTranslationParams,
}: { }: {
clearTranslation: () => void clearTranslation: () => void
translate: TranslationFunction translate: TranslationFunction
postText: string
postTextStyle?: StyleProp<TextStyle> postTextStyle?: StyleProp<TextStyle>
sourceLanguage: string | null resultSourceLanguage: string | null
translatedText: string translatedText: string
initialTranslationParams: TranslationFunctionParams
}) { }) {
const t = useTheme() const t = useTheme()
const langPrefs = useLanguagePrefs() const langPrefs = useLanguagePrefs()
const {i18n, t: l} = useLingui() const {i18n, t: l} = useLingui()
const langName = sourceLanguage const langName = resultSourceLanguage
? codeToLanguageName(sourceLanguage, i18n.locale) ? codeToLanguageName(resultSourceLanguage, i18n.locale)
: undefined : undefined
const flattenedStyle = flatten(postTextStyle) ?? {} const flattenedStyle = flatten(postTextStyle) ?? {}
@@ -323,7 +326,7 @@ function TranslationResult({
<Trans>Translated</Trans> <Trans>Translated</Trans>
</Text> </Text>
)} )}
{sourceLanguage != null && ( {resultSourceLanguage != null && (
<> <>
<Text <Text
style={[ style={[
@@ -336,9 +339,9 @@ function TranslationResult({
&middot;{' '} &middot;{' '}
</Text> </Text>
<TranslationLanguageSelect <TranslationLanguageSelect
sourceLanguage={sourceLanguage} resultSourceLanguage={resultSourceLanguage}
translate={translate} translate={translate}
postText={postText} initialTranslationParams={initialTranslationParams}
/> />
</> </>
)} )}
@@ -362,12 +365,12 @@ function TranslationResult({
function TranslationLanguageSelect({ function TranslationLanguageSelect({
translate, translate,
postText, resultSourceLanguage,
sourceLanguage, initialTranslationParams,
}: { }: {
translate: TranslationFunction translate: TranslationFunction
postText: string resultSourceLanguage: string
sourceLanguage: string initialTranslationParams: TranslationFunctionParams
}) { }) {
const t = useTheme() const t = useTheme()
const ax = useAnalytics() const ax = useAnalytics()
@@ -383,8 +386,8 @@ function TranslationLanguageSelect({
) )
.sort((a, b) => { .sort((a, b) => {
// Prioritize sourceLanguage at the top // Prioritize sourceLanguage at the top
if (a.code2 === sourceLanguage) return -1 if (a.code2 === resultSourceLanguage) return -1
if (b.code2 === sourceLanguage) return 1 if (b.code2 === resultSourceLanguage) return 1
// Localized sort // Localized sort
return languageName(a, langPrefs.appLanguage).localeCompare( return languageName(a, langPrefs.appLanguage).localeCompare(
languageName(b, langPrefs.appLanguage), 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 label: languageName(l, langPrefs.appLanguage), // The viewer may not be familiar with the source language, so localize the name
value: l.code2, value: l.code2,
})), })),
[langPrefs, sourceLanguage], [langPrefs, resultSourceLanguage],
) )
const handleChangeTranslationLanguage = (sourceLangCode: string) => { const handleChangeTranslationLanguage = (sourceLangCode: string) => {
ax.metric('translate:override', { ax.metric('translate:override', {
os: Platform.OS, os: Platform.OS,
sourceLanguage: sourceLangCode, possibleSourceLanguages: initialTranslationParams.possibleSourceLanguages,
targetLanguage: langPrefs.primaryLanguage, expectedSourceLanguage: sourceLangCode,
expectedTargetLanguage: initialTranslationParams.targetLangCode,
resultSourceLanguage,
}) })
void translate({ void translate({
text: postText, text: initialTranslationParams.text,
targetLangCode: langPrefs.primaryLanguage, targetLangCode: initialTranslationParams.targetLangCode,
sourceLangCode, sourceLangCode,
possibleSourceLanguages: initialTranslationParams.possibleSourceLanguages,
}) })
} }
return ( return (
<Select.Root <Select.Root
value={sourceLanguage} value={resultSourceLanguage}
onValueChange={handleChangeTranslationLanguage}> onValueChange={handleChangeTranslationLanguage}>
<Select.Trigger label={l`Change the source language`}> <Select.Trigger label={l`Change the source language`}>
{({props}) => { {({props}) => {
@@ -8,7 +8,7 @@ import {
import * as Clipboard from 'expo-clipboard' import * as Clipboard from 'expo-clipboard'
import { import {
type AppBskyFeedDefs, type AppBskyFeedDefs,
AppBskyFeedPost, type AppBskyFeedPost,
type AppBskyFeedThreadgate, type AppBskyFeedThreadgate,
AtUri, AtUri,
type RichText as RichTextAPI, type RichText as RichTextAPI,
@@ -96,7 +96,6 @@ import * as Prompt from '#/components/Prompt'
import * as Toast from '#/components/Toast' import * as Toast from '#/components/Toast'
import {useAnalytics} from '#/analytics' import {useAnalytics} from '#/analytics'
import {IS_INTERNAL} from '#/env' import {IS_INTERNAL} from '#/env'
import * as bsky from '#/types/bsky'
let PostMenuItems = ({ let PostMenuItems = ({
post, post,
@@ -138,7 +137,6 @@ let PostMenuItems = ({
const openLink = useOpenLink() const openLink = useOpenLink()
const {clearTranslation, translate, translationState} = useTranslate({ const {clearTranslation, translate, translationState} = useTranslate({
key: post.uri, key: post.uri,
postLangCodes: getPostLanguageTags(post),
forceGoogleTranslate, forceGoogleTranslate,
}) })
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
@@ -280,21 +278,8 @@ let PostMenuItems = ({
void translate({ void translate({
text: record.text, text: record.text,
targetLangCode: langPrefs.primaryLanguage, targetLangCode: langPrefs.primaryLanguage,
possibleSourceLanguages: getPostLanguageTags(post),
}) })
if (
bsky.dangerousIsType<AppBskyFeedPost.Record>(
post.record,
AppBskyFeedPost.isRecord,
)
) {
ax.metric('translate', {
os: Platform.OS,
sourceLanguages: post.record.langs ?? [],
targetLanguage: langPrefs.primaryLanguage,
textLength: post.record.text.length,
})
}
} }
const onHidePost = () => { const onHidePost = () => {
+2 -2
View File
@@ -68,8 +68,8 @@ export let MessageContextMenu = ({
ax.metric('translate', { ax.metric('translate', {
os: Platform.OS, os: Platform.OS,
sourceLanguages: [], possibleSourceLanguages: [], // N/A for chats
targetLanguage: langPrefs.primaryLanguage, expectedTargetLanguage: langPrefs.primaryLanguage,
textLength: message.text.length, textLength: message.text.length,
}) })
}, [ax, langPrefs.primaryLanguage, message.text, translate]) }, [ax, langPrefs.primaryLanguage, message.text, translate])
+12 -7
View File
@@ -103,7 +103,6 @@ async function attemptTranslation(
export function useTranslate({ export function useTranslate({
key, key,
forceGoogleTranslate = false, forceGoogleTranslate = false,
postLangCodes,
}: TranslationOptions) { }: TranslationOptions) {
const context = useContext(Context) const context = useContext(Context)
if (!context) { if (!context) {
@@ -125,10 +124,9 @@ export function useTranslate({
...params, ...params,
key, key,
forceGoogleTranslate, forceGoogleTranslate,
postLangCodes,
}) })
}, },
[context, forceGoogleTranslate, key, postLangCodes], [context, forceGoogleTranslate, key],
) )
const clearTranslation = useCallback( const clearTranslation = useCallback(
@@ -213,9 +211,16 @@ export function Provider({children}: React.PropsWithChildren<unknown>) {
text, text,
targetLangCode, targetLangCode,
sourceLangCode: expectedSourceLanguage, sourceLangCode: expectedSourceLanguage,
postLangCodes, possibleSourceLanguages,
...options ...options
}) => { }) => {
ax.metric('translate', {
os: Platform.OS,
possibleSourceLanguages,
expectedTargetLanguage: targetLangCode,
textLength: text.length,
})
if (options?.forceGoogleTranslate || !HAS_ON_DEVICE_TRANSLATION) { if (options?.forceGoogleTranslate || !HAS_ON_DEVICE_TRANSLATION) {
await googleTranslate(text, targetLangCode, expectedSourceLanguage) await googleTranslate(text, targetLangCode, expectedSourceLanguage)
return return
@@ -237,7 +242,7 @@ export function Provider({children}: React.PropsWithChildren<unknown>) {
ax.metric('translate:result', { ax.metric('translate:result', {
success: true, success: true,
os: Platform.OS, os: Platform.OS,
possibleSourceLanguages: postLangCodes, possibleSourceLanguages,
expectedSourceLanguage: expectedSourceLanguage ?? null, expectedSourceLanguage: expectedSourceLanguage ?? null,
expectedTargetLanguage: targetLangCode, expectedTargetLanguage: targetLangCode,
resultSourceLanguage: result.sourceLanguage, resultSourceLanguage: result.sourceLanguage,
@@ -254,7 +259,7 @@ export function Provider({children}: React.PropsWithChildren<unknown>) {
translatedText: result.translatedText, translatedText: result.translatedText,
sourceLanguage: result.sourceLanguage, sourceLanguage: result.sourceLanguage,
targetLanguage: result.targetLanguage, targetLanguage: result.targetLanguage,
postLanguages: postLangCodes, postLanguages: possibleSourceLanguages,
}, },
})) }))
} catch (e) { } catch (e) {
@@ -264,7 +269,7 @@ export function Provider({children}: React.PropsWithChildren<unknown>) {
ax.metric('translate:result', { ax.metric('translate:result', {
success: false, success: false,
os: Platform.OS, os: Platform.OS,
possibleSourceLanguages: postLangCodes, possibleSourceLanguages,
expectedSourceLanguage: expectedSourceLanguage ?? null, expectedSourceLanguage: expectedSourceLanguage ?? null,
expectedTargetLanguage: targetLangCode, expectedTargetLanguage: targetLangCode,
resultSourceLanguage: null, resultSourceLanguage: null,
+9 -4
View File
@@ -22,7 +22,7 @@ const clearTranslation = (_key: string) => {}
/** /**
* Web always opens Google Translate. * Web always opens Google Translate.
*/ */
export function useTranslate({key, postLangCodes}: TranslationOptions) { export function useTranslate({key}: TranslationOptions) {
const context = useContext(Context) const context = useContext(Context)
if (!context) { if (!context) {
throw new Error( throw new Error(
@@ -37,10 +37,9 @@ export function useTranslate({key, postLangCodes}: TranslationOptions) {
...params, ...params,
key, key,
forceGoogleTranslate: true, forceGoogleTranslate: true,
postLangCodes,
}) })
}, },
[key, context, postLangCodes], [key, context],
) )
const clearTranslation = useCallback(() => { const clearTranslation = useCallback(() => {
@@ -61,7 +60,13 @@ export function Provider({children}: React.PropsWithChildren<unknown>) {
const googleTranslate = useGoogleTranslate() const googleTranslate = useGoogleTranslate()
const translate = useCallback<ContextType['translate']>( const translate = useCallback<ContextType['translate']>(
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) await googleTranslate(text, targetLangCode, sourceLangCode)
}, },
[ax, googleTranslate], [ax, googleTranslate],
+15 -8
View File
@@ -30,19 +30,26 @@ export type TranslationFunctionParams = {
* source language and want to specify it explicitly. * source language and want to specify it explicitly.
*/ */
sourceLangCode?: string 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 = { export type TranslationOptions = {
key: string
forceGoogleTranslate?: boolean
/** /**
* The language(s) of the post being translated. Used for analytics purposes * A unique key to identify this translation instance e.g. the post URI
* 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[] key: string
/**
* Override the default behavior and always use Google Translate.
*/
forceGoogleTranslate?: boolean
} }
export type TranslationFunction = ( export type TranslationFunction = (
@@ -411,11 +411,7 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
shouldProxyLinks={true} shouldProxyLinks={true}
/> />
) : undefined} ) : undefined}
<TranslatedPost <TranslatedPost post={post} postTextStyle={[a.text_lg]} />
post={post}
postText={record.text}
postTextStyle={[a.text_lg]}
/>
{post.embed && ( {post.embed && (
<View style={[a.py_xs]}> <View style={[a.py_xs]}>
<Embed <Embed
@@ -321,11 +321,7 @@ const ThreadItemPostInner = memo(function ThreadItemPostInner({
)} )}
</View> </View>
) : undefined} ) : undefined}
<TranslatedPost <TranslatedPost hideTranslateLink post={post} />
hideTranslateLink={true}
post={post}
postText={record.text}
/>
{post.embed && ( {post.embed && (
<View style={[a.pb_xs]}> <View style={[a.pb_xs]}>
<Embed <Embed
@@ -361,11 +361,7 @@ const ThreadItemTreePostInner = memo(function ThreadItemTreePostInner({
)} )}
</View> </View>
) : null} ) : null}
<TranslatedPost <TranslatedPost hideTranslateLink post={post} />
hideTranslateLink={true}
post={post}
postText={record.text}
/>
{post.embed && ( {post.embed && (
<View style={[a.pb_xs]}> <View style={[a.pb_xs]}>
<Embed <Embed
+1 -5
View File
@@ -219,11 +219,7 @@ function PostInner({
)} )}
</View> </View>
) : undefined} ) : undefined}
<TranslatedPost <TranslatedPost hideTranslateLink post={post} />
hideTranslateLink={true}
post={post}
postText={record.text}
/>
{post.embed ? ( {post.embed ? (
<Embed <Embed
embed={post.embed} embed={post.embed}
+1 -7
View File
@@ -490,13 +490,7 @@ let PostContent = ({
)} )}
</View> </View>
) : undefined} ) : undefined}
{record && ( {record && <TranslatedPost hideTranslateLink post={post} />}
<TranslatedPost
hideTranslateLink={true}
post={post}
postText={record.text}
/>
)}
{postEmbed ? ( {postEmbed ? (
<View style={[a.pb_xs]}> <View style={[a.pb_xs]}>
<Embed <Embed