Make metrics really really clear
This commit is contained in:
@@ -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': {
|
||||
|
||||
@@ -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({
|
||||
·{' '}
|
||||
</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 = () => {
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user