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

Configure Feed

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

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