Add error handling with Google Translate link

This commit is contained in:
DS Boyce
2026-03-03 15:49:13 -08:00
parent c47c1e7aaf
commit ffce872adb
5 changed files with 100 additions and 12 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.4", "@bsky.app/expo-translate-text": "^0.2.5",
"@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/react": "^1.1.1", "@emoji-mart/react": "^1.1.1",
+78 -2
View File
@@ -18,6 +18,7 @@ import {atoms as a, native, useTheme} from '#/alf'
import {Button} from '#/components/Button' import {Button} from '#/components/Button'
import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRight} from '#/components/icons/Arrow' import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRight} from '#/components/icons/Arrow'
import {TimesLarge_Stroke2_Corner0_Rounded as Times} from '#/components/icons/Times' import {TimesLarge_Stroke2_Corner0_Rounded as Times} from '#/components/icons/Times'
import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning'
import {InlineLinkText} from '#/components/Link' import {InlineLinkText} from '#/components/Link'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import * as Select from '#/components/Select' import * as Select from '#/components/Select'
@@ -45,6 +46,17 @@ export function TranslatedPost({
[post, langPrefs.primaryLanguage], [post, langPrefs.primaryLanguage],
) )
if (translationState.status === 'error') {
return (
<TranslationError
clearTranslation={clearTranslation}
message={translationState.message}
postText={postText}
primaryLanguage={langPrefs.primaryLanguage}
/>
)
}
if (translationState.status === 'loading') { if (translationState.status === 'loading') {
return <TranslationLoading /> return <TranslationLoading />
} }
@@ -137,6 +149,63 @@ function TranslationLink({
) )
} }
function TranslationError({
clearTranslation,
message,
postText,
primaryLanguage,
}: {
clearTranslation: () => void
message: string
postText: string
primaryLanguage: string
}) {
const t = useTheme()
const {t: l} = useLingui()
return (
<View
style={[
a.px_lg,
a.py_md,
a.mt_sm,
a.border,
a.rounded_lg,
t.atoms.border_contrast_high,
]}>
<View
style={[a.flex_row, a.flex_wrap, a.align_center, a.justify_between]}>
<View style={[a.flex_row, a.align_center, a.mb_sm]}>
<Warning size="sm" fill={t.atoms.text_contrast_medium.color} />
<Text style={[a.text_xs, a.font_medium, t.atoms.text_contrast_high]}>
{' '}
{message}
</Text>
</View>
<View style={[a.flex_row, a.align_center, a.mb_xs]}>
<Button
label={l`Hide translation`}
hitSlop={HITSLOP_30}
hoverStyle={native({opacity: 0.5})}
onPress={clearTranslation}>
<Times size="sm" fill={t.atoms.text_contrast_medium.color} />
</Button>
</View>
</View>
<View style={[a.flex_row, a.align_center]}>
<Text>
<InlineLinkText
to={getTranslatorLink(postText, primaryLanguage)}
label={l`Try Google Translate`}
style={[a.text_xs, a.font_medium]}>
<Trans>Try Google Translate</Trans>
</InlineLinkText>
</Text>
</View>
</View>
)
}
function TranslationResult({ function TranslationResult({
clearTranslation, clearTranslation,
translate, translate,
@@ -213,7 +282,12 @@ function TranslationResult({
)} )}
{sourceLanguage != null && ( {sourceLanguage != null && (
<> <>
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}> <Text
style={[
a.text_xs,
a.font_medium,
t.atoms.text_contrast_medium,
]}>
{' '} {' '}
&middot;{' '} &middot;{' '}
</Text> </Text>
@@ -250,6 +324,7 @@ function TranslationLanguageSelect({
postText: string postText: string
sourceLanguage: string sourceLanguage: string
}) { }) {
const t = useTheme()
const ax = useAnalytics() const ax = useAnalytics()
const {t: l} = useLingui() const {t: l} = useLingui()
const langPrefs = useLanguagePrefs() const langPrefs = useLanguagePrefs()
@@ -300,7 +375,8 @@ function TranslationLanguageSelect({
{...props} {...props}
hitSlop={HITSLOP_30} hitSlop={HITSLOP_30}
hoverStyle={native({opacity: 0.5})}> hoverStyle={native({opacity: 0.5})}>
<Text style={[a.text_xs, a.font_medium]}> <Text
style={[a.text_xs, a.font_medium, t.atoms.text_contrast_high]}>
<Trans>Change</Trans> <Trans>Change</Trans>
</Text> </Text>
</Button> </Button>
+13 -5
View File
@@ -2,6 +2,7 @@ import {useCallback, useContext, useEffect, useMemo, useState} from 'react'
import {LayoutAnimation, Platform} from 'react-native' import {LayoutAnimation, Platform} from 'react-native'
import {getLocales} from 'expo-localization' import {getLocales} from 'expo-localization'
import {type TranslationTaskResult} from '@bsky.app/expo-translate-text/build/ExpoTranslateText.types' import {type TranslationTaskResult} from '@bsky.app/expo-translate-text/build/ExpoTranslateText.types'
import {useLingui} from '@lingui/react/macro'
import {useFocusEffect} from '@react-navigation/native' import {useFocusEffect} from '@react-navigation/native'
import {useGoogleTranslate} from '#/lib/hooks/useGoogleTranslate' import {useGoogleTranslate} from '#/lib/hooks/useGoogleTranslate'
@@ -61,9 +62,15 @@ async function attemptTranslation(
}) })
// Since `input` is always a string, the result should always be a string. // Since `input` is always a string, the result should always be a string.
const translatedText =
typeof result.translatedTexts === 'string' ? result.translatedTexts : ''
if (translatedText === input) {
throw new Error('Translation result is the same as the source text.')
}
return { return {
translatedText: translatedText,
typeof result.translatedTexts === 'string' ? result.translatedTexts : '',
targetLanguage: result.targetLanguage, targetLanguage: result.targetLanguage,
sourceLanguage: result.sourceLanguage ?? sourceLangCode ?? null, // iOS doesn't return the source language sourceLanguage: result.sourceLanguage ?? sourceLangCode ?? null, // iOS doesn't return the source language
} }
@@ -125,6 +132,7 @@ export function Provider({children}: React.PropsWithChildren<unknown>) {
>({}) >({})
const [refCounts, setRefCounts] = useState<Record<string, number>>({}) const [refCounts, setRefCounts] = useState<Record<string, number>>({})
const ax = useAnalytics() const ax = useAnalytics()
const {t: l} = useLingui()
const googleTranslate = useGoogleTranslate() const googleTranslate = useGoogleTranslate()
useEffect(() => { useEffect(() => {
@@ -235,12 +243,12 @@ export function Provider({children}: React.PropsWithChildren<unknown>) {
sourceLanguage: sourceLangCode ?? null, sourceLanguage: sourceLangCode ?? null,
targetLanguage: targetLangCode, targetLanguage: targetLangCode,
}) })
let errorMessage = l`Device failed to translate. :(`
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
setTranslationState(prev => ({ setTranslationState(prev => ({
...prev, ...prev,
[key]: {status: 'idle'}, [key]: {status: 'error', message: errorMessage},
})) }))
await googleTranslate(text, targetLangCode, sourceLangCode)
} }
} }
@@ -254,7 +262,7 @@ export function Provider({children}: React.PropsWithChildren<unknown>) {
[key]: {status: 'loading'}, [key]: {status: 'loading'},
})) }))
}, },
[ax, googleTranslate], [ax, googleTranslate, l],
) )
const ctx = useMemo( const ctx = useMemo(
+4
View File
@@ -9,6 +9,10 @@ export type TranslationState =
sourceLanguage: TranslationTaskResult['sourceLanguage'] sourceLanguage: TranslationTaskResult['sourceLanguage']
targetLanguage: TranslationTaskResult['targetLanguage'] targetLanguage: TranslationTaskResult['targetLanguage']
} }
| {
status: 'error'
message: string
}
export type TranslationFunctionParams = { export type TranslationFunctionParams = {
/** /**
+4 -4
View File
@@ -3731,10 +3731,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.4": "@bsky.app/expo-translate-text@^0.2.5":
version "0.2.4" version "0.2.5"
resolved "https://registry.yarnpkg.com/@bsky.app/expo-translate-text/-/expo-translate-text-0.2.4.tgz#6e7f20f286111ee4d550c0c84f57393fc215a675" resolved "https://registry.yarnpkg.com/@bsky.app/expo-translate-text/-/expo-translate-text-0.2.5.tgz#eb0bf41522b710b11e64d14a06d4ec770a72ad20"
integrity sha512-7mvFggNfkJEufI5A3WnjfjdN3H9P6Dpx7CpDkA9npWqA8Cb2icXq3k3nz3MaXGrVKTYiJnytAffYtos7mPoeOg== integrity sha512-nWGxK8bwKN6G7AtN7bvCIyO3Ynbzeg+NDzfGcWh0nEfuP4gKTrFO6QnBMMcZ9r+SzLP3EryqEs0AzeZ56n6Y+A==
"@bsky.app/react-native-mmkv@2.12.5": "@bsky.app/react-native-mmkv@2.12.5":
version "2.12.5" version "2.12.5"