Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at ece6dc251cdb7eaf260819a4005b3a3e3e74ac8b 341 lines 8.8 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 35const displayNamesCache = new Map<string, Intl.DisplayNames>() 36 37function getDisplayNames(appLang: string): Intl.DisplayNames { 38 let cached = displayNamesCache.get(appLang) 39 if (!cached) { 40 cached = new Intl.DisplayNames([appLang], { 41 type: 'language', 42 fallback: 'none', 43 languageDisplay: 'standard', 44 }) 45 displayNamesCache.set(appLang, cached) 46 } 47 return cached 48} 49 50function getLocalizedLanguage( 51 langCode: string, 52 appLang: string, 53): string | undefined { 54 try { 55 return getDisplayNames(appLang).of(langCode) || undefined 56 } catch (e) { 57 // ignore RangeError from Intl.DisplayNames APIs 58 if (!(e instanceof RangeError)) { 59 throw e 60 } 61 } 62} 63 64export function languageName(language: Language, appLang: string): string { 65 // if Intl.DisplayNames is unavailable on the target, display the English name 66 if (!Intl.DisplayNames) { 67 return language.name 68 } 69 70 return getLocalizedLanguage(language.code2, appLang) || language.name 71} 72 73export function codeToLanguageName(lang2or3: string, appLang: string): string { 74 const code2 = code3ToCode2(lang2or3) 75 const knownLanguage = LANGUAGES_MAP_CODE2[code2] 76 77 return knownLanguage ? languageName(knownLanguage, appLang) : code2 78} 79 80export function getPostLanguage( 81 post: AppBskyFeedDefs.PostView, 82): string | undefined { 83 let candidates: string[] = [] 84 let postText: string = '' 85 if (hasProp(post.record, 'text') && typeof post.record.text === 'string') { 86 postText = post.record.text 87 } 88 89 if ( 90 AppBskyFeedPost.isRecord(post.record) && 91 hasProp(post.record, 'langs') && 92 Array.isArray(post.record.langs) 93 ) { 94 candidates = post.record.langs 95 } 96 97 // if there's only one declared language, use that 98 if (candidates?.length === 1) { 99 return candidates[0] 100 } 101 102 // no text? can't determine 103 if (postText.trim().length === 0) { 104 return undefined 105 } 106 107 // run the language model 108 let langsProbabilityMap = lande(postText) 109 110 // filter down using declared languages 111 if (candidates?.length) { 112 langsProbabilityMap = langsProbabilityMap.filter( 113 ([lang, _probability]: [string, number]) => { 114 return candidates.includes(code3ToCode2(lang)) 115 }, 116 ) 117 } 118 119 if (langsProbabilityMap[0]) { 120 return code3ToCode2(langsProbabilityMap[0][0]) 121 } 122} 123 124export function isPostInLanguage( 125 post: AppBskyFeedDefs.PostView, 126 targetLangs: string[], 127): boolean { 128 const lang = getPostLanguage(post) 129 if (!lang) { 130 // the post has no text, so we just say "yes" for now 131 return true 132 } 133 return bcp47Match.basicFilter(lang, targetLangs).length > 0 134} 135 136export function getTranslatorLink( 137 text: string, 138 targetLangCode: string, 139 sourceLanguage?: string, 140): string { 141 return `https://translate.google.com/?sl=${sourceLanguage ?? 'auto'}&tl=${targetLangCode}&text=${encodeURIComponent( 142 text, 143 )}` 144} 145 146/** 147 * Returns a valid `appLanguage` value from an arbitrary string. 148 * 149 * Context: post-refactor, we populated some user's `appLanguage` setting with 150 * `postLanguage`, which can be a comma-separated list of values. This breaks 151 * `appLanguage` handling in the app, so we introduced this util to parse out a 152 * valid `appLanguage` from the pre-populated `postLanguage` values. 153 * 154 * The `appLanguage` will continue to be incorrect until the user returns to 155 * language settings and selects a new option, at which point we'll re-save 156 * their choice, which should then be a valid option. Since we don't know when 157 * this will happen, we should leave this here until we feel it's safe to 158 * remove, or we re-migrate their storage. 159 */ 160export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage { 161 const langs = appLanguage.split(',').filter(Boolean) 162 163 for (const lang of langs) { 164 switch (fixLegacyLanguageCode(lang)) { 165 case 'en': 166 return AppLanguage.en 167 case 'an': 168 return AppLanguage.an 169 case 'ast': 170 return AppLanguage.ast 171 case 'ca': 172 return AppLanguage.ca 173 case 'cy': 174 return AppLanguage.cy 175 case 'da': 176 return AppLanguage.da 177 case 'de': 178 return AppLanguage.de 179 case 'el': 180 return AppLanguage.el 181 case 'en-GB': 182 return AppLanguage.en_GB 183 case 'eo': 184 return AppLanguage.eo 185 case 'es': 186 return AppLanguage.es 187 case 'eu': 188 return AppLanguage.eu 189 case 'fi': 190 return AppLanguage.fi 191 case 'fr': 192 return AppLanguage.fr 193 case 'fy': 194 return AppLanguage.fy 195 case 'ga': 196 return AppLanguage.ga 197 case 'gd': 198 return AppLanguage.gd 199 case 'gl': 200 return AppLanguage.gl 201 case 'hi': 202 return AppLanguage.hi 203 case 'hu': 204 return AppLanguage.hu 205 case 'ia': 206 return AppLanguage.ia 207 case 'id': 208 return AppLanguage.id 209 case 'it': 210 return AppLanguage.it 211 case 'ja': 212 return AppLanguage.ja 213 case 'km': 214 return AppLanguage.km 215 case 'ko': 216 return AppLanguage.ko 217 case 'ne': 218 return AppLanguage.ne 219 case 'nl': 220 return AppLanguage.nl 221 case 'pl': 222 return AppLanguage.pl 223 case 'pt-BR': 224 return AppLanguage.pt_BR 225 case 'pt-PT': 226 return AppLanguage.pt_PT 227 case 'ro': 228 return AppLanguage.ro 229 case 'ru': 230 return AppLanguage.ru 231 case 'sv': 232 return AppLanguage.sv 233 case 'th': 234 return AppLanguage.th 235 case 'tr': 236 return AppLanguage.tr 237 case 'uk': 238 return AppLanguage.uk 239 case 'vi': 240 return AppLanguage.vi 241 case 'zh-Hans-CN': 242 return AppLanguage.zh_CN 243 case 'zh-Hant-HK': 244 return AppLanguage.zh_HK 245 case 'zh-Hant-TW': 246 return AppLanguage.zh_TW 247 default: 248 continue 249 } 250 } 251 return AppLanguage.en 252} 253 254/** 255 * Handles legacy migration for Java devices. 256 * 257 * {@link https://github.com/bluesky-social/social-app/pull/4461} 258 * {@link https://xml.coverpages.org/iso639a.html} 259 */ 260export function fixLegacyLanguageCode(code: string | null): string | null { 261 if (code === 'in') { 262 // indonesian 263 return 'id' 264 } 265 if (code === 'iw') { 266 // hebrew 267 return 'he' 268 } 269 if (code === 'ji') { 270 // yiddish 271 return 'yi' 272 } 273 return code 274} 275 276/** 277 * Find the first language supported by our translation infra. Values should be 278 * in order of preference, and match the values of {@link AppLanguage}. 279 * 280 * If no match, returns `en`. 281 */ 282export function findSupportedAppLanguage(languageTags: (string | undefined)[]) { 283 const supported = new Set(Object.values(AppLanguage)) 284 for (const tag of languageTags) { 285 if (!tag) continue 286 if (supported.has(tag as AppLanguage)) { 287 return tag 288 } 289 } 290 return AppLanguage.en 291} 292 293/** 294 * Gets region name for a given country code and language. 295 * 296 * Falls back to English if unavailable/error, and if that fails, returns the country code. 297 * 298 * Intl.DisplayNames is widely available + has been polyfilled on native 299 */ 300export function regionName(countryCode: string, appLang: string): string { 301 const translatedName = getLocalizedRegionName(countryCode, appLang) 302 303 if (translatedName) { 304 return translatedName 305 } 306 307 // Fallback: get English name. Needed for i.e. Esperanto 308 const englishName = getLocalizedRegionName(countryCode, 'en') 309 if (englishName) { 310 return englishName 311 } 312 313 // Final fallback: return country code 314 return countryCode 315} 316 317const regionNamesCache = new Map<string, Intl.DisplayNames>() 318 319function getRegionNames(appLang: string): Intl.DisplayNames { 320 let cached = regionNamesCache.get(appLang) 321 if (!cached) { 322 cached = new Intl.DisplayNames([appLang], { 323 type: 'region', 324 fallback: 'none', 325 }) 326 regionNamesCache.set(appLang, cached) 327 } 328 return cached 329} 330 331function getLocalizedRegionName( 332 countryCode: string, 333 appLang: string, 334): string | undefined { 335 try { 336 return getRegionNames(appLang).of(countryCode) 337 } catch (err) { 338 console.warn('Error getting localized region name:', err) 339 return undefined 340 } 341}