Bluesky app fork with some witchin' additions 馃挮
at main 7.3 kB view raw
1import {type AppBskyFeedDefs, AppBskyFeedPost} from '@atproto/api' 2import * as bcp47Match from 'bcp-47-match' 3import lande from 'lande' 4 5import {hasProp} from '#/lib/type-guards' 6import { 7 AppLanguage, 8 type Language, 9 LANGUAGES_MAP_CODE2, 10 LANGUAGES_MAP_CODE3, 11} from './languages' 12 13export function code2ToCode3(lang: string): string { 14 if (lang.length === 2) { 15 return LANGUAGES_MAP_CODE2[lang]?.code3 || lang 16 } 17 return lang 18} 19 20export function code3ToCode2(lang: string): string { 21 if (lang.length === 3) { 22 return LANGUAGES_MAP_CODE3[lang]?.code2 || lang 23 } 24 return lang 25} 26 27export function code3ToCode2Strict(lang: string): string | undefined { 28 if (lang.length === 3) { 29 return LANGUAGES_MAP_CODE3[lang]?.code2 30 } 31 32 return undefined 33} 34 35function getLocalizedLanguage( 36 langCode: string, 37 appLang: string, 38): string | undefined { 39 try { 40 const allNames = new Intl.DisplayNames([appLang], { 41 type: 'language', 42 fallback: 'none', 43 languageDisplay: 'standard', 44 }) 45 const translatedName = allNames.of(langCode) 46 47 if (translatedName) { 48 return translatedName 49 } 50 } catch (e) { 51 // ignore RangeError from Intl.DisplayNames APIs 52 if (!(e instanceof RangeError)) { 53 throw e 54 } 55 } 56} 57 58export function languageName(language: Language, appLang: string): string { 59 // if Intl.DisplayNames is unavailable on the target, display the English name 60 if (!(Intl as any).DisplayNames) { 61 return language.name 62 } 63 64 return getLocalizedLanguage(language.code2, appLang) || language.name 65} 66 67export function codeToLanguageName(lang2or3: string, appLang: string): string { 68 const code2 = code3ToCode2(lang2or3) 69 const knownLanguage = LANGUAGES_MAP_CODE2[code2] 70 71 return knownLanguage ? languageName(knownLanguage, appLang) : code2 72} 73 74export function getPostLanguage( 75 post: AppBskyFeedDefs.PostView, 76): string | undefined { 77 let candidates: string[] = [] 78 let postText: string = '' 79 if (hasProp(post.record, 'text') && typeof post.record.text === 'string') { 80 postText = post.record.text 81 } 82 83 if ( 84 AppBskyFeedPost.isRecord(post.record) && 85 hasProp(post.record, 'langs') && 86 Array.isArray(post.record.langs) 87 ) { 88 candidates = post.record.langs 89 } 90 91 // if there's only one declared language, use that 92 if (candidates?.length === 1) { 93 return candidates[0] 94 } 95 96 // no text? can't determine 97 if (postText.trim().length === 0) { 98 return undefined 99 } 100 101 // run the language model 102 let langsProbabilityMap = lande(postText) 103 104 // filter down using declared languages 105 if (candidates?.length) { 106 langsProbabilityMap = langsProbabilityMap.filter( 107 ([lang, _probability]: [string, number]) => { 108 return candidates.includes(code3ToCode2(lang)) 109 }, 110 ) 111 } 112 113 if (langsProbabilityMap[0]) { 114 return code3ToCode2(langsProbabilityMap[0][0]) 115 } 116} 117 118export function isPostInLanguage( 119 post: AppBskyFeedDefs.PostView, 120 targetLangs: string[], 121): boolean { 122 const lang = getPostLanguage(post) 123 if (!lang) { 124 // the post has no text, so we just say "yes" for now 125 return true 126 } 127 return bcp47Match.basicFilter(lang, targetLangs).length > 0 128} 129 130export function getTranslatorLink(text: string, lang: string): string { 131 return `https://translate.google.com/?sl=auto&tl=${lang}&text=${encodeURIComponent( 132 text, 133 )}` 134} 135 136/** 137 * Returns a valid `appLanguage` value from an arbitrary string. 138 * 139 * Context: post-refactor, we populated some user's `appLanguage` setting with 140 * `postLanguage`, which can be a comma-separated list of values. This breaks 141 * `appLanguage` handling in the app, so we introduced this util to parse out a 142 * valid `appLanguage` from the pre-populated `postLanguage` values. 143 * 144 * The `appLanguage` will continue to be incorrect until the user returns to 145 * language settings and selects a new option, at which point we'll re-save 146 * their choice, which should then be a valid option. Since we don't know when 147 * this will happen, we should leave this here until we feel it's safe to 148 * remove, or we re-migrate their storage. 149 */ 150export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage { 151 const langs = appLanguage.split(',').filter(Boolean) 152 153 for (const lang of langs) { 154 switch (fixLegacyLanguageCode(lang)) { 155 case 'en': 156 return AppLanguage.en 157 case 'an': 158 return AppLanguage.an 159 case 'ast': 160 return AppLanguage.ast 161 case 'ca': 162 return AppLanguage.ca 163 case 'cy': 164 return AppLanguage.cy 165 case 'da': 166 return AppLanguage.da 167 case 'de': 168 return AppLanguage.de 169 case 'el': 170 return AppLanguage.el 171 case 'en-GB': 172 return AppLanguage.en_GB 173 case 'eo': 174 return AppLanguage.eo 175 case 'es': 176 return AppLanguage.es 177 case 'eu': 178 return AppLanguage.eu 179 case 'fi': 180 return AppLanguage.fi 181 case 'fr': 182 return AppLanguage.fr 183 case 'fy': 184 return AppLanguage.fy 185 case 'ga': 186 return AppLanguage.ga 187 case 'gd': 188 return AppLanguage.gd 189 case 'gl': 190 return AppLanguage.gl 191 case 'hi': 192 return AppLanguage.hi 193 case 'hu': 194 return AppLanguage.hu 195 case 'ia': 196 return AppLanguage.ia 197 case 'id': 198 return AppLanguage.id 199 case 'it': 200 return AppLanguage.it 201 case 'ja': 202 return AppLanguage.ja 203 case 'km': 204 return AppLanguage.km 205 case 'ko': 206 return AppLanguage.ko 207 case 'ne': 208 return AppLanguage.ne 209 case 'nl': 210 return AppLanguage.nl 211 case 'pl': 212 return AppLanguage.pl 213 case 'pt-BR': 214 return AppLanguage.pt_BR 215 case 'pt-PT': 216 return AppLanguage.pt_PT 217 case 'ro': 218 return AppLanguage.ro 219 case 'ru': 220 return AppLanguage.ru 221 case 'sv': 222 return AppLanguage.sv 223 case 'th': 224 return AppLanguage.th 225 case 'tr': 226 return AppLanguage.tr 227 case 'uk': 228 return AppLanguage.uk 229 case 'vi': 230 return AppLanguage.vi 231 case 'zh-Hans-CN': 232 return AppLanguage.zh_CN 233 case 'zh-Hant-HK': 234 return AppLanguage.zh_HK 235 case 'zh-Hant-TW': 236 return AppLanguage.zh_TW 237 default: 238 continue 239 } 240 } 241 return AppLanguage.en 242} 243 244/** 245 * Handles legacy migration for Java devices. 246 * 247 * {@link https://github.com/bluesky-social/social-app/pull/4461} 248 * {@link https://xml.coverpages.org/iso639a.html} 249 */ 250export function fixLegacyLanguageCode(code: string | null): string | null { 251 if (code === 'in') { 252 // indonesian 253 return 'id' 254 } 255 if (code === 'iw') { 256 // hebrew 257 return 'he' 258 } 259 if (code === 'ji') { 260 // yiddish 261 return 'yi' 262 } 263 return code 264} 265 266/** 267 * Find the first language supported by our translation infra. Values should be 268 * in order of preference, and match the values of {@link AppLanguage}. 269 * 270 * If no match, returns `en`. 271 */ 272export function findSupportedAppLanguage(languageTags: (string | undefined)[]) { 273 const supported = new Set(Object.values(AppLanguage)) 274 for (const tag of languageTags) { 275 if (!tag) continue 276 if (supported.has(tag as AppLanguage)) { 277 return tag 278 } 279 } 280 return AppLanguage.en 281}