Log languages a post is tagged with after translating (#10014)

Co-authored-by: Eric Bailey <git@esb.lol>
This commit is contained in:
DS Boyce
2026-03-17 10:31:23 -07:00
committed by GitHub
parent be0d00de17
commit 1386a559b7
11 changed files with 102 additions and 81 deletions
+1 -1
View File
@@ -85,7 +85,7 @@
"@braintree/sanitize-url": "^6.0.2", "@braintree/sanitize-url": "^6.0.2",
"@bsky.app/alf": "^0.1.7", "@bsky.app/alf": "^0.1.7",
"@bsky.app/expo-image-crop-tool": "^0.5.0", "@bsky.app/expo-image-crop-tool": "^0.5.0",
"@bsky.app/expo-translate-text": "^0.2.7", "@bsky.app/expo-translate-text": "^0.2.9",
"@bsky.app/react-native-mmkv": "2.12.5", "@bsky.app/react-native-mmkv": "2.12.5",
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet", "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
"@emoji-mart/data": "^1.2.1", "@emoji-mart/data": "^1.2.1",
+6 -1
View File
@@ -707,15 +707,20 @@ export type Events = {
'reportDialog:failure': {} 'reportDialog:failure': {}
translate: { translate: {
os: Platform['OS']
sourceLanguages: string[] sourceLanguages: string[]
targetLanguage: string targetLanguage: string
textLength: number textLength: number
} }
'translate:result': { 'translate:result': {
method: 'on-device' | 'google-translate' | 'fallback-alert' method: 'on-device' | 'fallback-alert'
os: Platform['OS'] os: Platform['OS']
sourceSelection: 'automatic' | 'manual'
sourceLanguage: string | null sourceLanguage: string | null
targetLanguage: string targetLanguage: string
/* Only relevant to posts */
postLanguages?: string[]
} }
'translate:override': { 'translate:override': {
os: Platform['OS'] os: Platform['OS']
+4
View File
@@ -9,6 +9,7 @@ import {useTranslate} from '#/lib/translation'
import {type TranslationFunction} from '#/lib/translation' import {type TranslationFunction} from '#/lib/translation'
import { import {
codeToLanguageName, codeToLanguageName,
getPostLanguageTags,
isPostInLanguage, isPostInLanguage,
languageName, languageName,
} from '#/locale/helpers' } from '#/locale/helpers'
@@ -42,6 +43,7 @@ export function TranslatedPost({
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 needsTranslation = useMemo(() => { const needsTranslation = useMemo(() => {
@@ -122,6 +124,7 @@ function TranslationLink({
}) })
ax.metric('translate', { ax.metric('translate', {
os: Platform.OS,
sourceLanguages: [], // todo: get from post maybe? sourceLanguages: [], // todo: get from post maybe?
targetLanguage: primaryLanguage, targetLanguage: primaryLanguage,
textLength: postText.length, textLength: postText.length,
@@ -405,6 +408,7 @@ function TranslationLanguageSelect({
text: postText, text: postText,
targetLangCode: langPrefs.primaryLanguage, targetLangCode: langPrefs.primaryLanguage,
sourceLangCode, sourceLangCode,
sourceSelection: 'manual',
}) })
} }
@@ -28,6 +28,7 @@ import {
import {richTextToString} from '#/lib/strings/rich-text-helpers' import {richTextToString} from '#/lib/strings/rich-text-helpers'
import {toShareUrl} from '#/lib/strings/url-helpers' import {toShareUrl} from '#/lib/strings/url-helpers'
import {useTranslate} from '#/lib/translation' import {useTranslate} from '#/lib/translation'
import {getPostLanguageTags} from '#/locale/helpers'
import {logger} from '#/logger' import {logger} from '#/logger'
import {type Shadow} from '#/state/cache/post-shadow' import {type Shadow} from '#/state/cache/post-shadow'
import {useProfileShadow} from '#/state/cache/profile-shadow' import {useProfileShadow} from '#/state/cache/profile-shadow'
@@ -137,6 +138,7 @@ 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>()
@@ -287,6 +289,7 @@ let PostMenuItems = ({
) )
) { ) {
ax.metric('translate', { ax.metric('translate', {
os: Platform.OS,
sourceLanguages: post.record.langs ?? [], sourceLanguages: post.record.langs ?? [],
targetLanguage: langPrefs.primaryLanguage, targetLanguage: langPrefs.primaryLanguage,
textLength: post.record.text.length, textLength: post.record.text.length,
+2 -1
View File
@@ -1,5 +1,5 @@
import {memo, useCallback} from 'react' import {memo, useCallback} from 'react'
import {LayoutAnimation} from 'react-native' import {LayoutAnimation, Platform} from 'react-native'
import * as Clipboard from 'expo-clipboard' import * as Clipboard from 'expo-clipboard'
import {type ChatBskyConvoDefs, RichText} from '@atproto/api' import {type ChatBskyConvoDefs, RichText} from '@atproto/api'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
@@ -67,6 +67,7 @@ export let MessageContextMenu = ({
void translate(message.text, langPrefs.primaryLanguage) void translate(message.text, langPrefs.primaryLanguage)
ax.metric('translate', { ax.metric('translate', {
os: Platform.OS,
sourceLanguages: [], sourceLanguages: [],
targetLanguage: langPrefs.primaryLanguage, targetLanguage: langPrefs.primaryLanguage,
textLength: message.text.length, textLength: message.text.length,
+2 -12
View File
@@ -1,16 +1,6 @@
import {createContext} from 'react' import {createContext} from 'react'
import {type TranslationFunctionParams, type TranslationState} from './types' import {type ContextType} from './types'
export const Context = createContext<{ export const Context = createContext<ContextType | null>(null)
translationState: Record<string, TranslationState>
translate: (
parameters: TranslationFunctionParams & {
key: string
forceGoogleTranslate: boolean
},
) => Promise<void>
clearTranslation: (key: string) => void
acquireTranslation: (key: string) => () => void
} | null>(null)
Context.displayName = 'TranslationContext' Context.displayName = 'TranslationContext'
+25 -22
View File
@@ -11,7 +11,12 @@ import {logger} from '#/logger'
import {useAnalytics} from '#/analytics' import {useAnalytics} from '#/analytics'
import {HAS_ON_DEVICE_TRANSLATION, IS_ANDROID, IS_IOS} from '#/env' import {HAS_ON_DEVICE_TRANSLATION, IS_ANDROID, IS_IOS} from '#/env'
import {Context} from './context' import {Context} from './context'
import {type TranslationFunctionParams, type TranslationState} from './types' import {
type ContextType,
type TranslationFunctionParams,
type TranslationOptions,
type TranslationState,
} from './types'
import {guessLanguage} from './utils' import {guessLanguage} from './utils'
export * from './types' export * from './types'
@@ -98,10 +103,8 @@ async function attemptTranslation(
export function useTranslate({ export function useTranslate({
key, key,
forceGoogleTranslate = false, forceGoogleTranslate = false,
}: { postLangCodes,
key: string }: TranslationOptions) {
forceGoogleTranslate?: boolean
}) {
const context = useContext(Context) const context = useContext(Context)
if (!context) { if (!context) {
throw new Error( throw new Error(
@@ -118,9 +121,14 @@ export function useTranslate({
const translate = useCallback( const translate = useCallback(
async (params: TranslationFunctionParams) => { async (params: TranslationFunctionParams) => {
return context.translate({...params, key, forceGoogleTranslate}) return context.translate({
...params,
key,
forceGoogleTranslate,
postLangCodes,
})
}, },
[context, forceGoogleTranslate, key], [context, forceGoogleTranslate, key, postLangCodes],
) )
const clearTranslation = useCallback( const clearTranslation = useCallback(
@@ -199,27 +207,17 @@ export function Provider({children}: React.PropsWithChildren<unknown>) {
}) })
}, []) }, [])
const translate = useCallback( const translate = useCallback<ContextType['translate']>(
async ({ async ({
key, key,
text, text,
targetLangCode, targetLangCode,
sourceLangCode, sourceLangCode,
sourceSelection = 'automatic',
postLangCodes,
...options ...options
}: {
key: string
text: string
targetLangCode: string
sourceLangCode?: string
forceGoogleTranslate?: boolean
}) => { }) => {
if (options?.forceGoogleTranslate || !HAS_ON_DEVICE_TRANSLATION) { if (options?.forceGoogleTranslate || !HAS_ON_DEVICE_TRANSLATION) {
ax.metric('translate:result', {
method: 'google-translate',
os: Platform.OS,
sourceLanguage: sourceLangCode ?? null,
targetLanguage: targetLangCode,
})
await googleTranslate(text, targetLangCode, sourceLangCode) await googleTranslate(text, targetLangCode, sourceLangCode)
return return
} }
@@ -240,8 +238,10 @@ export function Provider({children}: React.PropsWithChildren<unknown>) {
ax.metric('translate:result', { ax.metric('translate:result', {
method: 'on-device', method: 'on-device',
os: Platform.OS, os: Platform.OS,
sourceSelection,
sourceLanguage: result.sourceLanguage, sourceLanguage: result.sourceLanguage,
targetLanguage: result.targetLanguage, targetLanguage: result.targetLanguage,
postLanguages: postLangCodes,
}) })
if (!IS_ANDROID) { if (!IS_ANDROID) {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
@@ -253,17 +253,20 @@ 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,
}, },
})) }))
} catch (e) { } catch (e) {
logger.error('Failed to translate post on device', {safeMessage: e}) logger.error('Failed to translate text on device', {safeMessage: e})
// On-device translation failed (language pack missing or user // On-device translation failed (language pack missing or user
// dismissed the download prompt). Fall back to Google Translate. // dismissed the download prompt).
ax.metric('translate:result', { ax.metric('translate:result', {
method: 'fallback-alert', method: 'fallback-alert',
os: Platform.OS, os: Platform.OS,
sourceSelection,
sourceLanguage: sourceLangCode ?? null, sourceLanguage: sourceLangCode ?? null,
targetLanguage: targetLangCode, targetLanguage: targetLangCode,
postLanguages: postLangCodes,
}) })
let errorMessage = l`Device failed to translate :(` let errorMessage = l`Device failed to translate :(`
if (!IS_ANDROID) { if (!IS_ANDROID) {
+16 -26
View File
@@ -3,7 +3,12 @@ import {useCallback, useContext, useMemo} from 'react'
import {useGoogleTranslate} from '#/lib/hooks/useGoogleTranslate' import {useGoogleTranslate} from '#/lib/hooks/useGoogleTranslate'
import {useAnalytics} from '#/analytics' import {useAnalytics} from '#/analytics'
import {Context} from './context' import {Context} from './context'
import {type TranslationFunctionParams, type TranslationState} from './types' import {
type ContextType,
type TranslationFunctionParams,
type TranslationOptions,
type TranslationState,
} from './types'
export * from './types' export * from './types'
export * from './utils' export * from './utils'
@@ -17,12 +22,7 @@ const clearTranslation = (_key: string) => {}
/** /**
* Web always opens Google Translate. * Web always opens Google Translate.
*/ */
export function useTranslate({ export function useTranslate({key, postLangCodes}: TranslationOptions) {
key,
}: {
key: string
forceGoogleTranslate?: boolean
}) {
const context = useContext(Context) const context = useContext(Context)
if (!context) { if (!context) {
throw new Error( throw new Error(
@@ -33,9 +33,14 @@ export function useTranslate({
// Always call hooks in consistent order // Always call hooks in consistent order
const translate = useCallback( const translate = useCallback(
async (params: TranslationFunctionParams) => { async (params: TranslationFunctionParams) => {
return context.translate({...params, key, forceGoogleTranslate: true}) return context.translate({
...params,
key,
forceGoogleTranslate: true,
postLangCodes,
})
}, },
[key, context], [key, context, postLangCodes],
) )
const clearTranslation = useCallback(() => { const clearTranslation = useCallback(() => {
@@ -55,23 +60,8 @@ export function Provider({children}: React.PropsWithChildren<unknown>) {
const ax = useAnalytics() const ax = useAnalytics()
const googleTranslate = useGoogleTranslate() const googleTranslate = useGoogleTranslate()
const translate = useCallback( const translate = useCallback<ContextType['translate']>(
async ({ async ({text, targetLangCode, sourceLangCode}) => {
text,
targetLangCode,
sourceLangCode,
}: {
key: string
text: string
targetLangCode: string
sourceLangCode?: string
}) => {
ax.metric('translate:result', {
method: 'google-translate',
os: 'web',
sourceLanguage: sourceLangCode ?? null,
targetLanguage: targetLangCode,
})
await googleTranslate(text, targetLangCode, sourceLangCode) await googleTranslate(text, targetLangCode, sourceLangCode)
}, },
[ax, googleTranslate], [ax, googleTranslate],
+26
View File
@@ -27,8 +27,34 @@ export type TranslationFunctionParams = {
* The source language of the text. Will auto-detect if not provided. * The source language of the text. Will auto-detect if not provided.
*/ */
sourceLangCode?: string sourceLangCode?: string
/**
* Whether we auto-detected the language or it was selected manually. Defaults to 'automatic'.
*/
sourceSelection?: 'automatic' | 'manual'
}
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).
*/
postLangCodes?: string[]
} }
export type TranslationFunction = ( export type TranslationFunction = (
parameters: TranslationFunctionParams, parameters: TranslationFunctionParams,
) => Promise<void> ) => Promise<void>
export type ContextType = {
translationState: Record<string, TranslationState>
translate: (
parameters: TranslationFunctionParams & TranslationOptions,
) => Promise<void>
clearTranslation: (key: string) => void
acquireTranslation: (key: string) => () => void
}
+13 -14
View File
@@ -61,6 +61,14 @@ function getLocalizedLanguage(
} }
} }
export function getPostLanguageTags(post: AppBskyFeedDefs.PostView) {
return AppBskyFeedPost.isRecord(post.record) &&
hasProp(post.record, 'langs') &&
Array.isArray(post.record.langs)
? post.record.langs
: []
}
export function languageName(language: Language, appLang: string): string { export function languageName(language: Language, appLang: string): string {
// if Intl.DisplayNames is unavailable on the target, display the English name // if Intl.DisplayNames is unavailable on the target, display the English name
if (!Intl.DisplayNames) { if (!Intl.DisplayNames) {
@@ -80,22 +88,14 @@ export function codeToLanguageName(lang2or3: string, appLang: string): string {
export function getPostLanguage( export function getPostLanguage(
post: AppBskyFeedDefs.PostView, post: AppBskyFeedDefs.PostView,
): string | undefined { ): string | undefined {
let candidates: string[] = [] let candidates: string[] = getPostLanguageTags(post)
let postText: string = '' let postText: string = ''
if (hasProp(post.record, 'text') && typeof post.record.text === 'string') { if (hasProp(post.record, 'text') && typeof post.record.text === 'string') {
postText = post.record.text postText = post.record.text
} }
if (
AppBskyFeedPost.isRecord(post.record) &&
hasProp(post.record, 'langs') &&
Array.isArray(post.record.langs)
) {
candidates = post.record.langs
}
// if there's only one declared language, use that // if there's only one declared language, use that
if (candidates?.length === 1) { if (candidates.length === 1) {
return candidates[0] return candidates[0]
} }
@@ -108,11 +108,10 @@ export function getPostLanguage(
let langsProbabilityMap = lande(postText) let langsProbabilityMap = lande(postText)
// filter down using declared languages // filter down using declared languages
if (candidates?.length) { if (candidates.length) {
langsProbabilityMap = langsProbabilityMap.filter( langsProbabilityMap = langsProbabilityMap.filter(
([lang, _probability]: [string, number]) => { ([lang, _probability]: [string, number]) =>
return candidates.includes(code3ToCode2(lang)) candidates.includes(code3ToCode2(lang)),
},
) )
} }
+4 -4
View File
@@ -2403,10 +2403,10 @@
resolved "https://registry.yarnpkg.com/@bsky.app/expo-image-crop-tool/-/expo-image-crop-tool-0.5.0.tgz#4308fbde5c15e6be9122601797bc3d9549c95e31" resolved "https://registry.yarnpkg.com/@bsky.app/expo-image-crop-tool/-/expo-image-crop-tool-0.5.0.tgz#4308fbde5c15e6be9122601797bc3d9549c95e31"
integrity sha512-gmhQr2HWTRFyPO00fn5OmtiEVtikXusHMrN5Zoq26pu1VZX3zVE+aoc668etTqrvsQcm2Qu8fo96k5F3Wu+6wg== integrity sha512-gmhQr2HWTRFyPO00fn5OmtiEVtikXusHMrN5Zoq26pu1VZX3zVE+aoc668etTqrvsQcm2Qu8fo96k5F3Wu+6wg==
"@bsky.app/expo-translate-text@^0.2.7": "@bsky.app/expo-translate-text@^0.2.9":
version "0.2.7" version "0.2.9"
resolved "https://registry.yarnpkg.com/@bsky.app/expo-translate-text/-/expo-translate-text-0.2.7.tgz#e34811d0f0300f8808762e5676aa50790ca5d5e8" resolved "https://registry.yarnpkg.com/@bsky.app/expo-translate-text/-/expo-translate-text-0.2.9.tgz#4ed4552cd50bca7d02d14e706e419bd728d4ab51"
integrity sha512-J9zctP9hLxX0eustTKk5CBnCkk6cEdlu1s7GzUnpT65qkCSNbYqbbUCpcU2Z2S2dN/1+w6L/iHb+vmCEbZMOaQ== integrity sha512-VmqMhc/YavjgkGhxT/fB8mGSi+VZHJET1tsbpTg8peqKRXFSju2F294NsRxH/4aaMQFlt5oRfPCRnLm1H5o3lA==
"@bsky.app/react-native-mmkv@2.12.5": "@bsky.app/react-native-mmkv@2.12.5":
version "2.12.5" version "2.12.5"