Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

Co-authored-by: Eric Bailey <git@esb.lol>

authored by

DS Boyce
Eric Bailey
and committed by
GitHub
1386a559 be0d00de

+102 -81
+1 -1
package.json
··· 85 85 "@braintree/sanitize-url": "^6.0.2", 86 86 "@bsky.app/alf": "^0.1.7", 87 87 "@bsky.app/expo-image-crop-tool": "^0.5.0", 88 - "@bsky.app/expo-translate-text": "^0.2.7", 88 + "@bsky.app/expo-translate-text": "^0.2.9", 89 89 "@bsky.app/react-native-mmkv": "2.12.5", 90 90 "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet", 91 91 "@emoji-mart/data": "^1.2.1",
+6 -1
src/analytics/metrics/types.ts
··· 707 707 'reportDialog:failure': {} 708 708 709 709 translate: { 710 + os: Platform['OS'] 710 711 sourceLanguages: string[] 711 712 targetLanguage: string 712 713 textLength: number 713 714 } 714 715 'translate:result': { 715 - method: 'on-device' | 'google-translate' | 'fallback-alert' 716 + method: 'on-device' | 'fallback-alert' 716 717 os: Platform['OS'] 718 + sourceSelection: 'automatic' | 'manual' 717 719 sourceLanguage: string | null 718 720 targetLanguage: string 721 + 722 + /* Only relevant to posts */ 723 + postLanguages?: string[] 719 724 } 720 725 'translate:override': { 721 726 os: Platform['OS']
+4
src/components/Post/Translated/index.tsx
··· 9 9 import {type TranslationFunction} from '#/lib/translation' 10 10 import { 11 11 codeToLanguageName, 12 + getPostLanguageTags, 12 13 isPostInLanguage, 13 14 languageName, 14 15 } from '#/locale/helpers' ··· 42 43 const langPrefs = useLanguagePrefs() 43 44 const {clearTranslation, translate, translationState} = useTranslate({ 44 45 key: post.uri, 46 + postLangCodes: getPostLanguageTags(post), 45 47 }) 46 48 47 49 const needsTranslation = useMemo(() => { ··· 122 124 }) 123 125 124 126 ax.metric('translate', { 127 + os: Platform.OS, 125 128 sourceLanguages: [], // todo: get from post maybe? 126 129 targetLanguage: primaryLanguage, 127 130 textLength: postText.length, ··· 405 408 text: postText, 406 409 targetLangCode: langPrefs.primaryLanguage, 407 410 sourceLangCode, 411 + sourceSelection: 'manual', 408 412 }) 409 413 } 410 414
+3
src/components/PostControls/PostMenu/PostMenuItems.tsx
··· 28 28 import {richTextToString} from '#/lib/strings/rich-text-helpers' 29 29 import {toShareUrl} from '#/lib/strings/url-helpers' 30 30 import {useTranslate} from '#/lib/translation' 31 + import {getPostLanguageTags} from '#/locale/helpers' 31 32 import {logger} from '#/logger' 32 33 import {type Shadow} from '#/state/cache/post-shadow' 33 34 import {useProfileShadow} from '#/state/cache/profile-shadow' ··· 137 138 const openLink = useOpenLink() 138 139 const {clearTranslation, translate, translationState} = useTranslate({ 139 140 key: post.uri, 141 + postLangCodes: getPostLanguageTags(post), 140 142 forceGoogleTranslate, 141 143 }) 142 144 const navigation = useNavigation<NavigationProp>() ··· 287 289 ) 288 290 ) { 289 291 ax.metric('translate', { 292 + os: Platform.OS, 290 293 sourceLanguages: post.record.langs ?? [], 291 294 targetLanguage: langPrefs.primaryLanguage, 292 295 textLength: post.record.text.length,
+2 -1
src/components/dms/MessageContextMenu.tsx
··· 1 1 import {memo, useCallback} from 'react' 2 - import {LayoutAnimation} from 'react-native' 2 + import {LayoutAnimation, Platform} from 'react-native' 3 3 import * as Clipboard from 'expo-clipboard' 4 4 import {type ChatBskyConvoDefs, RichText} from '@atproto/api' 5 5 import {msg} from '@lingui/core/macro' ··· 67 67 void translate(message.text, langPrefs.primaryLanguage) 68 68 69 69 ax.metric('translate', { 70 + os: Platform.OS, 70 71 sourceLanguages: [], 71 72 targetLanguage: langPrefs.primaryLanguage, 72 73 textLength: message.text.length,
+2 -12
src/lib/translation/context.ts
··· 1 1 import {createContext} from 'react' 2 2 3 - import {type TranslationFunctionParams, type TranslationState} from './types' 3 + import {type ContextType} from './types' 4 4 5 - export const Context = createContext<{ 6 - translationState: Record<string, TranslationState> 7 - translate: ( 8 - parameters: TranslationFunctionParams & { 9 - key: string 10 - forceGoogleTranslate: boolean 11 - }, 12 - ) => Promise<void> 13 - clearTranslation: (key: string) => void 14 - acquireTranslation: (key: string) => () => void 15 - } | null>(null) 5 + export const Context = createContext<ContextType | null>(null) 16 6 Context.displayName = 'TranslationContext'
+25 -22
src/lib/translation/index.tsx
··· 11 11 import {useAnalytics} from '#/analytics' 12 12 import {HAS_ON_DEVICE_TRANSLATION, IS_ANDROID, IS_IOS} from '#/env' 13 13 import {Context} from './context' 14 - import {type TranslationFunctionParams, type TranslationState} from './types' 14 + import { 15 + type ContextType, 16 + type TranslationFunctionParams, 17 + type TranslationOptions, 18 + type TranslationState, 19 + } from './types' 15 20 import {guessLanguage} from './utils' 16 21 17 22 export * from './types' ··· 98 103 export function useTranslate({ 99 104 key, 100 105 forceGoogleTranslate = false, 101 - }: { 102 - key: string 103 - forceGoogleTranslate?: boolean 104 - }) { 106 + postLangCodes, 107 + }: TranslationOptions) { 105 108 const context = useContext(Context) 106 109 if (!context) { 107 110 throw new Error( ··· 118 121 119 122 const translate = useCallback( 120 123 async (params: TranslationFunctionParams) => { 121 - return context.translate({...params, key, forceGoogleTranslate}) 124 + return context.translate({ 125 + ...params, 126 + key, 127 + forceGoogleTranslate, 128 + postLangCodes, 129 + }) 122 130 }, 123 - [context, forceGoogleTranslate, key], 131 + [context, forceGoogleTranslate, key, postLangCodes], 124 132 ) 125 133 126 134 const clearTranslation = useCallback( ··· 199 207 }) 200 208 }, []) 201 209 202 - const translate = useCallback( 210 + const translate = useCallback<ContextType['translate']>( 203 211 async ({ 204 212 key, 205 213 text, 206 214 targetLangCode, 207 215 sourceLangCode, 216 + sourceSelection = 'automatic', 217 + postLangCodes, 208 218 ...options 209 - }: { 210 - key: string 211 - text: string 212 - targetLangCode: string 213 - sourceLangCode?: string 214 - forceGoogleTranslate?: boolean 215 219 }) => { 216 220 if (options?.forceGoogleTranslate || !HAS_ON_DEVICE_TRANSLATION) { 217 - ax.metric('translate:result', { 218 - method: 'google-translate', 219 - os: Platform.OS, 220 - sourceLanguage: sourceLangCode ?? null, 221 - targetLanguage: targetLangCode, 222 - }) 223 221 await googleTranslate(text, targetLangCode, sourceLangCode) 224 222 return 225 223 } ··· 240 238 ax.metric('translate:result', { 241 239 method: 'on-device', 242 240 os: Platform.OS, 241 + sourceSelection, 243 242 sourceLanguage: result.sourceLanguage, 244 243 targetLanguage: result.targetLanguage, 244 + postLanguages: postLangCodes, 245 245 }) 246 246 if (!IS_ANDROID) { 247 247 LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) ··· 253 253 translatedText: result.translatedText, 254 254 sourceLanguage: result.sourceLanguage, 255 255 targetLanguage: result.targetLanguage, 256 + postLanguages: postLangCodes, 256 257 }, 257 258 })) 258 259 } catch (e) { 259 - logger.error('Failed to translate post on device', {safeMessage: e}) 260 + logger.error('Failed to translate text on device', {safeMessage: e}) 260 261 // On-device translation failed (language pack missing or user 261 - // dismissed the download prompt). Fall back to Google Translate. 262 + // dismissed the download prompt). 262 263 ax.metric('translate:result', { 263 264 method: 'fallback-alert', 264 265 os: Platform.OS, 266 + sourceSelection, 265 267 sourceLanguage: sourceLangCode ?? null, 266 268 targetLanguage: targetLangCode, 269 + postLanguages: postLangCodes, 267 270 }) 268 271 let errorMessage = l`Device failed to translate :(` 269 272 if (!IS_ANDROID) {
+16 -26
src/lib/translation/index.web.tsx
··· 3 3 import {useGoogleTranslate} from '#/lib/hooks/useGoogleTranslate' 4 4 import {useAnalytics} from '#/analytics' 5 5 import {Context} from './context' 6 - import {type TranslationFunctionParams, type TranslationState} from './types' 6 + import { 7 + type ContextType, 8 + type TranslationFunctionParams, 9 + type TranslationOptions, 10 + type TranslationState, 11 + } from './types' 7 12 8 13 export * from './types' 9 14 export * from './utils' ··· 17 22 /** 18 23 * Web always opens Google Translate. 19 24 */ 20 - export function useTranslate({ 21 - key, 22 - }: { 23 - key: string 24 - forceGoogleTranslate?: boolean 25 - }) { 25 + export function useTranslate({key, postLangCodes}: TranslationOptions) { 26 26 const context = useContext(Context) 27 27 if (!context) { 28 28 throw new Error( ··· 33 33 // Always call hooks in consistent order 34 34 const translate = useCallback( 35 35 async (params: TranslationFunctionParams) => { 36 - return context.translate({...params, key, forceGoogleTranslate: true}) 36 + return context.translate({ 37 + ...params, 38 + key, 39 + forceGoogleTranslate: true, 40 + postLangCodes, 41 + }) 37 42 }, 38 - [key, context], 43 + [key, context, postLangCodes], 39 44 ) 40 45 41 46 const clearTranslation = useCallback(() => { ··· 55 60 const ax = useAnalytics() 56 61 const googleTranslate = useGoogleTranslate() 57 62 58 - const translate = useCallback( 59 - async ({ 60 - text, 61 - targetLangCode, 62 - sourceLangCode, 63 - }: { 64 - key: string 65 - text: string 66 - targetLangCode: string 67 - sourceLangCode?: string 68 - }) => { 69 - ax.metric('translate:result', { 70 - method: 'google-translate', 71 - os: 'web', 72 - sourceLanguage: sourceLangCode ?? null, 73 - targetLanguage: targetLangCode, 74 - }) 63 + const translate = useCallback<ContextType['translate']>( 64 + async ({text, targetLangCode, sourceLangCode}) => { 75 65 await googleTranslate(text, targetLangCode, sourceLangCode) 76 66 }, 77 67 [ax, googleTranslate],
+26
src/lib/translation/types.ts
··· 27 27 * The source language of the text. Will auto-detect if not provided. 28 28 */ 29 29 sourceLangCode?: string 30 + /** 31 + * Whether we auto-detected the language or it was selected manually. Defaults to 'automatic'. 32 + */ 33 + sourceSelection?: 'automatic' | 'manual' 34 + } 35 + 36 + export type TranslationOptions = { 37 + key: string 38 + forceGoogleTranslate?: boolean 39 + /** 40 + * The language(s) of the post being translated. Used for analytics purposes 41 + * to understand translation usage patterns better. Optional because it may 42 + * not always be available (e.g. if the post text is empty or if the 43 + * translation is triggered from a non-post 44 + * context). 45 + */ 46 + postLangCodes?: string[] 30 47 } 31 48 32 49 export type TranslationFunction = ( 33 50 parameters: TranslationFunctionParams, 34 51 ) => Promise<void> 52 + 53 + export type ContextType = { 54 + translationState: Record<string, TranslationState> 55 + translate: ( 56 + parameters: TranslationFunctionParams & TranslationOptions, 57 + ) => Promise<void> 58 + clearTranslation: (key: string) => void 59 + acquireTranslation: (key: string) => () => void 60 + }
+13 -14
src/locale/helpers.ts
··· 61 61 } 62 62 } 63 63 64 + export function getPostLanguageTags(post: AppBskyFeedDefs.PostView) { 65 + return AppBskyFeedPost.isRecord(post.record) && 66 + hasProp(post.record, 'langs') && 67 + Array.isArray(post.record.langs) 68 + ? post.record.langs 69 + : [] 70 + } 71 + 64 72 export function languageName(language: Language, appLang: string): string { 65 73 // if Intl.DisplayNames is unavailable on the target, display the English name 66 74 if (!Intl.DisplayNames) { ··· 80 88 export function getPostLanguage( 81 89 post: AppBskyFeedDefs.PostView, 82 90 ): string | undefined { 83 - let candidates: string[] = [] 91 + let candidates: string[] = getPostLanguageTags(post) 84 92 let postText: string = '' 85 93 if (hasProp(post.record, 'text') && typeof post.record.text === 'string') { 86 94 postText = post.record.text 87 95 } 88 96 89 - if ( 90 - AppBskyFeedPost.isRecord(post.record) && 91 - hasProp(post.record, 'langs') && 92 - Array.isArray(post.record.langs) 93 - ) { 94 - candidates = post.record.langs 95 - } 96 - 97 97 // if there's only one declared language, use that 98 - if (candidates?.length === 1) { 98 + if (candidates.length === 1) { 99 99 return candidates[0] 100 100 } 101 101 ··· 108 108 let langsProbabilityMap = lande(postText) 109 109 110 110 // filter down using declared languages 111 - if (candidates?.length) { 111 + if (candidates.length) { 112 112 langsProbabilityMap = langsProbabilityMap.filter( 113 - ([lang, _probability]: [string, number]) => { 114 - return candidates.includes(code3ToCode2(lang)) 115 - }, 113 + ([lang, _probability]: [string, number]) => 114 + candidates.includes(code3ToCode2(lang)), 116 115 ) 117 116 } 118 117
+4 -4
yarn.lock
··· 2403 2403 resolved "https://registry.yarnpkg.com/@bsky.app/expo-image-crop-tool/-/expo-image-crop-tool-0.5.0.tgz#4308fbde5c15e6be9122601797bc3d9549c95e31" 2404 2404 integrity sha512-gmhQr2HWTRFyPO00fn5OmtiEVtikXusHMrN5Zoq26pu1VZX3zVE+aoc668etTqrvsQcm2Qu8fo96k5F3Wu+6wg== 2405 2405 2406 - "@bsky.app/expo-translate-text@^0.2.7": 2407 - version "0.2.7" 2408 - resolved "https://registry.yarnpkg.com/@bsky.app/expo-translate-text/-/expo-translate-text-0.2.7.tgz#e34811d0f0300f8808762e5676aa50790ca5d5e8" 2409 - integrity sha512-J9zctP9hLxX0eustTKk5CBnCkk6cEdlu1s7GzUnpT65qkCSNbYqbbUCpcU2Z2S2dN/1+w6L/iHb+vmCEbZMOaQ== 2406 + "@bsky.app/expo-translate-text@^0.2.9": 2407 + version "0.2.9" 2408 + resolved "https://registry.yarnpkg.com/@bsky.app/expo-translate-text/-/expo-translate-text-0.2.9.tgz#4ed4552cd50bca7d02d14e706e419bd728d4ab51" 2409 + integrity sha512-VmqMhc/YavjgkGhxT/fB8mGSi+VZHJET1tsbpTg8peqKRXFSju2F294NsRxH/4aaMQFlt5oRfPCRnLm1H5o3lA== 2410 2410 2411 2411 "@bsky.app/react-native-mmkv@2.12.5": 2412 2412 version "2.12.5"