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

Configure Feed

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

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