diff --git a/src/env/index.ts b/src/env/index.ts index 14abba55a4..13ae3dcc7d 100644 --- a/src/env/index.ts +++ b/src/env/index.ts @@ -10,6 +10,10 @@ const iOSMajorVersion = Platform.OS === 'ios' && typeof Platform.Version === 'string' ? parseInt(Platform.Version.split('.')[0], 10) : 0 +const androidPlatformVersion = + Platform.OS === 'android' && typeof Platform.Version === 'number' + ? Platform.Version + : 0 /** * The semver version of the app, specified in our `package.json`.file. On @@ -49,3 +53,7 @@ export const IS_WEB_FIREFOX: boolean = false export const IS_HIGH_DPI: boolean = true // ideally we'd use isLiquidGlassAvailable() from expo-glass-effect but checking iOS version is good enough for now export const IS_LIQUID_GLASS: boolean = iOSMajorVersion >= 26 +// So we can avoid attempting on-device translation when we know it's unsupported. +export const IS_TRANSLATION_SUPPORTED: boolean = + (IS_IOS && iOSMajorVersion >= 18) || + (IS_ANDROID && androidPlatformVersion > 22) diff --git a/src/env/index.web.ts b/src/env/index.web.ts index 0a078fdeb0..68604a2e5b 100644 --- a/src/env/index.web.ts +++ b/src/env/index.web.ts @@ -48,3 +48,4 @@ export const IS_HIGH_DPI: boolean = window.matchMedia( '(min-resolution: 2dppx)', ).matches export const IS_LIQUID_GLASS: boolean = false +export const IS_TRANSLATION_SUPPORTED: boolean = false diff --git a/src/lib/translation/index.tsx b/src/lib/translation/index.tsx index 710c9993d1..0e7b182a62 100644 --- a/src/lib/translation/index.tsx +++ b/src/lib/translation/index.tsx @@ -1,18 +1,17 @@ import {useCallback, useContext, useEffect, useMemo, useState} from 'react' import {LayoutAnimation, Platform} from 'react-native' import {getLocales} from 'expo-localization' -import { - isTranslationSupported, - onTranslateTask, -} from '@bsky.app/expo-translate-text' +import {onTranslateTask} from '@bsky.app/expo-translate-text' 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 {useGoogleTranslate} from '#/lib/hooks/useGoogleTranslate' +import {codeToLanguageName} from '#/locale/helpers' import {logger} from '#/logger' +import {useLanguagePrefs} from '#/state/preferences' import {useAnalytics} from '#/analytics' -import {IS_ANDROID, IS_IOS} from '#/env' +import {IS_ANDROID, IS_IOS, IS_TRANSLATION_SUPPORTED} from '#/env' import {Context} from './context' import { type ContextType, @@ -25,6 +24,11 @@ import {guessLanguage} from './utils' export * from './types' export * from './utils' +const E_SAME_AS_SOURCE_LANGUAGE = + 'Translation result is the same as the source text.' +const E_EMPTY_RESULT = 'Translation result is empty.' +const E_INVALID_SOURCE_LANGUAGE = 'Invalid source language' + /** * Attempts on-device translation via @bsky.app/expo-translate-text. * Uses a lazy import to avoid crashing if the native module isn't linked into @@ -80,11 +84,11 @@ async function attemptTranslation( typeof result.translatedTexts === 'string' ? result.translatedTexts : '' if (translatedText === input) { - throw new Error('Translation result is the same as the source text.') + throw new Error(E_SAME_AS_SOURCE_LANGUAGE) } if (translatedText === '') { - throw new Error('Translation result is empty.') + throw new Error(E_EMPTY_RESULT) } return { @@ -159,6 +163,7 @@ export function Provider({children}: React.PropsWithChildren) { >({}) const [refCounts, setRefCounts] = useState>({}) const ax = useAnalytics() + const langPrefs = useLanguagePrefs() const {t: l} = useLingui() const googleTranslate = useGoogleTranslate() @@ -235,7 +240,7 @@ export function Provider({children}: React.PropsWithChildren) { googleTranslate: shouldForceGoogleTranslate, }) - if (shouldForceGoogleTranslate || !isTranslationSupported()) { + if (shouldForceGoogleTranslate || !IS_TRANSLATION_SUPPORTED) { await googleTranslate( text, expectedTargetLanguage, @@ -280,7 +285,8 @@ export function Provider({children}: React.PropsWithChildren) { postLanguages: possibleSourceLanguages, }, })) - } catch (e) { + } catch (err) { + const e = err as Error logger.error('Failed to translate text on device', {safeMessage: e}) // On-device translation failed (language pack missing or user // dismissed the download prompt). @@ -295,6 +301,21 @@ export function Provider({children}: React.PropsWithChildren) { textLength: text.length, }) let errorMessage = l`Device failed to translate :(` + if (e.message === E_SAME_AS_SOURCE_LANGUAGE) { + errorMessage = l`Translation to the same language is unavailable on your device.` + } + if (e.message === E_EMPTY_RESULT) { + errorMessage = l`No translation received from your device.` + } + if ( + expectedSourceLanguage && + e.message.includes(E_INVALID_SOURCE_LANGUAGE) + ) { + errorMessage = l`${codeToLanguageName( + expectedSourceLanguage, + langPrefs.appLanguage, + )} is not supported by your device.` + } if (!IS_ANDROID) { LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) } @@ -304,7 +325,7 @@ export function Provider({children}: React.PropsWithChildren) { })) } }, - [ax, googleTranslate, l], + [ax, googleTranslate, l, langPrefs.appLanguage], ) const ctx = useMemo(