diff --git a/package.json b/package.json
index 48c3a01336..6daa929381 100644
--- a/package.json
+++ b/package.json
@@ -85,7 +85,7 @@
"@braintree/sanitize-url": "^6.0.2",
"@bsky.app/alf": "^0.1.7",
"@bsky.app/expo-image-crop-tool": "^0.5.0",
- "@bsky.app/expo-translate-text": "^0.2.4",
+ "@bsky.app/expo-translate-text": "^0.2.7",
"@bsky.app/react-native-mmkv": "2.12.5",
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
"@emoji-mart/data": "^1.2.1",
diff --git a/src/App.native.tsx b/src/App.native.tsx
index 63a6e1752b..aa6e9bd06b 100644
--- a/src/App.native.tsx
+++ b/src/App.native.tsx
@@ -19,6 +19,7 @@ import {Provider as HideBottomBarBorderProvider} from '#/lib/hooks/useHideBottom
import {QueryProvider} from '#/lib/react-query'
import {s} from '#/lib/styles'
import {ThemeProvider} from '#/lib/ThemeContext'
+import {Provider as TranslateOnDeviceProvider} from '#/lib/translation'
import I18nProvider from '#/locale/i18nProvider'
import {logger} from '#/logger'
import {Provider as A11yProvider} from '#/state/a11y'
@@ -179,9 +180,11 @@ function InnerApp() {
style={s.h100pct}>
-
-
-
+
+
+
+
+
diff --git a/src/App.web.tsx b/src/App.web.tsx
index d2adfd4f94..34225bab62 100644
--- a/src/App.web.tsx
+++ b/src/App.web.tsx
@@ -10,6 +10,7 @@ import * as Sentry from '@sentry/react-native'
import {QueryProvider} from '#/lib/react-query'
import {ThemeProvider} from '#/lib/ThemeContext'
+import {Provider as TranslateOnDeviceProvider} from '#/lib/translation'
import I18nProvider from '#/locale/i18nProvider'
import {logger} from '#/logger'
import {Provider as A11yProvider} from '#/state/a11y'
@@ -154,8 +155,10 @@ function InnerApp() {
-
-
+
+
+
+
diff --git a/src/components/Post/Translated/index.tsx b/src/components/Post/Translated/index.tsx
index 68176577a0..2ad5c74477 100644
--- a/src/components/Post/Translated/index.tsx
+++ b/src/components/Post/Translated/index.tsx
@@ -1,113 +1,345 @@
-import {useMemo} from 'react'
+import {useCallback, useMemo} from 'react'
import {Platform, View} from 'react-native'
-import {msg} from '@lingui/core/macro'
-import {useLingui} from '@lingui/react'
-import {Trans} from '@lingui/react/macro'
+import {type AppBskyFeedDefs} from '@atproto/api'
+import {Trans, useLingui} from '@lingui/react/macro'
+import {HITSLOP_30} from '#/lib/constants'
+import {useGoogleTranslate} from '#/lib/hooks/useGoogleTranslate'
+import {guessLanguage, useTranslate} from '#/lib/translation'
+import {type TranslationFunction} from '#/lib/translation'
import {codeToLanguageName, languageName} from '#/locale/helpers'
import {LANGUAGES} from '#/locale/languages'
import {useLanguagePrefs} from '#/state/preferences'
-import {atoms as a, useTheme} from '#/alf'
+import {atoms as a, 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 {useTranslateOnDevice} from '#/translation'
+import {IS_WEB} from '#/env'
export function TranslatedPost({
+ hideTranslateLink = false,
+ post,
postText,
- hideLoading = false,
}: {
+ hideTranslateLink?: boolean
+ post: AppBskyFeedDefs.PostView
postText: string
- hideLoading: boolean
}) {
- const {translationState} = useTranslateOnDevice()
+ const langPrefs = useLanguagePrefs()
+ const {clearTranslation, translate, translationState} = useTranslate({
+ key: post.uri,
+ })
- if (translationState.status === 'loading' && !hideLoading) {
- return
+ const postLanguage = useMemo(() => guessLanguage(postText), [postText])
+ const needsTranslation = postLanguage !== langPrefs.primaryLanguage
+
+ switch (translationState.status) {
+ case 'loading':
+ return
+ case 'success':
+ return (
+
+ )
+ case 'error':
+ return (
+
+ )
+ default:
+ return (
+ !hideTranslateLink &&
+ needsTranslation && (
+
+ )
+ )
}
-
- if (translationState.status === 'success') {
- return (
-
- )
- }
-
- return null
}
function TranslationLoading() {
const t = useTheme()
return (
-
-
-
- Translating…
-
+
+
+
+
+ Translating…
+
+
+
+ )
+}
+
+function TranslationLink({
+ postText,
+ primaryLanguage,
+ sourceLanguage,
+ translate,
+}: {
+ postText: string
+ primaryLanguage: string
+ sourceLanguage: string | null
+ translate: TranslationFunction
+}) {
+ const t = useTheme()
+ const {t: l} = useLingui()
+ const ax = useAnalytics()
+
+ const handleTranslate = useCallback(() => {
+ void translate({
+ text: postText,
+ targetLangCode: primaryLanguage,
+ })
+
+ ax.metric('translate', {
+ sourceLanguages: sourceLanguage ? [sourceLanguage] : [],
+ targetLanguage: primaryLanguage,
+ textLength: postText.length,
+ })
+ }, [ax, postText, primaryLanguage, translate, sourceLanguage])
+
+ return (
+
+ {
+ handleTranslate()
+ })}
+ label={l`Translate`}
+ hoverStyle={[
+ native({opacity: 0.5}),
+ web([a.underline, {textDecorationColor: t.palette.primary_500}]),
+ ]}
+ hitSlop={HITSLOP_30}>
+
+ Translate
+
+
+
+ )
+}
+
+function TranslationError({
+ clearTranslation,
+ message,
+ postText,
+ primaryLanguage,
+}: {
+ clearTranslation: () => void
+ message: string
+ postText: string
+ primaryLanguage: string
+}) {
+ const t = useTheme()
+ const {t: l} = useLingui()
+ const translate = useGoogleTranslate()
+
+ const handleFallback = () => {
+ void translate(postText, primaryLanguage)
+ }
+
+ 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,
postText,
sourceLanguage,
translatedText,
}: {
+ clearTranslation: () => void
+ translate: TranslationFunction
postText: string
sourceLanguage: string | null
translatedText: string
}) {
const t = useTheme()
- const {i18n} = useLingui()
+ const langPrefs = useLanguagePrefs()
+ const {i18n, t: l} = useLingui()
const langName = sourceLanguage
? codeToLanguageName(sourceLanguage, i18n.locale)
: undefined
return (
-
-
- {langName ? (
- Translated from {langName}
- ) : (
- Translated
- )}
- {sourceLanguage != null && (
- <>
-
- {' '}
- ·
- {' '}
-
- >
- )}
-
-
- {translatedText}
-
+
+
+
+ {langName ? (
+
+
+ {langName}{' '}
+
+
+
+
+
+ {' '}
+ {codeToLanguageName(
+ langPrefs.primaryLanguage,
+ langPrefs.appLanguage,
+ )}
+
+
+ ) : (
+
+ Translated
+
+ )}
+ {sourceLanguage != null && (
+ <>
+
+ {' '}
+ ·{' '}
+
+
+ >
+ )}
+
+
+ {translatedText}
+
+
+
)
}
function TranslationLanguageSelect({
+ translate,
postText,
sourceLanguage,
}: {
+ translate: TranslationFunction
postText: string
sourceLanguage: string
}) {
+ const t = useTheme()
const ax = useAnalytics()
- const {_} = useLingui()
+ const {t: l} = useLingui()
const langPrefs = useLanguagePrefs()
- const {translate} = useTranslateOnDevice()
const items = useMemo(
() =>
@@ -116,18 +348,21 @@ function TranslationLanguageSelect({
!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) =>
- languageName(a, langPrefs.appLanguage).localeCompare(
- languageName(b, langPrefs.appLanguage),
- langPrefs.appLanguage,
- ), // Localized sort
- )
+ .sort((a, b) => {
+ // Prioritize sourceLanguage at the top
+ if (a.code2 === sourceLanguage) return -1
+ if (b.code2 === sourceLanguage) 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],
+ [langPrefs, sourceLanguage],
)
const handleChangeTranslationLanguage = (sourceLangCode: string) => {
@@ -136,24 +371,35 @@ function TranslationLanguageSelect({
sourceLanguage: sourceLangCode,
targetLanguage: langPrefs.primaryLanguage,
})
- void translate(postText, langPrefs.primaryLanguage, sourceLangCode)
+ void translate({
+ text: postText,
+ targetLangCode: langPrefs.primaryLanguage,
+ sourceLangCode,
+ })
}
return (
-
+
{({props}) => {
return (
-
- Change
-
+
)
}}
(
diff --git a/src/components/PostControls/PostMenu/PostMenuItems.tsx b/src/components/PostControls/PostMenu/PostMenuItems.tsx
index 13168bd2a3..9f10838ef8 100644
--- a/src/components/PostControls/PostMenu/PostMenuItems.tsx
+++ b/src/components/PostControls/PostMenu/PostMenuItems.tsx
@@ -13,13 +13,12 @@ import {
AtUri,
type RichText as RichTextAPI,
} from '@atproto/api'
-import {msg, plural} from '@lingui/core/macro'
-import {useLingui} from '@lingui/react'
+import {plural} from '@lingui/core/macro'
+import {useLingui} from '@lingui/react/macro'
import {useNavigation} from '@react-navigation/native'
import {DISCOVER_DEBUG_DIDS} from '#/lib/constants'
import {useOpenLink} from '#/lib/hooks/useOpenLink'
-import {useTranslate} from '#/lib/hooks/useTranslate'
import {getCurrentRoute} from '#/lib/routes/helpers'
import {makeProfileLink} from '#/lib/routes/links'
import {
@@ -28,6 +27,7 @@ import {
} from '#/lib/routes/types'
import {richTextToString} from '#/lib/strings/rich-text-helpers'
import {toShareUrl} from '#/lib/strings/url-helpers'
+import {useTranslate} from '#/lib/translation'
import {logger} from '#/logger'
import {type Shadow} from '#/state/cache/post-shadow'
import {useProfileShadow} from '#/state/cache/profile-shadow'
@@ -106,6 +106,7 @@ let PostMenuItems = ({
threadgateRecord,
onShowLess,
logContext,
+ forceGoogleTranslate,
}: {
testID: string
post: Shadow
@@ -120,9 +121,10 @@ let PostMenuItems = ({
threadgateRecord?: AppBskyFeedThreadgate.Record
onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
+ forceGoogleTranslate: boolean
}): React.ReactNode => {
const {hasSession, currentAccount} = useSession()
- const {_} = useLingui()
+ const {t: l} = useLingui()
const ax = useAnalytics()
const langPrefs = useLanguagePrefs()
const {mutateAsync: deletePostMutate} = usePostDeleteMutation()
@@ -133,7 +135,10 @@ let PostMenuItems = ({
const {hidePost} = useHiddenPostsApi()
const feedFeedback = useFeedFeedbackContext()
const openLink = useOpenLink()
- const translate = useTranslate()
+ const {clearTranslation, translate, translationState} = useTranslate({
+ key: post.uri,
+ forceGoogleTranslate,
+ })
const navigation = useNavigation()
const {mutedWordsDialogControl} = useGlobalDialogsControlContext()
const blockPromptControl = useDialogControl()
@@ -191,7 +196,7 @@ let PostMenuItems = ({
const onDeletePost = () => {
deletePostMutate({uri: postUri}).then(
() => {
- Toast.show(_(msg({message: 'Post deleted', context: 'toast'})))
+ Toast.show(l({message: 'Post deleted', context: 'toast'}))
const route = getCurrentRoute(navigation.getState())
if (route.name === 'PostThread') {
@@ -211,7 +216,7 @@ let PostMenuItems = ({
},
e => {
logger.error('Failed to delete post', {message: e})
- Toast.show(_(msg`Failed to delete post, please try again`), 'xmark')
+ Toast.show(l`Failed to delete post, please try again`, 'xmark')
},
)
}
@@ -226,7 +231,7 @@ let PostMenuItems = ({
logContext,
feedDescriptor: feedFeedback.feedDescriptor,
})
- Toast.show(_(msg`You will now receive notifications for this thread`))
+ Toast.show(l`You will now receive notifications for this thread`)
} else {
void muteThread()
ax.metric('post:mute', {
@@ -235,18 +240,13 @@ let PostMenuItems = ({
logContext,
feedDescriptor: feedFeedback.feedDescriptor,
})
- Toast.show(
- _(msg`You will no longer receive notifications for this thread`),
- )
+ Toast.show(l`You will no longer receive notifications for this thread`)
}
} catch (err) {
const e = err as Error
if (e?.name !== 'AbortError') {
logger.error('Failed to toggle thread mute', {message: e})
- Toast.show(
- _(msg`Failed to toggle thread mute, please try again`),
- 'xmark',
- )
+ Toast.show(l`Failed to toggle thread mute, please try again`, 'xmark')
}
}
}
@@ -255,11 +255,14 @@ let PostMenuItems = ({
const str = richTextToString(richText, true)
void Clipboard.setStringAsync(str)
- Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
+ Toast.show(l`Copied to clipboard`, 'clipboard-check')
}
const onPressTranslate = () => {
- void translate(record.text, langPrefs.primaryLanguage)
+ void translate({
+ text: record.text,
+ targetLangCode: langPrefs.primaryLanguage,
+ })
if (
bsky.dangerousIsType(
@@ -297,9 +300,7 @@ let PostMenuItems = ({
logContext,
feedDescriptor: feedFeedback.feedDescriptor,
})
- Toast.show(
- _(msg({message: 'Feedback sent to feed operator', context: 'toast'})),
- )
+ Toast.show(l({message: 'Feedback sent to feed operator', context: 'toast'}))
}
const onPressShowLess = () => {
@@ -322,7 +323,7 @@ let PostMenuItems = ({
})
} else {
Toast.show(
- _(msg({message: 'Feedback sent to feed operator', context: 'toast'})),
+ l({message: 'Feedback sent to feed operator', context: 'toast'}),
)
}
}
@@ -341,13 +342,13 @@ let PostMenuItems = ({
})
Toast.show(
isDetach
- ? _(msg`Quote post was successfully detached`)
- : _(msg`Quote post was re-attached`),
+ ? l`Quote post was successfully detached`
+ : l`Quote post was re-attached`,
)
} catch (err) {
const e = err as Error
Toast.show(
- _(msg({message: 'Updating quote attachment failed', context: 'toast'})),
+ l({message: 'Updating quote attachment failed', context: 'toast'}),
)
logger.error(`Failed to ${action} quote`, {safeMessage: e.message})
}
@@ -379,31 +380,27 @@ let PostMenuItems = ({
Toast.show(
isHide
- ? _(msg`Reply was successfully hidden`)
- : _(msg({message: 'Reply visibility updated', context: 'toast'})),
+ ? l`Reply was successfully hidden`
+ : l({message: 'Reply visibility updated', context: 'toast'}),
)
} catch (err) {
const e = err as Error
if (e instanceof MaxHiddenRepliesError) {
Toast.show(
- _(
- plural(MAX_HIDDEN_REPLIES, {
- other: 'You can hide a maximum of # replies.',
- }),
- ),
+ plural(MAX_HIDDEN_REPLIES, {
+ other: 'You can hide a maximum of # replies.',
+ }),
)
} else if (e instanceof InvalidInteractionSettingsError) {
Toast.show(
- _(msg({message: 'Invalid interaction settings.', context: 'toast'})),
+ l({message: 'Invalid interaction settings.', context: 'toast'}),
)
} else {
Toast.show(
- _(
- msg({
- message: 'Updating reply visibility failed',
- context: 'toast',
- }),
- ),
+ l({
+ message: 'Updating reply visibility failed',
+ context: 'toast',
+ }),
)
logger.error(`Failed to ${action} reply`, {safeMessage: e.message})
}
@@ -422,12 +419,12 @@ let PostMenuItems = ({
const onBlockAuthor = async () => {
try {
await queueBlock()
- Toast.show(_(msg({message: 'Account blocked', context: 'toast'})))
+ Toast.show(l({message: 'Account blocked', context: 'toast'}))
} catch (err) {
const e = err as Error
if (e?.name !== 'AbortError') {
logger.error('Failed to block account', {message: e})
- Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
+ Toast.show(l`There was an issue! ${e.toString()}`, 'xmark')
}
}
}
@@ -436,23 +433,23 @@ let PostMenuItems = ({
if (postAuthor.viewer?.muted) {
try {
await queueUnmute()
- Toast.show(_(msg({message: 'Account unmuted', context: 'toast'})))
+ Toast.show(l({message: 'Account unmuted', context: 'toast'}))
} catch (err) {
const e = err as Error
if (e?.name !== 'AbortError') {
logger.error('Failed to unmute account', {message: e})
- Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
+ Toast.show(l`There was an issue! ${e.toString()}`, 'xmark')
}
}
} else {
try {
await queueMute()
- Toast.show(_(msg({message: 'Account muted', context: 'toast'})))
+ Toast.show(l({message: 'Account muted', context: 'toast'}))
} catch (err) {
const e = err as Error
if (e?.name !== 'AbortError') {
logger.error('Failed to mute account', {message: e})
- Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
+ Toast.show(l`There was an issue! ${e.toString()}`, 'xmark')
}
}
}
@@ -467,6 +464,8 @@ let PostMenuItems = ({
const onSignIn = () => requireSignIn(() => {})
+ const onPressHideTranslation = () => clearTranslation()
+
const isDiscoverDebugUser =
IS_INTERNAL ||
DISCOVER_DEBUG_DIDS[currentAccount?.did || ''] ||
@@ -481,16 +480,12 @@ let PostMenuItems = ({
- {isPinned
- ? _(msg`Unpin from profile`)
- : _(msg`Pin to your profile`)}
+ {isPinned ? l`Unpin from profile` : l`Pin to your profile`}
{!hideInPWI || hasSession ? (
<>
-
- {_(msg`Translate`)}
-
-
+ {translationState.status === 'loading' ? (
+ {}}>
+ {l`Translating…`}
+
+
+ ) : translationState.status === 'success' ? (
+
+ {l`Hide translation`}
+
+
+ ) : (
+
+ {l`Translate`}
+
+
+ )}
- {_(msg`Copy post text`)}
+ {l`Copy post text`}
>
) : (
- {_(msg`Sign in to view post`)}
+ {l`Sign in to view post`}
)}
@@ -538,17 +551,17 @@ let PostMenuItems = ({
- {_(msg`Show more like this`)}
+ {l`Show more like this`}
- {_(msg`Show less like this`)}
+ {l`Show less like this`}
@@ -560,9 +573,9 @@ let PostMenuItems = ({
- {_(msg`Assign topic for algo`)}
+ {l`Assign topic for algo`}
>
@@ -574,12 +587,10 @@ let PostMenuItems = ({
- {isThreadMuted ? _(msg`Unmute thread`) : _(msg`Mute thread`)}
+ {isThreadMuted ? l`Unmute thread` : l`Mute thread`}
mutedWordsDialogControl.open()}>
- {_(msg`Mute words & tags`)}
+ {l`Mute words & tags`}
@@ -606,16 +617,10 @@ let PostMenuItems = ({
{canHidePostForMe && (
hidePromptControl.open()}>
- {isReply
- ? _(msg`Hide reply for me`)
- : _(msg`Hide post for me`)}
+ {isReply ? l`Hide reply for me` : l`Hide post for me`}
@@ -625,8 +630,8 @@ let PostMenuItems = ({
testID="postDropdownHideBtn"
label={
isReplyHiddenByThreadgate
- ? _(msg`Show reply for everyone`)
- : _(msg`Hide reply for everyone`)
+ ? l`Show reply for everyone`
+ : l`Hide reply for everyone`
}
onPress={
isReplyHiddenByThreadgate
@@ -635,8 +640,8 @@ let PostMenuItems = ({
}>
{isReplyHiddenByThreadgate
- ? _(msg`Show reply for everyone`)
- : _(msg`Hide reply for everyone`)}
+ ? l`Show reply for everyone`
+ : l`Hide reply for everyone`}
{quoteEmbed.isDetached
- ? _(msg`Re-attach quote`)
- : _(msg`Detach quote`)}
+ ? l`Re-attach quote`
+ : l`Detach quote`}
void onMuteAuthor()}>
{postAuthor.viewer?.muted
- ? _(msg`Unmute account`)
- : _(msg`Mute account`)}
+ ? l`Unmute account`
+ : l`Mute account`}
blockPromptControl.open()}>
- {_(msg`Block account`)}
+ {l`Block account`}
)}
reportDialogControl.open()}>
- {_(msg`Report post`)}
+ {l`Report post`}
>
@@ -729,7 +734,7 @@ let PostMenuItems = ({
<>
postInteractionSettingsDialogControl.open()}
{...(isAuthor
? Platform.select({
@@ -742,15 +747,15 @@ let PostMenuItems = ({
})
: {})}>
- {_(msg`Edit interaction settings`)}
+ {l`Edit interaction settings`}
deletePromptControl.open()}>
- {_(msg`Delete post`)}
+ {l`Delete post`}
>
@@ -759,28 +764,21 @@ let PostMenuItems = ({
>
)}
-
-
-
-
-
void onToggleQuotePostAttachment()}
- confirmButtonCta={_(msg`Yes, detach`)}
+ confirmButtonCta={l`Yes, detach`}
/>
-
void onToggleReplyVisibility()}
- confirmButtonCta={_(msg`Yes, hide`)}
+ confirmButtonCta={l`Yes, hide`}
/>
-
void onBlockAuthor()}
- confirmButtonCta={_(msg`Block`)}
+ confirmButtonCta={l`Block`}
confirmButtonColor="negative"
/>
>
diff --git a/src/components/PostControls/PostMenu/index.tsx b/src/components/PostControls/PostMenu/index.tsx
index dbf0744990..7ed620bd64 100644
--- a/src/components/PostControls/PostMenu/index.tsx
+++ b/src/components/PostControls/PostMenu/index.tsx
@@ -6,8 +6,7 @@ import {
type AppBskyFeedThreadgate,
type RichText as RichTextAPI,
} from '@atproto/api'
-import {msg} from '@lingui/core/macro'
-import {useLingui} from '@lingui/react'
+import {useLingui} from '@lingui/react/macro'
import {type Shadow} from '#/state/cache/post-shadow'
import {EventStopper} from '#/view/com/util/EventStopper'
@@ -30,6 +29,7 @@ let PostMenuButton = ({
onShowLess,
hitSlop,
logContext,
+ forceGoogleTranslate,
}: {
testID: string
post: Shadow
@@ -43,8 +43,9 @@ let PostMenuButton = ({
onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void
hitSlop?: Insets
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
+ forceGoogleTranslate: boolean
}): React.ReactNode => {
- const {_} = useLingui()
+ const {t: l} = useLingui()
const menuControl = useMenuControl()
const [hasBeenOpen, setHasBeenOpen] = useState(false)
@@ -63,7 +64,7 @@ let PostMenuButton = ({
return (
-
+
{({props}) => {
return (
)}
diff --git a/src/components/PostControls/index.tsx b/src/components/PostControls/index.tsx
index a27ca75b47..43f99b5979 100644
--- a/src/components/PostControls/index.tsx
+++ b/src/components/PostControls/index.tsx
@@ -6,8 +6,8 @@ import {
type AppBskyFeedThreadgate,
type RichText as RichTextAPI,
} from '@atproto/api'
-import {msg, plural} from '@lingui/core/macro'
-import {useLingui} from '@lingui/react'
+import {plural} from '@lingui/core/macro'
+import {useLingui} from '@lingui/react/macro'
import {CountWheel} from '#/lib/custom-animations/CountWheel'
import {AnimatedLikeIcon} from '#/lib/custom-animations/LikeIcon'
@@ -55,6 +55,7 @@ let PostControls = ({
onShowLess,
viaRepost,
variant,
+ forceGoogleTranslate = false,
}: {
big?: boolean
post: Shadow
@@ -70,9 +71,10 @@ let PostControls = ({
onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void
viaRepost?: {uri: string; cid: string}
variant?: 'compact' | 'normal' | 'large'
+ forceGoogleTranslate?: boolean
}): React.ReactNode => {
const ax = useAnalytics()
- const {_} = useLingui()
+ const {t: l} = useLingui()
const {openComposer} = useOpenComposer()
const {feedDescriptor} = useFeedFeedbackContext()
const [queueLike, queueUnlike] = usePostLikeMutationQueue(
@@ -104,10 +106,7 @@ let PostControls = ({
const onPressToggleLike = async () => {
if (isBlocked) {
- Toast.show(
- _(msg`Cannot interact with a blocked user`),
- 'exclamation-circle',
- )
+ Toast.show(l`Cannot interact with a blocked user`, 'exclamation-circle')
return
}
@@ -126,7 +125,8 @@ let PostControls = ({
} else {
await queueUnlike()
}
- } catch (e: any) {
+ } catch (err) {
+ const e = err as Error
if (e?.name !== 'AbortError') {
throw e
}
@@ -135,10 +135,7 @@ let PostControls = ({
const onRepost = async () => {
if (isBlocked) {
- Toast.show(
- _(msg`Cannot interact with a blocked user`),
- 'exclamation-circle',
- )
+ Toast.show(l`Cannot interact with a blocked user`, 'exclamation-circle')
return
}
@@ -154,7 +151,8 @@ let PostControls = ({
} else {
await queueUnrepost()
}
- } catch (e: any) {
+ } catch (err) {
+ const e = err as Error
if (e?.name !== 'AbortError') {
throw e
}
@@ -163,10 +161,7 @@ let PostControls = ({
const onQuote = () => {
if (isBlocked) {
- Toast.show(
- _(msg`Cannot interact with a blocked user`),
- 'exclamation-circle',
- )
+ Toast.show(l`Cannot interact with a blocked user`, 'exclamation-circle')
return
}
@@ -238,16 +233,14 @@ let PostControls = ({
})
: undefined
}
- label={_(
- msg({
- message: `Reply (${plural(post.replyCount || 0, {
- one: '# reply',
- other: '# replies',
- })})`,
- comment:
- 'Accessibility label for the reply button, verb form followed by number of replies and noun form',
- }),
- )}
+ label={l({
+ message: `Reply (${plural(post.replyCount || 0, {
+ one: '# reply',
+ other: '# replies',
+ })})`,
+ comment:
+ 'Accessibility label for the reply button, verb form followed by number of replies and noun form',
+ })}
big={big}>
{typeof post.replyCount !== 'undefined' && post.replyCount > 0 && (
@@ -261,7 +254,7 @@ let PostControls = ({
void onRepost()}
onQuote={onQuote}
big={big}
embeddingDisabled={Boolean(post.viewer?.embeddingDisabled)}
@@ -274,26 +267,22 @@ let PostControls = ({
onPress={() => requireAuth(() => onPressToggleLike())}
label={
post.viewer?.like
- ? _(
- msg({
- message: `Unlike (${plural(post.likeCount || 0, {
- one: '# like',
- other: '# likes',
- })})`,
- comment:
- 'Accessibility label for the like button when the post has been liked, verb followed by number of likes and noun',
- }),
- )
- : _(
- msg({
- message: `Like (${plural(post.likeCount || 0, {
- one: '# like',
- other: '# likes',
- })})`,
- comment:
- 'Accessibility label for the like button when the post has not been liked, verb form followed by number of likes and noun form',
- }),
- )
+ ? l({
+ message: `Unlike (${plural(post.likeCount || 0, {
+ one: '# like',
+ other: '# likes',
+ })})`,
+ comment:
+ 'Accessibility label for the like button when the post has been liked, verb followed by number of likes and noun',
+ })
+ : l({
+ message: `Like (${plural(post.likeCount || 0, {
+ one: '# like',
+ other: '# likes',
+ })})`,
+ comment:
+ 'Accessibility label for the like button when the post has not been liked, verb form followed by number of likes and noun form',
+ })
}>
diff --git a/src/components/dms/MessageContextMenu.tsx b/src/components/dms/MessageContextMenu.tsx
index b9eacf1ee4..ea5b4a48b3 100644
--- a/src/components/dms/MessageContextMenu.tsx
+++ b/src/components/dms/MessageContextMenu.tsx
@@ -6,7 +6,7 @@ import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
-import {useTranslate} from '#/lib/hooks/useTranslate'
+import {useGoogleTranslate} from '#/lib/hooks/useGoogleTranslate'
import {richTextToString} from '#/lib/strings/rich-text-helpers'
import {useConvoActive} from '#/state/messages/convo'
import {useLanguagePrefs} from '#/state/preferences'
@@ -44,7 +44,7 @@ export let MessageContextMenu = ({
const reportControl = usePromptControl()
const blockOrDeleteControl = usePromptControl()
const langPrefs = useLanguagePrefs()
- const translate = useTranslate()
+ const translate = useGoogleTranslate()
const isFromSelf = message.sender?.did === currentAccount?.did
@@ -57,12 +57,12 @@ export let MessageContextMenu = ({
true,
)
- Clipboard.setStringAsync(str)
+ void Clipboard.setStringAsync(str)
Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
}, [_, message.text, message.facets])
const onPressTranslateMessage = useCallback(() => {
- translate(message.text, langPrefs.primaryLanguage)
+ void translate(message.text, langPrefs.primaryLanguage)
ax.metric('translate', {
sourceLanguages: [],
diff --git a/src/env/index.ts b/src/env/index.ts
index 14abba55a4..d11beafcf4 100644
--- a/src/env/index.ts
+++ b/src/env/index.ts
@@ -49,3 +49,6 @@ 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 HAS_ON_DEVICE_TRANSLATION: boolean =
+ (IS_IOS && iOSMajorVersion >= 18) || IS_ANDROID
diff --git a/src/env/index.web.ts b/src/env/index.web.ts
index 0a078fdeb0..ee7462ea84 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 HAS_ON_DEVICE_TRANSLATION: boolean = false
diff --git a/src/lib/hooks/useTranslate.ts b/src/lib/hooks/useGoogleTranslate.ts
similarity index 93%
rename from src/lib/hooks/useTranslate.ts
rename to src/lib/hooks/useGoogleTranslate.ts
index 233a2ae8b2..70f1637bd8 100644
--- a/src/lib/hooks/useTranslate.ts
+++ b/src/lib/hooks/useGoogleTranslate.ts
@@ -6,10 +6,9 @@ import {getTranslatorLink} from '#/locale/helpers'
import {IS_ANDROID} from '#/env'
/**
- * Will always link out to Google Translate. If inline translation is desired,
- * use `useTranslateOnDevice`
+ * @deprecated Will always link out to Google Translate. Prefer `useTranslate`.
*/
-export function useTranslate() {
+export function useGoogleTranslate() {
const openLink = useOpenLink()
return useCallback(
diff --git a/src/lib/translation/context.ts b/src/lib/translation/context.ts
new file mode 100644
index 0000000000..423efc4f1c
--- /dev/null
+++ b/src/lib/translation/context.ts
@@ -0,0 +1,16 @@
+import {createContext} from 'react'
+
+import {type TranslationFunctionParams, type TranslationState} from './types'
+
+export const Context = createContext<{
+ translationState: Record
+ translate: (
+ parameters: TranslationFunctionParams & {
+ key: string
+ forceGoogleTranslate: boolean
+ },
+ ) => Promise
+ clearTranslation: (key: string) => void
+ acquireTranslation: (key: string) => () => void
+} | null>(null)
+Context.displayName = 'TranslationContext'
diff --git a/src/lib/translation/index.tsx b/src/lib/translation/index.tsx
new file mode 100644
index 0000000000..d6c8a51733
--- /dev/null
+++ b/src/lib/translation/index.tsx
@@ -0,0 +1,282 @@
+import {useCallback, useContext, useEffect, useMemo, useState} from 'react'
+import {LayoutAnimation, Platform} from 'react-native'
+import {getLocales} from 'expo-localization'
+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 {logger} from '#/logger'
+import {useAnalytics} from '#/analytics'
+import {HAS_ON_DEVICE_TRANSLATION} from '#/env'
+import {Context} from './context'
+import {type TranslationFunctionParams, type TranslationState} from './types'
+import {guessLanguage} from './utils'
+
+export * from './types'
+export * from './utils'
+
+/**
+ * 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
+ * the current build.
+ */
+async function attemptTranslation(
+ input: string,
+ targetLangCodeOriginal: string,
+ sourceLangCodeOriginal?: string, // Auto-detects if not provided
+): Promise<{
+ translatedText: string
+ targetLanguage: TranslationTaskResult['targetLanguage']
+ sourceLanguage: TranslationTaskResult['sourceLanguage']
+}> {
+ // Note that Android only supports two-character language codes and will fail
+ // on other input.
+ // https://developers.google.com/android/reference/com/google/mlkit/nl/translate/TranslateLanguage
+ let targetLangCode =
+ Platform.OS === 'android'
+ ? targetLangCodeOriginal.split('-')[0]
+ : targetLangCodeOriginal
+ const sourceLangCode =
+ Platform.OS === 'android'
+ ? sourceLangCodeOriginal?.split('-')[0]
+ : sourceLangCodeOriginal
+
+ // Special cases for regional languages since iOS differentiates and missing
+ // language packs must be downloaded and installed.
+ if (Platform.OS === 'ios') {
+ const deviceLocales = getLocales()
+ const primaryLanguageTag = deviceLocales[0]?.languageTag
+ switch (targetLangCodeOriginal) {
+ case 'en': // en-US, en-GB
+ case 'es': // es-419, es-ES
+ case 'pt': // pt-BR, pt-PT
+ case 'zh': // zh-Hans-CN, zh-Hant-HK, zh-Hant-TW
+ if (
+ primaryLanguageTag &&
+ primaryLanguageTag.startsWith(targetLangCodeOriginal)
+ ) {
+ targetLangCode = primaryLanguageTag
+ }
+ break
+ }
+ }
+
+ const result = await onTranslateTask({
+ input,
+ targetLangCode,
+ sourceLangCode,
+ })
+
+ // 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.')
+ }
+
+ if (translatedText === '') {
+ throw new Error('Translation result is empty.')
+ }
+
+ return {
+ translatedText,
+ targetLanguage: result.targetLanguage,
+ sourceLanguage:
+ result.sourceLanguage ?? sourceLangCode ?? guessLanguage(input), // iOS doesn't return the source language
+ }
+}
+
+/**
+ * Native translation hook. Attempts on-device translation using Apple
+ * Translation (iOS 18+) or Google ML Kit (Android).
+ *
+ * Falls back to Google Translate URL if the language pack is unavailable.
+ *
+ * Web uses index.web.ts which always opens Google Translate.
+ */
+export function useTranslate({
+ key,
+ forceGoogleTranslate = false,
+}: {
+ key: string
+ forceGoogleTranslate?: boolean
+}) {
+ const context = useContext(Context)
+ if (!context) {
+ throw new Error(
+ 'useTranslate must be used within a TranslateOnDeviceProvider',
+ )
+ }
+
+ useFocusEffect(
+ useCallback(() => {
+ const cleanup = context.acquireTranslation(key)
+ return cleanup
+ }, [key, context]),
+ )
+
+ const translate = useCallback(
+ async (params: TranslationFunctionParams) => {
+ return context.translate({...params, key, forceGoogleTranslate})
+ },
+ [context, forceGoogleTranslate, key],
+ )
+
+ const clearTranslation = useCallback(
+ () => context.clearTranslation(key),
+ [context, key],
+ )
+
+ return useMemo(
+ () => ({
+ translationState: context.translationState[key] ?? {
+ status: 'idle',
+ },
+ translate,
+ clearTranslation,
+ }),
+ [clearTranslation, context.translationState, key, translate],
+ )
+}
+
+export function Provider({children}: React.PropsWithChildren) {
+ const [translationState, setTranslationState] = useState<
+ Record
+ >({})
+ const [refCounts, setRefCounts] = useState>({})
+ const ax = useAnalytics()
+ const {t: l} = useLingui()
+ const googleTranslate = useGoogleTranslate()
+
+ useEffect(() => {
+ setTranslationState(prev => {
+ const keysToDelete: string[] = []
+
+ for (const key of Object.keys(prev)) {
+ if ((refCounts[key] ?? 0) <= 0) {
+ keysToDelete.push(key)
+ }
+ }
+
+ if (keysToDelete.length > 0) {
+ const newState = {...prev}
+ keysToDelete.forEach(key => {
+ delete newState[key]
+ })
+ return newState
+ }
+
+ return prev
+ })
+ }, [refCounts])
+
+ const acquireTranslation = useCallback((key: string) => {
+ setRefCounts(prev => ({
+ ...prev,
+ [key]: (prev[key] ?? 0) + 1,
+ }))
+
+ return () => {
+ setRefCounts(prev => {
+ const newCount = (prev[key] ?? 1) - 1
+ if (newCount <= 0) {
+ const {[key]: _, ...rest} = prev
+ return rest
+ }
+ return {...prev, [key]: newCount}
+ })
+ }
+ }, [])
+
+ const clearTranslation = useCallback((key: string) => {
+ LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
+ setTranslationState(prev => {
+ delete prev[key]
+ return {...prev}
+ })
+ }, [])
+
+ const translate = useCallback(
+ async ({
+ key,
+ text,
+ targetLangCode,
+ sourceLangCode,
+ ...options
+ }: {
+ key: string
+ text: string
+ targetLangCode: string
+ sourceLangCode?: string
+ forceGoogleTranslate?: boolean
+ }) => {
+ 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)
+ return
+ }
+
+ // Translate after the next state change.
+ LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
+ setTranslationState(prev => ({
+ ...prev,
+ [key]: {status: 'loading'},
+ }))
+ try {
+ const result = await attemptTranslation(
+ text,
+ targetLangCode,
+ sourceLangCode,
+ )
+ ax.metric('translate:result', {
+ method: 'on-device',
+ os: Platform.OS,
+ sourceLanguage: result.sourceLanguage,
+ targetLanguage: result.targetLanguage,
+ })
+ LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
+ setTranslationState(prev => ({
+ ...prev,
+ [key]: {
+ status: 'success',
+ translatedText: result.translatedText,
+ sourceLanguage: result.sourceLanguage,
+ targetLanguage: result.targetLanguage,
+ },
+ }))
+ } catch (e) {
+ logger.error('Failed to translate post on device', {safeMessage: e})
+ // On-device translation failed (language pack missing or user
+ // dismissed the download prompt). Fall back to Google Translate.
+ ax.metric('translate:result', {
+ method: 'fallback-alert',
+ os: Platform.OS,
+ sourceLanguage: sourceLangCode ?? null,
+ targetLanguage: targetLangCode,
+ })
+ let errorMessage = l`Device failed to translate :(`
+ LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
+ setTranslationState(prev => ({
+ ...prev,
+ [key]: {status: 'error', message: errorMessage},
+ }))
+ }
+ },
+ [ax, googleTranslate, l],
+ )
+
+ const ctx = useMemo(
+ () => ({acquireTranslation, clearTranslation, translate, translationState}),
+ [acquireTranslation, clearTranslation, translate, translationState],
+ )
+
+ return {children}
+}
diff --git a/src/lib/translation/index.web.tsx b/src/lib/translation/index.web.tsx
new file mode 100644
index 0000000000..8fc46175fa
--- /dev/null
+++ b/src/lib/translation/index.web.tsx
@@ -0,0 +1,86 @@
+import {useCallback, useContext, useMemo} from 'react'
+
+import {useGoogleTranslate} from '#/lib/hooks/useGoogleTranslate'
+import {useAnalytics} from '#/analytics'
+import {Context} from './context'
+import {type TranslationFunctionParams, type TranslationState} from './types'
+
+export * from './types'
+export * from './utils'
+
+const translationState: Record = {}
+const acquireTranslation = (_key: string) => {
+ return () => {}
+}
+const clearTranslation = (_key: string) => {}
+
+/**
+ * Web always opens Google Translate.
+ */
+export function useTranslate({
+ key,
+}: {
+ key: string
+ forceGoogleTranslate?: boolean
+}) {
+ const context = useContext(Context)
+ if (!context) {
+ throw new Error(
+ 'useTranslate must be used within a TranslateOnDeviceProvider',
+ )
+ }
+
+ // Always call hooks in consistent order
+ const translate = useCallback(
+ async (params: TranslationFunctionParams) => {
+ return context.translate({...params, key, forceGoogleTranslate: true})
+ },
+ [key, context],
+ )
+
+ const clearTranslation = useCallback(() => {
+ return context.clearTranslation(key)
+ }, [key, context])
+
+ return {
+ translationState: context.translationState[key] ?? {
+ status: 'idle' as const,
+ },
+ translate,
+ clearTranslation,
+ }
+}
+
+export function Provider({children}: React.PropsWithChildren) {
+ const ax = useAnalytics()
+ const googleTranslate = useGoogleTranslate()
+
+ const translate = useCallback(
+ async ({
+ 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)
+ },
+ [ax, googleTranslate],
+ )
+
+ const ctx = useMemo(
+ () => ({acquireTranslation, clearTranslation, translate, translationState}),
+ [translate],
+ )
+
+ return {children}
+}
diff --git a/src/lib/translation/types.ts b/src/lib/translation/types.ts
new file mode 100644
index 0000000000..4bf2d021e7
--- /dev/null
+++ b/src/lib/translation/types.ts
@@ -0,0 +1,34 @@
+import {type TranslationTaskResult} from '@bsky.app/expo-translate-text/build/ExpoTranslateText.types'
+
+export type TranslationState =
+ | {status: 'idle'}
+ | {status: 'loading'}
+ | {
+ status: 'success'
+ translatedText: string
+ sourceLanguage: TranslationTaskResult['sourceLanguage']
+ targetLanguage: TranslationTaskResult['targetLanguage']
+ }
+ | {
+ status: 'error'
+ message: string
+ }
+
+export type TranslationFunctionParams = {
+ /**
+ * The text to be translated.
+ */
+ text: string
+ /**
+ * The language to translate the text into.
+ */
+ targetLangCode: string
+ /**
+ * The source language of the text. Will auto-detect if not provided.
+ */
+ sourceLangCode?: string
+}
+
+export type TranslationFunction = (
+ parameters: TranslationFunctionParams,
+) => Promise
diff --git a/src/lib/translation/utils.ts b/src/lib/translation/utils.ts
new file mode 100644
index 0000000000..0a3ab9121e
--- /dev/null
+++ b/src/lib/translation/utils.ts
@@ -0,0 +1,13 @@
+import lande from 'lande'
+
+import {code3ToCode2Strict} from '#/locale/helpers'
+
+// TODO: Replace with expo-guess-language
+export function guessLanguage(text: string): string | null {
+ const results = lande(text)
+ // only return high-confidence results
+ if (results[0] && results[0][1] > 0.97) {
+ return code3ToCode2Strict(results[0][0]) ?? null
+ }
+ return null
+}
diff --git a/src/locale/languages.ts b/src/locale/languages.ts
index 9d8008f019..af7ae9e20b 100644
--- a/src/locale/languages.ts
+++ b/src/locale/languages.ts
@@ -2,8 +2,6 @@ export interface Language {
code3: string
code2: string
name: string
- android: boolean
- ios: boolean
}
export enum AppLanguage {
@@ -103,3543 +101,2531 @@ export const APP_LANGUAGES: AppLanguageConfig[] = [
]
// Pre-generated list using Intl.DisplayNames to localize the language name.
-// https://developers.google.com/android/reference/com/google/mlkit/nl/translate/TranslateLanguage
-// https://developer.apple.com/documentation/foundation/nslocale/isolanguagecodes
export const LANGUAGES: Language[] = [
{
code3: 'aar',
code2: 'aa',
name: 'Afar',
- android: false,
- ios: false,
},
{
code3: 'abk',
code2: 'ab',
name: 'Abkhazian',
- android: false,
- ios: false,
},
{
code3: 'ace',
code2: '',
name: 'Achinese',
- android: false,
- ios: false,
},
{
code3: 'ach',
code2: '',
name: 'Acoli',
- android: false,
- ios: false,
},
{
code3: 'ada',
code2: '',
name: 'Adangme',
- android: false,
- ios: false,
},
{
code3: 'ady',
code2: '',
name: 'Adyghe; Adygei',
- android: false,
- ios: false,
},
{
code3: 'afa',
code2: '',
name: 'Afro-Asiatic languages',
- android: false,
- ios: false,
},
{
code3: 'afh',
code2: '',
name: 'Afrihili',
- android: false,
- ios: false,
},
{
code3: 'afr',
code2: 'af',
name: 'Afrikaans',
- android: false,
- ios: false,
},
{
code3: 'ain',
code2: '',
name: 'Ainu',
- android: false,
- ios: false,
},
{
code3: 'aka',
code2: 'ak',
name: 'Akan',
- android: false,
- ios: false,
},
{
code3: 'akk',
code2: '',
name: 'Akkadian',
- android: false,
- ios: false,
},
{
code3: 'alb',
code2: 'sq',
name: 'Albanian',
- android: true,
- ios: false,
},
{
code3: 'ale',
code2: '',
name: 'Aleut',
- android: false,
- ios: false,
},
{
code3: 'alg',
code2: '',
name: 'Algonquian languages',
- android: false,
- ios: false,
},
{
code3: 'alt',
code2: '',
name: 'Southern Altai',
- android: false,
- ios: false,
},
{
code3: 'amh',
code2: 'am',
name: 'Amharic',
- android: false,
- ios: false,
},
{
code3: 'ang',
code2: '',
name: 'English, Old (ca.450-1100)',
- android: false,
- ios: false,
},
{
code3: 'anp',
code2: '',
name: 'Angika',
- android: false,
- ios: false,
},
{
code3: 'apa',
code2: '',
name: 'Apache languages',
- android: false,
- ios: false,
},
{
code3: 'ara',
code2: 'ar',
name: 'Arabic',
- android: true,
- ios: false,
},
{
code3: 'arc',
code2: '',
name: 'Official Aramaic (700-300 BCE); Imperial Aramaic (700-300 BCE)',
- android: false,
- ios: false,
},
{
code3: 'arg',
code2: 'an',
name: 'Aragonese',
- android: false,
- ios: false,
},
{
code3: 'arm',
code2: 'hy',
name: 'Armenian',
- android: false,
- ios: false,
},
{
code3: 'arn',
code2: '',
name: 'Mapudungun; Mapuche',
- android: false,
- ios: false,
},
{
code3: 'arp',
code2: '',
name: 'Arapaho',
- android: false,
- ios: false,
},
{
code3: 'art',
code2: '',
name: 'Artificial languages',
- android: false,
- ios: false,
},
{
code3: 'arw',
code2: '',
name: 'Arawak',
- android: false,
- ios: false,
},
{
code3: 'asm',
code2: 'as',
name: 'Assamese',
- android: false,
- ios: false,
},
{
code3: 'ast',
code2: '',
name: 'Asturian',
- android: false,
- ios: false,
},
{
code3: 'ath',
code2: '',
name: 'Athapascan languages',
- android: false,
- ios: false,
},
{
code3: 'aus',
code2: '',
name: 'Australian languages',
- android: false,
- ios: false,
},
{
code3: 'ava',
code2: 'av',
name: 'Avaric',
- android: false,
- ios: false,
},
{
code3: 'ave',
code2: 'ae',
name: 'Avestan',
- android: false,
- ios: false,
},
{
code3: 'awa',
code2: '',
name: 'Awadhi',
- android: false,
- ios: false,
},
{
code3: 'aym',
code2: 'ay',
name: 'Aymara',
- android: false,
- ios: false,
},
{
code3: 'aze',
code2: 'az',
name: 'Azerbaijani',
- android: false,
- ios: false,
},
{
code3: 'bad',
code2: '',
name: 'Banda languages',
- android: false,
- ios: false,
},
{
code3: 'bai',
code2: '',
name: 'Bamileke languages',
- android: false,
- ios: false,
},
{
code3: 'bak',
code2: 'ba',
name: 'Bashkir',
- android: false,
- ios: false,
},
{
code3: 'bal',
code2: '',
name: 'Baluchi',
- android: false,
- ios: false,
},
{
code3: 'bam',
code2: 'bm',
name: 'Bambara',
- android: false,
- ios: false,
},
{
code3: 'ban',
code2: '',
name: 'Balinese',
- android: false,
- ios: false,
},
{
code3: 'baq',
code2: 'eu',
name: 'Basque',
- android: false,
- ios: false,
},
{
code3: 'bas',
code2: '',
name: 'Basa',
- android: false,
- ios: false,
},
{
code3: 'bat',
code2: '',
name: 'Baltic languages',
- android: false,
- ios: false,
},
{
code3: 'bej',
code2: '',
name: 'Beja; Bedawiyet',
- android: false,
- ios: false,
},
{
code3: 'bel',
code2: 'be',
name: 'Belarusian',
- android: true,
- ios: false,
},
{
code3: 'bem',
code2: '',
name: 'Bemba',
- android: false,
- ios: false,
},
{
code3: 'ben',
code2: 'bn',
name: 'Bangla',
- android: true,
- ios: false,
},
{
code3: 'ber',
code2: '',
name: 'Berber languages',
- android: false,
- ios: false,
},
{
code3: 'bho',
code2: '',
name: 'Bhojpuri',
- android: false,
- ios: false,
},
{
code3: 'bih',
code2: 'bh',
name: 'Bhojpuri',
- android: false,
- ios: false,
},
{
code3: 'bik',
code2: '',
name: 'Bikol',
- android: false,
- ios: false,
},
{
code3: 'bin',
code2: '',
name: 'Bini; Edo',
- android: false,
- ios: false,
},
{
code3: 'bis',
code2: 'bi',
name: 'Bislama',
- android: false,
- ios: false,
},
{
code3: 'bla',
code2: '',
name: 'Siksika',
- android: false,
- ios: false,
},
{
code3: 'bnt',
code2: '',
name: 'Bantu languages',
- android: false,
- ios: false,
},
{
code3: 'bod',
code2: 'bo',
name: 'Tibetan',
- android: false,
- ios: false,
},
{
code3: 'bos',
code2: 'bs',
name: 'Bosnian',
- android: false,
- ios: false,
},
{
code3: 'bra',
code2: '',
name: 'Braj',
- android: false,
- ios: false,
},
{
code3: 'bre',
code2: 'br',
name: 'Breton',
- android: false,
- ios: false,
},
{
code3: 'btk',
code2: '',
name: 'Batak languages',
- android: false,
- ios: false,
},
{
code3: 'bua',
code2: '',
name: 'Buriat',
- android: false,
- ios: false,
},
{
code3: 'bug',
code2: '',
name: 'Buginese',
- android: false,
- ios: false,
},
{
code3: 'bul',
code2: 'bg',
name: 'Bulgarian',
- android: true,
- ios: false,
},
{
code3: 'bur',
code2: 'my',
name: 'Burmese',
- android: false,
- ios: false,
},
{
code3: 'byn',
code2: '',
name: 'Blin; Bilin',
- android: false,
- ios: false,
},
{
code3: 'cad',
code2: '',
name: 'Caddo',
- android: false,
- ios: false,
},
{
code3: 'cai',
code2: '',
name: 'Central American Indian languages',
- android: false,
- ios: false,
},
{
code3: 'car',
code2: '',
name: 'Galibi Carib',
- android: false,
- ios: false,
},
{
code3: 'cat',
code2: 'ca',
name: 'Catalan',
- android: true,
- ios: false,
},
{
code3: 'cau',
code2: '',
name: 'Caucasian languages',
- android: false,
- ios: false,
},
{
code3: 'ceb',
code2: '',
name: 'Cebuano',
- android: false,
- ios: false,
},
{
code3: 'cel',
code2: '',
name: 'Celtic languages',
- android: false,
- ios: false,
},
{
code3: 'ces',
code2: 'cs',
name: 'Czech',
- android: true,
- ios: false,
},
{
code3: 'cha',
code2: 'ch',
name: 'Chamorro',
- android: false,
- ios: false,
},
{
code3: 'chb',
code2: '',
name: 'Chibcha',
- android: false,
- ios: false,
},
{
code3: 'che',
code2: 'ce',
name: 'Chechen',
- android: false,
- ios: false,
},
{
code3: 'chg',
code2: '',
name: 'Chagatai',
- android: false,
- ios: false,
},
{
code3: 'chi',
code2: 'zh',
name: 'Chinese',
- android: true,
- ios: false,
},
{
code3: 'chk',
code2: '',
name: 'Chuukese',
- android: false,
- ios: false,
},
{
code3: 'chm',
code2: '',
name: 'Mari',
- android: false,
- ios: false,
},
{
code3: 'chn',
code2: '',
name: 'Chinook jargon',
- android: false,
- ios: false,
},
{
code3: 'cho',
code2: '',
name: 'Choctaw',
- android: false,
- ios: false,
},
{
code3: 'chp',
code2: '',
name: 'Chipewyan; Dene Suline',
- android: false,
- ios: false,
},
{
code3: 'chr',
code2: '',
name: 'Cherokee',
- android: false,
- ios: false,
},
{
code3: 'chu',
code2: 'cu',
name: 'Church Slavic',
- android: false,
- ios: false,
},
{
code3: 'chv',
code2: 'cv',
name: 'Chuvash',
- android: false,
- ios: false,
},
{
code3: 'chy',
code2: '',
name: 'Cheyenne',
- android: false,
- ios: false,
},
{
code3: 'cmc',
code2: '',
name: 'Chamic languages',
- android: false,
- ios: false,
},
{
code3: 'cnr',
code2: '',
name: 'Serbian (Montenegro)',
- android: false,
- ios: false,
},
{
code3: 'cop',
code2: '',
name: 'Coptic',
- android: false,
- ios: false,
},
{
code3: 'cor',
code2: 'kw',
name: 'Cornish',
- android: false,
- ios: false,
},
{
code3: 'cos',
code2: 'co',
name: 'Corsican',
- android: false,
- ios: false,
},
{
code3: 'cpe',
code2: '',
name: 'Creoles and pidgins, English based',
- android: false,
- ios: false,
},
{
code3: 'cpf',
code2: '',
name: 'Creoles and pidgins, French-based',
- android: false,
- ios: false,
},
{
code3: 'cpp',
code2: '',
name: 'Creoles and pidgins, Portuguese-based',
- android: false,
- ios: false,
},
{
code3: 'cre',
code2: 'cr',
name: 'Cree',
- android: false,
- ios: false,
},
{
code3: 'crh',
code2: '',
name: 'Crimean Tatar; Crimean Turkish',
- android: false,
- ios: false,
},
{
code3: 'crp',
code2: '',
name: 'Creoles and pidgins',
- android: false,
- ios: false,
},
{
code3: 'csb',
code2: '',
name: 'Kashubian',
- android: false,
- ios: false,
},
{
code3: 'cus',
code2: '',
name: 'Cushitic languages',
- android: false,
- ios: false,
},
{
code3: 'cym',
code2: 'cy',
name: 'Welsh',
- android: true,
- ios: false,
},
{
code3: 'cze',
code2: 'cs',
name: 'Czech',
- android: true,
- ios: false,
},
{
code3: 'dak',
code2: '',
name: 'Dakota',
- android: false,
- ios: false,
},
{
code3: 'dan',
code2: 'da',
name: 'Danish',
- android: true,
- ios: false,
},
{
code3: 'dar',
code2: '',
name: 'Dargwa',
- android: false,
- ios: false,
},
{
code3: 'day',
code2: '',
name: 'Land Dayak languages',
- android: false,
- ios: false,
},
{
code3: 'del',
code2: '',
name: 'Delaware',
- android: false,
- ios: false,
},
{
code3: 'den',
code2: '',
name: 'Slave (Athapascan)',
- android: false,
- ios: false,
},
{
code3: 'deu',
code2: 'de',
name: 'German',
- android: true,
- ios: true,
},
{
code3: 'dgr',
code2: '',
name: 'Dogrib',
- android: false,
- ios: false,
},
{
code3: 'din',
code2: '',
name: 'Dinka',
- android: false,
- ios: false,
},
{
code3: 'div',
code2: 'dv',
name: 'Divehi',
- android: false,
- ios: false,
},
{
code3: 'doi',
code2: '',
name: 'Dogri',
- android: false,
- ios: false,
},
{
code3: 'dra',
code2: '',
name: 'Dravidian languages',
- android: false,
- ios: false,
},
{
code3: 'dsb',
code2: '',
name: 'Lower Sorbian',
- android: false,
- ios: false,
},
{
code3: 'dua',
code2: '',
name: 'Duala',
- android: false,
- ios: false,
},
{
code3: 'dum',
code2: '',
name: 'Dutch, Middle (ca.1050-1350)',
- android: false,
- ios: false,
},
{
code3: 'dut',
code2: 'nl',
name: 'Dutch',
- android: true,
- ios: true,
},
{
code3: 'dyu',
code2: '',
name: 'Dyula',
- android: false,
- ios: false,
},
{
code3: 'dzo',
code2: 'dz',
name: 'Dzongkha',
- android: false,
- ios: false,
},
{
code3: 'efi',
code2: '',
name: 'Efik',
- android: false,
- ios: false,
},
{
code3: 'egy',
code2: '',
name: 'Egyptian (Ancient)',
- android: false,
- ios: false,
},
{
code3: 'eka',
code2: '',
name: 'Ekajuk',
- android: false,
- ios: false,
},
{
code3: 'ell',
code2: 'el',
name: 'Greek',
- android: true,
- ios: false,
},
{
code3: 'elx',
code2: '',
name: 'Elamite',
- android: false,
- ios: false,
},
{
code3: 'eng',
code2: 'en',
name: 'English',
- android: true,
- ios: true,
},
{
code3: 'enm',
code2: '',
name: 'English, Middle (1100-1500)',
- android: false,
- ios: false,
},
{
code3: 'epo',
code2: 'eo',
name: 'Esperanto',
- android: true,
- ios: false,
},
{
code3: 'est',
code2: 'et',
name: 'Estonian',
- android: true,
- ios: false,
},
{
code3: 'eus',
code2: 'eu',
name: 'Basque',
- android: false,
- ios: false,
},
{
code3: 'ewe',
code2: 'ee',
name: 'Ewe',
- android: false,
- ios: false,
},
{
code3: 'ewo',
code2: '',
name: 'Ewondo',
- android: false,
- ios: false,
},
{
code3: 'fan',
code2: '',
name: 'Fang',
- android: false,
- ios: false,
},
{
code3: 'fao',
code2: 'fo',
name: 'Faroese',
- android: false,
- ios: false,
},
{
code3: 'fas',
code2: 'fa',
name: 'Persian',
- android: true,
- ios: false,
},
{
code3: 'fat',
code2: '',
name: 'Akan',
- android: false,
- ios: false,
},
{
code3: 'fij',
code2: 'fj',
name: 'Fijian',
- android: false,
- ios: false,
},
{
code3: 'fil',
code2: '',
name: 'Filipino',
- android: false,
- ios: false,
},
{
code3: 'fin',
code2: 'fi',
name: 'Finnish',
- android: true,
- ios: false,
},
{
code3: 'fiu',
code2: '',
name: 'Finno-Ugrian languages',
- android: false,
- ios: false,
},
{
code3: 'fon',
code2: '',
name: 'Fon',
- android: false,
- ios: false,
},
{
code3: 'fra',
code2: 'fr',
name: 'French',
- android: true,
- ios: true,
},
{
code3: 'fre',
code2: 'fr',
name: 'French',
- android: true,
- ios: true,
},
{
code3: 'frm',
code2: '',
name: 'French, Middle (ca.1400-1600)',
- android: false,
- ios: false,
},
{
code3: 'fro',
code2: '',
name: 'French, Old (842-ca.1400)',
- android: false,
- ios: false,
},
{
code3: 'frr',
code2: '',
name: 'Northern Frisian',
- android: false,
- ios: false,
},
{
code3: 'frs',
code2: '',
name: 'Eastern Frisian',
- android: false,
- ios: false,
},
{
code3: 'fry',
code2: 'fy',
name: 'Western Frisian',
- android: false,
- ios: false,
},
{
code3: 'ful',
code2: 'ff',
name: 'Fulah',
- android: false,
- ios: false,
},
{
code3: 'fur',
code2: '',
name: 'Friulian',
- android: false,
- ios: false,
},
{
code3: 'gaa',
code2: '',
name: 'Ga',
- android: false,
- ios: false,
},
{
code3: 'gay',
code2: '',
name: 'Gayo',
- android: false,
- ios: false,
},
{
code3: 'gba',
code2: '',
name: 'Gbaya',
- android: false,
- ios: false,
},
{
code3: 'gem',
code2: '',
name: 'Germanic languages',
- android: false,
- ios: false,
},
{
code3: 'geo',
code2: 'ka',
name: 'Georgian',
- android: true,
- ios: false,
},
{
code3: 'ger',
code2: 'de',
name: 'German',
- android: true,
- ios: true,
},
{
code3: 'gez',
code2: '',
name: 'Geez',
- android: false,
- ios: false,
},
{
code3: 'gil',
code2: '',
name: 'Gilbertese',
- android: false,
- ios: false,
},
{
code3: 'gla',
code2: 'gd',
name: 'Scottish Gaelic',
- android: false,
- ios: false,
},
{
code3: 'gle',
code2: 'ga',
name: 'Irish',
- android: true,
- ios: false,
},
{
code3: 'glg',
code2: 'gl',
name: 'Galician',
- android: true,
- ios: false,
},
{
code3: 'glv',
code2: 'gv',
name: 'Manx',
- android: false,
- ios: false,
},
{
code3: 'gmh',
code2: '',
name: 'German, Middle High (ca.1050-1500)',
- android: false,
- ios: false,
},
{
code3: 'goh',
code2: '',
name: 'German, Old High (ca.750-1050)',
- android: false,
- ios: false,
},
{
code3: 'gon',
code2: '',
name: 'Gondi',
- android: false,
- ios: false,
},
{
code3: 'gor',
code2: '',
name: 'Gorontalo',
- android: false,
- ios: false,
},
{
code3: 'got',
code2: '',
name: 'Gothic',
- android: false,
- ios: false,
},
{
code3: 'grb',
code2: '',
name: 'Grebo',
- android: false,
- ios: false,
},
{
code3: 'grc',
code2: '',
name: 'Ancient Greek',
- android: false,
- ios: false,
},
{
code3: 'gre',
code2: 'el',
name: 'Greek',
- android: true,
- ios: false,
},
{
code3: 'grn',
code2: 'gn',
name: 'Guarani',
- android: false,
- ios: false,
},
{
code3: 'gsw',
code2: '',
name: 'Swiss German; Alemannic; Alsatian',
- android: false,
- ios: false,
},
{
code3: 'guj',
code2: 'gu',
name: 'Gujarati',
- android: true,
- ios: false,
},
{
code3: 'gwi',
code2: '',
name: "Gwich'in",
- android: false,
- ios: false,
},
{
code3: 'hai',
code2: '',
name: 'Haida',
- android: false,
- ios: false,
},
{
code3: 'hat',
code2: 'ht',
name: 'Haitian Creole',
- android: true,
- ios: false,
},
{
code3: 'hau',
code2: 'ha',
name: 'Hausa',
- android: false,
- ios: false,
},
{
code3: 'haw',
code2: '',
name: 'Hawaiian',
- android: false,
- ios: false,
},
{
code3: 'heb',
code2: 'he',
name: 'Hebrew',
- android: true,
- ios: false,
},
{
code3: 'her',
code2: 'hz',
name: 'Herero',
- android: false,
- ios: false,
},
{
code3: 'hil',
code2: '',
name: 'Hiligaynon',
- android: false,
- ios: false,
},
{
code3: 'him',
code2: '',
name: 'Himachali languages; Western Pahari languages',
- android: false,
- ios: false,
},
{
code3: 'hin',
code2: 'hi',
name: 'Hindi',
- android: true,
- ios: true,
},
{
code3: 'hit',
code2: '',
name: 'Hittite',
- android: false,
- ios: false,
},
{
code3: 'hmn',
code2: '',
name: 'Hmong',
- android: false,
- ios: false,
},
{
code3: 'hmo',
code2: 'ho',
name: 'Hiri Motu',
- android: false,
- ios: false,
},
{
code3: 'hrv',
code2: 'hr',
name: 'Croatian',
- android: true,
- ios: false,
},
{
code3: 'hsb',
code2: '',
name: 'Upper Sorbian',
- android: false,
- ios: false,
},
{
code3: 'hun',
code2: 'hu',
name: 'Hungarian',
- android: true,
- ios: false,
},
{
code3: 'hup',
code2: '',
name: 'Hupa',
- android: false,
- ios: false,
},
{
code3: 'hye',
code2: 'hy',
name: 'Armenian',
- android: false,
- ios: false,
},
{
code3: 'iba',
code2: '',
name: 'Iban',
- android: false,
- ios: false,
},
{
code3: 'ibo',
code2: 'ig',
name: 'Igbo',
- android: false,
- ios: false,
},
{
code3: 'ice',
code2: 'is',
name: 'Icelandic',
- android: true,
- ios: false,
},
{
code3: 'ido',
code2: 'io',
name: 'Ido',
- android: false,
- ios: false,
},
{
code3: 'iii',
code2: 'ii',
name: 'Sichuan Yi; Nuosu',
- android: false,
- ios: false,
},
{
code3: 'ijo',
code2: '',
name: 'Ijo languages',
- android: false,
- ios: false,
},
{
code3: 'iku',
code2: 'iu',
name: 'Inuktitut',
- android: false,
- ios: false,
},
{
code3: 'ile',
code2: 'ie',
name: 'Interlingue',
- android: false,
- ios: false,
},
{
code3: 'ilo',
code2: '',
name: 'Iloko',
- android: false,
- ios: false,
},
{
code3: 'ina',
code2: 'ia',
name: 'Interlingua',
- android: false,
- ios: false,
},
{
code3: 'inc',
code2: '',
name: 'Indic languages',
- android: false,
- ios: false,
},
{
code3: 'ind',
code2: 'id',
name: 'Indonesian',
- android: true,
- ios: false,
},
{
code3: 'ine',
code2: '',
name: 'Indo-European languages',
- android: false,
- ios: false,
},
{
code3: 'inh',
code2: '',
name: 'Ingush',
- android: false,
- ios: false,
},
{
code3: 'ipk',
code2: 'ik',
name: 'Inupiaq',
- android: false,
- ios: false,
},
{
code3: 'ira',
code2: '',
name: 'Iranian languages',
- android: false,
- ios: false,
},
{
code3: 'iro',
code2: '',
name: 'Iroquoian languages',
- android: false,
- ios: false,
},
{
code3: 'isl',
code2: 'is',
name: 'Icelandic',
- android: true,
- ios: false,
},
{
code3: 'ita',
code2: 'it',
name: 'Italian',
- android: true,
- ios: true,
},
{
code3: 'jav',
code2: 'jv',
name: 'Javanese',
- android: false,
- ios: false,
},
{
code3: 'jbo',
code2: '',
name: 'Lojban',
- android: false,
- ios: false,
},
{
code3: 'jpn',
code2: 'ja',
name: 'Japanese',
- android: true,
- ios: true,
},
{
code3: 'jpr',
code2: '',
name: 'Judeo-Persian',
- android: false,
- ios: false,
},
{
code3: 'jrb',
code2: '',
name: 'Judeo-Arabic',
- android: false,
- ios: false,
},
{
code3: 'kaa',
code2: '',
name: 'Kara-Kalpak',
- android: false,
- ios: false,
},
{
code3: 'kab',
code2: '',
name: 'Kabyle',
- android: false,
- ios: false,
},
{
code3: 'kac',
code2: '',
name: 'Kachin; Jingpho',
- android: false,
- ios: false,
},
{
code3: 'kal',
code2: 'kl',
name: 'Kalaallisut',
- android: false,
- ios: false,
},
{
code3: 'kam',
code2: '',
name: 'Kamba',
- android: false,
- ios: false,
},
{
code3: 'kan',
code2: 'kn',
name: 'Kannada',
- android: true,
- ios: false,
},
{
code3: 'kar',
code2: '',
name: 'Karen languages',
- android: false,
- ios: false,
},
{
code3: 'kas',
code2: 'ks',
name: 'Kashmiri',
- android: false,
- ios: false,
},
{
code3: 'kat',
code2: 'ka',
name: 'Georgian',
- android: true,
- ios: false,
},
{
code3: 'kau',
code2: 'kr',
name: 'Kanuri',
- android: false,
- ios: false,
},
{
code3: 'kaw',
code2: '',
name: 'Kawi',
- android: false,
- ios: false,
},
{
code3: 'kaz',
code2: 'kk',
name: 'Kazakh',
- android: false,
- ios: false,
},
{
code3: 'kbd',
code2: '',
name: 'Kabardian',
- android: false,
- ios: false,
},
{
code3: 'kha',
code2: '',
name: 'Khasi',
- android: false,
- ios: false,
},
{
code3: 'khi',
code2: '',
name: 'Khoisan languages',
- android: false,
- ios: false,
},
{
code3: 'khm',
code2: 'km',
name: 'Khmer',
- android: false,
- ios: false,
},
{
code3: 'kho',
code2: '',
name: 'Khotanese; Sakan',
- android: false,
- ios: false,
},
{
code3: 'kik',
code2: 'ki',
name: 'Kikuyu; Gikuyu',
- android: false,
- ios: false,
},
{
code3: 'kin',
code2: 'rw',
name: 'Kinyarwanda',
- android: false,
- ios: false,
},
{
code3: 'kir',
code2: 'ky',
name: 'Kyrgyz',
- android: false,
- ios: false,
},
{
code3: 'kmb',
code2: '',
name: 'Kimbundu',
- android: false,
- ios: false,
},
{
code3: 'kok',
code2: '',
name: 'Konkani',
- android: false,
- ios: false,
},
{
code3: 'kom',
code2: 'kv',
name: 'Komi',
- android: false,
- ios: false,
},
{
code3: 'kon',
code2: 'kg',
name: 'Kongo',
- android: false,
- ios: false,
},
{
code3: 'kor',
code2: 'ko',
name: 'Korean',
- android: true,
- ios: true,
},
{
code3: 'kos',
code2: '',
name: 'Kosraean',
- android: false,
- ios: false,
},
{
code3: 'kpe',
code2: '',
name: 'Kpelle',
- android: false,
- ios: false,
},
{
code3: 'krc',
code2: '',
name: 'Karachay-Balkar',
- android: false,
- ios: false,
},
{
code3: 'krl',
code2: '',
name: 'Karelian',
- android: false,
- ios: false,
},
{
code3: 'kro',
code2: '',
name: 'Kru languages',
- android: false,
- ios: false,
},
{
code3: 'kru',
code2: '',
name: 'Kurukh',
- android: false,
- ios: false,
},
{
code3: 'kua',
code2: 'kj',
name: 'Kuanyama; Kwanyama',
- android: false,
- ios: false,
},
{
code3: 'kum',
code2: '',
name: 'Kumyk',
- android: false,
- ios: false,
},
{
code3: 'kur',
code2: 'ku',
name: 'Kurdish',
- android: false,
- ios: false,
},
{
code3: 'kut',
code2: '',
name: 'Kutenai',
- android: false,
- ios: false,
},
{
code3: 'lad',
code2: '',
name: 'Ladino',
- android: false,
- ios: false,
},
{
code3: 'lah',
code2: '',
name: 'Lahnda',
- android: false,
- ios: false,
},
{
code3: 'lam',
code2: '',
name: 'Lamba',
- android: false,
- ios: false,
},
{
code3: 'lao',
code2: 'lo',
name: 'Lao',
- android: false,
- ios: false,
},
{
code3: 'lat',
code2: 'la',
name: 'Latin',
- android: false,
- ios: false,
},
{
code3: 'lav',
code2: 'lv',
name: 'Latvian',
- android: true,
- ios: false,
},
{
code3: 'lez',
code2: '',
name: 'Lezghian',
- android: false,
- ios: false,
},
{
code3: 'lim',
code2: 'li',
name: 'Limburgish',
- android: false,
- ios: false,
},
{
code3: 'lin',
code2: 'ln',
name: 'Lingala',
- android: false,
- ios: false,
},
{
code3: 'lit',
code2: 'lt',
name: 'Lithuanian',
- android: true,
- ios: false,
},
{
code3: 'lol',
code2: '',
name: 'Mongo',
- android: false,
- ios: false,
},
{
code3: 'loz',
code2: '',
name: 'Lozi',
- android: false,
- ios: false,
},
{
code3: 'ltz',
code2: 'lb',
name: 'Luxembourgish',
- android: false,
- ios: false,
},
{
code3: 'lua',
code2: '',
name: 'Luba-Lulua',
- android: false,
- ios: false,
},
{
code3: 'lub',
code2: 'lu',
name: 'Luba-Katanga',
- android: false,
- ios: false,
},
{
code3: 'lug',
code2: 'lg',
name: 'Ganda',
- android: false,
- ios: false,
},
{
code3: 'lui',
code2: '',
name: 'Luiseno',
- android: false,
- ios: false,
},
{
code3: 'lun',
code2: '',
name: 'Lunda',
- android: false,
- ios: false,
},
{
code3: 'luo',
code2: '',
name: 'Luo (Kenya and Tanzania)',
- android: false,
- ios: false,
},
{
code3: 'lus',
code2: '',
name: 'Mizo',
- android: false,
- ios: false,
},
{
code3: 'mac',
code2: 'mk',
name: 'Macedonian',
- android: true,
- ios: false,
},
{
code3: 'mad',
code2: '',
name: 'Madurese',
- android: false,
- ios: false,
},
{
code3: 'mag',
code2: '',
name: 'Magahi',
- android: false,
- ios: false,
},
{
code3: 'mah',
code2: 'mh',
name: 'Marshallese',
- android: false,
- ios: false,
},
{
code3: 'mai',
code2: '',
name: 'Maithili',
- android: false,
- ios: false,
},
{
code3: 'mak',
code2: '',
name: 'Makasar',
- android: false,
- ios: false,
},
{
code3: 'mal',
code2: 'ml',
name: 'Malayalam',
- android: false,
- ios: false,
},
{
code3: 'man',
code2: '',
name: 'Mandingo',
- android: false,
- ios: false,
},
{
code3: 'mao',
code2: 'mi',
name: 'Māori',
- android: false,
- ios: false,
},
{
code3: 'map',
code2: '',
name: 'Austronesian languages',
- android: false,
- ios: false,
},
{
code3: 'mar',
code2: 'mr',
name: 'Marathi',
- android: true,
- ios: false,
},
{
code3: 'mas',
code2: '',
name: 'Masai',
- android: false,
- ios: false,
},
{
code3: 'may',
code2: 'ms',
name: 'Malay',
- android: true,
- ios: false,
},
{
code3: 'mdf',
code2: '',
name: 'Moksha',
- android: false,
- ios: false,
},
{
code3: 'mdr',
code2: '',
name: 'Mandar',
- android: false,
- ios: false,
},
{
code3: 'men',
code2: '',
name: 'Mende',
- android: false,
- ios: false,
},
{
code3: 'mga',
code2: '',
name: 'Irish, Middle (900-1200)',
- android: false,
- ios: false,
},
{
code3: 'mic',
code2: '',
name: "Mi'kmaq; Micmac",
- android: false,
- ios: false,
},
{
code3: 'min',
code2: '',
name: 'Minangkabau',
- android: false,
- ios: false,
},
{
code3: 'mis',
code2: '',
name: 'Uncoded languages',
- android: false,
- ios: false,
},
{
code3: 'mkd',
code2: 'mk',
name: 'Macedonian',
- android: true,
- ios: false,
},
{
code3: 'mkh',
code2: '',
name: 'Mon-Khmer languages',
- android: false,
- ios: false,
},
{
code3: 'mlg',
code2: 'mg',
name: 'Malagasy',
- android: false,
- ios: false,
},
{
code3: 'mlt',
code2: 'mt',
name: 'Maltese',
- android: true,
- ios: false,
},
{
code3: 'mnc',
code2: '',
name: 'Manchu',
- android: false,
- ios: false,
},
{
code3: 'mni',
code2: '',
name: 'Manipuri',
- android: false,
- ios: false,
},
{
code3: 'mno',
code2: '',
name: 'Manobo languages',
- android: false,
- ios: false,
},
{
code3: 'moh',
code2: '',
name: 'Mohawk',
- android: false,
- ios: false,
},
{
code3: 'mon',
code2: 'mn',
name: 'Mongolian',
- android: false,
- ios: false,
},
{
code3: 'mos',
code2: '',
name: 'Mossi',
- android: false,
- ios: false,
},
{
code3: 'mri',
code2: 'mi',
name: 'Māori',
- android: false,
- ios: false,
},
{
code3: 'msa',
code2: 'ms',
name: 'Malay',
- android: true,
- ios: false,
},
{
code3: 'mul',
code2: '',
name: 'Multiple languages',
- android: false,
- ios: false,
},
{
code3: 'mun',
code2: '',
name: 'Munda languages',
- android: false,
- ios: false,
},
{
code3: 'mus',
code2: '',
name: 'Creek',
- android: false,
- ios: false,
},
{
code3: 'mwl',
code2: '',
name: 'Mirandese',
- android: false,
- ios: false,
},
{
code3: 'mwr',
code2: '',
name: 'Marwari',
- android: false,
- ios: false,
},
{
code3: 'mya',
code2: 'my',
name: 'Burmese',
- android: false,
- ios: false,
},
{
code3: 'myn',
code2: '',
name: 'Mayan languages',
- android: false,
- ios: false,
},
{
code3: 'myv',
code2: '',
name: 'Erzya',
- android: false,
- ios: false,
},
{
code3: 'nah',
code2: '',
name: 'Nahuatl languages',
- android: false,
- ios: false,
},
{
code3: 'nai',
code2: '',
name: 'North American Indian languages',
- android: false,
- ios: false,
},
{
code3: 'nap',
code2: '',
name: 'Neapolitan',
- android: false,
- ios: false,
},
{
code3: 'nau',
code2: 'na',
name: 'Nauru',
- android: false,
- ios: false,
},
{
code3: 'nav',
code2: 'nv',
name: 'Navajo',
- android: false,
- ios: false,
},
{
code3: 'nbl',
code2: 'nr',
name: 'South Ndebele',
- android: false,
- ios: false,
},
{
code3: 'nde',
code2: 'nd',
name: 'North Ndebele',
- android: false,
- ios: false,
},
{
code3: 'ndo',
code2: 'ng',
name: 'Ndonga',
- android: false,
- ios: false,
},
{
code3: 'nds',
code2: '',
name: 'Low German; Low Saxon; German, Low; Saxon, Low',
- android: false,
- ios: false,
},
{
code3: 'nep',
code2: 'ne',
name: 'Nepali',
- android: false,
- ios: false,
},
{
code3: 'new',
code2: '',
name: 'Nepal Bhasa; Newari',
- android: false,
- ios: false,
},
{
code3: 'nia',
code2: '',
name: 'Nias',
- android: false,
- ios: false,
},
{
code3: 'nic',
code2: '',
name: 'Niger-Kordofanian languages',
- android: false,
- ios: false,
},
{
code3: 'niu',
code2: '',
name: 'Niuean',
- android: false,
- ios: false,
},
{
code3: 'nld',
code2: 'nl',
name: 'Dutch',
- android: true,
- ios: true,
},
{
code3: 'nno',
code2: 'nn',
name: 'Norwegian Nynorsk',
- android: false,
- ios: false,
},
{
code3: 'nob',
code2: 'nb',
name: 'Norwegian Bokmål',
- android: false,
- ios: false,
},
{
code3: 'nog',
code2: '',
name: 'Nogai',
- android: false,
- ios: false,
},
{
code3: 'non',
code2: '',
name: 'Norse, Old',
- android: false,
- ios: false,
},
{
code3: 'nor',
code2: 'no',
name: 'Norwegian',
- android: true,
- ios: false,
},
{
code3: 'nqo',
code2: '',
name: "N'Ko",
- android: false,
- ios: false,
},
{
code3: 'nso',
code2: '',
name: 'Northern Sotho',
- android: false,
- ios: false,
},
{
code3: 'nub',
code2: '',
name: 'Nubian languages',
- android: false,
- ios: false,
},
{
code3: 'nwc',
code2: '',
name: 'Classical Newari; Old Newari; Classical Nepal Bhasa',
- android: false,
- ios: false,
},
{
code3: 'nya',
code2: 'ny',
name: 'Nyanja',
- android: false,
- ios: false,
},
{
code3: 'nym',
code2: '',
name: 'Nyamwezi',
- android: false,
- ios: false,
},
{
code3: 'nyn',
code2: '',
name: 'Nyankole',
- android: false,
- ios: false,
},
{
code3: 'nyo',
code2: '',
name: 'Nyoro',
- android: false,
- ios: false,
},
{
code3: 'nzi',
code2: '',
name: 'Nzima',
- android: false,
- ios: false,
},
{
code3: 'oci',
code2: 'oc',
name: 'Occitan',
- android: false,
- ios: false,
},
{
code3: 'oji',
code2: 'oj',
name: 'Ojibwa',
- android: false,
- ios: false,
},
{
code3: 'ori',
code2: 'or',
name: 'Odia',
- android: false,
- ios: false,
},
{
code3: 'orm',
code2: 'om',
name: 'Oromo',
- android: false,
- ios: false,
},
{
code3: 'osa',
code2: '',
name: 'Osage',
- android: false,
- ios: false,
},
{
code3: 'oss',
code2: 'os',
name: 'Ossetic',
- android: false,
- ios: false,
},
{
code3: 'ota',
code2: '',
name: 'Turkish, Ottoman (1500-1928)',
- android: false,
- ios: false,
},
{
code3: 'oto',
code2: '',
name: 'Otomian languages',
- android: false,
- ios: false,
},
{
code3: 'paa',
code2: '',
name: 'Papuan languages',
- android: false,
- ios: false,
},
{
code3: 'pag',
code2: '',
name: 'Pangasinan',
- android: false,
- ios: false,
},
{
code3: 'pal',
code2: '',
name: 'Pahlavi',
- android: false,
- ios: false,
},
{
code3: 'pam',
code2: '',
name: 'Pampanga; Kapampangan',
- android: false,
- ios: false,
},
{
code3: 'pan',
code2: 'pa',
name: 'Punjabi',
- android: false,
- ios: false,
},
{
code3: 'pap',
code2: '',
name: 'Papiamento',
- android: false,
- ios: false,
},
{
code3: 'pau',
code2: '',
name: 'Palauan',
- android: false,
- ios: false,
},
{
code3: 'peo',
code2: '',
name: 'Persian, Old (ca.600-400 B.C.)',
- android: false,
- ios: false,
},
{
code3: 'per',
code2: 'fa',
name: 'Persian',
- android: true,
- ios: false,
},
{
code3: 'phi',
code2: '',
name: 'Philippine languages',
- android: false,
- ios: false,
},
{
code3: 'phn',
code2: '',
name: 'Phoenician',
- android: false,
- ios: false,
},
{
code3: 'pli',
code2: 'pi',
name: 'Pali',
- android: false,
- ios: false,
},
{
code3: 'pol',
code2: 'pl',
name: 'Polish',
- android: true,
- ios: true,
},
{
code3: 'pon',
code2: '',
name: 'Pohnpeian',
- android: false,
- ios: false,
},
{
code3: 'por',
code2: 'pt',
name: 'Portuguese',
- android: true,
- ios: true,
},
{
code3: 'pra',
code2: '',
name: 'Prakrit languages',
- android: false,
- ios: false,
},
{
code3: 'pro',
code2: '',
name: 'Provençal, Old (to 1500);Occitan, Old (to 1500)',
- android: false,
- ios: false,
},
{
code3: 'pus',
code2: 'ps',
name: 'Pashto',
- android: false,
- ios: false,
},
{
code3: 'que',
code2: 'qu',
name: 'Quechua',
- android: false,
- ios: false,
},
{
code3: 'raj',
code2: '',
name: 'Rajasthani',
- android: false,
- ios: false,
},
{
code3: 'rap',
code2: '',
name: 'Rapanui',
- android: false,
- ios: false,
},
{
code3: 'rar',
code2: '',
name: 'Rarotongan; Cook Islands Maori',
- android: false,
- ios: false,
},
{
code3: 'roa',
code2: '',
name: 'Romance languages',
- android: false,
- ios: false,
},
{
code3: 'roh',
code2: 'rm',
name: 'Romansh',
- android: false,
- ios: false,
},
{
code3: 'rom',
code2: '',
name: 'Romany',
- android: false,
- ios: false,
},
{
code3: 'rum',
code2: 'ro',
name: 'Romanian',
- android: true,
- ios: false,
},
{
code3: 'ron',
code2: 'ro',
name: 'Romanian',
- android: true,
- ios: false,
},
{
code3: 'run',
code2: 'rn',
name: 'Rundi',
- android: false,
- ios: false,
},
{
code3: 'rup',
code2: '',
name: 'Aromanian; Arumanian; Macedo-Romanian',
- android: false,
- ios: false,
},
{
code3: 'rus',
code2: 'ru',
name: 'Russian',
- android: true,
- ios: true,
},
{
code3: 'sad',
code2: '',
name: 'Sandawe',
- android: false,
- ios: false,
},
{
code3: 'sag',
code2: 'sg',
name: 'Sango',
- android: false,
- ios: false,
},
{
code3: 'sah',
code2: '',
name: 'Yakut',
- android: false,
- ios: false,
},
{
code3: 'sai',
code2: '',
name: 'South American Indian languages',
- android: false,
- ios: false,
},
{
code3: 'sal',
code2: '',
name: 'Salishan languages',
- android: false,
- ios: false,
},
{
code3: 'sam',
code2: '',
name: 'Samaritan Aramaic',
- android: false,
- ios: false,
},
{
code3: 'san',
code2: 'sa',
name: 'Sanskrit',
- android: false,
- ios: false,
},
{
code3: 'sas',
code2: '',
name: 'Sasak',
- android: false,
- ios: false,
},
{
code3: 'sat',
code2: '',
name: 'Santali',
- android: false,
- ios: false,
},
{
code3: 'scn',
code2: '',
name: 'Sicilian',
- android: false,
- ios: false,
},
{
code3: 'sco',
code2: '',
name: 'Scots',
- android: false,
- ios: false,
},
{
code3: 'sel',
code2: '',
name: 'Selkup',
- android: false,
- ios: false,
},
{
code3: 'sem',
code2: '',
name: 'Semitic languages',
- android: false,
- ios: false,
},
{
code3: 'sga',
code2: '',
name: 'Irish, Old (to 900)',
- android: false,
- ios: false,
},
{
code3: 'sgn',
code2: '',
name: 'Sign Languages',
- android: false,
- ios: false,
},
{
code3: 'shn',
code2: '',
name: 'Shan',
- android: false,
- ios: false,
},
{
code3: 'sid',
code2: '',
name: 'Sidamo',
- android: false,
- ios: false,
},
{
code3: 'sin',
code2: 'si',
name: 'Sinhala',
- android: false,
- ios: false,
},
{
code3: 'sio',
code2: '',
name: 'Siouan languages',
- android: false,
- ios: false,
},
{
code3: 'sit',
code2: '',
name: 'Sino-Tibetan languages',
- android: false,
- ios: false,
},
{
code3: 'sla',
code2: '',
name: 'Slavic languages',
- android: false,
- ios: false,
},
{
code3: 'slo',
code2: 'sk',
name: 'Slovak',
- android: true,
- ios: false,
},
{
code3: 'slk',
code2: 'sk',
name: 'Slovak',
- android: true,
- ios: false,
},
{
code3: 'slv',
code2: 'sl',
name: 'Slovenian',
- android: true,
- ios: false,
},
{
code3: 'sma',
code2: '',
name: 'Southern Sami',
- android: false,
- ios: false,
},
{
code3: 'sme',
code2: 'se',
name: 'Northern Sami',
- android: false,
- ios: false,
},
{
code3: 'smi',
code2: '',
name: 'Sami languages',
- android: false,
- ios: false,
},
{
code3: 'smj',
code2: '',
name: 'Lule Sami',
- android: false,
- ios: false,
},
{
code3: 'smn',
code2: '',
name: 'Inari Sami',
- android: false,
- ios: false,
},
{
code3: 'smo',
code2: 'sm',
name: 'Samoan',
- android: false,
- ios: false,
},
{
code3: 'sms',
code2: '',
name: 'Skolt Sami',
- android: false,
- ios: false,
},
{
code3: 'sna',
code2: 'sn',
name: 'Shona',
- android: false,
- ios: false,
},
{
code3: 'snd',
code2: 'sd',
name: 'Sindhi',
- android: false,
- ios: false,
},
{
code3: 'snk',
code2: '',
name: 'Soninke',
- android: false,
- ios: false,
},
{
code3: 'sog',
code2: '',
name: 'Sogdian',
- android: false,
- ios: false,
},
{
code3: 'som',
code2: 'so',
name: 'Somali',
- android: false,
- ios: false,
},
{
code3: 'son',
code2: '',
name: 'Songhai languages',
- android: false,
- ios: false,
},
{
code3: 'sot',
code2: 'st',
name: 'Southern Sotho',
- android: false,
- ios: false,
},
{
code3: 'spa',
code2: 'es',
name: 'Spanish',
- android: true,
- ios: true,
},
{
code3: 'sqi',
code2: 'sq',
name: 'Albanian',
- android: true,
- ios: false,
},
{
code3: 'srd',
code2: 'sc',
name: 'Sardinian',
- android: false,
- ios: false,
},
{
code3: 'srn',
code2: '',
name: 'Sranan Tongo',
- android: false,
- ios: false,
},
{
code3: 'srp',
code2: 'sr',
name: 'Serbian',
- android: false,
- ios: false,
},
{
code3: 'srr',
code2: '',
name: 'Serer',
- android: false,
- ios: false,
},
{
code3: 'ssa',
code2: '',
name: 'Nilo-Saharan languages',
- android: false,
- ios: false,
},
{
code3: 'ssw',
code2: 'ss',
name: 'Swati',
- android: false,
- ios: false,
},
{
code3: 'suk',
code2: '',
name: 'Sukuma',
- android: false,
- ios: false,
},
{
code3: 'sun',
code2: 'su',
name: 'Sundanese',
- android: false,
- ios: false,
},
{
code3: 'sus',
code2: '',
name: 'Susu',
- android: false,
- ios: false,
},
{
code3: 'sux',
code2: '',
name: 'Sumerian',
- android: false,
- ios: false,
},
{
code3: 'swa',
code2: 'sw',
name: 'Swahili',
- android: true,
- ios: false,
},
{
code3: 'swe',
code2: 'sv',
name: 'Swedish',
- android: true,
- ios: false,
},
{
code3: 'syc',
code2: '',
name: 'Classical Syriac',
- android: false,
- ios: false,
},
{
code3: 'syr',
code2: '',
name: 'Syriac',
- android: false,
- ios: false,
},
{
code3: 'tah',
code2: 'ty',
name: 'Tahitian',
- android: false,
- ios: false,
},
{
code3: 'tai',
code2: '',
name: 'Tai languages',
- android: false,
- ios: false,
},
{
code3: 'tam',
code2: 'ta',
name: 'Tamil',
- android: true,
- ios: false,
},
{
code3: 'tat',
code2: 'tt',
name: 'Tatar',
- android: false,
- ios: false,
},
{
code3: 'tel',
code2: 'te',
name: 'Telugu',
- android: true,
- ios: false,
},
{
code3: 'tem',
code2: '',
name: 'Timne',
- android: false,
- ios: false,
},
{
code3: 'ter',
code2: '',
name: 'Tereno',
- android: false,
- ios: false,
},
{
code3: 'tet',
code2: '',
name: 'Tetum',
- android: false,
- ios: false,
},
{
code3: 'tgk',
code2: 'tg',
name: 'Tajik',
- android: false,
- ios: false,
},
{
code3: 'tgl',
code2: 'tl',
name: 'Filipino',
- android: true,
- ios: false,
},
{
code3: 'tha',
code2: 'th',
name: 'Thai',
- android: true,
- ios: true,
},
{
code3: 'tib',
code2: 'bo',
name: 'Tibetan',
- android: false,
- ios: false,
},
{
code3: 'tig',
code2: '',
name: 'Tigre',
- android: false,
- ios: false,
},
{
code3: 'tir',
code2: 'ti',
name: 'Tigrinya',
- android: false,
- ios: false,
},
{
code3: 'tiv',
code2: '',
name: 'Tiv',
- android: false,
- ios: false,
},
{
code3: 'tkl',
code2: '',
name: 'Tokelau',
- android: false,
- ios: false,
},
{
code3: 'tlh',
code2: '',
name: 'Klingon; tlhIngan-Hol',
- android: false,
- ios: false,
},
{
code3: 'tli',
code2: '',
name: 'Tlingit',
- android: false,
- ios: false,
},
{
code3: 'tmh',
code2: '',
name: 'Tamashek',
- android: false,
- ios: false,
},
{
code3: 'tog',
code2: '',
name: 'Tonga (Nyasa)',
- android: false,
- ios: false,
},
{
code3: 'ton',
code2: 'to',
name: 'Tongan',
- android: false,
- ios: false,
},
{
code3: 'tpi',
code2: '',
name: 'Tok Pisin',
- android: false,
- ios: false,
},
{
code3: 'tsi',
code2: '',
name: 'Tsimshian',
- android: false,
- ios: false,
},
{
code3: 'tsn',
code2: 'tn',
name: 'Tswana',
- android: false,
- ios: false,
},
{
code3: 'tso',
code2: 'ts',
name: 'Tsonga',
- android: false,
- ios: false,
},
{
code3: 'tuk',
code2: 'tk',
name: 'Turkmen',
- android: false,
- ios: false,
},
{
code3: 'tum',
code2: '',
name: 'Tumbuka',
- android: false,
- ios: false,
},
{
code3: 'tup',
code2: '',
name: 'Tupi languages',
- android: false,
- ios: false,
},
{
code3: 'tur',
code2: 'tr',
name: 'Turkish',
- android: true,
- ios: true,
},
{
code3: 'tut',
code2: '',
name: 'Altaic languages',
- android: false,
- ios: false,
},
{
code3: 'tvl',
code2: '',
name: 'Tuvalu',
- android: false,
- ios: false,
},
{
code3: 'twi',
code2: 'tw',
name: 'Akan',
- android: false,
- ios: false,
},
{
code3: 'tyv',
code2: '',
name: 'Tuvinian',
- android: false,
- ios: false,
},
{
code3: 'udm',
code2: '',
name: 'Udmurt',
- android: false,
- ios: false,
},
{
code3: 'uga',
code2: '',
name: 'Ugaritic',
- android: false,
- ios: false,
},
{
code3: 'uig',
code2: 'ug',
name: 'Uyghur',
- android: false,
- ios: false,
},
{
code3: 'ukr',
code2: 'uk',
name: 'Ukrainian',
- android: true,
- ios: true,
},
{
code3: 'umb',
code2: '',
name: 'Umbundu',
- android: false,
- ios: false,
},
{
code3: 'und',
code2: '',
name: 'Undetermined',
- android: false,
- ios: false,
},
{
code3: 'urd',
code2: 'ur',
name: 'Urdu',
- android: true,
- ios: false,
},
{
code3: 'uzb',
code2: 'uz',
name: 'Uzbek',
- android: false,
- ios: false,
},
{
code3: 'vai',
code2: '',
name: 'Vai',
- android: false,
- ios: false,
},
{
code3: 'ven',
code2: 've',
name: 'Venda',
- android: false,
- ios: false,
},
{
code3: 'vie',
code2: 'vi',
name: 'Vietnamese',
- android: true,
- ios: true,
},
{
code3: 'vol',
code2: 'vo',
name: 'Volapük',
- android: false,
- ios: false,
},
{
code3: 'vot',
code2: '',
name: 'Votic',
- android: false,
- ios: false,
},
{
code3: 'wak',
code2: '',
name: 'Wakashan languages',
- android: false,
- ios: false,
},
{
code3: 'wal',
code2: '',
name: 'Wolaitta; Wolaytta',
- android: false,
- ios: false,
},
{
code3: 'war',
code2: '',
name: 'Waray',
- android: false,
- ios: false,
},
{
code3: 'was',
code2: '',
name: 'Washo',
- android: false,
- ios: false,
},
{
code3: 'wel',
code2: 'cy',
name: 'Welsh',
- android: true,
- ios: false,
},
{
code3: 'wen',
code2: '',
name: 'Sorbian languages',
- android: false,
- ios: false,
},
{
code3: 'wln',
code2: 'wa',
name: 'Walloon',
- android: false,
- ios: false,
},
{
code3: 'wol',
code2: 'wo',
name: 'Wolof',
- android: false,
- ios: false,
},
{
code3: 'xal',
code2: '',
name: 'Kalmyk; Oirat',
- android: false,
- ios: false,
},
{
code3: 'xho',
code2: 'xh',
name: 'Xhosa',
- android: false,
- ios: false,
},
{
code3: 'yao',
code2: '',
name: 'Yao',
- android: false,
- ios: false,
},
{
code3: 'yap',
code2: '',
name: 'Yapese',
- android: false,
- ios: false,
},
{
code3: 'yid',
code2: 'yi',
name: 'Yiddish',
- android: false,
- ios: false,
},
{
code3: 'yor',
code2: 'yo',
name: 'Yoruba',
- android: false,
- ios: false,
},
{
code3: 'ypk',
code2: '',
name: 'Yupik languages',
- android: false,
- ios: false,
},
{
code3: 'zap',
code2: '',
name: 'Zapotec',
- android: false,
- ios: false,
},
{
code3: 'zbl',
code2: '',
name: 'Blissymbols; Blissymbolics; Bliss',
- android: false,
- ios: false,
},
{
code3: 'zen',
code2: '',
name: 'Zenaga',
- android: false,
- ios: false,
},
{
code3: 'zgh',
code2: '',
name: 'Standard Moroccan Tamazight',
- android: false,
- ios: false,
},
{
code3: 'zha',
code2: 'za',
name: 'Zhuang; Chuang',
- android: false,
- ios: false,
},
{
code3: 'zho',
code2: 'zh',
name: 'Chinese',
- android: true,
- ios: true,
},
{
code3: 'znd',
code2: '',
name: 'Zande languages',
- android: false,
- ios: false,
},
{
code3: 'zul',
code2: 'zu',
name: 'Zulu',
- android: false,
- ios: false,
},
{
code3: 'zun',
code2: '',
name: 'Zuni',
- android: false,
- ios: false,
},
{
code3: 'zza',
code2: '',
name: 'Zaza; Dimili; Dimli; Kirdki; Kirmanjki; Zazaki',
- android: false,
- ios: false,
},
]
diff --git a/src/screens/PostThread/components/ThreadItemAnchor.tsx b/src/screens/PostThread/components/ThreadItemAnchor.tsx
index 38a339f540..1531784aee 100644
--- a/src/screens/PostThread/components/ThreadItemAnchor.tsx
+++ b/src/screens/PostThread/components/ThreadItemAnchor.tsx
@@ -1,5 +1,5 @@
import {memo, useCallback, useMemo} from 'react'
-import {type GestureResponderEvent, Text as RNText, View} from 'react-native'
+import {Text as RNText, View} from 'react-native'
import {
AppBskyFeedDefs,
AppBskyFeedPost,
@@ -14,11 +14,6 @@ import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {niceDate} from '#/lib/strings/time'
-import {
- getPostLanguage,
- getTranslatorLink,
- isPostInLanguage,
-} from '#/locale/helpers'
import {
POST_TOMBSTONE,
type Shadow,
@@ -26,7 +21,6 @@ import {
} from '#/state/cache/post-shadow'
import {useProfileShadow} from '#/state/cache/profile-shadow'
import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
-import {useLanguagePrefs} from '#/state/preferences'
import {type ThreadItem} from '#/state/queries/usePostThread/types'
import {useSession} from '#/state/session'
import {type OnPostSuccessData} from '#/state/shell/composer'
@@ -44,8 +38,7 @@ import {Button} from '#/components/Button'
import {DebugFieldDisplay} from '#/components/DebugFieldDisplay'
import {CalendarClock_Stroke2_Corner0_Rounded as CalendarClockIcon} from '#/components/icons/CalendarClock'
import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
-import {InlineLinkText, Link} from '#/components/Link'
-import {Loader} from '#/components/Loader'
+import {Link} from '#/components/Link'
import {ContentHider} from '#/components/moderation/ContentHider'
import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
import {PostAlerts} from '#/components/moderation/PostAlerts'
@@ -63,10 +56,6 @@ import {VerificationCheckButton} from '#/components/verification/VerificationChe
import {WhoCanReply} from '#/components/WhoCanReply'
import {useAnalytics} from '#/analytics'
import {useActorStatus} from '#/features/liveNow'
-import {
- Provider as TranslateOnDeviceProvider,
- useTranslateOnDevice,
-} from '#/translation'
import * as bsky from '#/types/bsky'
export function ThreadItemAnchor({
@@ -89,18 +78,16 @@ export function ThreadItemAnchor({
}
return (
-
-
-
+
)
}
@@ -420,8 +407,7 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
shouldProxyLinks={true}
/>
) : undefined}
-
-
+
{post.embed && (
) : undefined}
+
{post.embed && (