Bluesky app fork with some witchin' additions 💫 witchsky.app
bluesky fork client

Refine UX for on-device translation for posts (#9987)

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
Co-authored-by: Eric Bailey <git@esb.lol>

authored by

DS Boyce
Samuel Newman
Eric Bailey
and committed by
GitHub
afbade6f 1782a651

+1013 -1638
+1 -1
package.json
··· 85 "@braintree/sanitize-url": "^6.0.2", 86 "@bsky.app/alf": "^0.1.7", 87 "@bsky.app/expo-image-crop-tool": "^0.5.0", 88 - "@bsky.app/expo-translate-text": "^0.2.4", 89 "@bsky.app/react-native-mmkv": "2.12.5", 90 "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet", 91 "@emoji-mart/data": "^1.2.1",
··· 85 "@braintree/sanitize-url": "^6.0.2", 86 "@bsky.app/alf": "^0.1.7", 87 "@bsky.app/expo-image-crop-tool": "^0.5.0", 88 + "@bsky.app/expo-translate-text": "^0.2.7", 89 "@bsky.app/react-native-mmkv": "2.12.5", 90 "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet", 91 "@emoji-mart/data": "^1.2.1",
+6 -3
src/App.native.tsx
··· 19 import {QueryProvider} from '#/lib/react-query' 20 import {s} from '#/lib/styles' 21 import {ThemeProvider} from '#/lib/ThemeContext' 22 import I18nProvider from '#/locale/i18nProvider' 23 import {logger} from '#/logger' 24 import {Provider as A11yProvider} from '#/state/a11y' ··· 179 style={s.h100pct}> 180 <GlobalGestureEventsProvider> 181 <IntentDialogProvider> 182 - <TestCtrls /> 183 - <Shell /> 184 - <ToastOutlet /> 185 </IntentDialogProvider> 186 </GlobalGestureEventsProvider> 187 </GestureHandlerRootView>
··· 19 import {QueryProvider} from '#/lib/react-query' 20 import {s} from '#/lib/styles' 21 import {ThemeProvider} from '#/lib/ThemeContext' 22 + import {Provider as TranslateOnDeviceProvider} from '#/lib/translation' 23 import I18nProvider from '#/locale/i18nProvider' 24 import {logger} from '#/logger' 25 import {Provider as A11yProvider} from '#/state/a11y' ··· 180 style={s.h100pct}> 181 <GlobalGestureEventsProvider> 182 <IntentDialogProvider> 183 + <TranslateOnDeviceProvider> 184 + <TestCtrls /> 185 + <Shell /> 186 + <ToastOutlet /> 187 + </TranslateOnDeviceProvider> 188 </IntentDialogProvider> 189 </GlobalGestureEventsProvider> 190 </GestureHandlerRootView>
+5 -2
src/App.web.tsx
··· 10 11 import {QueryProvider} from '#/lib/react-query' 12 import {ThemeProvider} from '#/lib/ThemeContext' 13 import I18nProvider from '#/locale/i18nProvider' 14 import {logger} from '#/logger' 15 import {Provider as A11yProvider} from '#/state/a11y' ··· 154 <EmailVerificationProvider> 155 <HideBottomBarBorderProvider> 156 <IntentDialogProvider> 157 - <Shell /> 158 - <ToastOutlet /> 159 </IntentDialogProvider> 160 </HideBottomBarBorderProvider> 161 </EmailVerificationProvider>
··· 10 11 import {QueryProvider} from '#/lib/react-query' 12 import {ThemeProvider} from '#/lib/ThemeContext' 13 + import {Provider as TranslateOnDeviceProvider} from '#/lib/translation' 14 import I18nProvider from '#/locale/i18nProvider' 15 import {logger} from '#/logger' 16 import {Provider as A11yProvider} from '#/state/a11y' ··· 155 <EmailVerificationProvider> 156 <HideBottomBarBorderProvider> 157 <IntentDialogProvider> 158 + <TranslateOnDeviceProvider> 159 + <Shell /> 160 + <ToastOutlet /> 161 + </TranslateOnDeviceProvider> 162 </IntentDialogProvider> 163 </HideBottomBarBorderProvider> 164 </EmailVerificationProvider>
+313 -67
src/components/Post/Translated/index.tsx
··· 1 - import {useMemo} from 'react' 2 import {Platform, View} from 'react-native' 3 - import {msg} from '@lingui/core/macro' 4 - import {useLingui} from '@lingui/react' 5 - import {Trans} from '@lingui/react/macro' 6 7 import {codeToLanguageName, languageName} from '#/locale/helpers' 8 import {LANGUAGES} from '#/locale/languages' 9 import {useLanguagePrefs} from '#/state/preferences' 10 - import {atoms as a, useTheme} from '#/alf' 11 import {Loader} from '#/components/Loader' 12 import * as Select from '#/components/Select' 13 import {Text} from '#/components/Typography' 14 import {useAnalytics} from '#/analytics' 15 - import {useTranslateOnDevice} from '#/translation' 16 17 export function TranslatedPost({ 18 postText, 19 - hideLoading = false, 20 }: { 21 postText: string 22 - hideLoading: boolean 23 }) { 24 - const {translationState} = useTranslateOnDevice() 25 26 - if (translationState.status === 'loading' && !hideLoading) { 27 - return <TranslationLoading /> 28 - } 29 30 - if (translationState.status === 'success') { 31 - return ( 32 - <TranslationResult 33 - postText={postText} 34 - sourceLanguage={translationState.sourceLanguage} 35 - translatedText={translationState.translatedText} 36 - /> 37 - ) 38 } 39 - 40 - return null 41 } 42 43 function TranslationLoading() { 44 const t = useTheme() 45 46 return ( 47 - <View style={[a.flex_row, a.align_center, a.gap_sm, a.py_xs]}> 48 - <Loader size="sm" /> 49 - <Text style={[a.text_sm, t.atoms.text_contrast_medium]}> 50 - <Trans>Translating…</Trans> 51 - </Text> 52 </View> 53 ) 54 } 55 56 function TranslationResult({ 57 postText, 58 sourceLanguage, 59 translatedText, 60 }: { 61 postText: string 62 sourceLanguage: string | null 63 translatedText: string 64 }) { 65 const t = useTheme() 66 - const {i18n} = useLingui() 67 68 const langName = sourceLanguage 69 ? codeToLanguageName(sourceLanguage, i18n.locale) 70 : undefined 71 72 return ( 73 - <View style={[a.py_xs, a.gap_xs, a.mt_sm]}> 74 - <Text style={[a.text_xs, t.atoms.text_contrast_medium]}> 75 - {langName ? ( 76 - <Trans>Translated from {langName}</Trans> 77 - ) : ( 78 - <Trans>Translated</Trans> 79 - )} 80 - {sourceLanguage != null && ( 81 - <> 82 - <Text style={[a.text_sm, t.atoms.text_contrast_medium]}> 83 - {' '} 84 - &middot; 85 - </Text>{' '} 86 - <TranslationLanguageSelect 87 - sourceLanguage={sourceLanguage} 88 - postText={postText} 89 - /> 90 - </> 91 - )} 92 - </Text> 93 - <Text emoji selectable style={[a.text_md, a.leading_snug]}> 94 - {translatedText} 95 - </Text> 96 </View> 97 ) 98 } 99 100 function TranslationLanguageSelect({ 101 postText, 102 sourceLanguage, 103 }: { 104 postText: string 105 sourceLanguage: string 106 }) { 107 const ax = useAnalytics() 108 - const {_} = useLingui() 109 const langPrefs = useLanguagePrefs() 110 - const {translate} = useTranslateOnDevice() 111 112 const items = useMemo( 113 () => ··· 116 !langPrefs.primaryLanguage.startsWith(lang.code2) && // Don't show the current language as it would be redundant 117 index === self.findIndex(t => t.code2 === lang.code2), // Remove dupes (which will happen due to multiple code3 values mapping to the same code2) 118 ) 119 - .sort( 120 - (a, b) => 121 - languageName(a, langPrefs.appLanguage).localeCompare( 122 - languageName(b, langPrefs.appLanguage), 123 - langPrefs.appLanguage, 124 - ), // Localized sort 125 - ) 126 .map(l => ({ 127 label: languageName(l, langPrefs.appLanguage), // The viewer may not be familiar with the source language, so localize the name 128 value: l.code2, 129 })), 130 - [langPrefs], 131 ) 132 133 const handleChangeTranslationLanguage = (sourceLangCode: string) => { ··· 136 sourceLanguage: sourceLangCode, 137 targetLanguage: langPrefs.primaryLanguage, 138 }) 139 - void translate(postText, langPrefs.primaryLanguage, sourceLangCode) 140 } 141 142 return ( 143 <Select.Root 144 value={sourceLanguage} 145 onValueChange={handleChangeTranslationLanguage}> 146 - <Select.Trigger hitSlop={10} label={_(msg`Change source language`)}> 147 {({props}) => { 148 return ( 149 - <Text {...props} style={[a.text_xs]}> 150 - <Trans>Change</Trans> 151 - </Text> 152 ) 153 }} 154 </Select.Trigger> 155 <Select.Content 156 - label={_(msg`Select the source language`)} 157 renderItem={({label, value}) => ( 158 <Select.Item value={value} label={label}> 159 <Select.ItemIndicator />
··· 1 + import {useCallback, useMemo} from 'react' 2 import {Platform, View} from 'react-native' 3 + import {type AppBskyFeedDefs} from '@atproto/api' 4 + import {Trans, useLingui} from '@lingui/react/macro' 5 6 + import {HITSLOP_30} from '#/lib/constants' 7 + import {useGoogleTranslate} from '#/lib/hooks/useGoogleTranslate' 8 + import {guessLanguage, useTranslate} from '#/lib/translation' 9 + import {type TranslationFunction} from '#/lib/translation' 10 import {codeToLanguageName, languageName} from '#/locale/helpers' 11 import {LANGUAGES} from '#/locale/languages' 12 import {useLanguagePrefs} from '#/state/preferences' 13 + import {atoms as a, native, useTheme, web} from '#/alf' 14 + import {Button} from '#/components/Button' 15 + import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRightIcon} from '#/components/icons/Arrow' 16 + import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times' 17 + import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' 18 + import {createStaticClick, Link} from '#/components/Link' 19 import {Loader} from '#/components/Loader' 20 import * as Select from '#/components/Select' 21 import {Text} from '#/components/Typography' 22 import {useAnalytics} from '#/analytics' 23 + import {IS_WEB} from '#/env' 24 25 export function TranslatedPost({ 26 + hideTranslateLink = false, 27 + post, 28 postText, 29 }: { 30 + hideTranslateLink?: boolean 31 + post: AppBskyFeedDefs.PostView 32 postText: string 33 }) { 34 + const langPrefs = useLanguagePrefs() 35 + const {clearTranslation, translate, translationState} = useTranslate({ 36 + key: post.uri, 37 + }) 38 39 + const postLanguage = useMemo(() => guessLanguage(postText), [postText]) 40 + const needsTranslation = postLanguage !== langPrefs.primaryLanguage 41 42 + switch (translationState.status) { 43 + case 'loading': 44 + return <TranslationLoading /> 45 + case 'success': 46 + return ( 47 + <TranslationResult 48 + clearTranslation={clearTranslation} 49 + translate={translate} 50 + postText={postText} 51 + sourceLanguage={ 52 + translationState.sourceLanguage ?? postLanguage ?? null // Fallback primarily for iOS 53 + } 54 + translatedText={translationState.translatedText} 55 + /> 56 + ) 57 + case 'error': 58 + return ( 59 + <TranslationError 60 + clearTranslation={clearTranslation} 61 + message={translationState.message} 62 + postText={postText} 63 + primaryLanguage={langPrefs.primaryLanguage} 64 + /> 65 + ) 66 + default: 67 + return ( 68 + !hideTranslateLink && 69 + needsTranslation && ( 70 + <TranslationLink 71 + postText={postText} 72 + primaryLanguage={langPrefs.primaryLanguage} 73 + sourceLanguage={postLanguage} 74 + translate={translate} 75 + /> 76 + ) 77 + ) 78 } 79 } 80 81 function TranslationLoading() { 82 const t = useTheme() 83 84 return ( 85 + <View style={[a.gap_md, a.pt_md, a.align_start]}> 86 + <View style={[a.flex_row, a.align_center, a.gap_xs]}> 87 + <Loader size="xs" /> 88 + <Text style={[a.text_sm, t.atoms.text_contrast_medium]}> 89 + <Trans>Translating…</Trans> 90 + </Text> 91 + </View> 92 + </View> 93 + ) 94 + } 95 + 96 + function TranslationLink({ 97 + postText, 98 + primaryLanguage, 99 + sourceLanguage, 100 + translate, 101 + }: { 102 + postText: string 103 + primaryLanguage: string 104 + sourceLanguage: string | null 105 + translate: TranslationFunction 106 + }) { 107 + const t = useTheme() 108 + const {t: l} = useLingui() 109 + const ax = useAnalytics() 110 + 111 + const handleTranslate = useCallback(() => { 112 + void translate({ 113 + text: postText, 114 + targetLangCode: primaryLanguage, 115 + }) 116 + 117 + ax.metric('translate', { 118 + sourceLanguages: sourceLanguage ? [sourceLanguage] : [], 119 + targetLanguage: primaryLanguage, 120 + textLength: postText.length, 121 + }) 122 + }, [ax, postText, primaryLanguage, translate, sourceLanguage]) 123 + 124 + return ( 125 + <View 126 + style={[ 127 + a.gap_md, 128 + a.pt_md, 129 + a.align_start, 130 + a.flex_row, 131 + a.align_center, 132 + a.gap_xs, 133 + ]}> 134 + <Link 135 + role={IS_WEB ? 'link' : 'button'} 136 + {...createStaticClick(() => { 137 + handleTranslate() 138 + })} 139 + label={l`Translate`} 140 + hoverStyle={[ 141 + native({opacity: 0.5}), 142 + web([a.underline, {textDecorationColor: t.palette.primary_500}]), 143 + ]} 144 + hitSlop={HITSLOP_30}> 145 + <Text style={[a.text_sm, {color: t.palette.primary_500}]}> 146 + <Trans>Translate</Trans> 147 + </Text> 148 + </Link> 149 + </View> 150 + ) 151 + } 152 + 153 + function TranslationError({ 154 + clearTranslation, 155 + message, 156 + postText, 157 + primaryLanguage, 158 + }: { 159 + clearTranslation: () => void 160 + message: string 161 + postText: string 162 + primaryLanguage: string 163 + }) { 164 + const t = useTheme() 165 + const {t: l} = useLingui() 166 + const translate = useGoogleTranslate() 167 + 168 + const handleFallback = () => { 169 + void translate(postText, primaryLanguage) 170 + } 171 + 172 + return ( 173 + <View 174 + style={[ 175 + a.px_lg, 176 + a.pt_sm, 177 + a.pb_md, 178 + a.mt_sm, 179 + a.border, 180 + a.rounded_lg, 181 + t.atoms.border_contrast_high, 182 + ]}> 183 + <View style={[a.flex_row, a.align_center, a.justify_between]}> 184 + <View style={[a.flex_row, a.align_center, a.mb_sm, a.gap_xs]}> 185 + <WarningIcon size="sm" fill={t.atoms.text_contrast_medium.color} /> 186 + <Text style={[a.text_xs, a.font_medium, t.atoms.text_contrast_high]}> 187 + {message} 188 + </Text> 189 + </View> 190 + <View style={[a.flex_row, a.align_center, a.mb_xs]}> 191 + <Button 192 + label={l`Hide translation`} 193 + hitSlop={HITSLOP_30} 194 + hoverStyle={{opacity: 0.5}} 195 + onPress={clearTranslation}> 196 + <XIcon size="sm" fill={t.atoms.text_contrast_medium.color} /> 197 + </Button> 198 + </View> 199 + </View> 200 + <View style={[a.flex_row, a.align_center]}> 201 + <Link 202 + {...createStaticClick(() => { 203 + handleFallback() 204 + })} 205 + label={l`Try Google Translate`} 206 + hoverStyle={[ 207 + native({opacity: 0.5}), 208 + web([a.underline, {textDecorationColor: t.palette.primary_500}]), 209 + ]} 210 + hitSlop={HITSLOP_30}> 211 + <Text 212 + style={[a.text_xs, a.font_medium, {color: t.palette.primary_500}]}> 213 + <Trans>Try Google Translate</Trans> 214 + </Text> 215 + </Link> 216 + </View> 217 </View> 218 ) 219 } 220 221 function TranslationResult({ 222 + clearTranslation, 223 + translate, 224 postText, 225 sourceLanguage, 226 translatedText, 227 }: { 228 + clearTranslation: () => void 229 + translate: TranslationFunction 230 postText: string 231 sourceLanguage: string | null 232 translatedText: string 233 }) { 234 const t = useTheme() 235 + const langPrefs = useLanguagePrefs() 236 + const {i18n, t: l} = useLingui() 237 238 const langName = sourceLanguage 239 ? codeToLanguageName(sourceLanguage, i18n.locale) 240 : undefined 241 242 return ( 243 + <View> 244 + <View 245 + style={[ 246 + a.px_lg, 247 + a.pt_sm, 248 + a.pb_md, 249 + a.mt_sm, 250 + a.border, 251 + a.rounded_lg, 252 + t.atoms.border_contrast_high, 253 + ]}> 254 + <View style={[a.flex_row, a.align_center, a.mb_xs]}> 255 + {langName ? ( 256 + <View style={[a.flex_row, a.align_center]}> 257 + <Text 258 + style={[ 259 + a.text_xs, 260 + a.font_medium, 261 + t.atoms.text_contrast_medium, 262 + ]}> 263 + {langName}{' '} 264 + </Text> 265 + <View style={[a.mt_2xs]}> 266 + <ArrowRightIcon 267 + size="xs" 268 + fill={t.atoms.text_contrast_medium.color} 269 + /> 270 + </View> 271 + <Text 272 + style={[ 273 + a.text_xs, 274 + a.font_medium, 275 + t.atoms.text_contrast_medium, 276 + ]}> 277 + {' '} 278 + {codeToLanguageName( 279 + langPrefs.primaryLanguage, 280 + langPrefs.appLanguage, 281 + )} 282 + </Text> 283 + </View> 284 + ) : ( 285 + <Text 286 + style={[ 287 + a.text_xs, 288 + a.font_medium, 289 + t.atoms.text_contrast_medium, 290 + a.mb_xs, 291 + ]}> 292 + <Trans>Translated</Trans> 293 + </Text> 294 + )} 295 + {sourceLanguage != null && ( 296 + <> 297 + <Text 298 + style={[ 299 + a.text_xs, 300 + a.font_medium, 301 + t.atoms.text_contrast_medium, 302 + ]}> 303 + {' '} 304 + &middot;{' '} 305 + </Text> 306 + <TranslationLanguageSelect 307 + sourceLanguage={sourceLanguage} 308 + translate={translate} 309 + postText={postText} 310 + /> 311 + </> 312 + )} 313 + </View> 314 + <Text emoji selectable style={[a.text_md, a.leading_snug]}> 315 + {translatedText} 316 + </Text> 317 + <Button 318 + label={l`Hide translation`} 319 + hitSlop={HITSLOP_30} 320 + hoverStyle={native({opacity: 0.5})} 321 + style={[a.absolute, a.z_10, {top: 12, right: 14}]} 322 + onPress={clearTranslation}> 323 + <XIcon size="sm" fill={t.atoms.text_contrast_medium.color} /> 324 + </Button> 325 + </View> 326 </View> 327 ) 328 } 329 330 function TranslationLanguageSelect({ 331 + translate, 332 postText, 333 sourceLanguage, 334 }: { 335 + translate: TranslationFunction 336 postText: string 337 sourceLanguage: string 338 }) { 339 + const t = useTheme() 340 const ax = useAnalytics() 341 + const {t: l} = useLingui() 342 const langPrefs = useLanguagePrefs() 343 344 const items = useMemo( 345 () => ··· 348 !langPrefs.primaryLanguage.startsWith(lang.code2) && // Don't show the current language as it would be redundant 349 index === self.findIndex(t => t.code2 === lang.code2), // Remove dupes (which will happen due to multiple code3 values mapping to the same code2) 350 ) 351 + .sort((a, b) => { 352 + // Prioritize sourceLanguage at the top 353 + if (a.code2 === sourceLanguage) return -1 354 + if (b.code2 === sourceLanguage) return 1 355 + // Localized sort 356 + return languageName(a, langPrefs.appLanguage).localeCompare( 357 + languageName(b, langPrefs.appLanguage), 358 + langPrefs.appLanguage, 359 + ) 360 + }) 361 .map(l => ({ 362 label: languageName(l, langPrefs.appLanguage), // The viewer may not be familiar with the source language, so localize the name 363 value: l.code2, 364 })), 365 + [langPrefs, sourceLanguage], 366 ) 367 368 const handleChangeTranslationLanguage = (sourceLangCode: string) => { ··· 371 sourceLanguage: sourceLangCode, 372 targetLanguage: langPrefs.primaryLanguage, 373 }) 374 + void translate({ 375 + text: postText, 376 + targetLangCode: langPrefs.primaryLanguage, 377 + sourceLangCode, 378 + }) 379 } 380 381 return ( 382 <Select.Root 383 value={sourceLanguage} 384 onValueChange={handleChangeTranslationLanguage}> 385 + <Select.Trigger label={l`Change the source language`}> 386 {({props}) => { 387 return ( 388 + <Button 389 + label={props.accessibilityLabel} 390 + {...props} 391 + hitSlop={HITSLOP_30} 392 + hoverStyle={native({opacity: 0.5})}> 393 + <Text 394 + style={[a.text_xs, a.font_medium, t.atoms.text_contrast_high]}> 395 + <Trans>Change</Trans> 396 + </Text> 397 + </Button> 398 ) 399 }} 400 </Select.Trigger> 401 <Select.Content 402 + label={l`Select the source language`} 403 renderItem={({label, value}) => ( 404 <Select.Item value={value} label={label}> 405 <Select.ItemIndicator />
+121 -133
src/components/PostControls/PostMenu/PostMenuItems.tsx
··· 13 AtUri, 14 type RichText as RichTextAPI, 15 } from '@atproto/api' 16 - import {msg, plural} from '@lingui/core/macro' 17 - import {useLingui} from '@lingui/react' 18 import {useNavigation} from '@react-navigation/native' 19 20 import {DISCOVER_DEBUG_DIDS} from '#/lib/constants' 21 import {useOpenLink} from '#/lib/hooks/useOpenLink' 22 - import {useTranslate} from '#/lib/hooks/useTranslate' 23 import {getCurrentRoute} from '#/lib/routes/helpers' 24 import {makeProfileLink} from '#/lib/routes/links' 25 import { ··· 28 } from '#/lib/routes/types' 29 import {richTextToString} from '#/lib/strings/rich-text-helpers' 30 import {toShareUrl} from '#/lib/strings/url-helpers' 31 import {logger} from '#/logger' 32 import {type Shadow} from '#/state/cache/post-shadow' 33 import {useProfileShadow} from '#/state/cache/profile-shadow' ··· 106 threadgateRecord, 107 onShowLess, 108 logContext, 109 }: { 110 testID: string 111 post: Shadow<AppBskyFeedDefs.PostView> ··· 120 threadgateRecord?: AppBskyFeedThreadgate.Record 121 onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void 122 logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' 123 }): React.ReactNode => { 124 const {hasSession, currentAccount} = useSession() 125 - const {_} = useLingui() 126 const ax = useAnalytics() 127 const langPrefs = useLanguagePrefs() 128 const {mutateAsync: deletePostMutate} = usePostDeleteMutation() ··· 133 const {hidePost} = useHiddenPostsApi() 134 const feedFeedback = useFeedFeedbackContext() 135 const openLink = useOpenLink() 136 - const translate = useTranslate() 137 const navigation = useNavigation<NavigationProp>() 138 const {mutedWordsDialogControl} = useGlobalDialogsControlContext() 139 const blockPromptControl = useDialogControl() ··· 191 const onDeletePost = () => { 192 deletePostMutate({uri: postUri}).then( 193 () => { 194 - Toast.show(_(msg({message: 'Post deleted', context: 'toast'}))) 195 196 const route = getCurrentRoute(navigation.getState()) 197 if (route.name === 'PostThread') { ··· 211 }, 212 e => { 213 logger.error('Failed to delete post', {message: e}) 214 - Toast.show(_(msg`Failed to delete post, please try again`), 'xmark') 215 }, 216 ) 217 } ··· 226 logContext, 227 feedDescriptor: feedFeedback.feedDescriptor, 228 }) 229 - Toast.show(_(msg`You will now receive notifications for this thread`)) 230 } else { 231 void muteThread() 232 ax.metric('post:mute', { ··· 235 logContext, 236 feedDescriptor: feedFeedback.feedDescriptor, 237 }) 238 - Toast.show( 239 - _(msg`You will no longer receive notifications for this thread`), 240 - ) 241 } 242 } catch (err) { 243 const e = err as Error 244 if (e?.name !== 'AbortError') { 245 logger.error('Failed to toggle thread mute', {message: e}) 246 - Toast.show( 247 - _(msg`Failed to toggle thread mute, please try again`), 248 - 'xmark', 249 - ) 250 } 251 } 252 } ··· 255 const str = richTextToString(richText, true) 256 257 void Clipboard.setStringAsync(str) 258 - Toast.show(_(msg`Copied to clipboard`), 'clipboard-check') 259 } 260 261 const onPressTranslate = () => { 262 - void translate(record.text, langPrefs.primaryLanguage) 263 264 if ( 265 bsky.dangerousIsType<AppBskyFeedPost.Record>( ··· 297 logContext, 298 feedDescriptor: feedFeedback.feedDescriptor, 299 }) 300 - Toast.show( 301 - _(msg({message: 'Feedback sent to feed operator', context: 'toast'})), 302 - ) 303 } 304 305 const onPressShowLess = () => { ··· 322 }) 323 } else { 324 Toast.show( 325 - _(msg({message: 'Feedback sent to feed operator', context: 'toast'})), 326 ) 327 } 328 } ··· 341 }) 342 Toast.show( 343 isDetach 344 - ? _(msg`Quote post was successfully detached`) 345 - : _(msg`Quote post was re-attached`), 346 ) 347 } catch (err) { 348 const e = err as Error 349 Toast.show( 350 - _(msg({message: 'Updating quote attachment failed', context: 'toast'})), 351 ) 352 logger.error(`Failed to ${action} quote`, {safeMessage: e.message}) 353 } ··· 379 380 Toast.show( 381 isHide 382 - ? _(msg`Reply was successfully hidden`) 383 - : _(msg({message: 'Reply visibility updated', context: 'toast'})), 384 ) 385 } catch (err) { 386 const e = err as Error 387 if (e instanceof MaxHiddenRepliesError) { 388 Toast.show( 389 - _( 390 - plural(MAX_HIDDEN_REPLIES, { 391 - other: 'You can hide a maximum of # replies.', 392 - }), 393 - ), 394 ) 395 } else if (e instanceof InvalidInteractionSettingsError) { 396 Toast.show( 397 - _(msg({message: 'Invalid interaction settings.', context: 'toast'})), 398 ) 399 } else { 400 Toast.show( 401 - _( 402 - msg({ 403 - message: 'Updating reply visibility failed', 404 - context: 'toast', 405 - }), 406 - ), 407 ) 408 logger.error(`Failed to ${action} reply`, {safeMessage: e.message}) 409 } ··· 422 const onBlockAuthor = async () => { 423 try { 424 await queueBlock() 425 - Toast.show(_(msg({message: 'Account blocked', context: 'toast'}))) 426 } catch (err) { 427 const e = err as Error 428 if (e?.name !== 'AbortError') { 429 logger.error('Failed to block account', {message: e}) 430 - Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark') 431 } 432 } 433 } ··· 436 if (postAuthor.viewer?.muted) { 437 try { 438 await queueUnmute() 439 - Toast.show(_(msg({message: 'Account unmuted', context: 'toast'}))) 440 } catch (err) { 441 const e = err as Error 442 if (e?.name !== 'AbortError') { 443 logger.error('Failed to unmute account', {message: e}) 444 - Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark') 445 } 446 } 447 } else { 448 try { 449 await queueMute() 450 - Toast.show(_(msg({message: 'Account muted', context: 'toast'}))) 451 } catch (err) { 452 const e = err as Error 453 if (e?.name !== 'AbortError') { 454 logger.error('Failed to mute account', {message: e}) 455 - Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark') 456 } 457 } 458 } ··· 467 468 const onSignIn = () => requireSignIn(() => {}) 469 470 const isDiscoverDebugUser = 471 IS_INTERNAL || 472 DISCOVER_DEBUG_DIDS[currentAccount?.did || ''] || ··· 481 <Menu.Item 482 testID="pinPostBtn" 483 label={ 484 - isPinned 485 - ? _(msg`Unpin from profile`) 486 - : _(msg`Pin to your profile`) 487 } 488 disabled={isPinPending} 489 onPress={onPressPin}> 490 <Menu.ItemText> 491 - {isPinned 492 - ? _(msg`Unpin from profile`) 493 - : _(msg`Pin to your profile`)} 494 </Menu.ItemText> 495 <Menu.ItemIcon 496 icon={isPinPending ? Loader : PinIcon} ··· 505 <Menu.Group> 506 {!hideInPWI || hasSession ? ( 507 <> 508 - <Menu.Item 509 - testID="postDropdownTranslateBtn" 510 - label={_(msg`Translate`)} 511 - onPress={onPressTranslate}> 512 - <Menu.ItemText>{_(msg`Translate`)}</Menu.ItemText> 513 - <Menu.ItemIcon icon={Translate} position="right" /> 514 - </Menu.Item> 515 516 <Menu.Item 517 testID="postDropdownCopyTextBtn" 518 - label={_(msg`Copy post text`)} 519 onPress={onCopyPostText}> 520 - <Menu.ItemText>{_(msg`Copy post text`)}</Menu.ItemText> 521 <Menu.ItemIcon icon={ClipboardIcon} position="right" /> 522 </Menu.Item> 523 </> 524 ) : ( 525 <Menu.Item 526 testID="postDropdownSignInBtn" 527 - label={_(msg`Sign in to view post`)} 528 onPress={onSignIn}> 529 - <Menu.ItemText>{_(msg`Sign in to view post`)}</Menu.ItemText> 530 <Menu.ItemIcon icon={Eye} position="right" /> 531 </Menu.Item> 532 )} ··· 538 <Menu.Group> 539 <Menu.Item 540 testID="postDropdownShowMoreBtn" 541 - label={_(msg`Show more like this`)} 542 onPress={onPressShowMore}> 543 - <Menu.ItemText>{_(msg`Show more like this`)}</Menu.ItemText> 544 <Menu.ItemIcon icon={EmojiSmile} position="right" /> 545 </Menu.Item> 546 547 <Menu.Item 548 testID="postDropdownShowLessBtn" 549 - label={_(msg`Show less like this`)} 550 onPress={onPressShowLess}> 551 - <Menu.ItemText>{_(msg`Show less like this`)}</Menu.ItemText> 552 <Menu.ItemIcon icon={EmojiSad} position="right" /> 553 </Menu.Item> 554 </Menu.Group> ··· 560 <Menu.Divider /> 561 <Menu.Item 562 testID="postDropdownReportMisclassificationBtn" 563 - label={_(msg`Assign topic for algo`)} 564 onPress={onReportMisclassification}> 565 - <Menu.ItemText>{_(msg`Assign topic for algo`)}</Menu.ItemText> 566 <Menu.ItemIcon icon={AtomIcon} position="right" /> 567 </Menu.Item> 568 </> ··· 574 <Menu.Group> 575 <Menu.Item 576 testID="postDropdownMuteThreadBtn" 577 - label={ 578 - isThreadMuted ? _(msg`Unmute thread`) : _(msg`Mute thread`) 579 - } 580 onPress={onToggleThreadMute}> 581 <Menu.ItemText> 582 - {isThreadMuted ? _(msg`Unmute thread`) : _(msg`Mute thread`)} 583 </Menu.ItemText> 584 <Menu.ItemIcon 585 icon={isThreadMuted ? Unmute : Mute} ··· 589 590 <Menu.Item 591 testID="postDropdownMuteWordsBtn" 592 - label={_(msg`Mute words & tags`)} 593 onPress={() => mutedWordsDialogControl.open()}> 594 - <Menu.ItemText>{_(msg`Mute words & tags`)}</Menu.ItemText> 595 <Menu.ItemIcon icon={Filter} position="right" /> 596 </Menu.Item> 597 </Menu.Group> ··· 606 {canHidePostForMe && ( 607 <Menu.Item 608 testID="postDropdownHideBtn" 609 - label={ 610 - isReply 611 - ? _(msg`Hide reply for me`) 612 - : _(msg`Hide post for me`) 613 - } 614 onPress={() => hidePromptControl.open()}> 615 <Menu.ItemText> 616 - {isReply 617 - ? _(msg`Hide reply for me`) 618 - : _(msg`Hide post for me`)} 619 </Menu.ItemText> 620 <Menu.ItemIcon icon={EyeSlash} position="right" /> 621 </Menu.Item> ··· 625 testID="postDropdownHideBtn" 626 label={ 627 isReplyHiddenByThreadgate 628 - ? _(msg`Show reply for everyone`) 629 - : _(msg`Hide reply for everyone`) 630 } 631 onPress={ 632 isReplyHiddenByThreadgate ··· 635 }> 636 <Menu.ItemText> 637 {isReplyHiddenByThreadgate 638 - ? _(msg`Show reply for everyone`) 639 - : _(msg`Hide reply for everyone`)} 640 </Menu.ItemText> 641 <Menu.ItemIcon 642 icon={isReplyHiddenByThreadgate ? Eye : EyeSlash} ··· 651 testID="postDropdownHideBtn" 652 label={ 653 quoteEmbed.isDetached 654 - ? _(msg`Re-attach quote`) 655 - : _(msg`Detach quote`) 656 } 657 onPress={ 658 quoteEmbed.isDetached ··· 661 }> 662 <Menu.ItemText> 663 {quoteEmbed.isDetached 664 - ? _(msg`Re-attach quote`) 665 - : _(msg`Detach quote`)} 666 </Menu.ItemText> 667 <Menu.ItemIcon 668 icon={ ··· 690 testID="postDropdownMuteBtn" 691 label={ 692 postAuthor.viewer?.muted 693 - ? _(msg`Unmute account`) 694 - : _(msg`Mute account`) 695 } 696 onPress={() => void onMuteAuthor()}> 697 <Menu.ItemText> 698 {postAuthor.viewer?.muted 699 - ? _(msg`Unmute account`) 700 - : _(msg`Mute account`)} 701 </Menu.ItemText> 702 <Menu.ItemIcon 703 icon={postAuthor.viewer?.muted ? UnmuteIcon : MuteIcon} ··· 708 {!postAuthor.viewer?.blocking && ( 709 <Menu.Item 710 testID="postDropdownBlockBtn" 711 - label={_(msg`Block account`)} 712 onPress={() => blockPromptControl.open()}> 713 - <Menu.ItemText>{_(msg`Block account`)}</Menu.ItemText> 714 <Menu.ItemIcon icon={PersonX} position="right" /> 715 </Menu.Item> 716 )} 717 718 <Menu.Item 719 testID="postDropdownReportBtn" 720 - label={_(msg`Report post`)} 721 onPress={() => reportDialogControl.open()}> 722 - <Menu.ItemText>{_(msg`Report post`)}</Menu.ItemText> 723 <Menu.ItemIcon icon={Warning} position="right" /> 724 </Menu.Item> 725 </> ··· 729 <> 730 <Menu.Item 731 testID="postDropdownEditPostInteractions" 732 - label={_(msg`Edit interaction settings`)} 733 onPress={() => postInteractionSettingsDialogControl.open()} 734 {...(isAuthor 735 ? Platform.select({ ··· 742 }) 743 : {})}> 744 <Menu.ItemText> 745 - {_(msg`Edit interaction settings`)} 746 </Menu.ItemText> 747 <Menu.ItemIcon icon={Gear} position="right" /> 748 </Menu.Item> 749 <Menu.Item 750 testID="postDropdownDeleteBtn" 751 - label={_(msg`Delete post`)} 752 onPress={() => deletePromptControl.open()}> 753 - <Menu.ItemText>{_(msg`Delete post`)}</Menu.ItemText> 754 <Menu.ItemIcon icon={Trash} position="right" /> 755 </Menu.Item> 756 </> ··· 759 </> 760 )} 761 </Menu.Outer> 762 - 763 <Prompt.Basic 764 control={deletePromptControl} 765 - title={_(msg`Delete this post?`)} 766 - description={_( 767 - msg`If you remove this post, you won't be able to recover it.`, 768 - )} 769 onConfirm={onDeletePost} 770 - confirmButtonCta={_(msg`Delete`)} 771 confirmButtonColor="negative" 772 /> 773 - 774 <Prompt.Basic 775 control={hidePromptControl} 776 - title={isReply ? _(msg`Hide this reply?`) : _(msg`Hide this post?`)} 777 - description={_( 778 - msg`This post will be hidden from feeds and threads. This cannot be undone.`, 779 - )} 780 onConfirm={onHidePost} 781 - confirmButtonCta={_(msg`Hide`)} 782 /> 783 - 784 <ReportDialog 785 control={reportDialogControl} 786 subject={{ ··· 788 $type: 'app.bsky.feed.defs#postView', 789 }} 790 /> 791 - 792 <PostInteractionSettingsDialog 793 control={postInteractionSettingsDialogControl} 794 postUri={post.uri} 795 rootPostUri={rootUri} 796 initialThreadgateView={post.threadgate} 797 /> 798 - 799 <Prompt.Basic 800 control={quotePostDetachConfirmControl} 801 - title={_(msg`Detach quote post?`)} 802 - description={_( 803 - msg`This will remove your post from this quote post for all users, and replace it with a placeholder.`, 804 - )} 805 onConfirm={() => void onToggleQuotePostAttachment()} 806 - confirmButtonCta={_(msg`Yes, detach`)} 807 /> 808 - 809 <Prompt.Basic 810 control={hideReplyConfirmControl} 811 - title={_(msg`Hide this reply?`)} 812 - description={_( 813 - msg`This reply will be sorted into a hidden section at the bottom of your thread and will mute notifications for subsequent replies - both for yourself and others.`, 814 - )} 815 onConfirm={() => void onToggleReplyVisibility()} 816 - confirmButtonCta={_(msg`Yes, hide`)} 817 /> 818 - 819 <Prompt.Basic 820 control={blockPromptControl} 821 - title={_(msg`Block Account?`)} 822 - description={_( 823 - msg`Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`, 824 - )} 825 onConfirm={() => void onBlockAuthor()} 826 - confirmButtonCta={_(msg`Block`)} 827 confirmButtonColor="negative" 828 /> 829 </>
··· 13 AtUri, 14 type RichText as RichTextAPI, 15 } from '@atproto/api' 16 + import {plural} from '@lingui/core/macro' 17 + import {useLingui} from '@lingui/react/macro' 18 import {useNavigation} from '@react-navigation/native' 19 20 import {DISCOVER_DEBUG_DIDS} from '#/lib/constants' 21 import {useOpenLink} from '#/lib/hooks/useOpenLink' 22 import {getCurrentRoute} from '#/lib/routes/helpers' 23 import {makeProfileLink} from '#/lib/routes/links' 24 import { ··· 27 } from '#/lib/routes/types' 28 import {richTextToString} from '#/lib/strings/rich-text-helpers' 29 import {toShareUrl} from '#/lib/strings/url-helpers' 30 + import {useTranslate} from '#/lib/translation' 31 import {logger} from '#/logger' 32 import {type Shadow} from '#/state/cache/post-shadow' 33 import {useProfileShadow} from '#/state/cache/profile-shadow' ··· 106 threadgateRecord, 107 onShowLess, 108 logContext, 109 + forceGoogleTranslate, 110 }: { 111 testID: string 112 post: Shadow<AppBskyFeedDefs.PostView> ··· 121 threadgateRecord?: AppBskyFeedThreadgate.Record 122 onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void 123 logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' 124 + forceGoogleTranslate: boolean 125 }): React.ReactNode => { 126 const {hasSession, currentAccount} = useSession() 127 + const {t: l} = useLingui() 128 const ax = useAnalytics() 129 const langPrefs = useLanguagePrefs() 130 const {mutateAsync: deletePostMutate} = usePostDeleteMutation() ··· 135 const {hidePost} = useHiddenPostsApi() 136 const feedFeedback = useFeedFeedbackContext() 137 const openLink = useOpenLink() 138 + const {clearTranslation, translate, translationState} = useTranslate({ 139 + key: post.uri, 140 + forceGoogleTranslate, 141 + }) 142 const navigation = useNavigation<NavigationProp>() 143 const {mutedWordsDialogControl} = useGlobalDialogsControlContext() 144 const blockPromptControl = useDialogControl() ··· 196 const onDeletePost = () => { 197 deletePostMutate({uri: postUri}).then( 198 () => { 199 + Toast.show(l({message: 'Post deleted', context: 'toast'})) 200 201 const route = getCurrentRoute(navigation.getState()) 202 if (route.name === 'PostThread') { ··· 216 }, 217 e => { 218 logger.error('Failed to delete post', {message: e}) 219 + Toast.show(l`Failed to delete post, please try again`, 'xmark') 220 }, 221 ) 222 } ··· 231 logContext, 232 feedDescriptor: feedFeedback.feedDescriptor, 233 }) 234 + Toast.show(l`You will now receive notifications for this thread`) 235 } else { 236 void muteThread() 237 ax.metric('post:mute', { ··· 240 logContext, 241 feedDescriptor: feedFeedback.feedDescriptor, 242 }) 243 + Toast.show(l`You will no longer receive notifications for this thread`) 244 } 245 } catch (err) { 246 const e = err as Error 247 if (e?.name !== 'AbortError') { 248 logger.error('Failed to toggle thread mute', {message: e}) 249 + Toast.show(l`Failed to toggle thread mute, please try again`, 'xmark') 250 } 251 } 252 } ··· 255 const str = richTextToString(richText, true) 256 257 void Clipboard.setStringAsync(str) 258 + Toast.show(l`Copied to clipboard`, 'clipboard-check') 259 } 260 261 const onPressTranslate = () => { 262 + void translate({ 263 + text: record.text, 264 + targetLangCode: langPrefs.primaryLanguage, 265 + }) 266 267 if ( 268 bsky.dangerousIsType<AppBskyFeedPost.Record>( ··· 300 logContext, 301 feedDescriptor: feedFeedback.feedDescriptor, 302 }) 303 + Toast.show(l({message: 'Feedback sent to feed operator', context: 'toast'})) 304 } 305 306 const onPressShowLess = () => { ··· 323 }) 324 } else { 325 Toast.show( 326 + l({message: 'Feedback sent to feed operator', context: 'toast'}), 327 ) 328 } 329 } ··· 342 }) 343 Toast.show( 344 isDetach 345 + ? l`Quote post was successfully detached` 346 + : l`Quote post was re-attached`, 347 ) 348 } catch (err) { 349 const e = err as Error 350 Toast.show( 351 + l({message: 'Updating quote attachment failed', context: 'toast'}), 352 ) 353 logger.error(`Failed to ${action} quote`, {safeMessage: e.message}) 354 } ··· 380 381 Toast.show( 382 isHide 383 + ? l`Reply was successfully hidden` 384 + : l({message: 'Reply visibility updated', context: 'toast'}), 385 ) 386 } catch (err) { 387 const e = err as Error 388 if (e instanceof MaxHiddenRepliesError) { 389 Toast.show( 390 + plural(MAX_HIDDEN_REPLIES, { 391 + other: 'You can hide a maximum of # replies.', 392 + }), 393 ) 394 } else if (e instanceof InvalidInteractionSettingsError) { 395 Toast.show( 396 + l({message: 'Invalid interaction settings.', context: 'toast'}), 397 ) 398 } else { 399 Toast.show( 400 + l({ 401 + message: 'Updating reply visibility failed', 402 + context: 'toast', 403 + }), 404 ) 405 logger.error(`Failed to ${action} reply`, {safeMessage: e.message}) 406 } ··· 419 const onBlockAuthor = async () => { 420 try { 421 await queueBlock() 422 + Toast.show(l({message: 'Account blocked', context: 'toast'})) 423 } catch (err) { 424 const e = err as Error 425 if (e?.name !== 'AbortError') { 426 logger.error('Failed to block account', {message: e}) 427 + Toast.show(l`There was an issue! ${e.toString()}`, 'xmark') 428 } 429 } 430 } ··· 433 if (postAuthor.viewer?.muted) { 434 try { 435 await queueUnmute() 436 + Toast.show(l({message: 'Account unmuted', context: 'toast'})) 437 } catch (err) { 438 const e = err as Error 439 if (e?.name !== 'AbortError') { 440 logger.error('Failed to unmute account', {message: e}) 441 + Toast.show(l`There was an issue! ${e.toString()}`, 'xmark') 442 } 443 } 444 } else { 445 try { 446 await queueMute() 447 + Toast.show(l({message: 'Account muted', context: 'toast'})) 448 } catch (err) { 449 const e = err as Error 450 if (e?.name !== 'AbortError') { 451 logger.error('Failed to mute account', {message: e}) 452 + Toast.show(l`There was an issue! ${e.toString()}`, 'xmark') 453 } 454 } 455 } ··· 464 465 const onSignIn = () => requireSignIn(() => {}) 466 467 + const onPressHideTranslation = () => clearTranslation() 468 + 469 const isDiscoverDebugUser = 470 IS_INTERNAL || 471 DISCOVER_DEBUG_DIDS[currentAccount?.did || ''] || ··· 480 <Menu.Item 481 testID="pinPostBtn" 482 label={ 483 + isPinned ? l`Unpin from profile` : l`Pin to your profile` 484 } 485 disabled={isPinPending} 486 onPress={onPressPin}> 487 <Menu.ItemText> 488 + {isPinned ? l`Unpin from profile` : l`Pin to your profile`} 489 </Menu.ItemText> 490 <Menu.ItemIcon 491 icon={isPinPending ? Loader : PinIcon} ··· 500 <Menu.Group> 501 {!hideInPWI || hasSession ? ( 502 <> 503 + {translationState.status === 'loading' ? ( 504 + <Menu.Item 505 + testID="postDropdownTranslateBtn" 506 + label={l`Translating…`} 507 + onPress={() => {}}> 508 + <Menu.ItemText>{l`Translating…`}</Menu.ItemText> 509 + <Menu.ItemIcon icon={Translate} position="right" /> 510 + </Menu.Item> 511 + ) : translationState.status === 'success' ? ( 512 + <Menu.Item 513 + testID="postDropdownTranslateBtn" 514 + label={l`Hide translation`} 515 + onPress={onPressHideTranslation}> 516 + <Menu.ItemText>{l`Hide translation`}</Menu.ItemText> 517 + <Menu.ItemIcon icon={Translate} position="right" /> 518 + </Menu.Item> 519 + ) : ( 520 + <Menu.Item 521 + testID="postDropdownTranslateBtn" 522 + label={l`Translate`} 523 + onPress={onPressTranslate}> 524 + <Menu.ItemText>{l`Translate`}</Menu.ItemText> 525 + <Menu.ItemIcon icon={Translate} position="right" /> 526 + </Menu.Item> 527 + )} 528 529 <Menu.Item 530 testID="postDropdownCopyTextBtn" 531 + label={l`Copy post text`} 532 onPress={onCopyPostText}> 533 + <Menu.ItemText>{l`Copy post text`}</Menu.ItemText> 534 <Menu.ItemIcon icon={ClipboardIcon} position="right" /> 535 </Menu.Item> 536 </> 537 ) : ( 538 <Menu.Item 539 testID="postDropdownSignInBtn" 540 + label={l`Sign in to view post`} 541 onPress={onSignIn}> 542 + <Menu.ItemText>{l`Sign in to view post`}</Menu.ItemText> 543 <Menu.ItemIcon icon={Eye} position="right" /> 544 </Menu.Item> 545 )} ··· 551 <Menu.Group> 552 <Menu.Item 553 testID="postDropdownShowMoreBtn" 554 + label={l`Show more like this`} 555 onPress={onPressShowMore}> 556 + <Menu.ItemText>{l`Show more like this`}</Menu.ItemText> 557 <Menu.ItemIcon icon={EmojiSmile} position="right" /> 558 </Menu.Item> 559 560 <Menu.Item 561 testID="postDropdownShowLessBtn" 562 + label={l`Show less like this`} 563 onPress={onPressShowLess}> 564 + <Menu.ItemText>{l`Show less like this`}</Menu.ItemText> 565 <Menu.ItemIcon icon={EmojiSad} position="right" /> 566 </Menu.Item> 567 </Menu.Group> ··· 573 <Menu.Divider /> 574 <Menu.Item 575 testID="postDropdownReportMisclassificationBtn" 576 + label={l`Assign topic for algo`} 577 onPress={onReportMisclassification}> 578 + <Menu.ItemText>{l`Assign topic for algo`}</Menu.ItemText> 579 <Menu.ItemIcon icon={AtomIcon} position="right" /> 580 </Menu.Item> 581 </> ··· 587 <Menu.Group> 588 <Menu.Item 589 testID="postDropdownMuteThreadBtn" 590 + label={isThreadMuted ? l`Unmute thread` : l`Mute thread`} 591 onPress={onToggleThreadMute}> 592 <Menu.ItemText> 593 + {isThreadMuted ? l`Unmute thread` : l`Mute thread`} 594 </Menu.ItemText> 595 <Menu.ItemIcon 596 icon={isThreadMuted ? Unmute : Mute} ··· 600 601 <Menu.Item 602 testID="postDropdownMuteWordsBtn" 603 + label={l`Mute words & tags`} 604 onPress={() => mutedWordsDialogControl.open()}> 605 + <Menu.ItemText>{l`Mute words & tags`}</Menu.ItemText> 606 <Menu.ItemIcon icon={Filter} position="right" /> 607 </Menu.Item> 608 </Menu.Group> ··· 617 {canHidePostForMe && ( 618 <Menu.Item 619 testID="postDropdownHideBtn" 620 + label={isReply ? l`Hide reply for me` : l`Hide post for me`} 621 onPress={() => hidePromptControl.open()}> 622 <Menu.ItemText> 623 + {isReply ? l`Hide reply for me` : l`Hide post for me`} 624 </Menu.ItemText> 625 <Menu.ItemIcon icon={EyeSlash} position="right" /> 626 </Menu.Item> ··· 630 testID="postDropdownHideBtn" 631 label={ 632 isReplyHiddenByThreadgate 633 + ? l`Show reply for everyone` 634 + : l`Hide reply for everyone` 635 } 636 onPress={ 637 isReplyHiddenByThreadgate ··· 640 }> 641 <Menu.ItemText> 642 {isReplyHiddenByThreadgate 643 + ? l`Show reply for everyone` 644 + : l`Hide reply for everyone`} 645 </Menu.ItemText> 646 <Menu.ItemIcon 647 icon={isReplyHiddenByThreadgate ? Eye : EyeSlash} ··· 656 testID="postDropdownHideBtn" 657 label={ 658 quoteEmbed.isDetached 659 + ? l`Re-attach quote` 660 + : l`Detach quote` 661 } 662 onPress={ 663 quoteEmbed.isDetached ··· 666 }> 667 <Menu.ItemText> 668 {quoteEmbed.isDetached 669 + ? l`Re-attach quote` 670 + : l`Detach quote`} 671 </Menu.ItemText> 672 <Menu.ItemIcon 673 icon={ ··· 695 testID="postDropdownMuteBtn" 696 label={ 697 postAuthor.viewer?.muted 698 + ? l`Unmute account` 699 + : l`Mute account` 700 } 701 onPress={() => void onMuteAuthor()}> 702 <Menu.ItemText> 703 {postAuthor.viewer?.muted 704 + ? l`Unmute account` 705 + : l`Mute account`} 706 </Menu.ItemText> 707 <Menu.ItemIcon 708 icon={postAuthor.viewer?.muted ? UnmuteIcon : MuteIcon} ··· 713 {!postAuthor.viewer?.blocking && ( 714 <Menu.Item 715 testID="postDropdownBlockBtn" 716 + label={l`Block account`} 717 onPress={() => blockPromptControl.open()}> 718 + <Menu.ItemText>{l`Block account`}</Menu.ItemText> 719 <Menu.ItemIcon icon={PersonX} position="right" /> 720 </Menu.Item> 721 )} 722 723 <Menu.Item 724 testID="postDropdownReportBtn" 725 + label={l`Report post`} 726 onPress={() => reportDialogControl.open()}> 727 + <Menu.ItemText>{l`Report post`}</Menu.ItemText> 728 <Menu.ItemIcon icon={Warning} position="right" /> 729 </Menu.Item> 730 </> ··· 734 <> 735 <Menu.Item 736 testID="postDropdownEditPostInteractions" 737 + label={l`Edit interaction settings`} 738 onPress={() => postInteractionSettingsDialogControl.open()} 739 {...(isAuthor 740 ? Platform.select({ ··· 747 }) 748 : {})}> 749 <Menu.ItemText> 750 + {l`Edit interaction settings`} 751 </Menu.ItemText> 752 <Menu.ItemIcon icon={Gear} position="right" /> 753 </Menu.Item> 754 <Menu.Item 755 testID="postDropdownDeleteBtn" 756 + label={l`Delete post`} 757 onPress={() => deletePromptControl.open()}> 758 + <Menu.ItemText>{l`Delete post`}</Menu.ItemText> 759 <Menu.ItemIcon icon={Trash} position="right" /> 760 </Menu.Item> 761 </> ··· 764 </> 765 )} 766 </Menu.Outer> 767 <Prompt.Basic 768 control={deletePromptControl} 769 + title={l`Delete this post?`} 770 + description={l`If you remove this post, you won't be able to recover it.`} 771 onConfirm={onDeletePost} 772 + confirmButtonCta={l`Delete`} 773 confirmButtonColor="negative" 774 /> 775 <Prompt.Basic 776 control={hidePromptControl} 777 + title={isReply ? l`Hide this reply?` : l`Hide this post?`} 778 + description={l`This post will be hidden from feeds and threads. This cannot be undone.`} 779 onConfirm={onHidePost} 780 + confirmButtonCta={l`Hide`} 781 /> 782 <ReportDialog 783 control={reportDialogControl} 784 subject={{ ··· 786 $type: 'app.bsky.feed.defs#postView', 787 }} 788 /> 789 <PostInteractionSettingsDialog 790 control={postInteractionSettingsDialogControl} 791 postUri={post.uri} 792 rootPostUri={rootUri} 793 initialThreadgateView={post.threadgate} 794 /> 795 <Prompt.Basic 796 control={quotePostDetachConfirmControl} 797 + title={l`Detach quote post?`} 798 + description={l`This will remove your post from this quote post for all users, and replace it with a placeholder.`} 799 onConfirm={() => void onToggleQuotePostAttachment()} 800 + confirmButtonCta={l`Yes, detach`} 801 /> 802 <Prompt.Basic 803 control={hideReplyConfirmControl} 804 + title={l`Hide this reply?`} 805 + description={l`This reply will be sorted into a hidden section at the bottom of your thread and will mute notifications for subsequent replies - both for yourself and others.`} 806 onConfirm={() => void onToggleReplyVisibility()} 807 + confirmButtonCta={l`Yes, hide`} 808 /> 809 <Prompt.Basic 810 control={blockPromptControl} 811 + title={l`Block Account?`} 812 + description={l`Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`} 813 onConfirm={() => void onBlockAuthor()} 814 + confirmButtonCta={l`Block`} 815 confirmButtonColor="negative" 816 /> 817 </>
+6 -4
src/components/PostControls/PostMenu/index.tsx
··· 6 type AppBskyFeedThreadgate, 7 type RichText as RichTextAPI, 8 } from '@atproto/api' 9 - import {msg} from '@lingui/core/macro' 10 - import {useLingui} from '@lingui/react' 11 12 import {type Shadow} from '#/state/cache/post-shadow' 13 import {EventStopper} from '#/view/com/util/EventStopper' ··· 30 onShowLess, 31 hitSlop, 32 logContext, 33 }: { 34 testID: string 35 post: Shadow<AppBskyFeedDefs.PostView> ··· 43 onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void 44 hitSlop?: Insets 45 logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' 46 }): React.ReactNode => { 47 - const {_} = useLingui() 48 49 const menuControl = useMenuControl() 50 const [hasBeenOpen, setHasBeenOpen] = useState(false) ··· 63 return ( 64 <EventStopper onKeyDown={false}> 65 <Menu.Root control={lazyMenuControl}> 66 - <Menu.Trigger label={_(msg`Open post options menu`)}> 67 {({props}) => { 68 return ( 69 <PostControlButton ··· 90 threadgateRecord={threadgateRecord} 91 onShowLess={onShowLess} 92 logContext={logContext} 93 /> 94 )} 95 </Menu.Root>
··· 6 type AppBskyFeedThreadgate, 7 type RichText as RichTextAPI, 8 } from '@atproto/api' 9 + import {useLingui} from '@lingui/react/macro' 10 11 import {type Shadow} from '#/state/cache/post-shadow' 12 import {EventStopper} from '#/view/com/util/EventStopper' ··· 29 onShowLess, 30 hitSlop, 31 logContext, 32 + forceGoogleTranslate, 33 }: { 34 testID: string 35 post: Shadow<AppBskyFeedDefs.PostView> ··· 43 onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void 44 hitSlop?: Insets 45 logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo' 46 + forceGoogleTranslate: boolean 47 }): React.ReactNode => { 48 + const {t: l} = useLingui() 49 50 const menuControl = useMenuControl() 51 const [hasBeenOpen, setHasBeenOpen] = useState(false) ··· 64 return ( 65 <EventStopper onKeyDown={false}> 66 <Menu.Root control={lazyMenuControl}> 67 + <Menu.Trigger label={l`Open post options menu`}> 68 {({props}) => { 69 return ( 70 <PostControlButton ··· 91 threadgateRecord={threadgateRecord} 92 onShowLess={onShowLess} 93 logContext={logContext} 94 + forceGoogleTranslate={forceGoogleTranslate} 95 /> 96 )} 97 </Menu.Root>
+38 -48
src/components/PostControls/index.tsx
··· 6 type AppBskyFeedThreadgate, 7 type RichText as RichTextAPI, 8 } from '@atproto/api' 9 - import {msg, plural} from '@lingui/core/macro' 10 - import {useLingui} from '@lingui/react' 11 12 import {CountWheel} from '#/lib/custom-animations/CountWheel' 13 import {AnimatedLikeIcon} from '#/lib/custom-animations/LikeIcon' ··· 55 onShowLess, 56 viaRepost, 57 variant, 58 }: { 59 big?: boolean 60 post: Shadow<AppBskyFeedDefs.PostView> ··· 70 onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void 71 viaRepost?: {uri: string; cid: string} 72 variant?: 'compact' | 'normal' | 'large' 73 }): React.ReactNode => { 74 const ax = useAnalytics() 75 - const {_} = useLingui() 76 const {openComposer} = useOpenComposer() 77 const {feedDescriptor} = useFeedFeedbackContext() 78 const [queueLike, queueUnlike] = usePostLikeMutationQueue( ··· 104 105 const onPressToggleLike = async () => { 106 if (isBlocked) { 107 - Toast.show( 108 - _(msg`Cannot interact with a blocked user`), 109 - 'exclamation-circle', 110 - ) 111 return 112 } 113 ··· 126 } else { 127 await queueUnlike() 128 } 129 - } catch (e: any) { 130 if (e?.name !== 'AbortError') { 131 throw e 132 } ··· 135 136 const onRepost = async () => { 137 if (isBlocked) { 138 - Toast.show( 139 - _(msg`Cannot interact with a blocked user`), 140 - 'exclamation-circle', 141 - ) 142 return 143 } 144 ··· 154 } else { 155 await queueUnrepost() 156 } 157 - } catch (e: any) { 158 if (e?.name !== 'AbortError') { 159 throw e 160 } ··· 163 164 const onQuote = () => { 165 if (isBlocked) { 166 - Toast.show( 167 - _(msg`Cannot interact with a blocked user`), 168 - 'exclamation-circle', 169 - ) 170 return 171 } 172 ··· 238 }) 239 : undefined 240 } 241 - label={_( 242 - msg({ 243 - message: `Reply (${plural(post.replyCount || 0, { 244 - one: '# reply', 245 - other: '# replies', 246 - })})`, 247 - comment: 248 - 'Accessibility label for the reply button, verb form followed by number of replies and noun form', 249 - }), 250 - )} 251 big={big}> 252 <PostControlButtonIcon icon={Bubble} /> 253 {typeof post.replyCount !== 'undefined' && post.replyCount > 0 && ( ··· 261 <RepostButton 262 isReposted={!!post.viewer?.repost} 263 repostCount={(post.repostCount ?? 0) + (post.quoteCount ?? 0)} 264 - onRepost={onRepost} 265 onQuote={onQuote} 266 big={big} 267 embeddingDisabled={Boolean(post.viewer?.embeddingDisabled)} ··· 274 onPress={() => requireAuth(() => onPressToggleLike())} 275 label={ 276 post.viewer?.like 277 - ? _( 278 - msg({ 279 - message: `Unlike (${plural(post.likeCount || 0, { 280 - one: '# like', 281 - other: '# likes', 282 - })})`, 283 - comment: 284 - 'Accessibility label for the like button when the post has been liked, verb followed by number of likes and noun', 285 - }), 286 - ) 287 - : _( 288 - msg({ 289 - message: `Like (${plural(post.likeCount || 0, { 290 - one: '# like', 291 - other: '# likes', 292 - })})`, 293 - comment: 294 - 'Accessibility label for the like button when the post has not been liked, verb form followed by number of likes and noun form', 295 - }), 296 - ) 297 }> 298 <AnimatedLikeIcon 299 isLiked={Boolean(post.viewer?.like)} ··· 350 left: secondaryControlSpacingStyles.gap / 2, 351 }} 352 logContext={logContext} 353 /> 354 </View> 355 </View>
··· 6 type AppBskyFeedThreadgate, 7 type RichText as RichTextAPI, 8 } from '@atproto/api' 9 + import {plural} from '@lingui/core/macro' 10 + import {useLingui} from '@lingui/react/macro' 11 12 import {CountWheel} from '#/lib/custom-animations/CountWheel' 13 import {AnimatedLikeIcon} from '#/lib/custom-animations/LikeIcon' ··· 55 onShowLess, 56 viaRepost, 57 variant, 58 + forceGoogleTranslate = false, 59 }: { 60 big?: boolean 61 post: Shadow<AppBskyFeedDefs.PostView> ··· 71 onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void 72 viaRepost?: {uri: string; cid: string} 73 variant?: 'compact' | 'normal' | 'large' 74 + forceGoogleTranslate?: boolean 75 }): React.ReactNode => { 76 const ax = useAnalytics() 77 + const {t: l} = useLingui() 78 const {openComposer} = useOpenComposer() 79 const {feedDescriptor} = useFeedFeedbackContext() 80 const [queueLike, queueUnlike] = usePostLikeMutationQueue( ··· 106 107 const onPressToggleLike = async () => { 108 if (isBlocked) { 109 + Toast.show(l`Cannot interact with a blocked user`, 'exclamation-circle') 110 return 111 } 112 ··· 125 } else { 126 await queueUnlike() 127 } 128 + } catch (err) { 129 + const e = err as Error 130 if (e?.name !== 'AbortError') { 131 throw e 132 } ··· 135 136 const onRepost = async () => { 137 if (isBlocked) { 138 + Toast.show(l`Cannot interact with a blocked user`, 'exclamation-circle') 139 return 140 } 141 ··· 151 } else { 152 await queueUnrepost() 153 } 154 + } catch (err) { 155 + const e = err as Error 156 if (e?.name !== 'AbortError') { 157 throw e 158 } ··· 161 162 const onQuote = () => { 163 if (isBlocked) { 164 + Toast.show(l`Cannot interact with a blocked user`, 'exclamation-circle') 165 return 166 } 167 ··· 233 }) 234 : undefined 235 } 236 + label={l({ 237 + message: `Reply (${plural(post.replyCount || 0, { 238 + one: '# reply', 239 + other: '# replies', 240 + })})`, 241 + comment: 242 + 'Accessibility label for the reply button, verb form followed by number of replies and noun form', 243 + })} 244 big={big}> 245 <PostControlButtonIcon icon={Bubble} /> 246 {typeof post.replyCount !== 'undefined' && post.replyCount > 0 && ( ··· 254 <RepostButton 255 isReposted={!!post.viewer?.repost} 256 repostCount={(post.repostCount ?? 0) + (post.quoteCount ?? 0)} 257 + onRepost={() => void onRepost()} 258 onQuote={onQuote} 259 big={big} 260 embeddingDisabled={Boolean(post.viewer?.embeddingDisabled)} ··· 267 onPress={() => requireAuth(() => onPressToggleLike())} 268 label={ 269 post.viewer?.like 270 + ? l({ 271 + message: `Unlike (${plural(post.likeCount || 0, { 272 + one: '# like', 273 + other: '# likes', 274 + })})`, 275 + comment: 276 + 'Accessibility label for the like button when the post has been liked, verb followed by number of likes and noun', 277 + }) 278 + : l({ 279 + message: `Like (${plural(post.likeCount || 0, { 280 + one: '# like', 281 + other: '# likes', 282 + })})`, 283 + comment: 284 + 'Accessibility label for the like button when the post has not been liked, verb form followed by number of likes and noun form', 285 + }) 286 }> 287 <AnimatedLikeIcon 288 isLiked={Boolean(post.viewer?.like)} ··· 339 left: secondaryControlSpacingStyles.gap / 2, 340 }} 341 logContext={logContext} 342 + forceGoogleTranslate={forceGoogleTranslate} 343 /> 344 </View> 345 </View>
+4 -4
src/components/dms/MessageContextMenu.tsx
··· 6 import {useLingui} from '@lingui/react' 7 import {useQueryClient} from '@tanstack/react-query' 8 9 - import {useTranslate} from '#/lib/hooks/useTranslate' 10 import {richTextToString} from '#/lib/strings/rich-text-helpers' 11 import {useConvoActive} from '#/state/messages/convo' 12 import {useLanguagePrefs} from '#/state/preferences' ··· 44 const reportControl = usePromptControl() 45 const blockOrDeleteControl = usePromptControl() 46 const langPrefs = useLanguagePrefs() 47 - const translate = useTranslate() 48 49 const isFromSelf = message.sender?.did === currentAccount?.did 50 ··· 57 true, 58 ) 59 60 - Clipboard.setStringAsync(str) 61 Toast.show(_(msg`Copied to clipboard`), 'clipboard-check') 62 }, [_, message.text, message.facets]) 63 64 const onPressTranslateMessage = useCallback(() => { 65 - translate(message.text, langPrefs.primaryLanguage) 66 67 ax.metric('translate', { 68 sourceLanguages: [],
··· 6 import {useLingui} from '@lingui/react' 7 import {useQueryClient} from '@tanstack/react-query' 8 9 + import {useGoogleTranslate} from '#/lib/hooks/useGoogleTranslate' 10 import {richTextToString} from '#/lib/strings/rich-text-helpers' 11 import {useConvoActive} from '#/state/messages/convo' 12 import {useLanguagePrefs} from '#/state/preferences' ··· 44 const reportControl = usePromptControl() 45 const blockOrDeleteControl = usePromptControl() 46 const langPrefs = useLanguagePrefs() 47 + const translate = useGoogleTranslate() 48 49 const isFromSelf = message.sender?.did === currentAccount?.did 50 ··· 57 true, 58 ) 59 60 + void Clipboard.setStringAsync(str) 61 Toast.show(_(msg`Copied to clipboard`), 'clipboard-check') 62 }, [_, message.text, message.facets]) 63 64 const onPressTranslateMessage = useCallback(() => { 65 + void translate(message.text, langPrefs.primaryLanguage) 66 67 ax.metric('translate', { 68 sourceLanguages: [],
+3
src/env/index.ts
··· 49 export const IS_HIGH_DPI: boolean = true 50 // ideally we'd use isLiquidGlassAvailable() from expo-glass-effect but checking iOS version is good enough for now 51 export const IS_LIQUID_GLASS: boolean = iOSMajorVersion >= 26
··· 49 export const IS_HIGH_DPI: boolean = true 50 // ideally we'd use isLiquidGlassAvailable() from expo-glass-effect but checking iOS version is good enough for now 51 export const IS_LIQUID_GLASS: boolean = iOSMajorVersion >= 26 52 + // So we can avoid attempting on-device translation when we know it's unsupported. 53 + export const HAS_ON_DEVICE_TRANSLATION: boolean = 54 + (IS_IOS && iOSMajorVersion >= 18) || IS_ANDROID
+1
src/env/index.web.ts
··· 48 '(min-resolution: 2dppx)', 49 ).matches 50 export const IS_LIQUID_GLASS: boolean = false
··· 48 '(min-resolution: 2dppx)', 49 ).matches 50 export const IS_LIQUID_GLASS: boolean = false 51 + export const HAS_ON_DEVICE_TRANSLATION: boolean = false
+2 -3
src/lib/hooks/useTranslate.ts src/lib/hooks/useGoogleTranslate.ts
··· 6 import {IS_ANDROID} from '#/env' 7 8 /** 9 - * Will always link out to Google Translate. If inline translation is desired, 10 - * use `useTranslateOnDevice` 11 */ 12 - export function useTranslate() { 13 const openLink = useOpenLink() 14 15 return useCallback(
··· 6 import {IS_ANDROID} from '#/env' 7 8 /** 9 + * @deprecated Will always link out to Google Translate. Prefer `useTranslate`. 10 */ 11 + export function useGoogleTranslate() { 12 const openLink = useOpenLink() 13 14 return useCallback(
+16
src/lib/translation/context.ts
···
··· 1 + import {createContext} from 'react' 2 + 3 + import {type TranslationFunctionParams, type TranslationState} from './types' 4 + 5 + export const Context = createContext<{ 6 + translationState: Record<string, TranslationState> 7 + translate: ( 8 + parameters: TranslationFunctionParams & { 9 + key: string 10 + forceGoogleTranslate: boolean 11 + }, 12 + ) => Promise<void> 13 + clearTranslation: (key: string) => void 14 + acquireTranslation: (key: string) => () => void 15 + } | null>(null) 16 + Context.displayName = 'TranslationContext'
+282
src/lib/translation/index.tsx
···
··· 1 + import {useCallback, useContext, useEffect, useMemo, useState} from 'react' 2 + import {LayoutAnimation, Platform} from 'react-native' 3 + import {getLocales} from 'expo-localization' 4 + import {onTranslateTask} from '@bsky.app/expo-translate-text' 5 + import {type TranslationTaskResult} from '@bsky.app/expo-translate-text/build/ExpoTranslateText.types' 6 + import {useLingui} from '@lingui/react/macro' 7 + import {useFocusEffect} from '@react-navigation/native' 8 + 9 + import {useGoogleTranslate} from '#/lib/hooks/useGoogleTranslate' 10 + import {logger} from '#/logger' 11 + import {useAnalytics} from '#/analytics' 12 + import {HAS_ON_DEVICE_TRANSLATION} from '#/env' 13 + import {Context} from './context' 14 + import {type TranslationFunctionParams, type TranslationState} from './types' 15 + import {guessLanguage} from './utils' 16 + 17 + export * from './types' 18 + export * from './utils' 19 + 20 + /** 21 + * Attempts on-device translation via @bsky.app/expo-translate-text. 22 + * Uses a lazy import to avoid crashing if the native module isn't linked into 23 + * the current build. 24 + */ 25 + async function attemptTranslation( 26 + input: string, 27 + targetLangCodeOriginal: string, 28 + sourceLangCodeOriginal?: string, // Auto-detects if not provided 29 + ): Promise<{ 30 + translatedText: string 31 + targetLanguage: TranslationTaskResult['targetLanguage'] 32 + sourceLanguage: TranslationTaskResult['sourceLanguage'] 33 + }> { 34 + // Note that Android only supports two-character language codes and will fail 35 + // on other input. 36 + // https://developers.google.com/android/reference/com/google/mlkit/nl/translate/TranslateLanguage 37 + let targetLangCode = 38 + Platform.OS === 'android' 39 + ? targetLangCodeOriginal.split('-')[0] 40 + : targetLangCodeOriginal 41 + const sourceLangCode = 42 + Platform.OS === 'android' 43 + ? sourceLangCodeOriginal?.split('-')[0] 44 + : sourceLangCodeOriginal 45 + 46 + // Special cases for regional languages since iOS differentiates and missing 47 + // language packs must be downloaded and installed. 48 + if (Platform.OS === 'ios') { 49 + const deviceLocales = getLocales() 50 + const primaryLanguageTag = deviceLocales[0]?.languageTag 51 + switch (targetLangCodeOriginal) { 52 + case 'en': // en-US, en-GB 53 + case 'es': // es-419, es-ES 54 + case 'pt': // pt-BR, pt-PT 55 + case 'zh': // zh-Hans-CN, zh-Hant-HK, zh-Hant-TW 56 + if ( 57 + primaryLanguageTag && 58 + primaryLanguageTag.startsWith(targetLangCodeOriginal) 59 + ) { 60 + targetLangCode = primaryLanguageTag 61 + } 62 + break 63 + } 64 + } 65 + 66 + const result = await onTranslateTask({ 67 + input, 68 + targetLangCode, 69 + sourceLangCode, 70 + }) 71 + 72 + // Since `input` is always a string, the result should always be a string. 73 + const translatedText = 74 + typeof result.translatedTexts === 'string' ? result.translatedTexts : '' 75 + 76 + if (translatedText === input) { 77 + throw new Error('Translation result is the same as the source text.') 78 + } 79 + 80 + if (translatedText === '') { 81 + throw new Error('Translation result is empty.') 82 + } 83 + 84 + return { 85 + translatedText, 86 + targetLanguage: result.targetLanguage, 87 + sourceLanguage: 88 + result.sourceLanguage ?? sourceLangCode ?? guessLanguage(input), // iOS doesn't return the source language 89 + } 90 + } 91 + 92 + /** 93 + * Native translation hook. Attempts on-device translation using Apple 94 + * Translation (iOS 18+) or Google ML Kit (Android). 95 + * 96 + * Falls back to Google Translate URL if the language pack is unavailable. 97 + * 98 + * Web uses index.web.ts which always opens Google Translate. 99 + */ 100 + export function useTranslate({ 101 + key, 102 + forceGoogleTranslate = false, 103 + }: { 104 + key: string 105 + forceGoogleTranslate?: boolean 106 + }) { 107 + const context = useContext(Context) 108 + if (!context) { 109 + throw new Error( 110 + 'useTranslate must be used within a TranslateOnDeviceProvider', 111 + ) 112 + } 113 + 114 + useFocusEffect( 115 + useCallback(() => { 116 + const cleanup = context.acquireTranslation(key) 117 + return cleanup 118 + }, [key, context]), 119 + ) 120 + 121 + const translate = useCallback( 122 + async (params: TranslationFunctionParams) => { 123 + return context.translate({...params, key, forceGoogleTranslate}) 124 + }, 125 + [context, forceGoogleTranslate, key], 126 + ) 127 + 128 + const clearTranslation = useCallback( 129 + () => context.clearTranslation(key), 130 + [context, key], 131 + ) 132 + 133 + return useMemo( 134 + () => ({ 135 + translationState: context.translationState[key] ?? { 136 + status: 'idle', 137 + }, 138 + translate, 139 + clearTranslation, 140 + }), 141 + [clearTranslation, context.translationState, key, translate], 142 + ) 143 + } 144 + 145 + export function Provider({children}: React.PropsWithChildren<unknown>) { 146 + const [translationState, setTranslationState] = useState< 147 + Record<string, TranslationState> 148 + >({}) 149 + const [refCounts, setRefCounts] = useState<Record<string, number>>({}) 150 + const ax = useAnalytics() 151 + const {t: l} = useLingui() 152 + const googleTranslate = useGoogleTranslate() 153 + 154 + useEffect(() => { 155 + setTranslationState(prev => { 156 + const keysToDelete: string[] = [] 157 + 158 + for (const key of Object.keys(prev)) { 159 + if ((refCounts[key] ?? 0) <= 0) { 160 + keysToDelete.push(key) 161 + } 162 + } 163 + 164 + if (keysToDelete.length > 0) { 165 + const newState = {...prev} 166 + keysToDelete.forEach(key => { 167 + delete newState[key] 168 + }) 169 + return newState 170 + } 171 + 172 + return prev 173 + }) 174 + }, [refCounts]) 175 + 176 + const acquireTranslation = useCallback((key: string) => { 177 + setRefCounts(prev => ({ 178 + ...prev, 179 + [key]: (prev[key] ?? 0) + 1, 180 + })) 181 + 182 + return () => { 183 + setRefCounts(prev => { 184 + const newCount = (prev[key] ?? 1) - 1 185 + if (newCount <= 0) { 186 + const {[key]: _, ...rest} = prev 187 + return rest 188 + } 189 + return {...prev, [key]: newCount} 190 + }) 191 + } 192 + }, []) 193 + 194 + const clearTranslation = useCallback((key: string) => { 195 + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) 196 + setTranslationState(prev => { 197 + delete prev[key] 198 + return {...prev} 199 + }) 200 + }, []) 201 + 202 + const translate = useCallback( 203 + async ({ 204 + key, 205 + text, 206 + targetLangCode, 207 + sourceLangCode, 208 + ...options 209 + }: { 210 + key: string 211 + text: string 212 + targetLangCode: string 213 + sourceLangCode?: string 214 + forceGoogleTranslate?: boolean 215 + }) => { 216 + if (options?.forceGoogleTranslate || !HAS_ON_DEVICE_TRANSLATION) { 217 + ax.metric('translate:result', { 218 + method: 'google-translate', 219 + os: Platform.OS, 220 + sourceLanguage: sourceLangCode ?? null, 221 + targetLanguage: targetLangCode, 222 + }) 223 + await googleTranslate(text, targetLangCode, sourceLangCode) 224 + return 225 + } 226 + 227 + // Translate after the next state change. 228 + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) 229 + setTranslationState(prev => ({ 230 + ...prev, 231 + [key]: {status: 'loading'}, 232 + })) 233 + try { 234 + const result = await attemptTranslation( 235 + text, 236 + targetLangCode, 237 + sourceLangCode, 238 + ) 239 + ax.metric('translate:result', { 240 + method: 'on-device', 241 + os: Platform.OS, 242 + sourceLanguage: result.sourceLanguage, 243 + targetLanguage: result.targetLanguage, 244 + }) 245 + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) 246 + setTranslationState(prev => ({ 247 + ...prev, 248 + [key]: { 249 + status: 'success', 250 + translatedText: result.translatedText, 251 + sourceLanguage: result.sourceLanguage, 252 + targetLanguage: result.targetLanguage, 253 + }, 254 + })) 255 + } catch (e) { 256 + logger.error('Failed to translate post on device', {safeMessage: e}) 257 + // On-device translation failed (language pack missing or user 258 + // dismissed the download prompt). Fall back to Google Translate. 259 + ax.metric('translate:result', { 260 + method: 'fallback-alert', 261 + os: Platform.OS, 262 + sourceLanguage: sourceLangCode ?? null, 263 + targetLanguage: targetLangCode, 264 + }) 265 + let errorMessage = l`Device failed to translate :(` 266 + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) 267 + setTranslationState(prev => ({ 268 + ...prev, 269 + [key]: {status: 'error', message: errorMessage}, 270 + })) 271 + } 272 + }, 273 + [ax, googleTranslate, l], 274 + ) 275 + 276 + const ctx = useMemo( 277 + () => ({acquireTranslation, clearTranslation, translate, translationState}), 278 + [acquireTranslation, clearTranslation, translate, translationState], 279 + ) 280 + 281 + return <Context.Provider value={ctx}>{children}</Context.Provider> 282 + }
+86
src/lib/translation/index.web.tsx
···
··· 1 + import {useCallback, useContext, useMemo} from 'react' 2 + 3 + import {useGoogleTranslate} from '#/lib/hooks/useGoogleTranslate' 4 + import {useAnalytics} from '#/analytics' 5 + import {Context} from './context' 6 + import {type TranslationFunctionParams, type TranslationState} from './types' 7 + 8 + export * from './types' 9 + export * from './utils' 10 + 11 + const translationState: Record<string, TranslationState> = {} 12 + const acquireTranslation = (_key: string) => { 13 + return () => {} 14 + } 15 + const clearTranslation = (_key: string) => {} 16 + 17 + /** 18 + * Web always opens Google Translate. 19 + */ 20 + export function useTranslate({ 21 + key, 22 + }: { 23 + key: string 24 + forceGoogleTranslate?: boolean 25 + }) { 26 + const context = useContext(Context) 27 + if (!context) { 28 + throw new Error( 29 + 'useTranslate must be used within a TranslateOnDeviceProvider', 30 + ) 31 + } 32 + 33 + // Always call hooks in consistent order 34 + const translate = useCallback( 35 + async (params: TranslationFunctionParams) => { 36 + return context.translate({...params, key, forceGoogleTranslate: true}) 37 + }, 38 + [key, context], 39 + ) 40 + 41 + const clearTranslation = useCallback(() => { 42 + return context.clearTranslation(key) 43 + }, [key, context]) 44 + 45 + return { 46 + translationState: context.translationState[key] ?? { 47 + status: 'idle' as const, 48 + }, 49 + translate, 50 + clearTranslation, 51 + } 52 + } 53 + 54 + export function Provider({children}: React.PropsWithChildren<unknown>) { 55 + const ax = useAnalytics() 56 + const googleTranslate = useGoogleTranslate() 57 + 58 + const translate = useCallback( 59 + async ({ 60 + text, 61 + targetLangCode, 62 + sourceLangCode, 63 + }: { 64 + key: string 65 + text: string 66 + targetLangCode: string 67 + sourceLangCode?: string 68 + }) => { 69 + ax.metric('translate:result', { 70 + method: 'google-translate', 71 + os: 'web', 72 + sourceLanguage: sourceLangCode ?? null, 73 + targetLanguage: targetLangCode, 74 + }) 75 + await googleTranslate(text, targetLangCode, sourceLangCode) 76 + }, 77 + [ax, googleTranslate], 78 + ) 79 + 80 + const ctx = useMemo( 81 + () => ({acquireTranslation, clearTranslation, translate, translationState}), 82 + [translate], 83 + ) 84 + 85 + return <Context.Provider value={ctx}>{children}</Context.Provider> 86 + }
+34
src/lib/translation/types.ts
···
··· 1 + import {type TranslationTaskResult} from '@bsky.app/expo-translate-text/build/ExpoTranslateText.types' 2 + 3 + export type TranslationState = 4 + | {status: 'idle'} 5 + | {status: 'loading'} 6 + | { 7 + status: 'success' 8 + translatedText: string 9 + sourceLanguage: TranslationTaskResult['sourceLanguage'] 10 + targetLanguage: TranslationTaskResult['targetLanguage'] 11 + } 12 + | { 13 + status: 'error' 14 + message: string 15 + } 16 + 17 + export type TranslationFunctionParams = { 18 + /** 19 + * The text to be translated. 20 + */ 21 + text: string 22 + /** 23 + * The language to translate the text into. 24 + */ 25 + targetLangCode: string 26 + /** 27 + * The source language of the text. Will auto-detect if not provided. 28 + */ 29 + sourceLangCode?: string 30 + } 31 + 32 + export type TranslationFunction = ( 33 + parameters: TranslationFunctionParams, 34 + ) => Promise<void>
+13
src/lib/translation/utils.ts
···
··· 1 + import lande from 'lande' 2 + 3 + import {code3ToCode2Strict} from '#/locale/helpers' 4 + 5 + // TODO: Replace with expo-guess-language 6 + export function guessLanguage(text: string): string | null { 7 + const results = lande(text) 8 + // only return high-confidence results 9 + if (results[0] && results[0][1] > 0.97) { 10 + return code3ToCode2Strict(results[0][0]) ?? null 11 + } 12 + return null 13 + }
-1014
src/locale/languages.ts
··· 2 code3: string 3 code2: string 4 name: string 5 - android: boolean 6 - ios: boolean 7 } 8 9 export enum AppLanguage { ··· 103 ] 104 105 // Pre-generated list using Intl.DisplayNames to localize the language name. 106 - // https://developers.google.com/android/reference/com/google/mlkit/nl/translate/TranslateLanguage 107 - // https://developer.apple.com/documentation/foundation/nslocale/isolanguagecodes 108 export const LANGUAGES: Language[] = [ 109 { 110 code3: 'aar', 111 code2: 'aa', 112 name: 'Afar', 113 - android: false, 114 - ios: false, 115 }, 116 { 117 code3: 'abk', 118 code2: 'ab', 119 name: 'Abkhazian', 120 - android: false, 121 - ios: false, 122 }, 123 { 124 code3: 'ace', 125 code2: '', 126 name: 'Achinese', 127 - android: false, 128 - ios: false, 129 }, 130 { 131 code3: 'ach', 132 code2: '', 133 name: 'Acoli', 134 - android: false, 135 - ios: false, 136 }, 137 { 138 code3: 'ada', 139 code2: '', 140 name: 'Adangme', 141 - android: false, 142 - ios: false, 143 }, 144 { 145 code3: 'ady', 146 code2: '', 147 name: 'Adyghe; Adygei', 148 - android: false, 149 - ios: false, 150 }, 151 { 152 code3: 'afa', 153 code2: '', 154 name: 'Afro-Asiatic languages', 155 - android: false, 156 - ios: false, 157 }, 158 { 159 code3: 'afh', 160 code2: '', 161 name: 'Afrihili', 162 - android: false, 163 - ios: false, 164 }, 165 { 166 code3: 'afr', 167 code2: 'af', 168 name: 'Afrikaans', 169 - android: false, 170 - ios: false, 171 }, 172 { 173 code3: 'ain', 174 code2: '', 175 name: 'Ainu', 176 - android: false, 177 - ios: false, 178 }, 179 { 180 code3: 'aka', 181 code2: 'ak', 182 name: 'Akan', 183 - android: false, 184 - ios: false, 185 }, 186 { 187 code3: 'akk', 188 code2: '', 189 name: 'Akkadian', 190 - android: false, 191 - ios: false, 192 }, 193 { 194 code3: 'alb', 195 code2: 'sq', 196 name: 'Albanian', 197 - android: true, 198 - ios: false, 199 }, 200 { 201 code3: 'ale', 202 code2: '', 203 name: 'Aleut', 204 - android: false, 205 - ios: false, 206 }, 207 { 208 code3: 'alg', 209 code2: '', 210 name: 'Algonquian languages', 211 - android: false, 212 - ios: false, 213 }, 214 { 215 code3: 'alt', 216 code2: '', 217 name: 'Southern Altai', 218 - android: false, 219 - ios: false, 220 }, 221 { 222 code3: 'amh', 223 code2: 'am', 224 name: 'Amharic', 225 - android: false, 226 - ios: false, 227 }, 228 { 229 code3: 'ang', 230 code2: '', 231 name: 'English, Old (ca.450-1100)', 232 - android: false, 233 - ios: false, 234 }, 235 { 236 code3: 'anp', 237 code2: '', 238 name: 'Angika', 239 - android: false, 240 - ios: false, 241 }, 242 { 243 code3: 'apa', 244 code2: '', 245 name: 'Apache languages', 246 - android: false, 247 - ios: false, 248 }, 249 { 250 code3: 'ara', 251 code2: 'ar', 252 name: 'Arabic', 253 - android: true, 254 - ios: false, 255 }, 256 { 257 code3: 'arc', 258 code2: '', 259 name: 'Official Aramaic (700-300 BCE); Imperial Aramaic (700-300 BCE)', 260 - android: false, 261 - ios: false, 262 }, 263 { 264 code3: 'arg', 265 code2: 'an', 266 name: 'Aragonese', 267 - android: false, 268 - ios: false, 269 }, 270 { 271 code3: 'arm', 272 code2: 'hy', 273 name: 'Armenian', 274 - android: false, 275 - ios: false, 276 }, 277 { 278 code3: 'arn', 279 code2: '', 280 name: 'Mapudungun; Mapuche', 281 - android: false, 282 - ios: false, 283 }, 284 { 285 code3: 'arp', 286 code2: '', 287 name: 'Arapaho', 288 - android: false, 289 - ios: false, 290 }, 291 { 292 code3: 'art', 293 code2: '', 294 name: 'Artificial languages', 295 - android: false, 296 - ios: false, 297 }, 298 { 299 code3: 'arw', 300 code2: '', 301 name: 'Arawak', 302 - android: false, 303 - ios: false, 304 }, 305 { 306 code3: 'asm', 307 code2: 'as', 308 name: 'Assamese', 309 - android: false, 310 - ios: false, 311 }, 312 { 313 code3: 'ast', 314 code2: '', 315 name: 'Asturian', 316 - android: false, 317 - ios: false, 318 }, 319 { 320 code3: 'ath', 321 code2: '', 322 name: 'Athapascan languages', 323 - android: false, 324 - ios: false, 325 }, 326 { 327 code3: 'aus', 328 code2: '', 329 name: 'Australian languages', 330 - android: false, 331 - ios: false, 332 }, 333 { 334 code3: 'ava', 335 code2: 'av', 336 name: 'Avaric', 337 - android: false, 338 - ios: false, 339 }, 340 { 341 code3: 'ave', 342 code2: 'ae', 343 name: 'Avestan', 344 - android: false, 345 - ios: false, 346 }, 347 { 348 code3: 'awa', 349 code2: '', 350 name: 'Awadhi', 351 - android: false, 352 - ios: false, 353 }, 354 { 355 code3: 'aym', 356 code2: 'ay', 357 name: 'Aymara', 358 - android: false, 359 - ios: false, 360 }, 361 { 362 code3: 'aze', 363 code2: 'az', 364 name: 'Azerbaijani', 365 - android: false, 366 - ios: false, 367 }, 368 { 369 code3: 'bad', 370 code2: '', 371 name: 'Banda languages', 372 - android: false, 373 - ios: false, 374 }, 375 { 376 code3: 'bai', 377 code2: '', 378 name: 'Bamileke languages', 379 - android: false, 380 - ios: false, 381 }, 382 { 383 code3: 'bak', 384 code2: 'ba', 385 name: 'Bashkir', 386 - android: false, 387 - ios: false, 388 }, 389 { 390 code3: 'bal', 391 code2: '', 392 name: 'Baluchi', 393 - android: false, 394 - ios: false, 395 }, 396 { 397 code3: 'bam', 398 code2: 'bm', 399 name: 'Bambara', 400 - android: false, 401 - ios: false, 402 }, 403 { 404 code3: 'ban', 405 code2: '', 406 name: 'Balinese', 407 - android: false, 408 - ios: false, 409 }, 410 { 411 code3: 'baq', 412 code2: 'eu', 413 name: 'Basque', 414 - android: false, 415 - ios: false, 416 }, 417 { 418 code3: 'bas', 419 code2: '', 420 name: 'Basa', 421 - android: false, 422 - ios: false, 423 }, 424 { 425 code3: 'bat', 426 code2: '', 427 name: 'Baltic languages', 428 - android: false, 429 - ios: false, 430 }, 431 { 432 code3: 'bej', 433 code2: '', 434 name: 'Beja; Bedawiyet', 435 - android: false, 436 - ios: false, 437 }, 438 { 439 code3: 'bel', 440 code2: 'be', 441 name: 'Belarusian', 442 - android: true, 443 - ios: false, 444 }, 445 { 446 code3: 'bem', 447 code2: '', 448 name: 'Bemba', 449 - android: false, 450 - ios: false, 451 }, 452 { 453 code3: 'ben', 454 code2: 'bn', 455 name: 'Bangla', 456 - android: true, 457 - ios: false, 458 }, 459 { 460 code3: 'ber', 461 code2: '', 462 name: 'Berber languages', 463 - android: false, 464 - ios: false, 465 }, 466 { 467 code3: 'bho', 468 code2: '', 469 name: 'Bhojpuri', 470 - android: false, 471 - ios: false, 472 }, 473 { 474 code3: 'bih', 475 code2: 'bh', 476 name: 'Bhojpuri', 477 - android: false, 478 - ios: false, 479 }, 480 { 481 code3: 'bik', 482 code2: '', 483 name: 'Bikol', 484 - android: false, 485 - ios: false, 486 }, 487 { 488 code3: 'bin', 489 code2: '', 490 name: 'Bini; Edo', 491 - android: false, 492 - ios: false, 493 }, 494 { 495 code3: 'bis', 496 code2: 'bi', 497 name: 'Bislama', 498 - android: false, 499 - ios: false, 500 }, 501 { 502 code3: 'bla', 503 code2: '', 504 name: 'Siksika', 505 - android: false, 506 - ios: false, 507 }, 508 { 509 code3: 'bnt', 510 code2: '', 511 name: 'Bantu languages', 512 - android: false, 513 - ios: false, 514 }, 515 { 516 code3: 'bod', 517 code2: 'bo', 518 name: 'Tibetan', 519 - android: false, 520 - ios: false, 521 }, 522 { 523 code3: 'bos', 524 code2: 'bs', 525 name: 'Bosnian', 526 - android: false, 527 - ios: false, 528 }, 529 { 530 code3: 'bra', 531 code2: '', 532 name: 'Braj', 533 - android: false, 534 - ios: false, 535 }, 536 { 537 code3: 'bre', 538 code2: 'br', 539 name: 'Breton', 540 - android: false, 541 - ios: false, 542 }, 543 { 544 code3: 'btk', 545 code2: '', 546 name: 'Batak languages', 547 - android: false, 548 - ios: false, 549 }, 550 { 551 code3: 'bua', 552 code2: '', 553 name: 'Buriat', 554 - android: false, 555 - ios: false, 556 }, 557 { 558 code3: 'bug', 559 code2: '', 560 name: 'Buginese', 561 - android: false, 562 - ios: false, 563 }, 564 { 565 code3: 'bul', 566 code2: 'bg', 567 name: 'Bulgarian', 568 - android: true, 569 - ios: false, 570 }, 571 { 572 code3: 'bur', 573 code2: 'my', 574 name: 'Burmese', 575 - android: false, 576 - ios: false, 577 }, 578 { 579 code3: 'byn', 580 code2: '', 581 name: 'Blin; Bilin', 582 - android: false, 583 - ios: false, 584 }, 585 { 586 code3: 'cad', 587 code2: '', 588 name: 'Caddo', 589 - android: false, 590 - ios: false, 591 }, 592 { 593 code3: 'cai', 594 code2: '', 595 name: 'Central American Indian languages', 596 - android: false, 597 - ios: false, 598 }, 599 { 600 code3: 'car', 601 code2: '', 602 name: 'Galibi Carib', 603 - android: false, 604 - ios: false, 605 }, 606 { 607 code3: 'cat', 608 code2: 'ca', 609 name: 'Catalan', 610 - android: true, 611 - ios: false, 612 }, 613 { 614 code3: 'cau', 615 code2: '', 616 name: 'Caucasian languages', 617 - android: false, 618 - ios: false, 619 }, 620 { 621 code3: 'ceb', 622 code2: '', 623 name: 'Cebuano', 624 - android: false, 625 - ios: false, 626 }, 627 { 628 code3: 'cel', 629 code2: '', 630 name: 'Celtic languages', 631 - android: false, 632 - ios: false, 633 }, 634 { 635 code3: 'ces', 636 code2: 'cs', 637 name: 'Czech', 638 - android: true, 639 - ios: false, 640 }, 641 { 642 code3: 'cha', 643 code2: 'ch', 644 name: 'Chamorro', 645 - android: false, 646 - ios: false, 647 }, 648 { 649 code3: 'chb', 650 code2: '', 651 name: 'Chibcha', 652 - android: false, 653 - ios: false, 654 }, 655 { 656 code3: 'che', 657 code2: 'ce', 658 name: 'Chechen', 659 - android: false, 660 - ios: false, 661 }, 662 { 663 code3: 'chg', 664 code2: '', 665 name: 'Chagatai', 666 - android: false, 667 - ios: false, 668 }, 669 { 670 code3: 'chi', 671 code2: 'zh', 672 name: 'Chinese', 673 - android: true, 674 - ios: false, 675 }, 676 { 677 code3: 'chk', 678 code2: '', 679 name: 'Chuukese', 680 - android: false, 681 - ios: false, 682 }, 683 { 684 code3: 'chm', 685 code2: '', 686 name: 'Mari', 687 - android: false, 688 - ios: false, 689 }, 690 { 691 code3: 'chn', 692 code2: '', 693 name: 'Chinook jargon', 694 - android: false, 695 - ios: false, 696 }, 697 { 698 code3: 'cho', 699 code2: '', 700 name: 'Choctaw', 701 - android: false, 702 - ios: false, 703 }, 704 { 705 code3: 'chp', 706 code2: '', 707 name: 'Chipewyan; Dene Suline', 708 - android: false, 709 - ios: false, 710 }, 711 { 712 code3: 'chr', 713 code2: '', 714 name: 'Cherokee', 715 - android: false, 716 - ios: false, 717 }, 718 { 719 code3: 'chu', 720 code2: 'cu', 721 name: 'Church Slavic', 722 - android: false, 723 - ios: false, 724 }, 725 { 726 code3: 'chv', 727 code2: 'cv', 728 name: 'Chuvash', 729 - android: false, 730 - ios: false, 731 }, 732 { 733 code3: 'chy', 734 code2: '', 735 name: 'Cheyenne', 736 - android: false, 737 - ios: false, 738 }, 739 { 740 code3: 'cmc', 741 code2: '', 742 name: 'Chamic languages', 743 - android: false, 744 - ios: false, 745 }, 746 { 747 code3: 'cnr', 748 code2: '', 749 name: 'Serbian (Montenegro)', 750 - android: false, 751 - ios: false, 752 }, 753 { 754 code3: 'cop', 755 code2: '', 756 name: 'Coptic', 757 - android: false, 758 - ios: false, 759 }, 760 { 761 code3: 'cor', 762 code2: 'kw', 763 name: 'Cornish', 764 - android: false, 765 - ios: false, 766 }, 767 { 768 code3: 'cos', 769 code2: 'co', 770 name: 'Corsican', 771 - android: false, 772 - ios: false, 773 }, 774 { 775 code3: 'cpe', 776 code2: '', 777 name: 'Creoles and pidgins, English based', 778 - android: false, 779 - ios: false, 780 }, 781 { 782 code3: 'cpf', 783 code2: '', 784 name: 'Creoles and pidgins, French-based', 785 - android: false, 786 - ios: false, 787 }, 788 { 789 code3: 'cpp', 790 code2: '', 791 name: 'Creoles and pidgins, Portuguese-based', 792 - android: false, 793 - ios: false, 794 }, 795 { 796 code3: 'cre', 797 code2: 'cr', 798 name: 'Cree', 799 - android: false, 800 - ios: false, 801 }, 802 { 803 code3: 'crh', 804 code2: '', 805 name: 'Crimean Tatar; Crimean Turkish', 806 - android: false, 807 - ios: false, 808 }, 809 { 810 code3: 'crp', 811 code2: '', 812 name: 'Creoles and pidgins', 813 - android: false, 814 - ios: false, 815 }, 816 { 817 code3: 'csb', 818 code2: '', 819 name: 'Kashubian', 820 - android: false, 821 - ios: false, 822 }, 823 { 824 code3: 'cus', 825 code2: '', 826 name: 'Cushitic languages', 827 - android: false, 828 - ios: false, 829 }, 830 { 831 code3: 'cym', 832 code2: 'cy', 833 name: 'Welsh', 834 - android: true, 835 - ios: false, 836 }, 837 { 838 code3: 'cze', 839 code2: 'cs', 840 name: 'Czech', 841 - android: true, 842 - ios: false, 843 }, 844 { 845 code3: 'dak', 846 code2: '', 847 name: 'Dakota', 848 - android: false, 849 - ios: false, 850 }, 851 { 852 code3: 'dan', 853 code2: 'da', 854 name: 'Danish', 855 - android: true, 856 - ios: false, 857 }, 858 { 859 code3: 'dar', 860 code2: '', 861 name: 'Dargwa', 862 - android: false, 863 - ios: false, 864 }, 865 { 866 code3: 'day', 867 code2: '', 868 name: 'Land Dayak languages', 869 - android: false, 870 - ios: false, 871 }, 872 { 873 code3: 'del', 874 code2: '', 875 name: 'Delaware', 876 - android: false, 877 - ios: false, 878 }, 879 { 880 code3: 'den', 881 code2: '', 882 name: 'Slave (Athapascan)', 883 - android: false, 884 - ios: false, 885 }, 886 { 887 code3: 'deu', 888 code2: 'de', 889 name: 'German', 890 - android: true, 891 - ios: true, 892 }, 893 { 894 code3: 'dgr', 895 code2: '', 896 name: 'Dogrib', 897 - android: false, 898 - ios: false, 899 }, 900 { 901 code3: 'din', 902 code2: '', 903 name: 'Dinka', 904 - android: false, 905 - ios: false, 906 }, 907 { 908 code3: 'div', 909 code2: 'dv', 910 name: 'Divehi', 911 - android: false, 912 - ios: false, 913 }, 914 { 915 code3: 'doi', 916 code2: '', 917 name: 'Dogri', 918 - android: false, 919 - ios: false, 920 }, 921 { 922 code3: 'dra', 923 code2: '', 924 name: 'Dravidian languages', 925 - android: false, 926 - ios: false, 927 }, 928 { 929 code3: 'dsb', 930 code2: '', 931 name: 'Lower Sorbian', 932 - android: false, 933 - ios: false, 934 }, 935 { 936 code3: 'dua', 937 code2: '', 938 name: 'Duala', 939 - android: false, 940 - ios: false, 941 }, 942 { 943 code3: 'dum', 944 code2: '', 945 name: 'Dutch, Middle (ca.1050-1350)', 946 - android: false, 947 - ios: false, 948 }, 949 { 950 code3: 'dut', 951 code2: 'nl', 952 name: 'Dutch', 953 - android: true, 954 - ios: true, 955 }, 956 { 957 code3: 'dyu', 958 code2: '', 959 name: 'Dyula', 960 - android: false, 961 - ios: false, 962 }, 963 { 964 code3: 'dzo', 965 code2: 'dz', 966 name: 'Dzongkha', 967 - android: false, 968 - ios: false, 969 }, 970 { 971 code3: 'efi', 972 code2: '', 973 name: 'Efik', 974 - android: false, 975 - ios: false, 976 }, 977 { 978 code3: 'egy', 979 code2: '', 980 name: 'Egyptian (Ancient)', 981 - android: false, 982 - ios: false, 983 }, 984 { 985 code3: 'eka', 986 code2: '', 987 name: 'Ekajuk', 988 - android: false, 989 - ios: false, 990 }, 991 { 992 code3: 'ell', 993 code2: 'el', 994 name: 'Greek', 995 - android: true, 996 - ios: false, 997 }, 998 { 999 code3: 'elx', 1000 code2: '', 1001 name: 'Elamite', 1002 - android: false, 1003 - ios: false, 1004 }, 1005 { 1006 code3: 'eng', 1007 code2: 'en', 1008 name: 'English', 1009 - android: true, 1010 - ios: true, 1011 }, 1012 { 1013 code3: 'enm', 1014 code2: '', 1015 name: 'English, Middle (1100-1500)', 1016 - android: false, 1017 - ios: false, 1018 }, 1019 { 1020 code3: 'epo', 1021 code2: 'eo', 1022 name: 'Esperanto', 1023 - android: true, 1024 - ios: false, 1025 }, 1026 { 1027 code3: 'est', 1028 code2: 'et', 1029 name: 'Estonian', 1030 - android: true, 1031 - ios: false, 1032 }, 1033 { 1034 code3: 'eus', 1035 code2: 'eu', 1036 name: 'Basque', 1037 - android: false, 1038 - ios: false, 1039 }, 1040 { 1041 code3: 'ewe', 1042 code2: 'ee', 1043 name: 'Ewe', 1044 - android: false, 1045 - ios: false, 1046 }, 1047 { 1048 code3: 'ewo', 1049 code2: '', 1050 name: 'Ewondo', 1051 - android: false, 1052 - ios: false, 1053 }, 1054 { 1055 code3: 'fan', 1056 code2: '', 1057 name: 'Fang', 1058 - android: false, 1059 - ios: false, 1060 }, 1061 { 1062 code3: 'fao', 1063 code2: 'fo', 1064 name: 'Faroese', 1065 - android: false, 1066 - ios: false, 1067 }, 1068 { 1069 code3: 'fas', 1070 code2: 'fa', 1071 name: 'Persian', 1072 - android: true, 1073 - ios: false, 1074 }, 1075 { 1076 code3: 'fat', 1077 code2: '', 1078 name: 'Akan', 1079 - android: false, 1080 - ios: false, 1081 }, 1082 { 1083 code3: 'fij', 1084 code2: 'fj', 1085 name: 'Fijian', 1086 - android: false, 1087 - ios: false, 1088 }, 1089 { 1090 code3: 'fil', 1091 code2: '', 1092 name: 'Filipino', 1093 - android: false, 1094 - ios: false, 1095 }, 1096 { 1097 code3: 'fin', 1098 code2: 'fi', 1099 name: 'Finnish', 1100 - android: true, 1101 - ios: false, 1102 }, 1103 { 1104 code3: 'fiu', 1105 code2: '', 1106 name: 'Finno-Ugrian languages', 1107 - android: false, 1108 - ios: false, 1109 }, 1110 { 1111 code3: 'fon', 1112 code2: '', 1113 name: 'Fon', 1114 - android: false, 1115 - ios: false, 1116 }, 1117 { 1118 code3: 'fra', 1119 code2: 'fr', 1120 name: 'French', 1121 - android: true, 1122 - ios: true, 1123 }, 1124 { 1125 code3: 'fre', 1126 code2: 'fr', 1127 name: 'French', 1128 - android: true, 1129 - ios: true, 1130 }, 1131 { 1132 code3: 'frm', 1133 code2: '', 1134 name: 'French, Middle (ca.1400-1600)', 1135 - android: false, 1136 - ios: false, 1137 }, 1138 { 1139 code3: 'fro', 1140 code2: '', 1141 name: 'French, Old (842-ca.1400)', 1142 - android: false, 1143 - ios: false, 1144 }, 1145 { 1146 code3: 'frr', 1147 code2: '', 1148 name: 'Northern Frisian', 1149 - android: false, 1150 - ios: false, 1151 }, 1152 { 1153 code3: 'frs', 1154 code2: '', 1155 name: 'Eastern Frisian', 1156 - android: false, 1157 - ios: false, 1158 }, 1159 { 1160 code3: 'fry', 1161 code2: 'fy', 1162 name: 'Western Frisian', 1163 - android: false, 1164 - ios: false, 1165 }, 1166 { 1167 code3: 'ful', 1168 code2: 'ff', 1169 name: 'Fulah', 1170 - android: false, 1171 - ios: false, 1172 }, 1173 { 1174 code3: 'fur', 1175 code2: '', 1176 name: 'Friulian', 1177 - android: false, 1178 - ios: false, 1179 }, 1180 { 1181 code3: 'gaa', 1182 code2: '', 1183 name: 'Ga', 1184 - android: false, 1185 - ios: false, 1186 }, 1187 { 1188 code3: 'gay', 1189 code2: '', 1190 name: 'Gayo', 1191 - android: false, 1192 - ios: false, 1193 }, 1194 { 1195 code3: 'gba', 1196 code2: '', 1197 name: 'Gbaya', 1198 - android: false, 1199 - ios: false, 1200 }, 1201 { 1202 code3: 'gem', 1203 code2: '', 1204 name: 'Germanic languages', 1205 - android: false, 1206 - ios: false, 1207 }, 1208 { 1209 code3: 'geo', 1210 code2: 'ka', 1211 name: 'Georgian', 1212 - android: true, 1213 - ios: false, 1214 }, 1215 { 1216 code3: 'ger', 1217 code2: 'de', 1218 name: 'German', 1219 - android: true, 1220 - ios: true, 1221 }, 1222 { 1223 code3: 'gez', 1224 code2: '', 1225 name: 'Geez', 1226 - android: false, 1227 - ios: false, 1228 }, 1229 { 1230 code3: 'gil', 1231 code2: '', 1232 name: 'Gilbertese', 1233 - android: false, 1234 - ios: false, 1235 }, 1236 { 1237 code3: 'gla', 1238 code2: 'gd', 1239 name: 'Scottish Gaelic', 1240 - android: false, 1241 - ios: false, 1242 }, 1243 { 1244 code3: 'gle', 1245 code2: 'ga', 1246 name: 'Irish', 1247 - android: true, 1248 - ios: false, 1249 }, 1250 { 1251 code3: 'glg', 1252 code2: 'gl', 1253 name: 'Galician', 1254 - android: true, 1255 - ios: false, 1256 }, 1257 { 1258 code3: 'glv', 1259 code2: 'gv', 1260 name: 'Manx', 1261 - android: false, 1262 - ios: false, 1263 }, 1264 { 1265 code3: 'gmh', 1266 code2: '', 1267 name: 'German, Middle High (ca.1050-1500)', 1268 - android: false, 1269 - ios: false, 1270 }, 1271 { 1272 code3: 'goh', 1273 code2: '', 1274 name: 'German, Old High (ca.750-1050)', 1275 - android: false, 1276 - ios: false, 1277 }, 1278 { 1279 code3: 'gon', 1280 code2: '', 1281 name: 'Gondi', 1282 - android: false, 1283 - ios: false, 1284 }, 1285 { 1286 code3: 'gor', 1287 code2: '', 1288 name: 'Gorontalo', 1289 - android: false, 1290 - ios: false, 1291 }, 1292 { 1293 code3: 'got', 1294 code2: '', 1295 name: 'Gothic', 1296 - android: false, 1297 - ios: false, 1298 }, 1299 { 1300 code3: 'grb', 1301 code2: '', 1302 name: 'Grebo', 1303 - android: false, 1304 - ios: false, 1305 }, 1306 { 1307 code3: 'grc', 1308 code2: '', 1309 name: 'Ancient Greek', 1310 - android: false, 1311 - ios: false, 1312 }, 1313 { 1314 code3: 'gre', 1315 code2: 'el', 1316 name: 'Greek', 1317 - android: true, 1318 - ios: false, 1319 }, 1320 { 1321 code3: 'grn', 1322 code2: 'gn', 1323 name: 'Guarani', 1324 - android: false, 1325 - ios: false, 1326 }, 1327 { 1328 code3: 'gsw', 1329 code2: '', 1330 name: 'Swiss German; Alemannic; Alsatian', 1331 - android: false, 1332 - ios: false, 1333 }, 1334 { 1335 code3: 'guj', 1336 code2: 'gu', 1337 name: 'Gujarati', 1338 - android: true, 1339 - ios: false, 1340 }, 1341 { 1342 code3: 'gwi', 1343 code2: '', 1344 name: "Gwich'in", 1345 - android: false, 1346 - ios: false, 1347 }, 1348 { 1349 code3: 'hai', 1350 code2: '', 1351 name: 'Haida', 1352 - android: false, 1353 - ios: false, 1354 }, 1355 { 1356 code3: 'hat', 1357 code2: 'ht', 1358 name: 'Haitian Creole', 1359 - android: true, 1360 - ios: false, 1361 }, 1362 { 1363 code3: 'hau', 1364 code2: 'ha', 1365 name: 'Hausa', 1366 - android: false, 1367 - ios: false, 1368 }, 1369 { 1370 code3: 'haw', 1371 code2: '', 1372 name: 'Hawaiian', 1373 - android: false, 1374 - ios: false, 1375 }, 1376 { 1377 code3: 'heb', 1378 code2: 'he', 1379 name: 'Hebrew', 1380 - android: true, 1381 - ios: false, 1382 }, 1383 { 1384 code3: 'her', 1385 code2: 'hz', 1386 name: 'Herero', 1387 - android: false, 1388 - ios: false, 1389 }, 1390 { 1391 code3: 'hil', 1392 code2: '', 1393 name: 'Hiligaynon', 1394 - android: false, 1395 - ios: false, 1396 }, 1397 { 1398 code3: 'him', 1399 code2: '', 1400 name: 'Himachali languages; Western Pahari languages', 1401 - android: false, 1402 - ios: false, 1403 }, 1404 { 1405 code3: 'hin', 1406 code2: 'hi', 1407 name: 'Hindi', 1408 - android: true, 1409 - ios: true, 1410 }, 1411 { 1412 code3: 'hit', 1413 code2: '', 1414 name: 'Hittite', 1415 - android: false, 1416 - ios: false, 1417 }, 1418 { 1419 code3: 'hmn', 1420 code2: '', 1421 name: 'Hmong', 1422 - android: false, 1423 - ios: false, 1424 }, 1425 { 1426 code3: 'hmo', 1427 code2: 'ho', 1428 name: 'Hiri Motu', 1429 - android: false, 1430 - ios: false, 1431 }, 1432 { 1433 code3: 'hrv', 1434 code2: 'hr', 1435 name: 'Croatian', 1436 - android: true, 1437 - ios: false, 1438 }, 1439 { 1440 code3: 'hsb', 1441 code2: '', 1442 name: 'Upper Sorbian', 1443 - android: false, 1444 - ios: false, 1445 }, 1446 { 1447 code3: 'hun', 1448 code2: 'hu', 1449 name: 'Hungarian', 1450 - android: true, 1451 - ios: false, 1452 }, 1453 { 1454 code3: 'hup', 1455 code2: '', 1456 name: 'Hupa', 1457 - android: false, 1458 - ios: false, 1459 }, 1460 { 1461 code3: 'hye', 1462 code2: 'hy', 1463 name: 'Armenian', 1464 - android: false, 1465 - ios: false, 1466 }, 1467 { 1468 code3: 'iba', 1469 code2: '', 1470 name: 'Iban', 1471 - android: false, 1472 - ios: false, 1473 }, 1474 { 1475 code3: 'ibo', 1476 code2: 'ig', 1477 name: 'Igbo', 1478 - android: false, 1479 - ios: false, 1480 }, 1481 { 1482 code3: 'ice', 1483 code2: 'is', 1484 name: 'Icelandic', 1485 - android: true, 1486 - ios: false, 1487 }, 1488 { 1489 code3: 'ido', 1490 code2: 'io', 1491 name: 'Ido', 1492 - android: false, 1493 - ios: false, 1494 }, 1495 { 1496 code3: 'iii', 1497 code2: 'ii', 1498 name: 'Sichuan Yi; Nuosu', 1499 - android: false, 1500 - ios: false, 1501 }, 1502 { 1503 code3: 'ijo', 1504 code2: '', 1505 name: 'Ijo languages', 1506 - android: false, 1507 - ios: false, 1508 }, 1509 { 1510 code3: 'iku', 1511 code2: 'iu', 1512 name: 'Inuktitut', 1513 - android: false, 1514 - ios: false, 1515 }, 1516 { 1517 code3: 'ile', 1518 code2: 'ie', 1519 name: 'Interlingue', 1520 - android: false, 1521 - ios: false, 1522 }, 1523 { 1524 code3: 'ilo', 1525 code2: '', 1526 name: 'Iloko', 1527 - android: false, 1528 - ios: false, 1529 }, 1530 { 1531 code3: 'ina', 1532 code2: 'ia', 1533 name: 'Interlingua', 1534 - android: false, 1535 - ios: false, 1536 }, 1537 { 1538 code3: 'inc', 1539 code2: '', 1540 name: 'Indic languages', 1541 - android: false, 1542 - ios: false, 1543 }, 1544 { 1545 code3: 'ind', 1546 code2: 'id', 1547 name: 'Indonesian', 1548 - android: true, 1549 - ios: false, 1550 }, 1551 { 1552 code3: 'ine', 1553 code2: '', 1554 name: 'Indo-European languages', 1555 - android: false, 1556 - ios: false, 1557 }, 1558 { 1559 code3: 'inh', 1560 code2: '', 1561 name: 'Ingush', 1562 - android: false, 1563 - ios: false, 1564 }, 1565 { 1566 code3: 'ipk', 1567 code2: 'ik', 1568 name: 'Inupiaq', 1569 - android: false, 1570 - ios: false, 1571 }, 1572 { 1573 code3: 'ira', 1574 code2: '', 1575 name: 'Iranian languages', 1576 - android: false, 1577 - ios: false, 1578 }, 1579 { 1580 code3: 'iro', 1581 code2: '', 1582 name: 'Iroquoian languages', 1583 - android: false, 1584 - ios: false, 1585 }, 1586 { 1587 code3: 'isl', 1588 code2: 'is', 1589 name: 'Icelandic', 1590 - android: true, 1591 - ios: false, 1592 }, 1593 { 1594 code3: 'ita', 1595 code2: 'it', 1596 name: 'Italian', 1597 - android: true, 1598 - ios: true, 1599 }, 1600 { 1601 code3: 'jav', 1602 code2: 'jv', 1603 name: 'Javanese', 1604 - android: false, 1605 - ios: false, 1606 }, 1607 { 1608 code3: 'jbo', 1609 code2: '', 1610 name: 'Lojban', 1611 - android: false, 1612 - ios: false, 1613 }, 1614 { 1615 code3: 'jpn', 1616 code2: 'ja', 1617 name: 'Japanese', 1618 - android: true, 1619 - ios: true, 1620 }, 1621 { 1622 code3: 'jpr', 1623 code2: '', 1624 name: 'Judeo-Persian', 1625 - android: false, 1626 - ios: false, 1627 }, 1628 { 1629 code3: 'jrb', 1630 code2: '', 1631 name: 'Judeo-Arabic', 1632 - android: false, 1633 - ios: false, 1634 }, 1635 { 1636 code3: 'kaa', 1637 code2: '', 1638 name: 'Kara-Kalpak', 1639 - android: false, 1640 - ios: false, 1641 }, 1642 { 1643 code3: 'kab', 1644 code2: '', 1645 name: 'Kabyle', 1646 - android: false, 1647 - ios: false, 1648 }, 1649 { 1650 code3: 'kac', 1651 code2: '', 1652 name: 'Kachin; Jingpho', 1653 - android: false, 1654 - ios: false, 1655 }, 1656 { 1657 code3: 'kal', 1658 code2: 'kl', 1659 name: 'Kalaallisut', 1660 - android: false, 1661 - ios: false, 1662 }, 1663 { 1664 code3: 'kam', 1665 code2: '', 1666 name: 'Kamba', 1667 - android: false, 1668 - ios: false, 1669 }, 1670 { 1671 code3: 'kan', 1672 code2: 'kn', 1673 name: 'Kannada', 1674 - android: true, 1675 - ios: false, 1676 }, 1677 { 1678 code3: 'kar', 1679 code2: '', 1680 name: 'Karen languages', 1681 - android: false, 1682 - ios: false, 1683 }, 1684 { 1685 code3: 'kas', 1686 code2: 'ks', 1687 name: 'Kashmiri', 1688 - android: false, 1689 - ios: false, 1690 }, 1691 { 1692 code3: 'kat', 1693 code2: 'ka', 1694 name: 'Georgian', 1695 - android: true, 1696 - ios: false, 1697 }, 1698 { 1699 code3: 'kau', 1700 code2: 'kr', 1701 name: 'Kanuri', 1702 - android: false, 1703 - ios: false, 1704 }, 1705 { 1706 code3: 'kaw', 1707 code2: '', 1708 name: 'Kawi', 1709 - android: false, 1710 - ios: false, 1711 }, 1712 { 1713 code3: 'kaz', 1714 code2: 'kk', 1715 name: 'Kazakh', 1716 - android: false, 1717 - ios: false, 1718 }, 1719 { 1720 code3: 'kbd', 1721 code2: '', 1722 name: 'Kabardian', 1723 - android: false, 1724 - ios: false, 1725 }, 1726 { 1727 code3: 'kha', 1728 code2: '', 1729 name: 'Khasi', 1730 - android: false, 1731 - ios: false, 1732 }, 1733 { 1734 code3: 'khi', 1735 code2: '', 1736 name: 'Khoisan languages', 1737 - android: false, 1738 - ios: false, 1739 }, 1740 { 1741 code3: 'khm', 1742 code2: 'km', 1743 name: 'Khmer', 1744 - android: false, 1745 - ios: false, 1746 }, 1747 { 1748 code3: 'kho', 1749 code2: '', 1750 name: 'Khotanese; Sakan', 1751 - android: false, 1752 - ios: false, 1753 }, 1754 { 1755 code3: 'kik', 1756 code2: 'ki', 1757 name: 'Kikuyu; Gikuyu', 1758 - android: false, 1759 - ios: false, 1760 }, 1761 { 1762 code3: 'kin', 1763 code2: 'rw', 1764 name: 'Kinyarwanda', 1765 - android: false, 1766 - ios: false, 1767 }, 1768 { 1769 code3: 'kir', 1770 code2: 'ky', 1771 name: 'Kyrgyz', 1772 - android: false, 1773 - ios: false, 1774 }, 1775 { 1776 code3: 'kmb', 1777 code2: '', 1778 name: 'Kimbundu', 1779 - android: false, 1780 - ios: false, 1781 }, 1782 { 1783 code3: 'kok', 1784 code2: '', 1785 name: 'Konkani', 1786 - android: false, 1787 - ios: false, 1788 }, 1789 { 1790 code3: 'kom', 1791 code2: 'kv', 1792 name: 'Komi', 1793 - android: false, 1794 - ios: false, 1795 }, 1796 { 1797 code3: 'kon', 1798 code2: 'kg', 1799 name: 'Kongo', 1800 - android: false, 1801 - ios: false, 1802 }, 1803 { 1804 code3: 'kor', 1805 code2: 'ko', 1806 name: 'Korean', 1807 - android: true, 1808 - ios: true, 1809 }, 1810 { 1811 code3: 'kos', 1812 code2: '', 1813 name: 'Kosraean', 1814 - android: false, 1815 - ios: false, 1816 }, 1817 { 1818 code3: 'kpe', 1819 code2: '', 1820 name: 'Kpelle', 1821 - android: false, 1822 - ios: false, 1823 }, 1824 { 1825 code3: 'krc', 1826 code2: '', 1827 name: 'Karachay-Balkar', 1828 - android: false, 1829 - ios: false, 1830 }, 1831 { 1832 code3: 'krl', 1833 code2: '', 1834 name: 'Karelian', 1835 - android: false, 1836 - ios: false, 1837 }, 1838 { 1839 code3: 'kro', 1840 code2: '', 1841 name: 'Kru languages', 1842 - android: false, 1843 - ios: false, 1844 }, 1845 { 1846 code3: 'kru', 1847 code2: '', 1848 name: 'Kurukh', 1849 - android: false, 1850 - ios: false, 1851 }, 1852 { 1853 code3: 'kua', 1854 code2: 'kj', 1855 name: 'Kuanyama; Kwanyama', 1856 - android: false, 1857 - ios: false, 1858 }, 1859 { 1860 code3: 'kum', 1861 code2: '', 1862 name: 'Kumyk', 1863 - android: false, 1864 - ios: false, 1865 }, 1866 { 1867 code3: 'kur', 1868 code2: 'ku', 1869 name: 'Kurdish', 1870 - android: false, 1871 - ios: false, 1872 }, 1873 { 1874 code3: 'kut', 1875 code2: '', 1876 name: 'Kutenai', 1877 - android: false, 1878 - ios: false, 1879 }, 1880 { 1881 code3: 'lad', 1882 code2: '', 1883 name: 'Ladino', 1884 - android: false, 1885 - ios: false, 1886 }, 1887 { 1888 code3: 'lah', 1889 code2: '', 1890 name: 'Lahnda', 1891 - android: false, 1892 - ios: false, 1893 }, 1894 { 1895 code3: 'lam', 1896 code2: '', 1897 name: 'Lamba', 1898 - android: false, 1899 - ios: false, 1900 }, 1901 { 1902 code3: 'lao', 1903 code2: 'lo', 1904 name: 'Lao', 1905 - android: false, 1906 - ios: false, 1907 }, 1908 { 1909 code3: 'lat', 1910 code2: 'la', 1911 name: 'Latin', 1912 - android: false, 1913 - ios: false, 1914 }, 1915 { 1916 code3: 'lav', 1917 code2: 'lv', 1918 name: 'Latvian', 1919 - android: true, 1920 - ios: false, 1921 }, 1922 { 1923 code3: 'lez', 1924 code2: '', 1925 name: 'Lezghian', 1926 - android: false, 1927 - ios: false, 1928 }, 1929 { 1930 code3: 'lim', 1931 code2: 'li', 1932 name: 'Limburgish', 1933 - android: false, 1934 - ios: false, 1935 }, 1936 { 1937 code3: 'lin', 1938 code2: 'ln', 1939 name: 'Lingala', 1940 - android: false, 1941 - ios: false, 1942 }, 1943 { 1944 code3: 'lit', 1945 code2: 'lt', 1946 name: 'Lithuanian', 1947 - android: true, 1948 - ios: false, 1949 }, 1950 { 1951 code3: 'lol', 1952 code2: '', 1953 name: 'Mongo', 1954 - android: false, 1955 - ios: false, 1956 }, 1957 { 1958 code3: 'loz', 1959 code2: '', 1960 name: 'Lozi', 1961 - android: false, 1962 - ios: false, 1963 }, 1964 { 1965 code3: 'ltz', 1966 code2: 'lb', 1967 name: 'Luxembourgish', 1968 - android: false, 1969 - ios: false, 1970 }, 1971 { 1972 code3: 'lua', 1973 code2: '', 1974 name: 'Luba-Lulua', 1975 - android: false, 1976 - ios: false, 1977 }, 1978 { 1979 code3: 'lub', 1980 code2: 'lu', 1981 name: 'Luba-Katanga', 1982 - android: false, 1983 - ios: false, 1984 }, 1985 { 1986 code3: 'lug', 1987 code2: 'lg', 1988 name: 'Ganda', 1989 - android: false, 1990 - ios: false, 1991 }, 1992 { 1993 code3: 'lui', 1994 code2: '', 1995 name: 'Luiseno', 1996 - android: false, 1997 - ios: false, 1998 }, 1999 { 2000 code3: 'lun', 2001 code2: '', 2002 name: 'Lunda', 2003 - android: false, 2004 - ios: false, 2005 }, 2006 { 2007 code3: 'luo', 2008 code2: '', 2009 name: 'Luo (Kenya and Tanzania)', 2010 - android: false, 2011 - ios: false, 2012 }, 2013 { 2014 code3: 'lus', 2015 code2: '', 2016 name: 'Mizo', 2017 - android: false, 2018 - ios: false, 2019 }, 2020 { 2021 code3: 'mac', 2022 code2: 'mk', 2023 name: 'Macedonian', 2024 - android: true, 2025 - ios: false, 2026 }, 2027 { 2028 code3: 'mad', 2029 code2: '', 2030 name: 'Madurese', 2031 - android: false, 2032 - ios: false, 2033 }, 2034 { 2035 code3: 'mag', 2036 code2: '', 2037 name: 'Magahi', 2038 - android: false, 2039 - ios: false, 2040 }, 2041 { 2042 code3: 'mah', 2043 code2: 'mh', 2044 name: 'Marshallese', 2045 - android: false, 2046 - ios: false, 2047 }, 2048 { 2049 code3: 'mai', 2050 code2: '', 2051 name: 'Maithili', 2052 - android: false, 2053 - ios: false, 2054 }, 2055 { 2056 code3: 'mak', 2057 code2: '', 2058 name: 'Makasar', 2059 - android: false, 2060 - ios: false, 2061 }, 2062 { 2063 code3: 'mal', 2064 code2: 'ml', 2065 name: 'Malayalam', 2066 - android: false, 2067 - ios: false, 2068 }, 2069 { 2070 code3: 'man', 2071 code2: '', 2072 name: 'Mandingo', 2073 - android: false, 2074 - ios: false, 2075 }, 2076 { 2077 code3: 'mao', 2078 code2: 'mi', 2079 name: 'Māori', 2080 - android: false, 2081 - ios: false, 2082 }, 2083 { 2084 code3: 'map', 2085 code2: '', 2086 name: 'Austronesian languages', 2087 - android: false, 2088 - ios: false, 2089 }, 2090 { 2091 code3: 'mar', 2092 code2: 'mr', 2093 name: 'Marathi', 2094 - android: true, 2095 - ios: false, 2096 }, 2097 { 2098 code3: 'mas', 2099 code2: '', 2100 name: 'Masai', 2101 - android: false, 2102 - ios: false, 2103 }, 2104 { 2105 code3: 'may', 2106 code2: 'ms', 2107 name: 'Malay', 2108 - android: true, 2109 - ios: false, 2110 }, 2111 { 2112 code3: 'mdf', 2113 code2: '', 2114 name: 'Moksha', 2115 - android: false, 2116 - ios: false, 2117 }, 2118 { 2119 code3: 'mdr', 2120 code2: '', 2121 name: 'Mandar', 2122 - android: false, 2123 - ios: false, 2124 }, 2125 { 2126 code3: 'men', 2127 code2: '', 2128 name: 'Mende', 2129 - android: false, 2130 - ios: false, 2131 }, 2132 { 2133 code3: 'mga', 2134 code2: '', 2135 name: 'Irish, Middle (900-1200)', 2136 - android: false, 2137 - ios: false, 2138 }, 2139 { 2140 code3: 'mic', 2141 code2: '', 2142 name: "Mi'kmaq; Micmac", 2143 - android: false, 2144 - ios: false, 2145 }, 2146 { 2147 code3: 'min', 2148 code2: '', 2149 name: 'Minangkabau', 2150 - android: false, 2151 - ios: false, 2152 }, 2153 { 2154 code3: 'mis', 2155 code2: '', 2156 name: 'Uncoded languages', 2157 - android: false, 2158 - ios: false, 2159 }, 2160 { 2161 code3: 'mkd', 2162 code2: 'mk', 2163 name: 'Macedonian', 2164 - android: true, 2165 - ios: false, 2166 }, 2167 { 2168 code3: 'mkh', 2169 code2: '', 2170 name: 'Mon-Khmer languages', 2171 - android: false, 2172 - ios: false, 2173 }, 2174 { 2175 code3: 'mlg', 2176 code2: 'mg', 2177 name: 'Malagasy', 2178 - android: false, 2179 - ios: false, 2180 }, 2181 { 2182 code3: 'mlt', 2183 code2: 'mt', 2184 name: 'Maltese', 2185 - android: true, 2186 - ios: false, 2187 }, 2188 { 2189 code3: 'mnc', 2190 code2: '', 2191 name: 'Manchu', 2192 - android: false, 2193 - ios: false, 2194 }, 2195 { 2196 code3: 'mni', 2197 code2: '', 2198 name: 'Manipuri', 2199 - android: false, 2200 - ios: false, 2201 }, 2202 { 2203 code3: 'mno', 2204 code2: '', 2205 name: 'Manobo languages', 2206 - android: false, 2207 - ios: false, 2208 }, 2209 { 2210 code3: 'moh', 2211 code2: '', 2212 name: 'Mohawk', 2213 - android: false, 2214 - ios: false, 2215 }, 2216 { 2217 code3: 'mon', 2218 code2: 'mn', 2219 name: 'Mongolian', 2220 - android: false, 2221 - ios: false, 2222 }, 2223 { 2224 code3: 'mos', 2225 code2: '', 2226 name: 'Mossi', 2227 - android: false, 2228 - ios: false, 2229 }, 2230 { 2231 code3: 'mri', 2232 code2: 'mi', 2233 name: 'Māori', 2234 - android: false, 2235 - ios: false, 2236 }, 2237 { 2238 code3: 'msa', 2239 code2: 'ms', 2240 name: 'Malay', 2241 - android: true, 2242 - ios: false, 2243 }, 2244 { 2245 code3: 'mul', 2246 code2: '', 2247 name: 'Multiple languages', 2248 - android: false, 2249 - ios: false, 2250 }, 2251 { 2252 code3: 'mun', 2253 code2: '', 2254 name: 'Munda languages', 2255 - android: false, 2256 - ios: false, 2257 }, 2258 { 2259 code3: 'mus', 2260 code2: '', 2261 name: 'Creek', 2262 - android: false, 2263 - ios: false, 2264 }, 2265 { 2266 code3: 'mwl', 2267 code2: '', 2268 name: 'Mirandese', 2269 - android: false, 2270 - ios: false, 2271 }, 2272 { 2273 code3: 'mwr', 2274 code2: '', 2275 name: 'Marwari', 2276 - android: false, 2277 - ios: false, 2278 }, 2279 { 2280 code3: 'mya', 2281 code2: 'my', 2282 name: 'Burmese', 2283 - android: false, 2284 - ios: false, 2285 }, 2286 { 2287 code3: 'myn', 2288 code2: '', 2289 name: 'Mayan languages', 2290 - android: false, 2291 - ios: false, 2292 }, 2293 { 2294 code3: 'myv', 2295 code2: '', 2296 name: 'Erzya', 2297 - android: false, 2298 - ios: false, 2299 }, 2300 { 2301 code3: 'nah', 2302 code2: '', 2303 name: 'Nahuatl languages', 2304 - android: false, 2305 - ios: false, 2306 }, 2307 { 2308 code3: 'nai', 2309 code2: '', 2310 name: 'North American Indian languages', 2311 - android: false, 2312 - ios: false, 2313 }, 2314 { 2315 code3: 'nap', 2316 code2: '', 2317 name: 'Neapolitan', 2318 - android: false, 2319 - ios: false, 2320 }, 2321 { 2322 code3: 'nau', 2323 code2: 'na', 2324 name: 'Nauru', 2325 - android: false, 2326 - ios: false, 2327 }, 2328 { 2329 code3: 'nav', 2330 code2: 'nv', 2331 name: 'Navajo', 2332 - android: false, 2333 - ios: false, 2334 }, 2335 { 2336 code3: 'nbl', 2337 code2: 'nr', 2338 name: 'South Ndebele', 2339 - android: false, 2340 - ios: false, 2341 }, 2342 { 2343 code3: 'nde', 2344 code2: 'nd', 2345 name: 'North Ndebele', 2346 - android: false, 2347 - ios: false, 2348 }, 2349 { 2350 code3: 'ndo', 2351 code2: 'ng', 2352 name: 'Ndonga', 2353 - android: false, 2354 - ios: false, 2355 }, 2356 { 2357 code3: 'nds', 2358 code2: '', 2359 name: 'Low German; Low Saxon; German, Low; Saxon, Low', 2360 - android: false, 2361 - ios: false, 2362 }, 2363 { 2364 code3: 'nep', 2365 code2: 'ne', 2366 name: 'Nepali', 2367 - android: false, 2368 - ios: false, 2369 }, 2370 { 2371 code3: 'new', 2372 code2: '', 2373 name: 'Nepal Bhasa; Newari', 2374 - android: false, 2375 - ios: false, 2376 }, 2377 { 2378 code3: 'nia', 2379 code2: '', 2380 name: 'Nias', 2381 - android: false, 2382 - ios: false, 2383 }, 2384 { 2385 code3: 'nic', 2386 code2: '', 2387 name: 'Niger-Kordofanian languages', 2388 - android: false, 2389 - ios: false, 2390 }, 2391 { 2392 code3: 'niu', 2393 code2: '', 2394 name: 'Niuean', 2395 - android: false, 2396 - ios: false, 2397 }, 2398 { 2399 code3: 'nld', 2400 code2: 'nl', 2401 name: 'Dutch', 2402 - android: true, 2403 - ios: true, 2404 }, 2405 { 2406 code3: 'nno', 2407 code2: 'nn', 2408 name: 'Norwegian Nynorsk', 2409 - android: false, 2410 - ios: false, 2411 }, 2412 { 2413 code3: 'nob', 2414 code2: 'nb', 2415 name: 'Norwegian Bokmål', 2416 - android: false, 2417 - ios: false, 2418 }, 2419 { 2420 code3: 'nog', 2421 code2: '', 2422 name: 'Nogai', 2423 - android: false, 2424 - ios: false, 2425 }, 2426 { 2427 code3: 'non', 2428 code2: '', 2429 name: 'Norse, Old', 2430 - android: false, 2431 - ios: false, 2432 }, 2433 { 2434 code3: 'nor', 2435 code2: 'no', 2436 name: 'Norwegian', 2437 - android: true, 2438 - ios: false, 2439 }, 2440 { 2441 code3: 'nqo', 2442 code2: '', 2443 name: "N'Ko", 2444 - android: false, 2445 - ios: false, 2446 }, 2447 { 2448 code3: 'nso', 2449 code2: '', 2450 name: 'Northern Sotho', 2451 - android: false, 2452 - ios: false, 2453 }, 2454 { 2455 code3: 'nub', 2456 code2: '', 2457 name: 'Nubian languages', 2458 - android: false, 2459 - ios: false, 2460 }, 2461 { 2462 code3: 'nwc', 2463 code2: '', 2464 name: 'Classical Newari; Old Newari; Classical Nepal Bhasa', 2465 - android: false, 2466 - ios: false, 2467 }, 2468 { 2469 code3: 'nya', 2470 code2: 'ny', 2471 name: 'Nyanja', 2472 - android: false, 2473 - ios: false, 2474 }, 2475 { 2476 code3: 'nym', 2477 code2: '', 2478 name: 'Nyamwezi', 2479 - android: false, 2480 - ios: false, 2481 }, 2482 { 2483 code3: 'nyn', 2484 code2: '', 2485 name: 'Nyankole', 2486 - android: false, 2487 - ios: false, 2488 }, 2489 { 2490 code3: 'nyo', 2491 code2: '', 2492 name: 'Nyoro', 2493 - android: false, 2494 - ios: false, 2495 }, 2496 { 2497 code3: 'nzi', 2498 code2: '', 2499 name: 'Nzima', 2500 - android: false, 2501 - ios: false, 2502 }, 2503 { 2504 code3: 'oci', 2505 code2: 'oc', 2506 name: 'Occitan', 2507 - android: false, 2508 - ios: false, 2509 }, 2510 { 2511 code3: 'oji', 2512 code2: 'oj', 2513 name: 'Ojibwa', 2514 - android: false, 2515 - ios: false, 2516 }, 2517 { 2518 code3: 'ori', 2519 code2: 'or', 2520 name: 'Odia', 2521 - android: false, 2522 - ios: false, 2523 }, 2524 { 2525 code3: 'orm', 2526 code2: 'om', 2527 name: 'Oromo', 2528 - android: false, 2529 - ios: false, 2530 }, 2531 { 2532 code3: 'osa', 2533 code2: '', 2534 name: 'Osage', 2535 - android: false, 2536 - ios: false, 2537 }, 2538 { 2539 code3: 'oss', 2540 code2: 'os', 2541 name: 'Ossetic', 2542 - android: false, 2543 - ios: false, 2544 }, 2545 { 2546 code3: 'ota', 2547 code2: '', 2548 name: 'Turkish, Ottoman (1500-1928)', 2549 - android: false, 2550 - ios: false, 2551 }, 2552 { 2553 code3: 'oto', 2554 code2: '', 2555 name: 'Otomian languages', 2556 - android: false, 2557 - ios: false, 2558 }, 2559 { 2560 code3: 'paa', 2561 code2: '', 2562 name: 'Papuan languages', 2563 - android: false, 2564 - ios: false, 2565 }, 2566 { 2567 code3: 'pag', 2568 code2: '', 2569 name: 'Pangasinan', 2570 - android: false, 2571 - ios: false, 2572 }, 2573 { 2574 code3: 'pal', 2575 code2: '', 2576 name: 'Pahlavi', 2577 - android: false, 2578 - ios: false, 2579 }, 2580 { 2581 code3: 'pam', 2582 code2: '', 2583 name: 'Pampanga; Kapampangan', 2584 - android: false, 2585 - ios: false, 2586 }, 2587 { 2588 code3: 'pan', 2589 code2: 'pa', 2590 name: 'Punjabi', 2591 - android: false, 2592 - ios: false, 2593 }, 2594 { 2595 code3: 'pap', 2596 code2: '', 2597 name: 'Papiamento', 2598 - android: false, 2599 - ios: false, 2600 }, 2601 { 2602 code3: 'pau', 2603 code2: '', 2604 name: 'Palauan', 2605 - android: false, 2606 - ios: false, 2607 }, 2608 { 2609 code3: 'peo', 2610 code2: '', 2611 name: 'Persian, Old (ca.600-400 B.C.)', 2612 - android: false, 2613 - ios: false, 2614 }, 2615 { 2616 code3: 'per', 2617 code2: 'fa', 2618 name: 'Persian', 2619 - android: true, 2620 - ios: false, 2621 }, 2622 { 2623 code3: 'phi', 2624 code2: '', 2625 name: 'Philippine languages', 2626 - android: false, 2627 - ios: false, 2628 }, 2629 { 2630 code3: 'phn', 2631 code2: '', 2632 name: 'Phoenician', 2633 - android: false, 2634 - ios: false, 2635 }, 2636 { 2637 code3: 'pli', 2638 code2: 'pi', 2639 name: 'Pali', 2640 - android: false, 2641 - ios: false, 2642 }, 2643 { 2644 code3: 'pol', 2645 code2: 'pl', 2646 name: 'Polish', 2647 - android: true, 2648 - ios: true, 2649 }, 2650 { 2651 code3: 'pon', 2652 code2: '', 2653 name: 'Pohnpeian', 2654 - android: false, 2655 - ios: false, 2656 }, 2657 { 2658 code3: 'por', 2659 code2: 'pt', 2660 name: 'Portuguese', 2661 - android: true, 2662 - ios: true, 2663 }, 2664 { 2665 code3: 'pra', 2666 code2: '', 2667 name: 'Prakrit languages', 2668 - android: false, 2669 - ios: false, 2670 }, 2671 { 2672 code3: 'pro', 2673 code2: '', 2674 name: 'Provençal, Old (to 1500);Occitan, Old (to 1500)', 2675 - android: false, 2676 - ios: false, 2677 }, 2678 { 2679 code3: 'pus', 2680 code2: 'ps', 2681 name: 'Pashto', 2682 - android: false, 2683 - ios: false, 2684 }, 2685 { 2686 code3: 'que', 2687 code2: 'qu', 2688 name: 'Quechua', 2689 - android: false, 2690 - ios: false, 2691 }, 2692 { 2693 code3: 'raj', 2694 code2: '', 2695 name: 'Rajasthani', 2696 - android: false, 2697 - ios: false, 2698 }, 2699 { 2700 code3: 'rap', 2701 code2: '', 2702 name: 'Rapanui', 2703 - android: false, 2704 - ios: false, 2705 }, 2706 { 2707 code3: 'rar', 2708 code2: '', 2709 name: 'Rarotongan; Cook Islands Maori', 2710 - android: false, 2711 - ios: false, 2712 }, 2713 { 2714 code3: 'roa', 2715 code2: '', 2716 name: 'Romance languages', 2717 - android: false, 2718 - ios: false, 2719 }, 2720 { 2721 code3: 'roh', 2722 code2: 'rm', 2723 name: 'Romansh', 2724 - android: false, 2725 - ios: false, 2726 }, 2727 { 2728 code3: 'rom', 2729 code2: '', 2730 name: 'Romany', 2731 - android: false, 2732 - ios: false, 2733 }, 2734 { 2735 code3: 'rum', 2736 code2: 'ro', 2737 name: 'Romanian', 2738 - android: true, 2739 - ios: false, 2740 }, 2741 { 2742 code3: 'ron', 2743 code2: 'ro', 2744 name: 'Romanian', 2745 - android: true, 2746 - ios: false, 2747 }, 2748 { 2749 code3: 'run', 2750 code2: 'rn', 2751 name: 'Rundi', 2752 - android: false, 2753 - ios: false, 2754 }, 2755 { 2756 code3: 'rup', 2757 code2: '', 2758 name: 'Aromanian; Arumanian; Macedo-Romanian', 2759 - android: false, 2760 - ios: false, 2761 }, 2762 { 2763 code3: 'rus', 2764 code2: 'ru', 2765 name: 'Russian', 2766 - android: true, 2767 - ios: true, 2768 }, 2769 { 2770 code3: 'sad', 2771 code2: '', 2772 name: 'Sandawe', 2773 - android: false, 2774 - ios: false, 2775 }, 2776 { 2777 code3: 'sag', 2778 code2: 'sg', 2779 name: 'Sango', 2780 - android: false, 2781 - ios: false, 2782 }, 2783 { 2784 code3: 'sah', 2785 code2: '', 2786 name: 'Yakut', 2787 - android: false, 2788 - ios: false, 2789 }, 2790 { 2791 code3: 'sai', 2792 code2: '', 2793 name: 'South American Indian languages', 2794 - android: false, 2795 - ios: false, 2796 }, 2797 { 2798 code3: 'sal', 2799 code2: '', 2800 name: 'Salishan languages', 2801 - android: false, 2802 - ios: false, 2803 }, 2804 { 2805 code3: 'sam', 2806 code2: '', 2807 name: 'Samaritan Aramaic', 2808 - android: false, 2809 - ios: false, 2810 }, 2811 { 2812 code3: 'san', 2813 code2: 'sa', 2814 name: 'Sanskrit', 2815 - android: false, 2816 - ios: false, 2817 }, 2818 { 2819 code3: 'sas', 2820 code2: '', 2821 name: 'Sasak', 2822 - android: false, 2823 - ios: false, 2824 }, 2825 { 2826 code3: 'sat', 2827 code2: '', 2828 name: 'Santali', 2829 - android: false, 2830 - ios: false, 2831 }, 2832 { 2833 code3: 'scn', 2834 code2: '', 2835 name: 'Sicilian', 2836 - android: false, 2837 - ios: false, 2838 }, 2839 { 2840 code3: 'sco', 2841 code2: '', 2842 name: 'Scots', 2843 - android: false, 2844 - ios: false, 2845 }, 2846 { 2847 code3: 'sel', 2848 code2: '', 2849 name: 'Selkup', 2850 - android: false, 2851 - ios: false, 2852 }, 2853 { 2854 code3: 'sem', 2855 code2: '', 2856 name: 'Semitic languages', 2857 - android: false, 2858 - ios: false, 2859 }, 2860 { 2861 code3: 'sga', 2862 code2: '', 2863 name: 'Irish, Old (to 900)', 2864 - android: false, 2865 - ios: false, 2866 }, 2867 { 2868 code3: 'sgn', 2869 code2: '', 2870 name: 'Sign Languages', 2871 - android: false, 2872 - ios: false, 2873 }, 2874 { 2875 code3: 'shn', 2876 code2: '', 2877 name: 'Shan', 2878 - android: false, 2879 - ios: false, 2880 }, 2881 { 2882 code3: 'sid', 2883 code2: '', 2884 name: 'Sidamo', 2885 - android: false, 2886 - ios: false, 2887 }, 2888 { 2889 code3: 'sin', 2890 code2: 'si', 2891 name: 'Sinhala', 2892 - android: false, 2893 - ios: false, 2894 }, 2895 { 2896 code3: 'sio', 2897 code2: '', 2898 name: 'Siouan languages', 2899 - android: false, 2900 - ios: false, 2901 }, 2902 { 2903 code3: 'sit', 2904 code2: '', 2905 name: 'Sino-Tibetan languages', 2906 - android: false, 2907 - ios: false, 2908 }, 2909 { 2910 code3: 'sla', 2911 code2: '', 2912 name: 'Slavic languages', 2913 - android: false, 2914 - ios: false, 2915 }, 2916 { 2917 code3: 'slo', 2918 code2: 'sk', 2919 name: 'Slovak', 2920 - android: true, 2921 - ios: false, 2922 }, 2923 { 2924 code3: 'slk', 2925 code2: 'sk', 2926 name: 'Slovak', 2927 - android: true, 2928 - ios: false, 2929 }, 2930 { 2931 code3: 'slv', 2932 code2: 'sl', 2933 name: 'Slovenian', 2934 - android: true, 2935 - ios: false, 2936 }, 2937 { 2938 code3: 'sma', 2939 code2: '', 2940 name: 'Southern Sami', 2941 - android: false, 2942 - ios: false, 2943 }, 2944 { 2945 code3: 'sme', 2946 code2: 'se', 2947 name: 'Northern Sami', 2948 - android: false, 2949 - ios: false, 2950 }, 2951 { 2952 code3: 'smi', 2953 code2: '', 2954 name: 'Sami languages', 2955 - android: false, 2956 - ios: false, 2957 }, 2958 { 2959 code3: 'smj', 2960 code2: '', 2961 name: 'Lule Sami', 2962 - android: false, 2963 - ios: false, 2964 }, 2965 { 2966 code3: 'smn', 2967 code2: '', 2968 name: 'Inari Sami', 2969 - android: false, 2970 - ios: false, 2971 }, 2972 { 2973 code3: 'smo', 2974 code2: 'sm', 2975 name: 'Samoan', 2976 - android: false, 2977 - ios: false, 2978 }, 2979 { 2980 code3: 'sms', 2981 code2: '', 2982 name: 'Skolt Sami', 2983 - android: false, 2984 - ios: false, 2985 }, 2986 { 2987 code3: 'sna', 2988 code2: 'sn', 2989 name: 'Shona', 2990 - android: false, 2991 - ios: false, 2992 }, 2993 { 2994 code3: 'snd', 2995 code2: 'sd', 2996 name: 'Sindhi', 2997 - android: false, 2998 - ios: false, 2999 }, 3000 { 3001 code3: 'snk', 3002 code2: '', 3003 name: 'Soninke', 3004 - android: false, 3005 - ios: false, 3006 }, 3007 { 3008 code3: 'sog', 3009 code2: '', 3010 name: 'Sogdian', 3011 - android: false, 3012 - ios: false, 3013 }, 3014 { 3015 code3: 'som', 3016 code2: 'so', 3017 name: 'Somali', 3018 - android: false, 3019 - ios: false, 3020 }, 3021 { 3022 code3: 'son', 3023 code2: '', 3024 name: 'Songhai languages', 3025 - android: false, 3026 - ios: false, 3027 }, 3028 { 3029 code3: 'sot', 3030 code2: 'st', 3031 name: 'Southern Sotho', 3032 - android: false, 3033 - ios: false, 3034 }, 3035 { 3036 code3: 'spa', 3037 code2: 'es', 3038 name: 'Spanish', 3039 - android: true, 3040 - ios: true, 3041 }, 3042 { 3043 code3: 'sqi', 3044 code2: 'sq', 3045 name: 'Albanian', 3046 - android: true, 3047 - ios: false, 3048 }, 3049 { 3050 code3: 'srd', 3051 code2: 'sc', 3052 name: 'Sardinian', 3053 - android: false, 3054 - ios: false, 3055 }, 3056 { 3057 code3: 'srn', 3058 code2: '', 3059 name: 'Sranan Tongo', 3060 - android: false, 3061 - ios: false, 3062 }, 3063 { 3064 code3: 'srp', 3065 code2: 'sr', 3066 name: 'Serbian', 3067 - android: false, 3068 - ios: false, 3069 }, 3070 { 3071 code3: 'srr', 3072 code2: '', 3073 name: 'Serer', 3074 - android: false, 3075 - ios: false, 3076 }, 3077 { 3078 code3: 'ssa', 3079 code2: '', 3080 name: 'Nilo-Saharan languages', 3081 - android: false, 3082 - ios: false, 3083 }, 3084 { 3085 code3: 'ssw', 3086 code2: 'ss', 3087 name: 'Swati', 3088 - android: false, 3089 - ios: false, 3090 }, 3091 { 3092 code3: 'suk', 3093 code2: '', 3094 name: 'Sukuma', 3095 - android: false, 3096 - ios: false, 3097 }, 3098 { 3099 code3: 'sun', 3100 code2: 'su', 3101 name: 'Sundanese', 3102 - android: false, 3103 - ios: false, 3104 }, 3105 { 3106 code3: 'sus', 3107 code2: '', 3108 name: 'Susu', 3109 - android: false, 3110 - ios: false, 3111 }, 3112 { 3113 code3: 'sux', 3114 code2: '', 3115 name: 'Sumerian', 3116 - android: false, 3117 - ios: false, 3118 }, 3119 { 3120 code3: 'swa', 3121 code2: 'sw', 3122 name: 'Swahili', 3123 - android: true, 3124 - ios: false, 3125 }, 3126 { 3127 code3: 'swe', 3128 code2: 'sv', 3129 name: 'Swedish', 3130 - android: true, 3131 - ios: false, 3132 }, 3133 { 3134 code3: 'syc', 3135 code2: '', 3136 name: 'Classical Syriac', 3137 - android: false, 3138 - ios: false, 3139 }, 3140 { 3141 code3: 'syr', 3142 code2: '', 3143 name: 'Syriac', 3144 - android: false, 3145 - ios: false, 3146 }, 3147 { 3148 code3: 'tah', 3149 code2: 'ty', 3150 name: 'Tahitian', 3151 - android: false, 3152 - ios: false, 3153 }, 3154 { 3155 code3: 'tai', 3156 code2: '', 3157 name: 'Tai languages', 3158 - android: false, 3159 - ios: false, 3160 }, 3161 { 3162 code3: 'tam', 3163 code2: 'ta', 3164 name: 'Tamil', 3165 - android: true, 3166 - ios: false, 3167 }, 3168 { 3169 code3: 'tat', 3170 code2: 'tt', 3171 name: 'Tatar', 3172 - android: false, 3173 - ios: false, 3174 }, 3175 { 3176 code3: 'tel', 3177 code2: 'te', 3178 name: 'Telugu', 3179 - android: true, 3180 - ios: false, 3181 }, 3182 { 3183 code3: 'tem', 3184 code2: '', 3185 name: 'Timne', 3186 - android: false, 3187 - ios: false, 3188 }, 3189 { 3190 code3: 'ter', 3191 code2: '', 3192 name: 'Tereno', 3193 - android: false, 3194 - ios: false, 3195 }, 3196 { 3197 code3: 'tet', 3198 code2: '', 3199 name: 'Tetum', 3200 - android: false, 3201 - ios: false, 3202 }, 3203 { 3204 code3: 'tgk', 3205 code2: 'tg', 3206 name: 'Tajik', 3207 - android: false, 3208 - ios: false, 3209 }, 3210 { 3211 code3: 'tgl', 3212 code2: 'tl', 3213 name: 'Filipino', 3214 - android: true, 3215 - ios: false, 3216 }, 3217 { 3218 code3: 'tha', 3219 code2: 'th', 3220 name: 'Thai', 3221 - android: true, 3222 - ios: true, 3223 }, 3224 { 3225 code3: 'tib', 3226 code2: 'bo', 3227 name: 'Tibetan', 3228 - android: false, 3229 - ios: false, 3230 }, 3231 { 3232 code3: 'tig', 3233 code2: '', 3234 name: 'Tigre', 3235 - android: false, 3236 - ios: false, 3237 }, 3238 { 3239 code3: 'tir', 3240 code2: 'ti', 3241 name: 'Tigrinya', 3242 - android: false, 3243 - ios: false, 3244 }, 3245 { 3246 code3: 'tiv', 3247 code2: '', 3248 name: 'Tiv', 3249 - android: false, 3250 - ios: false, 3251 }, 3252 { 3253 code3: 'tkl', 3254 code2: '', 3255 name: 'Tokelau', 3256 - android: false, 3257 - ios: false, 3258 }, 3259 { 3260 code3: 'tlh', 3261 code2: '', 3262 name: 'Klingon; tlhIngan-Hol', 3263 - android: false, 3264 - ios: false, 3265 }, 3266 { 3267 code3: 'tli', 3268 code2: '', 3269 name: 'Tlingit', 3270 - android: false, 3271 - ios: false, 3272 }, 3273 { 3274 code3: 'tmh', 3275 code2: '', 3276 name: 'Tamashek', 3277 - android: false, 3278 - ios: false, 3279 }, 3280 { 3281 code3: 'tog', 3282 code2: '', 3283 name: 'Tonga (Nyasa)', 3284 - android: false, 3285 - ios: false, 3286 }, 3287 { 3288 code3: 'ton', 3289 code2: 'to', 3290 name: 'Tongan', 3291 - android: false, 3292 - ios: false, 3293 }, 3294 { 3295 code3: 'tpi', 3296 code2: '', 3297 name: 'Tok Pisin', 3298 - android: false, 3299 - ios: false, 3300 }, 3301 { 3302 code3: 'tsi', 3303 code2: '', 3304 name: 'Tsimshian', 3305 - android: false, 3306 - ios: false, 3307 }, 3308 { 3309 code3: 'tsn', 3310 code2: 'tn', 3311 name: 'Tswana', 3312 - android: false, 3313 - ios: false, 3314 }, 3315 { 3316 code3: 'tso', 3317 code2: 'ts', 3318 name: 'Tsonga', 3319 - android: false, 3320 - ios: false, 3321 }, 3322 { 3323 code3: 'tuk', 3324 code2: 'tk', 3325 name: 'Turkmen', 3326 - android: false, 3327 - ios: false, 3328 }, 3329 { 3330 code3: 'tum', 3331 code2: '', 3332 name: 'Tumbuka', 3333 - android: false, 3334 - ios: false, 3335 }, 3336 { 3337 code3: 'tup', 3338 code2: '', 3339 name: 'Tupi languages', 3340 - android: false, 3341 - ios: false, 3342 }, 3343 { 3344 code3: 'tur', 3345 code2: 'tr', 3346 name: 'Turkish', 3347 - android: true, 3348 - ios: true, 3349 }, 3350 { 3351 code3: 'tut', 3352 code2: '', 3353 name: 'Altaic languages', 3354 - android: false, 3355 - ios: false, 3356 }, 3357 { 3358 code3: 'tvl', 3359 code2: '', 3360 name: 'Tuvalu', 3361 - android: false, 3362 - ios: false, 3363 }, 3364 { 3365 code3: 'twi', 3366 code2: 'tw', 3367 name: 'Akan', 3368 - android: false, 3369 - ios: false, 3370 }, 3371 { 3372 code3: 'tyv', 3373 code2: '', 3374 name: 'Tuvinian', 3375 - android: false, 3376 - ios: false, 3377 }, 3378 { 3379 code3: 'udm', 3380 code2: '', 3381 name: 'Udmurt', 3382 - android: false, 3383 - ios: false, 3384 }, 3385 { 3386 code3: 'uga', 3387 code2: '', 3388 name: 'Ugaritic', 3389 - android: false, 3390 - ios: false, 3391 }, 3392 { 3393 code3: 'uig', 3394 code2: 'ug', 3395 name: 'Uyghur', 3396 - android: false, 3397 - ios: false, 3398 }, 3399 { 3400 code3: 'ukr', 3401 code2: 'uk', 3402 name: 'Ukrainian', 3403 - android: true, 3404 - ios: true, 3405 }, 3406 { 3407 code3: 'umb', 3408 code2: '', 3409 name: 'Umbundu', 3410 - android: false, 3411 - ios: false, 3412 }, 3413 { 3414 code3: 'und', 3415 code2: '', 3416 name: 'Undetermined', 3417 - android: false, 3418 - ios: false, 3419 }, 3420 { 3421 code3: 'urd', 3422 code2: 'ur', 3423 name: 'Urdu', 3424 - android: true, 3425 - ios: false, 3426 }, 3427 { 3428 code3: 'uzb', 3429 code2: 'uz', 3430 name: 'Uzbek', 3431 - android: false, 3432 - ios: false, 3433 }, 3434 { 3435 code3: 'vai', 3436 code2: '', 3437 name: 'Vai', 3438 - android: false, 3439 - ios: false, 3440 }, 3441 { 3442 code3: 'ven', 3443 code2: 've', 3444 name: 'Venda', 3445 - android: false, 3446 - ios: false, 3447 }, 3448 { 3449 code3: 'vie', 3450 code2: 'vi', 3451 name: 'Vietnamese', 3452 - android: true, 3453 - ios: true, 3454 }, 3455 { 3456 code3: 'vol', 3457 code2: 'vo', 3458 name: 'Volapük', 3459 - android: false, 3460 - ios: false, 3461 }, 3462 { 3463 code3: 'vot', 3464 code2: '', 3465 name: 'Votic', 3466 - android: false, 3467 - ios: false, 3468 }, 3469 { 3470 code3: 'wak', 3471 code2: '', 3472 name: 'Wakashan languages', 3473 - android: false, 3474 - ios: false, 3475 }, 3476 { 3477 code3: 'wal', 3478 code2: '', 3479 name: 'Wolaitta; Wolaytta', 3480 - android: false, 3481 - ios: false, 3482 }, 3483 { 3484 code3: 'war', 3485 code2: '', 3486 name: 'Waray', 3487 - android: false, 3488 - ios: false, 3489 }, 3490 { 3491 code3: 'was', 3492 code2: '', 3493 name: 'Washo', 3494 - android: false, 3495 - ios: false, 3496 }, 3497 { 3498 code3: 'wel', 3499 code2: 'cy', 3500 name: 'Welsh', 3501 - android: true, 3502 - ios: false, 3503 }, 3504 { 3505 code3: 'wen', 3506 code2: '', 3507 name: 'Sorbian languages', 3508 - android: false, 3509 - ios: false, 3510 }, 3511 { 3512 code3: 'wln', 3513 code2: 'wa', 3514 name: 'Walloon', 3515 - android: false, 3516 - ios: false, 3517 }, 3518 { 3519 code3: 'wol', 3520 code2: 'wo', 3521 name: 'Wolof', 3522 - android: false, 3523 - ios: false, 3524 }, 3525 { 3526 code3: 'xal', 3527 code2: '', 3528 name: 'Kalmyk; Oirat', 3529 - android: false, 3530 - ios: false, 3531 }, 3532 { 3533 code3: 'xho', 3534 code2: 'xh', 3535 name: 'Xhosa', 3536 - android: false, 3537 - ios: false, 3538 }, 3539 { 3540 code3: 'yao', 3541 code2: '', 3542 name: 'Yao', 3543 - android: false, 3544 - ios: false, 3545 }, 3546 { 3547 code3: 'yap', 3548 code2: '', 3549 name: 'Yapese', 3550 - android: false, 3551 - ios: false, 3552 }, 3553 { 3554 code3: 'yid', 3555 code2: 'yi', 3556 name: 'Yiddish', 3557 - android: false, 3558 - ios: false, 3559 }, 3560 { 3561 code3: 'yor', 3562 code2: 'yo', 3563 name: 'Yoruba', 3564 - android: false, 3565 - ios: false, 3566 }, 3567 { 3568 code3: 'ypk', 3569 code2: '', 3570 name: 'Yupik languages', 3571 - android: false, 3572 - ios: false, 3573 }, 3574 { 3575 code3: 'zap', 3576 code2: '', 3577 name: 'Zapotec', 3578 - android: false, 3579 - ios: false, 3580 }, 3581 { 3582 code3: 'zbl', 3583 code2: '', 3584 name: 'Blissymbols; Blissymbolics; Bliss', 3585 - android: false, 3586 - ios: false, 3587 }, 3588 { 3589 code3: 'zen', 3590 code2: '', 3591 name: 'Zenaga', 3592 - android: false, 3593 - ios: false, 3594 }, 3595 { 3596 code3: 'zgh', 3597 code2: '', 3598 name: 'Standard Moroccan Tamazight', 3599 - android: false, 3600 - ios: false, 3601 }, 3602 { 3603 code3: 'zha', 3604 code2: 'za', 3605 name: 'Zhuang; Chuang', 3606 - android: false, 3607 - ios: false, 3608 }, 3609 { 3610 code3: 'zho', 3611 code2: 'zh', 3612 name: 'Chinese', 3613 - android: true, 3614 - ios: true, 3615 }, 3616 { 3617 code3: 'znd', 3618 code2: '', 3619 name: 'Zande languages', 3620 - android: false, 3621 - ios: false, 3622 }, 3623 { 3624 code3: 'zul', 3625 code2: 'zu', 3626 name: 'Zulu', 3627 - android: false, 3628 - ios: false, 3629 }, 3630 { 3631 code3: 'zun', 3632 code2: '', 3633 name: 'Zuni', 3634 - android: false, 3635 - ios: false, 3636 }, 3637 { 3638 code3: 'zza', 3639 code2: '', 3640 name: 'Zaza; Dimili; Dimli; Kirdki; Kirmanjki; Zazaki', 3641 - android: false, 3642 - ios: false, 3643 }, 3644 ] 3645
··· 2 code3: string 3 code2: string 4 name: string 5 } 6 7 export enum AppLanguage { ··· 101 ] 102 103 // Pre-generated list using Intl.DisplayNames to localize the language name. 104 export const LANGUAGES: Language[] = [ 105 { 106 code3: 'aar', 107 code2: 'aa', 108 name: 'Afar', 109 }, 110 { 111 code3: 'abk', 112 code2: 'ab', 113 name: 'Abkhazian', 114 }, 115 { 116 code3: 'ace', 117 code2: '', 118 name: 'Achinese', 119 }, 120 { 121 code3: 'ach', 122 code2: '', 123 name: 'Acoli', 124 }, 125 { 126 code3: 'ada', 127 code2: '', 128 name: 'Adangme', 129 }, 130 { 131 code3: 'ady', 132 code2: '', 133 name: 'Adyghe; Adygei', 134 }, 135 { 136 code3: 'afa', 137 code2: '', 138 name: 'Afro-Asiatic languages', 139 }, 140 { 141 code3: 'afh', 142 code2: '', 143 name: 'Afrihili', 144 }, 145 { 146 code3: 'afr', 147 code2: 'af', 148 name: 'Afrikaans', 149 }, 150 { 151 code3: 'ain', 152 code2: '', 153 name: 'Ainu', 154 }, 155 { 156 code3: 'aka', 157 code2: 'ak', 158 name: 'Akan', 159 }, 160 { 161 code3: 'akk', 162 code2: '', 163 name: 'Akkadian', 164 }, 165 { 166 code3: 'alb', 167 code2: 'sq', 168 name: 'Albanian', 169 }, 170 { 171 code3: 'ale', 172 code2: '', 173 name: 'Aleut', 174 }, 175 { 176 code3: 'alg', 177 code2: '', 178 name: 'Algonquian languages', 179 }, 180 { 181 code3: 'alt', 182 code2: '', 183 name: 'Southern Altai', 184 }, 185 { 186 code3: 'amh', 187 code2: 'am', 188 name: 'Amharic', 189 }, 190 { 191 code3: 'ang', 192 code2: '', 193 name: 'English, Old (ca.450-1100)', 194 }, 195 { 196 code3: 'anp', 197 code2: '', 198 name: 'Angika', 199 }, 200 { 201 code3: 'apa', 202 code2: '', 203 name: 'Apache languages', 204 }, 205 { 206 code3: 'ara', 207 code2: 'ar', 208 name: 'Arabic', 209 }, 210 { 211 code3: 'arc', 212 code2: '', 213 name: 'Official Aramaic (700-300 BCE); Imperial Aramaic (700-300 BCE)', 214 }, 215 { 216 code3: 'arg', 217 code2: 'an', 218 name: 'Aragonese', 219 }, 220 { 221 code3: 'arm', 222 code2: 'hy', 223 name: 'Armenian', 224 }, 225 { 226 code3: 'arn', 227 code2: '', 228 name: 'Mapudungun; Mapuche', 229 }, 230 { 231 code3: 'arp', 232 code2: '', 233 name: 'Arapaho', 234 }, 235 { 236 code3: 'art', 237 code2: '', 238 name: 'Artificial languages', 239 }, 240 { 241 code3: 'arw', 242 code2: '', 243 name: 'Arawak', 244 }, 245 { 246 code3: 'asm', 247 code2: 'as', 248 name: 'Assamese', 249 }, 250 { 251 code3: 'ast', 252 code2: '', 253 name: 'Asturian', 254 }, 255 { 256 code3: 'ath', 257 code2: '', 258 name: 'Athapascan languages', 259 }, 260 { 261 code3: 'aus', 262 code2: '', 263 name: 'Australian languages', 264 }, 265 { 266 code3: 'ava', 267 code2: 'av', 268 name: 'Avaric', 269 }, 270 { 271 code3: 'ave', 272 code2: 'ae', 273 name: 'Avestan', 274 }, 275 { 276 code3: 'awa', 277 code2: '', 278 name: 'Awadhi', 279 }, 280 { 281 code3: 'aym', 282 code2: 'ay', 283 name: 'Aymara', 284 }, 285 { 286 code3: 'aze', 287 code2: 'az', 288 name: 'Azerbaijani', 289 }, 290 { 291 code3: 'bad', 292 code2: '', 293 name: 'Banda languages', 294 }, 295 { 296 code3: 'bai', 297 code2: '', 298 name: 'Bamileke languages', 299 }, 300 { 301 code3: 'bak', 302 code2: 'ba', 303 name: 'Bashkir', 304 }, 305 { 306 code3: 'bal', 307 code2: '', 308 name: 'Baluchi', 309 }, 310 { 311 code3: 'bam', 312 code2: 'bm', 313 name: 'Bambara', 314 }, 315 { 316 code3: 'ban', 317 code2: '', 318 name: 'Balinese', 319 }, 320 { 321 code3: 'baq', 322 code2: 'eu', 323 name: 'Basque', 324 }, 325 { 326 code3: 'bas', 327 code2: '', 328 name: 'Basa', 329 }, 330 { 331 code3: 'bat', 332 code2: '', 333 name: 'Baltic languages', 334 }, 335 { 336 code3: 'bej', 337 code2: '', 338 name: 'Beja; Bedawiyet', 339 }, 340 { 341 code3: 'bel', 342 code2: 'be', 343 name: 'Belarusian', 344 }, 345 { 346 code3: 'bem', 347 code2: '', 348 name: 'Bemba', 349 }, 350 { 351 code3: 'ben', 352 code2: 'bn', 353 name: 'Bangla', 354 }, 355 { 356 code3: 'ber', 357 code2: '', 358 name: 'Berber languages', 359 }, 360 { 361 code3: 'bho', 362 code2: '', 363 name: 'Bhojpuri', 364 }, 365 { 366 code3: 'bih', 367 code2: 'bh', 368 name: 'Bhojpuri', 369 }, 370 { 371 code3: 'bik', 372 code2: '', 373 name: 'Bikol', 374 }, 375 { 376 code3: 'bin', 377 code2: '', 378 name: 'Bini; Edo', 379 }, 380 { 381 code3: 'bis', 382 code2: 'bi', 383 name: 'Bislama', 384 }, 385 { 386 code3: 'bla', 387 code2: '', 388 name: 'Siksika', 389 }, 390 { 391 code3: 'bnt', 392 code2: '', 393 name: 'Bantu languages', 394 }, 395 { 396 code3: 'bod', 397 code2: 'bo', 398 name: 'Tibetan', 399 }, 400 { 401 code3: 'bos', 402 code2: 'bs', 403 name: 'Bosnian', 404 }, 405 { 406 code3: 'bra', 407 code2: '', 408 name: 'Braj', 409 }, 410 { 411 code3: 'bre', 412 code2: 'br', 413 name: 'Breton', 414 }, 415 { 416 code3: 'btk', 417 code2: '', 418 name: 'Batak languages', 419 }, 420 { 421 code3: 'bua', 422 code2: '', 423 name: 'Buriat', 424 }, 425 { 426 code3: 'bug', 427 code2: '', 428 name: 'Buginese', 429 }, 430 { 431 code3: 'bul', 432 code2: 'bg', 433 name: 'Bulgarian', 434 }, 435 { 436 code3: 'bur', 437 code2: 'my', 438 name: 'Burmese', 439 }, 440 { 441 code3: 'byn', 442 code2: '', 443 name: 'Blin; Bilin', 444 }, 445 { 446 code3: 'cad', 447 code2: '', 448 name: 'Caddo', 449 }, 450 { 451 code3: 'cai', 452 code2: '', 453 name: 'Central American Indian languages', 454 }, 455 { 456 code3: 'car', 457 code2: '', 458 name: 'Galibi Carib', 459 }, 460 { 461 code3: 'cat', 462 code2: 'ca', 463 name: 'Catalan', 464 }, 465 { 466 code3: 'cau', 467 code2: '', 468 name: 'Caucasian languages', 469 }, 470 { 471 code3: 'ceb', 472 code2: '', 473 name: 'Cebuano', 474 }, 475 { 476 code3: 'cel', 477 code2: '', 478 name: 'Celtic languages', 479 }, 480 { 481 code3: 'ces', 482 code2: 'cs', 483 name: 'Czech', 484 }, 485 { 486 code3: 'cha', 487 code2: 'ch', 488 name: 'Chamorro', 489 }, 490 { 491 code3: 'chb', 492 code2: '', 493 name: 'Chibcha', 494 }, 495 { 496 code3: 'che', 497 code2: 'ce', 498 name: 'Chechen', 499 }, 500 { 501 code3: 'chg', 502 code2: '', 503 name: 'Chagatai', 504 }, 505 { 506 code3: 'chi', 507 code2: 'zh', 508 name: 'Chinese', 509 }, 510 { 511 code3: 'chk', 512 code2: '', 513 name: 'Chuukese', 514 }, 515 { 516 code3: 'chm', 517 code2: '', 518 name: 'Mari', 519 }, 520 { 521 code3: 'chn', 522 code2: '', 523 name: 'Chinook jargon', 524 }, 525 { 526 code3: 'cho', 527 code2: '', 528 name: 'Choctaw', 529 }, 530 { 531 code3: 'chp', 532 code2: '', 533 name: 'Chipewyan; Dene Suline', 534 }, 535 { 536 code3: 'chr', 537 code2: '', 538 name: 'Cherokee', 539 }, 540 { 541 code3: 'chu', 542 code2: 'cu', 543 name: 'Church Slavic', 544 }, 545 { 546 code3: 'chv', 547 code2: 'cv', 548 name: 'Chuvash', 549 }, 550 { 551 code3: 'chy', 552 code2: '', 553 name: 'Cheyenne', 554 }, 555 { 556 code3: 'cmc', 557 code2: '', 558 name: 'Chamic languages', 559 }, 560 { 561 code3: 'cnr', 562 code2: '', 563 name: 'Serbian (Montenegro)', 564 }, 565 { 566 code3: 'cop', 567 code2: '', 568 name: 'Coptic', 569 }, 570 { 571 code3: 'cor', 572 code2: 'kw', 573 name: 'Cornish', 574 }, 575 { 576 code3: 'cos', 577 code2: 'co', 578 name: 'Corsican', 579 }, 580 { 581 code3: 'cpe', 582 code2: '', 583 name: 'Creoles and pidgins, English based', 584 }, 585 { 586 code3: 'cpf', 587 code2: '', 588 name: 'Creoles and pidgins, French-based', 589 }, 590 { 591 code3: 'cpp', 592 code2: '', 593 name: 'Creoles and pidgins, Portuguese-based', 594 }, 595 { 596 code3: 'cre', 597 code2: 'cr', 598 name: 'Cree', 599 }, 600 { 601 code3: 'crh', 602 code2: '', 603 name: 'Crimean Tatar; Crimean Turkish', 604 }, 605 { 606 code3: 'crp', 607 code2: '', 608 name: 'Creoles and pidgins', 609 }, 610 { 611 code3: 'csb', 612 code2: '', 613 name: 'Kashubian', 614 }, 615 { 616 code3: 'cus', 617 code2: '', 618 name: 'Cushitic languages', 619 }, 620 { 621 code3: 'cym', 622 code2: 'cy', 623 name: 'Welsh', 624 }, 625 { 626 code3: 'cze', 627 code2: 'cs', 628 name: 'Czech', 629 }, 630 { 631 code3: 'dak', 632 code2: '', 633 name: 'Dakota', 634 }, 635 { 636 code3: 'dan', 637 code2: 'da', 638 name: 'Danish', 639 }, 640 { 641 code3: 'dar', 642 code2: '', 643 name: 'Dargwa', 644 }, 645 { 646 code3: 'day', 647 code2: '', 648 name: 'Land Dayak languages', 649 }, 650 { 651 code3: 'del', 652 code2: '', 653 name: 'Delaware', 654 }, 655 { 656 code3: 'den', 657 code2: '', 658 name: 'Slave (Athapascan)', 659 }, 660 { 661 code3: 'deu', 662 code2: 'de', 663 name: 'German', 664 }, 665 { 666 code3: 'dgr', 667 code2: '', 668 name: 'Dogrib', 669 }, 670 { 671 code3: 'din', 672 code2: '', 673 name: 'Dinka', 674 }, 675 { 676 code3: 'div', 677 code2: 'dv', 678 name: 'Divehi', 679 }, 680 { 681 code3: 'doi', 682 code2: '', 683 name: 'Dogri', 684 }, 685 { 686 code3: 'dra', 687 code2: '', 688 name: 'Dravidian languages', 689 }, 690 { 691 code3: 'dsb', 692 code2: '', 693 name: 'Lower Sorbian', 694 }, 695 { 696 code3: 'dua', 697 code2: '', 698 name: 'Duala', 699 }, 700 { 701 code3: 'dum', 702 code2: '', 703 name: 'Dutch, Middle (ca.1050-1350)', 704 }, 705 { 706 code3: 'dut', 707 code2: 'nl', 708 name: 'Dutch', 709 }, 710 { 711 code3: 'dyu', 712 code2: '', 713 name: 'Dyula', 714 }, 715 { 716 code3: 'dzo', 717 code2: 'dz', 718 name: 'Dzongkha', 719 }, 720 { 721 code3: 'efi', 722 code2: '', 723 name: 'Efik', 724 }, 725 { 726 code3: 'egy', 727 code2: '', 728 name: 'Egyptian (Ancient)', 729 }, 730 { 731 code3: 'eka', 732 code2: '', 733 name: 'Ekajuk', 734 }, 735 { 736 code3: 'ell', 737 code2: 'el', 738 name: 'Greek', 739 }, 740 { 741 code3: 'elx', 742 code2: '', 743 name: 'Elamite', 744 }, 745 { 746 code3: 'eng', 747 code2: 'en', 748 name: 'English', 749 }, 750 { 751 code3: 'enm', 752 code2: '', 753 name: 'English, Middle (1100-1500)', 754 }, 755 { 756 code3: 'epo', 757 code2: 'eo', 758 name: 'Esperanto', 759 }, 760 { 761 code3: 'est', 762 code2: 'et', 763 name: 'Estonian', 764 }, 765 { 766 code3: 'eus', 767 code2: 'eu', 768 name: 'Basque', 769 }, 770 { 771 code3: 'ewe', 772 code2: 'ee', 773 name: 'Ewe', 774 }, 775 { 776 code3: 'ewo', 777 code2: '', 778 name: 'Ewondo', 779 }, 780 { 781 code3: 'fan', 782 code2: '', 783 name: 'Fang', 784 }, 785 { 786 code3: 'fao', 787 code2: 'fo', 788 name: 'Faroese', 789 }, 790 { 791 code3: 'fas', 792 code2: 'fa', 793 name: 'Persian', 794 }, 795 { 796 code3: 'fat', 797 code2: '', 798 name: 'Akan', 799 }, 800 { 801 code3: 'fij', 802 code2: 'fj', 803 name: 'Fijian', 804 }, 805 { 806 code3: 'fil', 807 code2: '', 808 name: 'Filipino', 809 }, 810 { 811 code3: 'fin', 812 code2: 'fi', 813 name: 'Finnish', 814 }, 815 { 816 code3: 'fiu', 817 code2: '', 818 name: 'Finno-Ugrian languages', 819 }, 820 { 821 code3: 'fon', 822 code2: '', 823 name: 'Fon', 824 }, 825 { 826 code3: 'fra', 827 code2: 'fr', 828 name: 'French', 829 }, 830 { 831 code3: 'fre', 832 code2: 'fr', 833 name: 'French', 834 }, 835 { 836 code3: 'frm', 837 code2: '', 838 name: 'French, Middle (ca.1400-1600)', 839 }, 840 { 841 code3: 'fro', 842 code2: '', 843 name: 'French, Old (842-ca.1400)', 844 }, 845 { 846 code3: 'frr', 847 code2: '', 848 name: 'Northern Frisian', 849 }, 850 { 851 code3: 'frs', 852 code2: '', 853 name: 'Eastern Frisian', 854 }, 855 { 856 code3: 'fry', 857 code2: 'fy', 858 name: 'Western Frisian', 859 }, 860 { 861 code3: 'ful', 862 code2: 'ff', 863 name: 'Fulah', 864 }, 865 { 866 code3: 'fur', 867 code2: '', 868 name: 'Friulian', 869 }, 870 { 871 code3: 'gaa', 872 code2: '', 873 name: 'Ga', 874 }, 875 { 876 code3: 'gay', 877 code2: '', 878 name: 'Gayo', 879 }, 880 { 881 code3: 'gba', 882 code2: '', 883 name: 'Gbaya', 884 }, 885 { 886 code3: 'gem', 887 code2: '', 888 name: 'Germanic languages', 889 }, 890 { 891 code3: 'geo', 892 code2: 'ka', 893 name: 'Georgian', 894 }, 895 { 896 code3: 'ger', 897 code2: 'de', 898 name: 'German', 899 }, 900 { 901 code3: 'gez', 902 code2: '', 903 name: 'Geez', 904 }, 905 { 906 code3: 'gil', 907 code2: '', 908 name: 'Gilbertese', 909 }, 910 { 911 code3: 'gla', 912 code2: 'gd', 913 name: 'Scottish Gaelic', 914 }, 915 { 916 code3: 'gle', 917 code2: 'ga', 918 name: 'Irish', 919 }, 920 { 921 code3: 'glg', 922 code2: 'gl', 923 name: 'Galician', 924 }, 925 { 926 code3: 'glv', 927 code2: 'gv', 928 name: 'Manx', 929 }, 930 { 931 code3: 'gmh', 932 code2: '', 933 name: 'German, Middle High (ca.1050-1500)', 934 }, 935 { 936 code3: 'goh', 937 code2: '', 938 name: 'German, Old High (ca.750-1050)', 939 }, 940 { 941 code3: 'gon', 942 code2: '', 943 name: 'Gondi', 944 }, 945 { 946 code3: 'gor', 947 code2: '', 948 name: 'Gorontalo', 949 }, 950 { 951 code3: 'got', 952 code2: '', 953 name: 'Gothic', 954 }, 955 { 956 code3: 'grb', 957 code2: '', 958 name: 'Grebo', 959 }, 960 { 961 code3: 'grc', 962 code2: '', 963 name: 'Ancient Greek', 964 }, 965 { 966 code3: 'gre', 967 code2: 'el', 968 name: 'Greek', 969 }, 970 { 971 code3: 'grn', 972 code2: 'gn', 973 name: 'Guarani', 974 }, 975 { 976 code3: 'gsw', 977 code2: '', 978 name: 'Swiss German; Alemannic; Alsatian', 979 }, 980 { 981 code3: 'guj', 982 code2: 'gu', 983 name: 'Gujarati', 984 }, 985 { 986 code3: 'gwi', 987 code2: '', 988 name: "Gwich'in", 989 }, 990 { 991 code3: 'hai', 992 code2: '', 993 name: 'Haida', 994 }, 995 { 996 code3: 'hat', 997 code2: 'ht', 998 name: 'Haitian Creole', 999 }, 1000 { 1001 code3: 'hau', 1002 code2: 'ha', 1003 name: 'Hausa', 1004 }, 1005 { 1006 code3: 'haw', 1007 code2: '', 1008 name: 'Hawaiian', 1009 }, 1010 { 1011 code3: 'heb', 1012 code2: 'he', 1013 name: 'Hebrew', 1014 }, 1015 { 1016 code3: 'her', 1017 code2: 'hz', 1018 name: 'Herero', 1019 }, 1020 { 1021 code3: 'hil', 1022 code2: '', 1023 name: 'Hiligaynon', 1024 }, 1025 { 1026 code3: 'him', 1027 code2: '', 1028 name: 'Himachali languages; Western Pahari languages', 1029 }, 1030 { 1031 code3: 'hin', 1032 code2: 'hi', 1033 name: 'Hindi', 1034 }, 1035 { 1036 code3: 'hit', 1037 code2: '', 1038 name: 'Hittite', 1039 }, 1040 { 1041 code3: 'hmn', 1042 code2: '', 1043 name: 'Hmong', 1044 }, 1045 { 1046 code3: 'hmo', 1047 code2: 'ho', 1048 name: 'Hiri Motu', 1049 }, 1050 { 1051 code3: 'hrv', 1052 code2: 'hr', 1053 name: 'Croatian', 1054 }, 1055 { 1056 code3: 'hsb', 1057 code2: '', 1058 name: 'Upper Sorbian', 1059 }, 1060 { 1061 code3: 'hun', 1062 code2: 'hu', 1063 name: 'Hungarian', 1064 }, 1065 { 1066 code3: 'hup', 1067 code2: '', 1068 name: 'Hupa', 1069 }, 1070 { 1071 code3: 'hye', 1072 code2: 'hy', 1073 name: 'Armenian', 1074 }, 1075 { 1076 code3: 'iba', 1077 code2: '', 1078 name: 'Iban', 1079 }, 1080 { 1081 code3: 'ibo', 1082 code2: 'ig', 1083 name: 'Igbo', 1084 }, 1085 { 1086 code3: 'ice', 1087 code2: 'is', 1088 name: 'Icelandic', 1089 }, 1090 { 1091 code3: 'ido', 1092 code2: 'io', 1093 name: 'Ido', 1094 }, 1095 { 1096 code3: 'iii', 1097 code2: 'ii', 1098 name: 'Sichuan Yi; Nuosu', 1099 }, 1100 { 1101 code3: 'ijo', 1102 code2: '', 1103 name: 'Ijo languages', 1104 }, 1105 { 1106 code3: 'iku', 1107 code2: 'iu', 1108 name: 'Inuktitut', 1109 }, 1110 { 1111 code3: 'ile', 1112 code2: 'ie', 1113 name: 'Interlingue', 1114 }, 1115 { 1116 code3: 'ilo', 1117 code2: '', 1118 name: 'Iloko', 1119 }, 1120 { 1121 code3: 'ina', 1122 code2: 'ia', 1123 name: 'Interlingua', 1124 }, 1125 { 1126 code3: 'inc', 1127 code2: '', 1128 name: 'Indic languages', 1129 }, 1130 { 1131 code3: 'ind', 1132 code2: 'id', 1133 name: 'Indonesian', 1134 }, 1135 { 1136 code3: 'ine', 1137 code2: '', 1138 name: 'Indo-European languages', 1139 }, 1140 { 1141 code3: 'inh', 1142 code2: '', 1143 name: 'Ingush', 1144 }, 1145 { 1146 code3: 'ipk', 1147 code2: 'ik', 1148 name: 'Inupiaq', 1149 }, 1150 { 1151 code3: 'ira', 1152 code2: '', 1153 name: 'Iranian languages', 1154 }, 1155 { 1156 code3: 'iro', 1157 code2: '', 1158 name: 'Iroquoian languages', 1159 }, 1160 { 1161 code3: 'isl', 1162 code2: 'is', 1163 name: 'Icelandic', 1164 }, 1165 { 1166 code3: 'ita', 1167 code2: 'it', 1168 name: 'Italian', 1169 }, 1170 { 1171 code3: 'jav', 1172 code2: 'jv', 1173 name: 'Javanese', 1174 }, 1175 { 1176 code3: 'jbo', 1177 code2: '', 1178 name: 'Lojban', 1179 }, 1180 { 1181 code3: 'jpn', 1182 code2: 'ja', 1183 name: 'Japanese', 1184 }, 1185 { 1186 code3: 'jpr', 1187 code2: '', 1188 name: 'Judeo-Persian', 1189 }, 1190 { 1191 code3: 'jrb', 1192 code2: '', 1193 name: 'Judeo-Arabic', 1194 }, 1195 { 1196 code3: 'kaa', 1197 code2: '', 1198 name: 'Kara-Kalpak', 1199 }, 1200 { 1201 code3: 'kab', 1202 code2: '', 1203 name: 'Kabyle', 1204 }, 1205 { 1206 code3: 'kac', 1207 code2: '', 1208 name: 'Kachin; Jingpho', 1209 }, 1210 { 1211 code3: 'kal', 1212 code2: 'kl', 1213 name: 'Kalaallisut', 1214 }, 1215 { 1216 code3: 'kam', 1217 code2: '', 1218 name: 'Kamba', 1219 }, 1220 { 1221 code3: 'kan', 1222 code2: 'kn', 1223 name: 'Kannada', 1224 }, 1225 { 1226 code3: 'kar', 1227 code2: '', 1228 name: 'Karen languages', 1229 }, 1230 { 1231 code3: 'kas', 1232 code2: 'ks', 1233 name: 'Kashmiri', 1234 }, 1235 { 1236 code3: 'kat', 1237 code2: 'ka', 1238 name: 'Georgian', 1239 }, 1240 { 1241 code3: 'kau', 1242 code2: 'kr', 1243 name: 'Kanuri', 1244 }, 1245 { 1246 code3: 'kaw', 1247 code2: '', 1248 name: 'Kawi', 1249 }, 1250 { 1251 code3: 'kaz', 1252 code2: 'kk', 1253 name: 'Kazakh', 1254 }, 1255 { 1256 code3: 'kbd', 1257 code2: '', 1258 name: 'Kabardian', 1259 }, 1260 { 1261 code3: 'kha', 1262 code2: '', 1263 name: 'Khasi', 1264 }, 1265 { 1266 code3: 'khi', 1267 code2: '', 1268 name: 'Khoisan languages', 1269 }, 1270 { 1271 code3: 'khm', 1272 code2: 'km', 1273 name: 'Khmer', 1274 }, 1275 { 1276 code3: 'kho', 1277 code2: '', 1278 name: 'Khotanese; Sakan', 1279 }, 1280 { 1281 code3: 'kik', 1282 code2: 'ki', 1283 name: 'Kikuyu; Gikuyu', 1284 }, 1285 { 1286 code3: 'kin', 1287 code2: 'rw', 1288 name: 'Kinyarwanda', 1289 }, 1290 { 1291 code3: 'kir', 1292 code2: 'ky', 1293 name: 'Kyrgyz', 1294 }, 1295 { 1296 code3: 'kmb', 1297 code2: '', 1298 name: 'Kimbundu', 1299 }, 1300 { 1301 code3: 'kok', 1302 code2: '', 1303 name: 'Konkani', 1304 }, 1305 { 1306 code3: 'kom', 1307 code2: 'kv', 1308 name: 'Komi', 1309 }, 1310 { 1311 code3: 'kon', 1312 code2: 'kg', 1313 name: 'Kongo', 1314 }, 1315 { 1316 code3: 'kor', 1317 code2: 'ko', 1318 name: 'Korean', 1319 }, 1320 { 1321 code3: 'kos', 1322 code2: '', 1323 name: 'Kosraean', 1324 }, 1325 { 1326 code3: 'kpe', 1327 code2: '', 1328 name: 'Kpelle', 1329 }, 1330 { 1331 code3: 'krc', 1332 code2: '', 1333 name: 'Karachay-Balkar', 1334 }, 1335 { 1336 code3: 'krl', 1337 code2: '', 1338 name: 'Karelian', 1339 }, 1340 { 1341 code3: 'kro', 1342 code2: '', 1343 name: 'Kru languages', 1344 }, 1345 { 1346 code3: 'kru', 1347 code2: '', 1348 name: 'Kurukh', 1349 }, 1350 { 1351 code3: 'kua', 1352 code2: 'kj', 1353 name: 'Kuanyama; Kwanyama', 1354 }, 1355 { 1356 code3: 'kum', 1357 code2: '', 1358 name: 'Kumyk', 1359 }, 1360 { 1361 code3: 'kur', 1362 code2: 'ku', 1363 name: 'Kurdish', 1364 }, 1365 { 1366 code3: 'kut', 1367 code2: '', 1368 name: 'Kutenai', 1369 }, 1370 { 1371 code3: 'lad', 1372 code2: '', 1373 name: 'Ladino', 1374 }, 1375 { 1376 code3: 'lah', 1377 code2: '', 1378 name: 'Lahnda', 1379 }, 1380 { 1381 code3: 'lam', 1382 code2: '', 1383 name: 'Lamba', 1384 }, 1385 { 1386 code3: 'lao', 1387 code2: 'lo', 1388 name: 'Lao', 1389 }, 1390 { 1391 code3: 'lat', 1392 code2: 'la', 1393 name: 'Latin', 1394 }, 1395 { 1396 code3: 'lav', 1397 code2: 'lv', 1398 name: 'Latvian', 1399 }, 1400 { 1401 code3: 'lez', 1402 code2: '', 1403 name: 'Lezghian', 1404 }, 1405 { 1406 code3: 'lim', 1407 code2: 'li', 1408 name: 'Limburgish', 1409 }, 1410 { 1411 code3: 'lin', 1412 code2: 'ln', 1413 name: 'Lingala', 1414 }, 1415 { 1416 code3: 'lit', 1417 code2: 'lt', 1418 name: 'Lithuanian', 1419 }, 1420 { 1421 code3: 'lol', 1422 code2: '', 1423 name: 'Mongo', 1424 }, 1425 { 1426 code3: 'loz', 1427 code2: '', 1428 name: 'Lozi', 1429 }, 1430 { 1431 code3: 'ltz', 1432 code2: 'lb', 1433 name: 'Luxembourgish', 1434 }, 1435 { 1436 code3: 'lua', 1437 code2: '', 1438 name: 'Luba-Lulua', 1439 }, 1440 { 1441 code3: 'lub', 1442 code2: 'lu', 1443 name: 'Luba-Katanga', 1444 }, 1445 { 1446 code3: 'lug', 1447 code2: 'lg', 1448 name: 'Ganda', 1449 }, 1450 { 1451 code3: 'lui', 1452 code2: '', 1453 name: 'Luiseno', 1454 }, 1455 { 1456 code3: 'lun', 1457 code2: '', 1458 name: 'Lunda', 1459 }, 1460 { 1461 code3: 'luo', 1462 code2: '', 1463 name: 'Luo (Kenya and Tanzania)', 1464 }, 1465 { 1466 code3: 'lus', 1467 code2: '', 1468 name: 'Mizo', 1469 }, 1470 { 1471 code3: 'mac', 1472 code2: 'mk', 1473 name: 'Macedonian', 1474 }, 1475 { 1476 code3: 'mad', 1477 code2: '', 1478 name: 'Madurese', 1479 }, 1480 { 1481 code3: 'mag', 1482 code2: '', 1483 name: 'Magahi', 1484 }, 1485 { 1486 code3: 'mah', 1487 code2: 'mh', 1488 name: 'Marshallese', 1489 }, 1490 { 1491 code3: 'mai', 1492 code2: '', 1493 name: 'Maithili', 1494 }, 1495 { 1496 code3: 'mak', 1497 code2: '', 1498 name: 'Makasar', 1499 }, 1500 { 1501 code3: 'mal', 1502 code2: 'ml', 1503 name: 'Malayalam', 1504 }, 1505 { 1506 code3: 'man', 1507 code2: '', 1508 name: 'Mandingo', 1509 }, 1510 { 1511 code3: 'mao', 1512 code2: 'mi', 1513 name: 'Māori', 1514 }, 1515 { 1516 code3: 'map', 1517 code2: '', 1518 name: 'Austronesian languages', 1519 }, 1520 { 1521 code3: 'mar', 1522 code2: 'mr', 1523 name: 'Marathi', 1524 }, 1525 { 1526 code3: 'mas', 1527 code2: '', 1528 name: 'Masai', 1529 }, 1530 { 1531 code3: 'may', 1532 code2: 'ms', 1533 name: 'Malay', 1534 }, 1535 { 1536 code3: 'mdf', 1537 code2: '', 1538 name: 'Moksha', 1539 }, 1540 { 1541 code3: 'mdr', 1542 code2: '', 1543 name: 'Mandar', 1544 }, 1545 { 1546 code3: 'men', 1547 code2: '', 1548 name: 'Mende', 1549 }, 1550 { 1551 code3: 'mga', 1552 code2: '', 1553 name: 'Irish, Middle (900-1200)', 1554 }, 1555 { 1556 code3: 'mic', 1557 code2: '', 1558 name: "Mi'kmaq; Micmac", 1559 }, 1560 { 1561 code3: 'min', 1562 code2: '', 1563 name: 'Minangkabau', 1564 }, 1565 { 1566 code3: 'mis', 1567 code2: '', 1568 name: 'Uncoded languages', 1569 }, 1570 { 1571 code3: 'mkd', 1572 code2: 'mk', 1573 name: 'Macedonian', 1574 }, 1575 { 1576 code3: 'mkh', 1577 code2: '', 1578 name: 'Mon-Khmer languages', 1579 }, 1580 { 1581 code3: 'mlg', 1582 code2: 'mg', 1583 name: 'Malagasy', 1584 }, 1585 { 1586 code3: 'mlt', 1587 code2: 'mt', 1588 name: 'Maltese', 1589 }, 1590 { 1591 code3: 'mnc', 1592 code2: '', 1593 name: 'Manchu', 1594 }, 1595 { 1596 code3: 'mni', 1597 code2: '', 1598 name: 'Manipuri', 1599 }, 1600 { 1601 code3: 'mno', 1602 code2: '', 1603 name: 'Manobo languages', 1604 }, 1605 { 1606 code3: 'moh', 1607 code2: '', 1608 name: 'Mohawk', 1609 }, 1610 { 1611 code3: 'mon', 1612 code2: 'mn', 1613 name: 'Mongolian', 1614 }, 1615 { 1616 code3: 'mos', 1617 code2: '', 1618 name: 'Mossi', 1619 }, 1620 { 1621 code3: 'mri', 1622 code2: 'mi', 1623 name: 'Māori', 1624 }, 1625 { 1626 code3: 'msa', 1627 code2: 'ms', 1628 name: 'Malay', 1629 }, 1630 { 1631 code3: 'mul', 1632 code2: '', 1633 name: 'Multiple languages', 1634 }, 1635 { 1636 code3: 'mun', 1637 code2: '', 1638 name: 'Munda languages', 1639 }, 1640 { 1641 code3: 'mus', 1642 code2: '', 1643 name: 'Creek', 1644 }, 1645 { 1646 code3: 'mwl', 1647 code2: '', 1648 name: 'Mirandese', 1649 }, 1650 { 1651 code3: 'mwr', 1652 code2: '', 1653 name: 'Marwari', 1654 }, 1655 { 1656 code3: 'mya', 1657 code2: 'my', 1658 name: 'Burmese', 1659 }, 1660 { 1661 code3: 'myn', 1662 code2: '', 1663 name: 'Mayan languages', 1664 }, 1665 { 1666 code3: 'myv', 1667 code2: '', 1668 name: 'Erzya', 1669 }, 1670 { 1671 code3: 'nah', 1672 code2: '', 1673 name: 'Nahuatl languages', 1674 }, 1675 { 1676 code3: 'nai', 1677 code2: '', 1678 name: 'North American Indian languages', 1679 }, 1680 { 1681 code3: 'nap', 1682 code2: '', 1683 name: 'Neapolitan', 1684 }, 1685 { 1686 code3: 'nau', 1687 code2: 'na', 1688 name: 'Nauru', 1689 }, 1690 { 1691 code3: 'nav', 1692 code2: 'nv', 1693 name: 'Navajo', 1694 }, 1695 { 1696 code3: 'nbl', 1697 code2: 'nr', 1698 name: 'South Ndebele', 1699 }, 1700 { 1701 code3: 'nde', 1702 code2: 'nd', 1703 name: 'North Ndebele', 1704 }, 1705 { 1706 code3: 'ndo', 1707 code2: 'ng', 1708 name: 'Ndonga', 1709 }, 1710 { 1711 code3: 'nds', 1712 code2: '', 1713 name: 'Low German; Low Saxon; German, Low; Saxon, Low', 1714 }, 1715 { 1716 code3: 'nep', 1717 code2: 'ne', 1718 name: 'Nepali', 1719 }, 1720 { 1721 code3: 'new', 1722 code2: '', 1723 name: 'Nepal Bhasa; Newari', 1724 }, 1725 { 1726 code3: 'nia', 1727 code2: '', 1728 name: 'Nias', 1729 }, 1730 { 1731 code3: 'nic', 1732 code2: '', 1733 name: 'Niger-Kordofanian languages', 1734 }, 1735 { 1736 code3: 'niu', 1737 code2: '', 1738 name: 'Niuean', 1739 }, 1740 { 1741 code3: 'nld', 1742 code2: 'nl', 1743 name: 'Dutch', 1744 }, 1745 { 1746 code3: 'nno', 1747 code2: 'nn', 1748 name: 'Norwegian Nynorsk', 1749 }, 1750 { 1751 code3: 'nob', 1752 code2: 'nb', 1753 name: 'Norwegian Bokmål', 1754 }, 1755 { 1756 code3: 'nog', 1757 code2: '', 1758 name: 'Nogai', 1759 }, 1760 { 1761 code3: 'non', 1762 code2: '', 1763 name: 'Norse, Old', 1764 }, 1765 { 1766 code3: 'nor', 1767 code2: 'no', 1768 name: 'Norwegian', 1769 }, 1770 { 1771 code3: 'nqo', 1772 code2: '', 1773 name: "N'Ko", 1774 }, 1775 { 1776 code3: 'nso', 1777 code2: '', 1778 name: 'Northern Sotho', 1779 }, 1780 { 1781 code3: 'nub', 1782 code2: '', 1783 name: 'Nubian languages', 1784 }, 1785 { 1786 code3: 'nwc', 1787 code2: '', 1788 name: 'Classical Newari; Old Newari; Classical Nepal Bhasa', 1789 }, 1790 { 1791 code3: 'nya', 1792 code2: 'ny', 1793 name: 'Nyanja', 1794 }, 1795 { 1796 code3: 'nym', 1797 code2: '', 1798 name: 'Nyamwezi', 1799 }, 1800 { 1801 code3: 'nyn', 1802 code2: '', 1803 name: 'Nyankole', 1804 }, 1805 { 1806 code3: 'nyo', 1807 code2: '', 1808 name: 'Nyoro', 1809 }, 1810 { 1811 code3: 'nzi', 1812 code2: '', 1813 name: 'Nzima', 1814 }, 1815 { 1816 code3: 'oci', 1817 code2: 'oc', 1818 name: 'Occitan', 1819 }, 1820 { 1821 code3: 'oji', 1822 code2: 'oj', 1823 name: 'Ojibwa', 1824 }, 1825 { 1826 code3: 'ori', 1827 code2: 'or', 1828 name: 'Odia', 1829 }, 1830 { 1831 code3: 'orm', 1832 code2: 'om', 1833 name: 'Oromo', 1834 }, 1835 { 1836 code3: 'osa', 1837 code2: '', 1838 name: 'Osage', 1839 }, 1840 { 1841 code3: 'oss', 1842 code2: 'os', 1843 name: 'Ossetic', 1844 }, 1845 { 1846 code3: 'ota', 1847 code2: '', 1848 name: 'Turkish, Ottoman (1500-1928)', 1849 }, 1850 { 1851 code3: 'oto', 1852 code2: '', 1853 name: 'Otomian languages', 1854 }, 1855 { 1856 code3: 'paa', 1857 code2: '', 1858 name: 'Papuan languages', 1859 }, 1860 { 1861 code3: 'pag', 1862 code2: '', 1863 name: 'Pangasinan', 1864 }, 1865 { 1866 code3: 'pal', 1867 code2: '', 1868 name: 'Pahlavi', 1869 }, 1870 { 1871 code3: 'pam', 1872 code2: '', 1873 name: 'Pampanga; Kapampangan', 1874 }, 1875 { 1876 code3: 'pan', 1877 code2: 'pa', 1878 name: 'Punjabi', 1879 }, 1880 { 1881 code3: 'pap', 1882 code2: '', 1883 name: 'Papiamento', 1884 }, 1885 { 1886 code3: 'pau', 1887 code2: '', 1888 name: 'Palauan', 1889 }, 1890 { 1891 code3: 'peo', 1892 code2: '', 1893 name: 'Persian, Old (ca.600-400 B.C.)', 1894 }, 1895 { 1896 code3: 'per', 1897 code2: 'fa', 1898 name: 'Persian', 1899 }, 1900 { 1901 code3: 'phi', 1902 code2: '', 1903 name: 'Philippine languages', 1904 }, 1905 { 1906 code3: 'phn', 1907 code2: '', 1908 name: 'Phoenician', 1909 }, 1910 { 1911 code3: 'pli', 1912 code2: 'pi', 1913 name: 'Pali', 1914 }, 1915 { 1916 code3: 'pol', 1917 code2: 'pl', 1918 name: 'Polish', 1919 }, 1920 { 1921 code3: 'pon', 1922 code2: '', 1923 name: 'Pohnpeian', 1924 }, 1925 { 1926 code3: 'por', 1927 code2: 'pt', 1928 name: 'Portuguese', 1929 }, 1930 { 1931 code3: 'pra', 1932 code2: '', 1933 name: 'Prakrit languages', 1934 }, 1935 { 1936 code3: 'pro', 1937 code2: '', 1938 name: 'Provençal, Old (to 1500);Occitan, Old (to 1500)', 1939 }, 1940 { 1941 code3: 'pus', 1942 code2: 'ps', 1943 name: 'Pashto', 1944 }, 1945 { 1946 code3: 'que', 1947 code2: 'qu', 1948 name: 'Quechua', 1949 }, 1950 { 1951 code3: 'raj', 1952 code2: '', 1953 name: 'Rajasthani', 1954 }, 1955 { 1956 code3: 'rap', 1957 code2: '', 1958 name: 'Rapanui', 1959 }, 1960 { 1961 code3: 'rar', 1962 code2: '', 1963 name: 'Rarotongan; Cook Islands Maori', 1964 }, 1965 { 1966 code3: 'roa', 1967 code2: '', 1968 name: 'Romance languages', 1969 }, 1970 { 1971 code3: 'roh', 1972 code2: 'rm', 1973 name: 'Romansh', 1974 }, 1975 { 1976 code3: 'rom', 1977 code2: '', 1978 name: 'Romany', 1979 }, 1980 { 1981 code3: 'rum', 1982 code2: 'ro', 1983 name: 'Romanian', 1984 }, 1985 { 1986 code3: 'ron', 1987 code2: 'ro', 1988 name: 'Romanian', 1989 }, 1990 { 1991 code3: 'run', 1992 code2: 'rn', 1993 name: 'Rundi', 1994 }, 1995 { 1996 code3: 'rup', 1997 code2: '', 1998 name: 'Aromanian; Arumanian; Macedo-Romanian', 1999 }, 2000 { 2001 code3: 'rus', 2002 code2: 'ru', 2003 name: 'Russian', 2004 }, 2005 { 2006 code3: 'sad', 2007 code2: '', 2008 name: 'Sandawe', 2009 }, 2010 { 2011 code3: 'sag', 2012 code2: 'sg', 2013 name: 'Sango', 2014 }, 2015 { 2016 code3: 'sah', 2017 code2: '', 2018 name: 'Yakut', 2019 }, 2020 { 2021 code3: 'sai', 2022 code2: '', 2023 name: 'South American Indian languages', 2024 }, 2025 { 2026 code3: 'sal', 2027 code2: '', 2028 name: 'Salishan languages', 2029 }, 2030 { 2031 code3: 'sam', 2032 code2: '', 2033 name: 'Samaritan Aramaic', 2034 }, 2035 { 2036 code3: 'san', 2037 code2: 'sa', 2038 name: 'Sanskrit', 2039 }, 2040 { 2041 code3: 'sas', 2042 code2: '', 2043 name: 'Sasak', 2044 }, 2045 { 2046 code3: 'sat', 2047 code2: '', 2048 name: 'Santali', 2049 }, 2050 { 2051 code3: 'scn', 2052 code2: '', 2053 name: 'Sicilian', 2054 }, 2055 { 2056 code3: 'sco', 2057 code2: '', 2058 name: 'Scots', 2059 }, 2060 { 2061 code3: 'sel', 2062 code2: '', 2063 name: 'Selkup', 2064 }, 2065 { 2066 code3: 'sem', 2067 code2: '', 2068 name: 'Semitic languages', 2069 }, 2070 { 2071 code3: 'sga', 2072 code2: '', 2073 name: 'Irish, Old (to 900)', 2074 }, 2075 { 2076 code3: 'sgn', 2077 code2: '', 2078 name: 'Sign Languages', 2079 }, 2080 { 2081 code3: 'shn', 2082 code2: '', 2083 name: 'Shan', 2084 }, 2085 { 2086 code3: 'sid', 2087 code2: '', 2088 name: 'Sidamo', 2089 }, 2090 { 2091 code3: 'sin', 2092 code2: 'si', 2093 name: 'Sinhala', 2094 }, 2095 { 2096 code3: 'sio', 2097 code2: '', 2098 name: 'Siouan languages', 2099 }, 2100 { 2101 code3: 'sit', 2102 code2: '', 2103 name: 'Sino-Tibetan languages', 2104 }, 2105 { 2106 code3: 'sla', 2107 code2: '', 2108 name: 'Slavic languages', 2109 }, 2110 { 2111 code3: 'slo', 2112 code2: 'sk', 2113 name: 'Slovak', 2114 }, 2115 { 2116 code3: 'slk', 2117 code2: 'sk', 2118 name: 'Slovak', 2119 }, 2120 { 2121 code3: 'slv', 2122 code2: 'sl', 2123 name: 'Slovenian', 2124 }, 2125 { 2126 code3: 'sma', 2127 code2: '', 2128 name: 'Southern Sami', 2129 }, 2130 { 2131 code3: 'sme', 2132 code2: 'se', 2133 name: 'Northern Sami', 2134 }, 2135 { 2136 code3: 'smi', 2137 code2: '', 2138 name: 'Sami languages', 2139 }, 2140 { 2141 code3: 'smj', 2142 code2: '', 2143 name: 'Lule Sami', 2144 }, 2145 { 2146 code3: 'smn', 2147 code2: '', 2148 name: 'Inari Sami', 2149 }, 2150 { 2151 code3: 'smo', 2152 code2: 'sm', 2153 name: 'Samoan', 2154 }, 2155 { 2156 code3: 'sms', 2157 code2: '', 2158 name: 'Skolt Sami', 2159 }, 2160 { 2161 code3: 'sna', 2162 code2: 'sn', 2163 name: 'Shona', 2164 }, 2165 { 2166 code3: 'snd', 2167 code2: 'sd', 2168 name: 'Sindhi', 2169 }, 2170 { 2171 code3: 'snk', 2172 code2: '', 2173 name: 'Soninke', 2174 }, 2175 { 2176 code3: 'sog', 2177 code2: '', 2178 name: 'Sogdian', 2179 }, 2180 { 2181 code3: 'som', 2182 code2: 'so', 2183 name: 'Somali', 2184 }, 2185 { 2186 code3: 'son', 2187 code2: '', 2188 name: 'Songhai languages', 2189 }, 2190 { 2191 code3: 'sot', 2192 code2: 'st', 2193 name: 'Southern Sotho', 2194 }, 2195 { 2196 code3: 'spa', 2197 code2: 'es', 2198 name: 'Spanish', 2199 }, 2200 { 2201 code3: 'sqi', 2202 code2: 'sq', 2203 name: 'Albanian', 2204 }, 2205 { 2206 code3: 'srd', 2207 code2: 'sc', 2208 name: 'Sardinian', 2209 }, 2210 { 2211 code3: 'srn', 2212 code2: '', 2213 name: 'Sranan Tongo', 2214 }, 2215 { 2216 code3: 'srp', 2217 code2: 'sr', 2218 name: 'Serbian', 2219 }, 2220 { 2221 code3: 'srr', 2222 code2: '', 2223 name: 'Serer', 2224 }, 2225 { 2226 code3: 'ssa', 2227 code2: '', 2228 name: 'Nilo-Saharan languages', 2229 }, 2230 { 2231 code3: 'ssw', 2232 code2: 'ss', 2233 name: 'Swati', 2234 }, 2235 { 2236 code3: 'suk', 2237 code2: '', 2238 name: 'Sukuma', 2239 }, 2240 { 2241 code3: 'sun', 2242 code2: 'su', 2243 name: 'Sundanese', 2244 }, 2245 { 2246 code3: 'sus', 2247 code2: '', 2248 name: 'Susu', 2249 }, 2250 { 2251 code3: 'sux', 2252 code2: '', 2253 name: 'Sumerian', 2254 }, 2255 { 2256 code3: 'swa', 2257 code2: 'sw', 2258 name: 'Swahili', 2259 }, 2260 { 2261 code3: 'swe', 2262 code2: 'sv', 2263 name: 'Swedish', 2264 }, 2265 { 2266 code3: 'syc', 2267 code2: '', 2268 name: 'Classical Syriac', 2269 }, 2270 { 2271 code3: 'syr', 2272 code2: '', 2273 name: 'Syriac', 2274 }, 2275 { 2276 code3: 'tah', 2277 code2: 'ty', 2278 name: 'Tahitian', 2279 }, 2280 { 2281 code3: 'tai', 2282 code2: '', 2283 name: 'Tai languages', 2284 }, 2285 { 2286 code3: 'tam', 2287 code2: 'ta', 2288 name: 'Tamil', 2289 }, 2290 { 2291 code3: 'tat', 2292 code2: 'tt', 2293 name: 'Tatar', 2294 }, 2295 { 2296 code3: 'tel', 2297 code2: 'te', 2298 name: 'Telugu', 2299 }, 2300 { 2301 code3: 'tem', 2302 code2: '', 2303 name: 'Timne', 2304 }, 2305 { 2306 code3: 'ter', 2307 code2: '', 2308 name: 'Tereno', 2309 }, 2310 { 2311 code3: 'tet', 2312 code2: '', 2313 name: 'Tetum', 2314 }, 2315 { 2316 code3: 'tgk', 2317 code2: 'tg', 2318 name: 'Tajik', 2319 }, 2320 { 2321 code3: 'tgl', 2322 code2: 'tl', 2323 name: 'Filipino', 2324 }, 2325 { 2326 code3: 'tha', 2327 code2: 'th', 2328 name: 'Thai', 2329 }, 2330 { 2331 code3: 'tib', 2332 code2: 'bo', 2333 name: 'Tibetan', 2334 }, 2335 { 2336 code3: 'tig', 2337 code2: '', 2338 name: 'Tigre', 2339 }, 2340 { 2341 code3: 'tir', 2342 code2: 'ti', 2343 name: 'Tigrinya', 2344 }, 2345 { 2346 code3: 'tiv', 2347 code2: '', 2348 name: 'Tiv', 2349 }, 2350 { 2351 code3: 'tkl', 2352 code2: '', 2353 name: 'Tokelau', 2354 }, 2355 { 2356 code3: 'tlh', 2357 code2: '', 2358 name: 'Klingon; tlhIngan-Hol', 2359 }, 2360 { 2361 code3: 'tli', 2362 code2: '', 2363 name: 'Tlingit', 2364 }, 2365 { 2366 code3: 'tmh', 2367 code2: '', 2368 name: 'Tamashek', 2369 }, 2370 { 2371 code3: 'tog', 2372 code2: '', 2373 name: 'Tonga (Nyasa)', 2374 }, 2375 { 2376 code3: 'ton', 2377 code2: 'to', 2378 name: 'Tongan', 2379 }, 2380 { 2381 code3: 'tpi', 2382 code2: '', 2383 name: 'Tok Pisin', 2384 }, 2385 { 2386 code3: 'tsi', 2387 code2: '', 2388 name: 'Tsimshian', 2389 }, 2390 { 2391 code3: 'tsn', 2392 code2: 'tn', 2393 name: 'Tswana', 2394 }, 2395 { 2396 code3: 'tso', 2397 code2: 'ts', 2398 name: 'Tsonga', 2399 }, 2400 { 2401 code3: 'tuk', 2402 code2: 'tk', 2403 name: 'Turkmen', 2404 }, 2405 { 2406 code3: 'tum', 2407 code2: '', 2408 name: 'Tumbuka', 2409 }, 2410 { 2411 code3: 'tup', 2412 code2: '', 2413 name: 'Tupi languages', 2414 }, 2415 { 2416 code3: 'tur', 2417 code2: 'tr', 2418 name: 'Turkish', 2419 }, 2420 { 2421 code3: 'tut', 2422 code2: '', 2423 name: 'Altaic languages', 2424 }, 2425 { 2426 code3: 'tvl', 2427 code2: '', 2428 name: 'Tuvalu', 2429 }, 2430 { 2431 code3: 'twi', 2432 code2: 'tw', 2433 name: 'Akan', 2434 }, 2435 { 2436 code3: 'tyv', 2437 code2: '', 2438 name: 'Tuvinian', 2439 }, 2440 { 2441 code3: 'udm', 2442 code2: '', 2443 name: 'Udmurt', 2444 }, 2445 { 2446 code3: 'uga', 2447 code2: '', 2448 name: 'Ugaritic', 2449 }, 2450 { 2451 code3: 'uig', 2452 code2: 'ug', 2453 name: 'Uyghur', 2454 }, 2455 { 2456 code3: 'ukr', 2457 code2: 'uk', 2458 name: 'Ukrainian', 2459 }, 2460 { 2461 code3: 'umb', 2462 code2: '', 2463 name: 'Umbundu', 2464 }, 2465 { 2466 code3: 'und', 2467 code2: '', 2468 name: 'Undetermined', 2469 }, 2470 { 2471 code3: 'urd', 2472 code2: 'ur', 2473 name: 'Urdu', 2474 }, 2475 { 2476 code3: 'uzb', 2477 code2: 'uz', 2478 name: 'Uzbek', 2479 }, 2480 { 2481 code3: 'vai', 2482 code2: '', 2483 name: 'Vai', 2484 }, 2485 { 2486 code3: 'ven', 2487 code2: 've', 2488 name: 'Venda', 2489 }, 2490 { 2491 code3: 'vie', 2492 code2: 'vi', 2493 name: 'Vietnamese', 2494 }, 2495 { 2496 code3: 'vol', 2497 code2: 'vo', 2498 name: 'Volapük', 2499 }, 2500 { 2501 code3: 'vot', 2502 code2: '', 2503 name: 'Votic', 2504 }, 2505 { 2506 code3: 'wak', 2507 code2: '', 2508 name: 'Wakashan languages', 2509 }, 2510 { 2511 code3: 'wal', 2512 code2: '', 2513 name: 'Wolaitta; Wolaytta', 2514 }, 2515 { 2516 code3: 'war', 2517 code2: '', 2518 name: 'Waray', 2519 }, 2520 { 2521 code3: 'was', 2522 code2: '', 2523 name: 'Washo', 2524 }, 2525 { 2526 code3: 'wel', 2527 code2: 'cy', 2528 name: 'Welsh', 2529 }, 2530 { 2531 code3: 'wen', 2532 code2: '', 2533 name: 'Sorbian languages', 2534 }, 2535 { 2536 code3: 'wln', 2537 code2: 'wa', 2538 name: 'Walloon', 2539 }, 2540 { 2541 code3: 'wol', 2542 code2: 'wo', 2543 name: 'Wolof', 2544 }, 2545 { 2546 code3: 'xal', 2547 code2: '', 2548 name: 'Kalmyk; Oirat', 2549 }, 2550 { 2551 code3: 'xho', 2552 code2: 'xh', 2553 name: 'Xhosa', 2554 }, 2555 { 2556 code3: 'yao', 2557 code2: '', 2558 name: 'Yao', 2559 }, 2560 { 2561 code3: 'yap', 2562 code2: '', 2563 name: 'Yapese', 2564 }, 2565 { 2566 code3: 'yid', 2567 code2: 'yi', 2568 name: 'Yiddish', 2569 }, 2570 { 2571 code3: 'yor', 2572 code2: 'yo', 2573 name: 'Yoruba', 2574 }, 2575 { 2576 code3: 'ypk', 2577 code2: '', 2578 name: 'Yupik languages', 2579 }, 2580 { 2581 code3: 'zap', 2582 code2: '', 2583 name: 'Zapotec', 2584 }, 2585 { 2586 code3: 'zbl', 2587 code2: '', 2588 name: 'Blissymbols; Blissymbolics; Bliss', 2589 }, 2590 { 2591 code3: 'zen', 2592 code2: '', 2593 name: 'Zenaga', 2594 }, 2595 { 2596 code3: 'zgh', 2597 code2: '', 2598 name: 'Standard Moroccan Tamazight', 2599 }, 2600 { 2601 code3: 'zha', 2602 code2: 'za', 2603 name: 'Zhuang; Chuang', 2604 }, 2605 { 2606 code3: 'zho', 2607 code2: 'zh', 2608 name: 'Chinese', 2609 }, 2610 { 2611 code3: 'znd', 2612 code2: '', 2613 name: 'Zande languages', 2614 }, 2615 { 2616 code3: 'zul', 2617 code2: 'zu', 2618 name: 'Zulu', 2619 }, 2620 { 2621 code3: 'zun', 2622 code2: '', 2623 name: 'Zuni', 2624 }, 2625 { 2626 code3: 'zza', 2627 code2: '', 2628 name: 'Zaza; Dimili; Dimli; Kirdki; Kirmanjki; Zazaki', 2629 }, 2630 ] 2631
+13 -118
src/screens/PostThread/components/ThreadItemAnchor.tsx
··· 1 import {memo, useCallback, useMemo} from 'react' 2 - import {type GestureResponderEvent, Text as RNText, View} from 'react-native' 3 import { 4 AppBskyFeedDefs, 5 AppBskyFeedPost, ··· 15 import {sanitizeHandle} from '#/lib/strings/handles' 16 import {niceDate} from '#/lib/strings/time' 17 import { 18 - getPostLanguage, 19 - getTranslatorLink, 20 - isPostInLanguage, 21 - } from '#/locale/helpers' 22 - import { 23 POST_TOMBSTONE, 24 type Shadow, 25 usePostShadow, 26 } from '#/state/cache/post-shadow' 27 import {useProfileShadow} from '#/state/cache/profile-shadow' 28 import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback' 29 - import {useLanguagePrefs} from '#/state/preferences' 30 import {type ThreadItem} from '#/state/queries/usePostThread/types' 31 import {useSession} from '#/state/session' 32 import {type OnPostSuccessData} from '#/state/shell/composer' ··· 44 import {DebugFieldDisplay} from '#/components/DebugFieldDisplay' 45 import {CalendarClock_Stroke2_Corner0_Rounded as CalendarClockIcon} from '#/components/icons/CalendarClock' 46 import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash' 47 - import {InlineLinkText, Link} from '#/components/Link' 48 - import {Loader} from '#/components/Loader' 49 import {ContentHider} from '#/components/moderation/ContentHider' 50 import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe' 51 import {PostAlerts} from '#/components/moderation/PostAlerts' ··· 63 import {WhoCanReply} from '#/components/WhoCanReply' 64 import {useAnalytics} from '#/analytics' 65 import {useActorStatus} from '#/features/liveNow' 66 - import { 67 - Provider as TranslateOnDeviceProvider, 68 - useTranslateOnDevice, 69 - } from '#/translation' 70 import * as bsky from '#/types/bsky' 71 72 export function ThreadItemAnchor({ ··· 89 } 90 91 return ( 92 - <TranslateOnDeviceProvider> 93 - <ThreadItemAnchorInner 94 - // Safeguard from clobbering per-post state below: 95 - key={postShadow.uri} 96 - item={item} 97 - isRoot={isRoot} 98 - postShadow={postShadow} 99 - onPostSuccess={onPostSuccess} 100 - threadgateRecord={threadgateRecord} 101 - postSource={postSource} 102 - /> 103 - </TranslateOnDeviceProvider> 104 ) 105 } 106 ··· 420 shouldProxyLinks={true} 421 /> 422 ) : undefined} 423 - <TranslatedPost postText={record.text} hideLoading /> 424 - <TranslateLink post={item.value.post} /> 425 {post.embed && ( 426 <View style={[a.py_xs]}> 427 <Embed ··· 556 </> 557 ) 558 }) 559 - 560 - function TranslateLink({ 561 - post, 562 - }: { 563 - post: Extract<ThreadItem, {type: 'threadPost'}>['value']['post'] 564 - }) { 565 - const t = useTheme() 566 - const ax = useAnalytics() 567 - const {t: l} = useLingui() 568 - const langPrefs = useLanguagePrefs() 569 - 570 - const {translate, clearTranslation, translationState} = useTranslateOnDevice() 571 - 572 - const needsTranslation = useMemo( 573 - () => 574 - Boolean( 575 - langPrefs.primaryLanguage && 576 - !isPostInLanguage(post, [langPrefs.primaryLanguage]), 577 - ), 578 - [post, langPrefs.primaryLanguage], 579 - ) 580 - 581 - const sourceLanguage = getPostLanguage(post) 582 - 583 - const onTranslatePress = useCallback( 584 - (e: GestureResponderEvent) => { 585 - e.preventDefault() 586 - void translate( 587 - post.record.text || '', 588 - langPrefs.primaryLanguage, 589 - sourceLanguage, 590 - ) 591 - 592 - if ( 593 - bsky.dangerousIsType<AppBskyFeedPost.Record>( 594 - post.record, 595 - AppBskyFeedPost.isRecord, 596 - ) 597 - ) { 598 - ax.metric('translate', { 599 - sourceLanguages: post.record.langs ?? [], 600 - targetLanguage: langPrefs.primaryLanguage, 601 - textLength: post.record.text.length, 602 - }) 603 - } 604 - 605 - return false 606 - }, 607 - [ax, sourceLanguage, translate, langPrefs, post], 608 - ) 609 - 610 - const onHideTranslation = useCallback( 611 - (e: GestureResponderEvent) => { 612 - e.preventDefault() 613 - clearTranslation() 614 - return false 615 - }, 616 - [clearTranslation], 617 - ) 618 - 619 - return ( 620 - needsTranslation && ( 621 - <View style={[a.gap_md, a.pt_md, a.align_start]}> 622 - {translationState.status === 'loading' ? ( 623 - <View style={[a.flex_row, a.align_center, a.gap_xs]}> 624 - <Loader size="xs" /> 625 - <Text style={[a.text_sm, t.atoms.text_contrast_medium]}> 626 - <Trans>Translating…</Trans> 627 - </Text> 628 - </View> 629 - ) : translationState.status === 'success' ? ( 630 - <InlineLinkText 631 - to="#" 632 - label={l`Hide translation`} 633 - style={[a.text_sm]} 634 - onPress={onHideTranslation}> 635 - <Trans>Hide translation</Trans> 636 - </InlineLinkText> 637 - ) : ( 638 - <InlineLinkText 639 - to={getTranslatorLink(post.record.text, langPrefs.primaryLanguage)} 640 - label={l`Translate`} 641 - style={[a.text_sm]} 642 - onPress={onTranslatePress}> 643 - <Trans>Translate</Trans> 644 - </InlineLinkText> 645 - )} 646 - </View> 647 - ) 648 - ) 649 - } 650 651 function ExpandedPostDetails({ 652 post,
··· 1 import {memo, useCallback, useMemo} from 'react' 2 + import {Text as RNText, View} from 'react-native' 3 import { 4 AppBskyFeedDefs, 5 AppBskyFeedPost, ··· 15 import {sanitizeHandle} from '#/lib/strings/handles' 16 import {niceDate} from '#/lib/strings/time' 17 import { 18 POST_TOMBSTONE, 19 type Shadow, 20 usePostShadow, 21 } from '#/state/cache/post-shadow' 22 import {useProfileShadow} from '#/state/cache/profile-shadow' 23 import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback' 24 import {type ThreadItem} from '#/state/queries/usePostThread/types' 25 import {useSession} from '#/state/session' 26 import {type OnPostSuccessData} from '#/state/shell/composer' ··· 38 import {DebugFieldDisplay} from '#/components/DebugFieldDisplay' 39 import {CalendarClock_Stroke2_Corner0_Rounded as CalendarClockIcon} from '#/components/icons/CalendarClock' 40 import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash' 41 + import {Link} from '#/components/Link' 42 import {ContentHider} from '#/components/moderation/ContentHider' 43 import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe' 44 import {PostAlerts} from '#/components/moderation/PostAlerts' ··· 56 import {WhoCanReply} from '#/components/WhoCanReply' 57 import {useAnalytics} from '#/analytics' 58 import {useActorStatus} from '#/features/liveNow' 59 import * as bsky from '#/types/bsky' 60 61 export function ThreadItemAnchor({ ··· 78 } 79 80 return ( 81 + <ThreadItemAnchorInner 82 + // Safeguard from clobbering per-post state below: 83 + key={postShadow.uri} 84 + item={item} 85 + isRoot={isRoot} 86 + postShadow={postShadow} 87 + onPostSuccess={onPostSuccess} 88 + threadgateRecord={threadgateRecord} 89 + postSource={postSource} 90 + /> 91 ) 92 } 93 ··· 407 shouldProxyLinks={true} 408 /> 409 ) : undefined} 410 + <TranslatedPost post={post} postText={record.text} /> 411 {post.embed && ( 412 <View style={[a.py_xs]}> 413 <Embed ··· 542 </> 543 ) 544 }) 545 546 function ExpandedPostDetails({ 547 post,
+6
src/screens/PostThread/components/ThreadItemPost.tsx
··· 38 import {type AppModerationCause} from '#/components/Pills' 39 import {Embed, PostEmbedViewContext} from '#/components/Post/Embed' 40 import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton' 41 import {PostControls, PostControlsSkeleton} from '#/components/PostControls' 42 import {RichText} from '#/components/RichText' 43 import * as Skele from '#/components/Skeleton' ··· 320 )} 321 </View> 322 ) : undefined} 323 {post.embed && ( 324 <View style={[a.pb_xs]}> 325 <Embed
··· 38 import {type AppModerationCause} from '#/components/Pills' 39 import {Embed, PostEmbedViewContext} from '#/components/Post/Embed' 40 import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton' 41 + import {TranslatedPost} from '#/components/Post/Translated' 42 import {PostControls, PostControlsSkeleton} from '#/components/PostControls' 43 import {RichText} from '#/components/RichText' 44 import * as Skele from '#/components/Skeleton' ··· 321 )} 322 </View> 323 ) : undefined} 324 + <TranslatedPost 325 + hideTranslateLink={true} 326 + post={post} 327 + postText={record.text} 328 + /> 329 {post.embed && ( 330 <View style={[a.pb_xs]}> 331 <Embed
+6
src/screens/PostThread/components/ThreadItemTreePost.tsx
··· 38 import {type AppModerationCause} from '#/components/Pills' 39 import {Embed, PostEmbedViewContext} from '#/components/Post/Embed' 40 import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton' 41 import {PostControls, PostControlsSkeleton} from '#/components/PostControls' 42 import {RichText} from '#/components/RichText' 43 import * as Skele from '#/components/Skeleton' ··· 360 )} 361 </View> 362 ) : null} 363 {post.embed && ( 364 <View style={[a.pb_xs]}> 365 <Embed
··· 38 import {type AppModerationCause} from '#/components/Pills' 39 import {Embed, PostEmbedViewContext} from '#/components/Post/Embed' 40 import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton' 41 + import {TranslatedPost} from '#/components/Post/Translated' 42 import {PostControls, PostControlsSkeleton} from '#/components/PostControls' 43 import {RichText} from '#/components/RichText' 44 import * as Skele from '#/components/Skeleton' ··· 361 )} 362 </View> 363 ) : null} 364 + <TranslatedPost 365 + hideTranslateLink={true} 366 + post={post} 367 + postText={record.text} 368 + /> 369 {post.embed && ( 370 <View style={[a.pb_xs]}> 371 <Embed
+30 -38
src/screens/VideoFeed/index.tsx
··· 33 type ModerationDecision, 34 RichText as RichTextAPI, 35 } from '@atproto/api' 36 - import {msg} from '@lingui/core/macro' 37 - import {useLingui} from '@lingui/react' 38 - import {Trans} from '@lingui/react/macro' 39 import { 40 type RouteProp, 41 useFocusEffect, ··· 451 } 452 onEndReached={() => { 453 if (hasNextPage && !isFetchingNextPage) { 454 - fetchNextPage() 455 } 456 }} 457 showsVerticalScrollIndicator={false} ··· 515 } 516 } 517 }, [ 518 active, 519 post.uri, 520 post.author.did, ··· 621 embed: AppBskyEmbedVideo.View 622 onPressShow: () => void 623 }) { 624 - const {_} = useLingui() 625 const hider = Hider.useHider() 626 const {bottom} = useSafeAreaInsets() 627 ··· 648 <Trans>Hidden by your moderation settings.</Trans> 649 </Text> 650 <Button 651 - label={_(msg`Show anyway`)} 652 size="small" 653 variant="solid" 654 color="secondary_inverted" ··· 676 <Divider style={{borderColor: 'white'}} /> 677 <View> 678 <Button 679 - label={_(msg`View details`)} 680 onPress={() => { 681 hider.showInfoDialog() 682 }} ··· 724 feedContext: string | undefined 725 reqId: string | undefined 726 }) { 727 - const {_} = useLingui() 728 const t = useTheme() 729 const {openComposer} = useOpenComposer() 730 const {currentAccount} = useSession() ··· 811 <Animated.View style={[a.px_md, animatedStyle]}> 812 <View style={[a.w_full, a.flex_row, a.align_center, a.gap_md]}> 813 <Link 814 - label={_( 815 - msg`View ${sanitizeDisplayName( 816 - post.author.displayName || post.author.handle, 817 - )}'s profile`, 818 - )} 819 to={{ 820 screen: 'Profile', 821 params: {name: post.author.did}, ··· 848 <Button 849 label={ 850 profile.viewer?.following 851 - ? _(msg`Following ${handle}`) 852 - : _(msg`Follow ${handle}`) 853 } 854 accessibilityHint={ 855 - profile.viewer?.following 856 - ? _(msg`Unfollows the user`) 857 - : '' 858 } 859 size="small" 860 variant="solid" ··· 862 style={[a.mb_xs]} 863 onPress={() => 864 profile.viewer?.following 865 - ? queueUnfollow() 866 - : queueFollow() 867 }> 868 {!!profile.viewer?.following && ( 869 <ButtonIcon icon={CheckIcon} /> ··· 892 record={record} 893 feedContext={feedContext} 894 logContext="FeedItem" 895 onPressReply={() => 896 navigation.navigate('PostThread', { 897 name: post.author.did, ··· 947 const [hasBeenExpanded, setHasBeenExpanded] = useState(false) 948 const [constrained, setConstrained] = useState(false) 949 const [contentHeight, setContentHeight] = useState(0) 950 - const {_} = useLingui() 951 const {screenReaderEnabled} = useA11y() 952 953 if (expanded && !hasBeenExpanded) { ··· 988 /> 989 {constrained && !screenReaderEnabled && ( 990 <Pressable 991 - accessibilityHint={_(msg`Expands or collapses post text`)} 992 - accessibilityLabel={expanded ? _(msg`Read less`) : _(msg`Read more`)} 993 hitSlop={HITSLOP_20} 994 onPress={() => setExpanded(prev => !prev)} 995 style={[a.absolute, a.inset_0]} ··· 1049 feedContext: string | undefined 1050 reqId: string | undefined 1051 }) { 1052 - const {_} = useLingui() 1053 const doubleTapRef = useRef<ReturnType<typeof setTimeout> | null>(null) 1054 const playHaptic = useHaptics() 1055 // TODO: implement viaRepost -sfn ··· 1092 clearTimeout(doubleTapRef.current) 1093 doubleTapRef.current = null 1094 playHaptic('Light') 1095 - queueLike() 1096 sendInteraction({ 1097 item: post.uri, 1098 event: 'app.bsky.feed.defs#interactionLike', ··· 1107 return ( 1108 <Button 1109 disabled={!player} 1110 - aria-valuetext={ 1111 - isPlaying ? _(msg`Video is playing`) : _(msg`Video is paused`) 1112 - } 1113 - label={_( 1114 - msg`Video from ${sanitizeHandle( 1115 - post.author.handle, 1116 - '@', 1117 - )}. Tap to play or pause the video`, 1118 - )} 1119 - accessibilityHint={_(msg`Double tap to like`)} 1120 onPress={onPress} 1121 style={[a.absolute, a.inset_0, a.z_10]}> 1122 <View /> ··· 1126 1127 function EndMessage() { 1128 const navigation = useNavigation<NavigationProp>() 1129 - const {_} = useLingui() 1130 const t = useTheme() 1131 return ( 1132 <View ··· 1177 variant="solid" 1178 color="secondary_inverted" 1179 size="small" 1180 - label={_(msg`Go back`)} 1181 - accessibilityHint={_(msg`Returns to previous page`)}> 1182 <ButtonIcon icon={ArrowLeftIcon} /> 1183 <ButtonText> 1184 <Trans>Go back</Trans>
··· 33 type ModerationDecision, 34 RichText as RichTextAPI, 35 } from '@atproto/api' 36 + import {Trans, useLingui} from '@lingui/react/macro' 37 import { 38 type RouteProp, 39 useFocusEffect, ··· 449 } 450 onEndReached={() => { 451 if (hasNextPage && !isFetchingNextPage) { 452 + void fetchNextPage() 453 } 454 }} 455 showsVerticalScrollIndicator={false} ··· 513 } 514 } 515 }, [ 516 + ax, 517 active, 518 post.uri, 519 post.author.did, ··· 620 embed: AppBskyEmbedVideo.View 621 onPressShow: () => void 622 }) { 623 + const {t: l} = useLingui() 624 const hider = Hider.useHider() 625 const {bottom} = useSafeAreaInsets() 626 ··· 647 <Trans>Hidden by your moderation settings.</Trans> 648 </Text> 649 <Button 650 + label={l`Show anyway`} 651 size="small" 652 variant="solid" 653 color="secondary_inverted" ··· 675 <Divider style={{borderColor: 'white'}} /> 676 <View> 677 <Button 678 + label={l`View details`} 679 onPress={() => { 680 hider.showInfoDialog() 681 }} ··· 723 feedContext: string | undefined 724 reqId: string | undefined 725 }) { 726 + const {t: l} = useLingui() 727 const t = useTheme() 728 const {openComposer} = useOpenComposer() 729 const {currentAccount} = useSession() ··· 810 <Animated.View style={[a.px_md, animatedStyle]}> 811 <View style={[a.w_full, a.flex_row, a.align_center, a.gap_md]}> 812 <Link 813 + label={l`View ${sanitizeDisplayName( 814 + post.author.displayName || post.author.handle, 815 + )}'s profile`} 816 to={{ 817 screen: 'Profile', 818 params: {name: post.author.did}, ··· 845 <Button 846 label={ 847 profile.viewer?.following 848 + ? l`Following ${handle}` 849 + : l`Follow ${handle}` 850 } 851 accessibilityHint={ 852 + profile.viewer?.following ? l`Unfollows the user` : '' 853 } 854 size="small" 855 variant="solid" ··· 857 style={[a.mb_xs]} 858 onPress={() => 859 profile.viewer?.following 860 + ? void queueUnfollow() 861 + : void queueFollow() 862 }> 863 {!!profile.viewer?.following && ( 864 <ButtonIcon icon={CheckIcon} /> ··· 887 record={record} 888 feedContext={feedContext} 889 logContext="FeedItem" 890 + forceGoogleTranslate={true} 891 onPressReply={() => 892 navigation.navigate('PostThread', { 893 name: post.author.did, ··· 943 const [hasBeenExpanded, setHasBeenExpanded] = useState(false) 944 const [constrained, setConstrained] = useState(false) 945 const [contentHeight, setContentHeight] = useState(0) 946 + const {t: l} = useLingui() 947 const {screenReaderEnabled} = useA11y() 948 949 if (expanded && !hasBeenExpanded) { ··· 984 /> 985 {constrained && !screenReaderEnabled && ( 986 <Pressable 987 + accessibilityHint={l`Expands or collapses post text`} 988 + accessibilityLabel={expanded ? l`Read less` : l`Read more`} 989 hitSlop={HITSLOP_20} 990 onPress={() => setExpanded(prev => !prev)} 991 style={[a.absolute, a.inset_0]} ··· 1045 feedContext: string | undefined 1046 reqId: string | undefined 1047 }) { 1048 + const {t: l} = useLingui() 1049 const doubleTapRef = useRef<ReturnType<typeof setTimeout> | null>(null) 1050 const playHaptic = useHaptics() 1051 // TODO: implement viaRepost -sfn ··· 1088 clearTimeout(doubleTapRef.current) 1089 doubleTapRef.current = null 1090 playHaptic('Light') 1091 + void queueLike() 1092 sendInteraction({ 1093 item: post.uri, 1094 event: 'app.bsky.feed.defs#interactionLike', ··· 1103 return ( 1104 <Button 1105 disabled={!player} 1106 + aria-valuetext={isPlaying ? l`Video is playing` : l`Video is paused`} 1107 + label={l`Video from ${sanitizeHandle( 1108 + post.author.handle, 1109 + '@', 1110 + )}. Tap to play or pause the video`} 1111 + accessibilityHint={l`Double tap to like`} 1112 onPress={onPress} 1113 style={[a.absolute, a.inset_0, a.z_10]}> 1114 <View /> ··· 1118 1119 function EndMessage() { 1120 const navigation = useNavigation<NavigationProp>() 1121 + const {t: l} = useLingui() 1122 const t = useTheme() 1123 return ( 1124 <View ··· 1169 variant="solid" 1170 color="secondary_inverted" 1171 size="small" 1172 + label={l`Go back`} 1173 + accessibilityHint={l`Returns to previous page`}> 1174 <ButtonIcon icon={ArrowLeftIcon} /> 1175 <ButtonText> 1176 <Trans>Go back</Trans>
-199
src/translation/index.tsx
··· 1 - import React, { 2 - createContext, 3 - useCallback, 4 - useContext, 5 - useMemo, 6 - useState, 7 - } from 'react' 8 - import {LayoutAnimation, Platform} from 'react-native' 9 - import {getLocales} from 'expo-localization' 10 - import {type TranslationTaskResult} from '@bsky.app/expo-translate-text/build/ExpoTranslateText.types' 11 - 12 - import {useOpenLink} from '#/lib/hooks/useOpenLink' 13 - import {getTranslatorLink} from '#/locale/helpers' 14 - import {logger} from '#/logger' 15 - import {useLanguagePrefs} from '#/state/preferences' 16 - import {useAnalytics} from '#/analytics' 17 - import {IS_WEB} from '#/env' 18 - 19 - type TranslationState = 20 - | {status: 'idle'} 21 - | {status: 'loading'} 22 - | { 23 - status: 'success' 24 - translatedText: string 25 - sourceLanguage: TranslationTaskResult['sourceLanguage'] 26 - targetLanguage: TranslationTaskResult['targetLanguage'] 27 - } 28 - 29 - const IDLE: TranslationState = {status: 'idle'} 30 - 31 - /** 32 - * Attempts on-device translation via @bsky.app/expo-translate-text. 33 - * Uses a lazy import to avoid crashing if the native module isn't linked into 34 - * the current build. 35 - */ 36 - async function attemptTranslation( 37 - input: string, 38 - targetLangCodeOriginal: string, 39 - sourceLangCodeOriginal?: string, // Auto-detects if not provided 40 - ): Promise<{ 41 - translatedText: string 42 - targetLanguage: TranslationTaskResult['targetLanguage'] 43 - sourceLanguage: TranslationTaskResult['sourceLanguage'] 44 - }> { 45 - // Note that Android only supports two-character language codes and will fail 46 - // on other input. 47 - // https://developers.google.com/android/reference/com/google/mlkit/nl/translate/TranslateLanguage 48 - let targetLangCode = 49 - Platform.OS === 'android' 50 - ? targetLangCodeOriginal.split('-')[0] 51 - : targetLangCodeOriginal 52 - const sourceLangCode = 53 - Platform.OS === 'android' 54 - ? sourceLangCodeOriginal?.split('-')[0] 55 - : sourceLangCodeOriginal 56 - 57 - // Special cases for regional languages 58 - if (Platform.OS !== 'android') { 59 - const deviceLocales = getLocales() 60 - const primaryLanguageTag = deviceLocales[0]?.languageTag 61 - switch (targetLangCodeOriginal) { 62 - case 'en': // en-US, en-GB 63 - case 'es': // es-419, es-ES 64 - case 'pt': // pt-BR, pt-PT 65 - case 'zh': // zh-Hans-CN, zh-Hant-HK, zh-Hant-TW 66 - targetLangCode = primaryLanguageTag ?? targetLangCodeOriginal 67 - break 68 - } 69 - } 70 - 71 - const {onTranslateTask} = 72 - // Needed in order to type check the dynamically imported module. 73 - // eslint-disable-next-line @typescript-eslint/consistent-type-imports 74 - require('@bsky.app/expo-translate-text') as typeof import('@bsky.app/expo-translate-text') 75 - const result = await onTranslateTask({ 76 - input, 77 - targetLangCode, 78 - sourceLangCode, 79 - }) 80 - 81 - // Since `input` is always a string, the result should always be a string. 82 - return { 83 - translatedText: 84 - typeof result.translatedTexts === 'string' ? result.translatedTexts : '', 85 - targetLanguage: result.targetLanguage, 86 - sourceLanguage: result.sourceLanguage ?? sourceLangCode ?? null, // iOS doesn't return the source language 87 - } 88 - } 89 - 90 - const Context = createContext<{ 91 - translationState: TranslationState 92 - translate: ( 93 - text: string, 94 - targetLangCode: string, 95 - sourceLangCode?: string, 96 - ) => Promise<void> 97 - clearTranslation: () => void 98 - }>({ 99 - translationState: IDLE, 100 - translate: async () => {}, 101 - clearTranslation: () => {}, 102 - }) 103 - Context.displayName = 'TranslationContext' 104 - 105 - /** 106 - * Native translation hook. Attempts on-device translation using Apple 107 - * Translation (iOS 18+) or Google ML Kit (Android). 108 - * 109 - * Falls back to Google Translate URL if the language pack is unavailable. 110 - * 111 - * Web uses index.web.ts which always opens Google Translate. 112 - */ 113 - export function useTranslateOnDevice() { 114 - const context = useContext(Context) 115 - if (!context) { 116 - throw new Error( 117 - 'useTranslateOnDevice must be used within a TranslateOnDeviceProvider', 118 - ) 119 - } 120 - return context 121 - } 122 - 123 - export function Provider({children}: React.PropsWithChildren<unknown>) { 124 - const [translationState, setTranslationState] = 125 - useState<TranslationState>(IDLE) 126 - const openLink = useOpenLink() 127 - const ax = useAnalytics() 128 - const {primaryLanguage} = useLanguagePrefs() 129 - 130 - const clearTranslation = useCallback(() => { 131 - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) 132 - setTranslationState(IDLE) 133 - }, []) 134 - 135 - const translate = useCallback( 136 - async ( 137 - text: string, 138 - targetLangCode: string = primaryLanguage, 139 - sourceLangCode?: string, 140 - ) => { 141 - setTranslationState({status: 'loading'}) 142 - try { 143 - const result = await attemptTranslation( 144 - text, 145 - targetLangCode, 146 - sourceLangCode, 147 - ) 148 - ax.metric('translate:result', { 149 - method: 'on-device', 150 - os: Platform.OS, 151 - sourceLanguage: result.sourceLanguage, 152 - targetLanguage: result.targetLanguage, 153 - }) 154 - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) 155 - setTranslationState({ 156 - status: 'success', 157 - translatedText: result.translatedText, 158 - sourceLanguage: result.sourceLanguage, 159 - targetLanguage: result.targetLanguage, 160 - }) 161 - } catch (e) { 162 - if (IS_WEB) { 163 - // Web always opens Google Translate. 164 - ax.metric('translate:result', { 165 - method: 'google-translate', 166 - os: Platform.OS, 167 - sourceLanguage: sourceLangCode ?? null, 168 - targetLanguage: targetLangCode, 169 - }) 170 - } else { 171 - logger.error('Failed to translate post on device', {safeMessage: e}) 172 - // On-device translation failed (language pack missing or user dismissed 173 - // the download prompt). Fall back to Google Translate. 174 - ax.metric('translate:result', { 175 - method: 'fallback-alert', 176 - os: Platform.OS, 177 - sourceLanguage: sourceLangCode ?? null, 178 - targetLanguage: targetLangCode, 179 - }) 180 - } 181 - setTranslationState({status: 'idle'}) 182 - const translateUrl = getTranslatorLink( 183 - text, 184 - targetLangCode, 185 - sourceLangCode, 186 - ) 187 - await openLink(translateUrl) 188 - } 189 - }, 190 - [ax, openLink, primaryLanguage, setTranslationState], 191 - ) 192 - 193 - const ctx = useMemo( 194 - () => ({clearTranslation, translate, translationState}), 195 - [clearTranslation, translate, translationState], 196 - ) 197 - 198 - return <Context.Provider value={ctx}>{children}</Context.Provider> 199 - }
···
+7
src/view/com/post/Post.tsx
··· 33 import {Embed, PostEmbedViewContext} from '#/components/Post/Embed' 34 import {PostRepliedTo} from '#/components/Post/PostRepliedTo' 35 import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton' 36 import {PostControls} from '#/components/PostControls' 37 import {RichText} from '#/components/RichText' 38 import {SubtleHover} from '#/components/SubtleHover' ··· 152 }, [queryClient, post.author, outerOnBeforePress]) 153 154 const [hover, setHover] = useState(false) 155 return ( 156 <Link 157 href={itemHref} ··· 217 )} 218 </View> 219 ) : undefined} 220 {post.embed ? ( 221 <Embed 222 embed={post.embed}
··· 33 import {Embed, PostEmbedViewContext} from '#/components/Post/Embed' 34 import {PostRepliedTo} from '#/components/Post/PostRepliedTo' 35 import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton' 36 + import {TranslatedPost} from '#/components/Post/Translated' 37 import {PostControls} from '#/components/PostControls' 38 import {RichText} from '#/components/RichText' 39 import {SubtleHover} from '#/components/SubtleHover' ··· 153 }, [queryClient, post.author, outerOnBeforePress]) 154 155 const [hover, setHover] = useState(false) 156 + 157 return ( 158 <Link 159 href={itemHref} ··· 219 )} 220 </View> 221 ) : undefined} 222 + <TranslatedPost 223 + hideTranslateLink={true} 224 + post={post} 225 + postText={record.text} 226 + /> 227 {post.embed ? ( 228 <Embed 229 embed={post.embed}
+16
src/view/com/posts/PostFeedItem.tsx
··· 42 import {PostEmbedViewContext} from '#/components/Post/Embed/types' 43 import {PostRepliedTo} from '#/components/Post/PostRepliedTo' 44 import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton' 45 import {PostControls} from '#/components/PostControls' 46 import {DiscoverDebug} from '#/components/PostControls/DiscoverDebug' 47 import {RichText} from '#/components/RichText' ··· 450 : [] 451 }, [post, currentAccount?.did, threadgateHiddenReplies]) 452 453 const onPressShowMore = useCallback(() => { 454 setLimitLines(false) 455 }, [setLimitLines]) ··· 481 )} 482 </View> 483 ) : undefined} 484 {postEmbed ? ( 485 <View style={[a.pb_xs]}> 486 <Embed
··· 42 import {PostEmbedViewContext} from '#/components/Post/Embed/types' 43 import {PostRepliedTo} from '#/components/Post/PostRepliedTo' 44 import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton' 45 + import {TranslatedPost} from '#/components/Post/Translated' 46 import {PostControls} from '#/components/PostControls' 47 import {DiscoverDebug} from '#/components/PostControls/DiscoverDebug' 48 import {RichText} from '#/components/RichText' ··· 451 : [] 452 }, [post, currentAccount?.did, threadgateHiddenReplies]) 453 454 + const record = useMemo<AppBskyFeedPost.Record | undefined>( 455 + () => 456 + bsky.validate(post.record, AppBskyFeedPost.validateRecord) 457 + ? post.record 458 + : undefined, 459 + [post], 460 + ) 461 + 462 const onPressShowMore = useCallback(() => { 463 setLimitLines(false) 464 }, [setLimitLines]) ··· 490 )} 491 </View> 492 ) : undefined} 493 + {record && ( 494 + <TranslatedPost 495 + hideTranslateLink={true} 496 + post={post} 497 + postText={record.text} 498 + /> 499 + )} 500 {postEmbed ? ( 501 <View style={[a.pb_xs]}> 502 <Embed
+4 -4
yarn.lock
··· 3791 resolved "https://registry.yarnpkg.com/@bsky.app/expo-image-crop-tool/-/expo-image-crop-tool-0.5.0.tgz#4308fbde5c15e6be9122601797bc3d9549c95e31" 3792 integrity sha512-gmhQr2HWTRFyPO00fn5OmtiEVtikXusHMrN5Zoq26pu1VZX3zVE+aoc668etTqrvsQcm2Qu8fo96k5F3Wu+6wg== 3793 3794 - "@bsky.app/expo-translate-text@^0.2.4": 3795 - version "0.2.4" 3796 - resolved "https://registry.yarnpkg.com/@bsky.app/expo-translate-text/-/expo-translate-text-0.2.4.tgz#6e7f20f286111ee4d550c0c84f57393fc215a675" 3797 - integrity sha512-7mvFggNfkJEufI5A3WnjfjdN3H9P6Dpx7CpDkA9npWqA8Cb2icXq3k3nz3MaXGrVKTYiJnytAffYtos7mPoeOg== 3798 3799 "@bsky.app/react-native-mmkv@2.12.5": 3800 version "2.12.5"
··· 3791 resolved "https://registry.yarnpkg.com/@bsky.app/expo-image-crop-tool/-/expo-image-crop-tool-0.5.0.tgz#4308fbde5c15e6be9122601797bc3d9549c95e31" 3792 integrity sha512-gmhQr2HWTRFyPO00fn5OmtiEVtikXusHMrN5Zoq26pu1VZX3zVE+aoc668etTqrvsQcm2Qu8fo96k5F3Wu+6wg== 3793 3794 + "@bsky.app/expo-translate-text@^0.2.7": 3795 + version "0.2.7" 3796 + resolved "https://registry.yarnpkg.com/@bsky.app/expo-translate-text/-/expo-translate-text-0.2.7.tgz#e34811d0f0300f8808762e5676aa50790ca5d5e8" 3797 + integrity sha512-J9zctP9hLxX0eustTKk5CBnCkk6cEdlu1s7GzUnpT65qkCSNbYqbbUCpcU2Z2S2dN/1+w6L/iHb+vmCEbZMOaQ== 3798 3799 "@bsky.app/react-native-mmkv@2.12.5": 3800 version "2.12.5"