import {useCallback, useMemo} from 'react' import {Platform, type StyleProp, type TextStyle, View} from 'react-native' import {type AppBskyFeedDefs, AppBskyFeedPost} from '@atproto/api' import {Trans, useLingui} from '@lingui/react/macro' import {HITSLOP_30} from '#/lib/constants' import {useTranslate} from '#/lib/translation' import { type TranslationFunction, type TranslationFunctionParams, } from '#/lib/translation' import { codeToLanguageName, getPostLanguageTags, isPostInLanguage, languageName, } from '#/locale/helpers' import {LANGUAGES} from '#/locale/languages' import {useLanguagePrefs} from '#/state/preferences' import {atoms as a, flatten, native, useTheme, web} from '#/alf' import {Button} from '#/components/Button' import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRightIcon} from '#/components/icons/Arrow' import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times' import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' import {createStaticClick, Link} from '#/components/Link' import {Loader} from '#/components/Loader' 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, postTextStyle = a.text_md, }: { hideTranslateLink?: boolean post: AppBskyFeedDefs.PostView postTextStyle?: StyleProp }) { const langPrefs = useLanguagePrefs() const {clearTranslation, translate, translationState} = useTranslate({ key: post.uri, }) const record = useMemo(() => { return bsky.dangerousIsType( post.record, AppBskyFeedPost.isRecord, ) ? post.record : undefined }, [post]) const initialTranslationParams = useMemo(() => { return { text: record?.text || '', expectedTargetLanguage: langPrefs.primaryLanguage, possibleSourceLanguages: getPostLanguageTags(post), } }, [post, record, langPrefs]) const needsTranslation = useMemo(() => { if (hideTranslateLink) return false return !isPostInLanguage(post, [langPrefs.primaryLanguage]) }, [hideTranslateLink, post, langPrefs.primaryLanguage]) switch (translationState.status) { case 'loading': return case 'success': return ( ) case 'error': return ( ) default: return ( needsTranslation && ( ) ) } } function TranslationLoading() { const t = useTheme() return ( Translating ) } function TranslationLink({ translate, initialTranslationParams, }: { translate: TranslationFunction initialTranslationParams: TranslationFunctionParams }) { const t = useTheme() const {t: l} = useLingui() const handleTranslate = useCallback(() => { void translate(initialTranslationParams) }, [initialTranslationParams, translate]) return ( { handleTranslate() })} label={l`Translate`} hoverStyle={[ native({opacity: 0.5}), web([a.underline, {textDecorationColor: t.palette.primary_500}]), ]} hitSlop={HITSLOP_30}> Translate ) } function TranslationError({ translate, clearTranslation, message, initialTranslationParams, }: { translate: TranslationFunction clearTranslation: () => void message: string initialTranslationParams: TranslationFunctionParams }) { const t = useTheme() const {t: l} = useLingui() const handleFallback = () => { void translate({ ...initialTranslationParams, forceGoogleTranslate: true, }) } return ( {message} { handleFallback() })} label={l`Try Google Translate`} hoverStyle={[ native({opacity: 0.5}), web([a.underline, {textDecorationColor: t.palette.primary_500}]), ]} hitSlop={HITSLOP_30}> Try Google Translate ) } function TranslationResult({ clearTranslation, translate, postTextStyle, resultSourceLanguage, translatedText, initialTranslationParams, }: { clearTranslation: () => void translate: TranslationFunction postTextStyle?: StyleProp resultSourceLanguage: string | null translatedText: string initialTranslationParams: TranslationFunctionParams }) { const t = useTheme() const langPrefs = useLanguagePrefs() const {i18n, t: l} = useLingui() const langName = resultSourceLanguage ? codeToLanguageName(resultSourceLanguage, i18n.locale) : undefined const flattenedStyle = flatten(postTextStyle) ?? {} const fontSize = flattenedStyle.fontSize return ( {langName ? ( <> {langName}{' '} {' '} {codeToLanguageName( langPrefs.primaryLanguage, langPrefs.appLanguage, )} ) : ( Translated )} {resultSourceLanguage != null && ( <> {' '} ·{' '} )} {translatedText} ) } function TranslationLanguageSelect({ translate, resultSourceLanguage, initialTranslationParams, }: { translate: TranslationFunction resultSourceLanguage: string initialTranslationParams: TranslationFunctionParams }) { const t = useTheme() const ax = useAnalytics() const {t: l} = useLingui() const langPrefs = useLanguagePrefs() const items = useMemo( () => LANGUAGES.filter( (lang, index, self) => !langPrefs.primaryLanguage.startsWith(lang.code2) && // Don't show the current language as it would be redundant index === self.findIndex(t => t.code2 === lang.code2), // Remove dupes (which will happen due to multiple code3 values mapping to the same code2) ) .sort((a, b) => { // Prioritize sourceLanguage at the top if (a.code2 === resultSourceLanguage) return -1 if (b.code2 === resultSourceLanguage) return 1 // Localized sort return languageName(a, langPrefs.appLanguage).localeCompare( languageName(b, langPrefs.appLanguage), langPrefs.appLanguage, ) }) .map(l => ({ label: languageName(l, langPrefs.appLanguage), // The viewer may not be familiar with the source language, so localize the name value: l.code2, })), [langPrefs, resultSourceLanguage], ) const handleChangeTranslationLanguage = (sourceLangCode: string) => { ax.metric('translate:override', { os: Platform.OS, possibleSourceLanguages: initialTranslationParams.possibleSourceLanguages, expectedSourceLanguage: sourceLangCode, expectedTargetLanguage: initialTranslationParams.expectedTargetLanguage, resultSourceLanguage, }) void translate({ text: initialTranslationParams.text, expectedTargetLanguage: initialTranslationParams.expectedTargetLanguage, expectedSourceLanguage: sourceLangCode, possibleSourceLanguages: initialTranslationParams.possibleSourceLanguages, }) } return ( {({props}) => { return ( ) }} ( {label} )} items={items} /> ) }