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: {
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': {
+63 -57
View File
@@ -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<TextStyle>
}) {
const langPrefs = useLanguagePrefs()
const {clearTranslation, translate, translationState} = useTranslate({
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(() => {
if (hideTranslateLink) return false
return !isPostInLanguage(post, [langPrefs.primaryLanguage])
@@ -57,11 +72,11 @@ export function TranslatedPost({
case 'success':
return (
<TranslationResult
clearTranslation={clearTranslation}
translate={translate}
postText={postText}
clearTranslation={clearTranslation}
initialTranslationParams={initialTranslationParams}
postTextStyle={postTextStyle}
sourceLanguage={
resultSourceLanguage={
translationState.sourceLanguage ?? null // Fallback primarily for iOS
}
translatedText={translationState.translatedText}
@@ -70,19 +85,18 @@ export function TranslatedPost({
case 'error':
return (
<TranslationError
translate={translate}
clearTranslation={clearTranslation}
message={translationState.message}
postText={postText}
primaryLanguage={langPrefs.primaryLanguage}
initialTranslationParams={initialTranslationParams}
/>
)
default:
return (
needsTranslation && (
<TranslationLink
postText={postText}
primaryLanguage={langPrefs.primaryLanguage}
translate={translate}
initialTranslationParams={initialTranslationParams}
/>
)
)
@@ -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 (
<View
@@ -161,22 +162,24 @@ function TranslationLink({
}
function TranslationError({
translate,
clearTranslation,
message,
postText,
primaryLanguage,
initialTranslationParams,
}: {
translate: TranslationFunction
clearTranslation: () => 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<TextStyle>
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({
<Trans>Translated</Trans>
</Text>
)}
{sourceLanguage != null && (
{resultSourceLanguage != null && (
<>
<Text
style={[
@@ -336,9 +339,9 @@ function TranslationResult({
&middot;{' '}
</Text>
<TranslationLanguageSelect
sourceLanguage={sourceLanguage}
resultSourceLanguage={resultSourceLanguage}
translate={translate}
postText={postText}
initialTranslationParams={initialTranslationParams}
/>
</>
)}
@@ -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 (
<Select.Root
value={sourceLanguage}
value={resultSourceLanguage}
onValueChange={handleChangeTranslationLanguage}>
<Select.Trigger label={l`Change the source language`}>
{({props}) => {
@@ -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<NavigationProp>()
@@ -280,21 +278,8 @@ let PostMenuItems = ({
void translate({
text: record.text,
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 = () => {
+2 -2
View File
@@ -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])
+12 -7
View File
@@ -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<unknown>) {
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<unknown>) {
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<unknown>) {
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<unknown>) {
ax.metric('translate:result', {
success: false,
os: Platform.OS,
possibleSourceLanguages: postLangCodes,
possibleSourceLanguages,
expectedSourceLanguage: expectedSourceLanguage ?? null,
expectedTargetLanguage: targetLangCode,
resultSourceLanguage: null,
+9 -4
View File
@@ -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<unknown>) {
const googleTranslate = useGoogleTranslate()
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)
},
[ax, googleTranslate],
+15 -8
View File
@@ -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 = (
@@ -411,11 +411,7 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
shouldProxyLinks={true}
/>
) : undefined}
<TranslatedPost
post={post}
postText={record.text}
postTextStyle={[a.text_lg]}
/>
<TranslatedPost post={post} postTextStyle={[a.text_lg]} />
{post.embed && (
<View style={[a.py_xs]}>
<Embed
@@ -321,11 +321,7 @@ const ThreadItemPostInner = memo(function ThreadItemPostInner({
)}
</View>
) : undefined}
<TranslatedPost
hideTranslateLink={true}
post={post}
postText={record.text}
/>
<TranslatedPost hideTranslateLink post={post} />
{post.embed && (
<View style={[a.pb_xs]}>
<Embed
@@ -361,11 +361,7 @@ const ThreadItemTreePostInner = memo(function ThreadItemTreePostInner({
)}
</View>
) : null}
<TranslatedPost
hideTranslateLink={true}
post={post}
postText={record.text}
/>
<TranslatedPost hideTranslateLink post={post} />
{post.embed && (
<View style={[a.pb_xs]}>
<Embed
+1 -5
View File
@@ -219,11 +219,7 @@ function PostInner({
)}
</View>
) : undefined}
<TranslatedPost
hideTranslateLink={true}
post={post}
postText={record.text}
/>
<TranslatedPost hideTranslateLink post={post} />
{post.embed ? (
<Embed
embed={post.embed}
+1 -7
View File
@@ -490,13 +490,7 @@ let PostContent = ({
)}
</View>
) : undefined}
{record && (
<TranslatedPost
hideTranslateLink={true}
post={post}
postText={record.text}
/>
)}
{record && <TranslatedPost hideTranslateLink post={post} />}
{postEmbed ? (
<View style={[a.pb_xs]}>
<Embed