Bluesky app fork with some witchin' additions 馃挮
at readme-update 329 lines 8.6 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.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 130// we cant hook into functions like this, so we will make other translator functions n swap between em 131export function getTranslatorLink(text: string, lang: string): string { 132 return `https://translate.google.com/?sl=auto&tl=${lang}&text=${encodeURIComponent( 133 text, 134 )}` 135} 136 137export function getTranslatorLinkKagi(text: string, lang: string): string { 138 return `https://translate.kagi.com/?from=auto&to=${lang}&text=${encodeURIComponent( 139 text, 140 )}` 141} 142 143/** 144 * Returns a valid `appLanguage` value from an arbitrary string. 145 * 146 * Context: post-refactor, we populated some user's `appLanguage` setting with 147 * `postLanguage`, which can be a comma-separated list of values. This breaks 148 * `appLanguage` handling in the app, so we introduced this util to parse out a 149 * valid `appLanguage` from the pre-populated `postLanguage` values. 150 * 151 * The `appLanguage` will continue to be incorrect until the user returns to 152 * language settings and selects a new option, at which point we'll re-save 153 * their choice, which should then be a valid option. Since we don't know when 154 * this will happen, we should leave this here until we feel it's safe to 155 * remove, or we re-migrate their storage. 156 */ 157export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage { 158 const langs = appLanguage.split(',').filter(Boolean) 159 160 for (const lang of langs) { 161 switch (fixLegacyLanguageCode(lang)) { 162 case 'en': 163 return AppLanguage.en 164 case 'an': 165 return AppLanguage.an 166 case 'ast': 167 return AppLanguage.ast 168 case 'ca': 169 return AppLanguage.ca 170 case 'cy': 171 return AppLanguage.cy 172 case 'da': 173 return AppLanguage.da 174 case 'de': 175 return AppLanguage.de 176 case 'el': 177 return AppLanguage.el 178 case 'en-GB': 179 return AppLanguage.en_GB 180 case 'eo': 181 return AppLanguage.eo 182 case 'es': 183 return AppLanguage.es 184 case 'eu': 185 return AppLanguage.eu 186 case 'fi': 187 return AppLanguage.fi 188 case 'fr': 189 return AppLanguage.fr 190 case 'fy': 191 return AppLanguage.fy 192 case 'ga': 193 return AppLanguage.ga 194 case 'gd': 195 return AppLanguage.gd 196 case 'gl': 197 return AppLanguage.gl 198 case 'hi': 199 return AppLanguage.hi 200 case 'hu': 201 return AppLanguage.hu 202 case 'ia': 203 return AppLanguage.ia 204 case 'id': 205 return AppLanguage.id 206 case 'it': 207 return AppLanguage.it 208 case 'ja': 209 return AppLanguage.ja 210 case 'km': 211 return AppLanguage.km 212 case 'ko': 213 return AppLanguage.ko 214 case 'ne': 215 return AppLanguage.ne 216 case 'nl': 217 return AppLanguage.nl 218 case 'pl': 219 return AppLanguage.pl 220 case 'pt-BR': 221 return AppLanguage.pt_BR 222 case 'pt-PT': 223 return AppLanguage.pt_PT 224 case 'ro': 225 return AppLanguage.ro 226 case 'ru': 227 return AppLanguage.ru 228 case 'sv': 229 return AppLanguage.sv 230 case 'th': 231 return AppLanguage.th 232 case 'tr': 233 return AppLanguage.tr 234 case 'uk': 235 return AppLanguage.uk 236 case 'vi': 237 return AppLanguage.vi 238 case 'zh-Hans-CN': 239 return AppLanguage.zh_CN 240 case 'zh-Hant-HK': 241 return AppLanguage.zh_HK 242 case 'zh-Hant-TW': 243 return AppLanguage.zh_TW 244 default: 245 continue 246 } 247 } 248 return AppLanguage.en 249} 250 251/** 252 * Handles legacy migration for Java devices. 253 * 254 * {@link https://github.com/bluesky-social/social-app/pull/4461} 255 * {@link https://xml.coverpages.org/iso639a.html} 256 */ 257export function fixLegacyLanguageCode(code: string | null): string | null { 258 if (code === 'in') { 259 // indonesian 260 return 'id' 261 } 262 if (code === 'iw') { 263 // hebrew 264 return 'he' 265 } 266 if (code === 'ji') { 267 // yiddish 268 return 'yi' 269 } 270 return code 271} 272 273/** 274 * Find the first language supported by our translation infra. Values should be 275 * in order of preference, and match the values of {@link AppLanguage}. 276 * 277 * If no match, returns `en`. 278 */ 279export function findSupportedAppLanguage(languageTags: (string | undefined)[]) { 280 const supported = new Set(Object.values(AppLanguage)) 281 for (const tag of languageTags) { 282 if (!tag) continue 283 if (supported.has(tag as AppLanguage)) { 284 return tag 285 } 286 } 287 return AppLanguage.en 288} 289 290/** 291 * Gets region name for a given country code and language. 292 * 293 * Falls back to English if unavailable/error, and if that fails, returns the country code. 294 * 295 * Intl.DisplayNames is widely available + has been polyfilled on native 296 */ 297export function regionName(countryCode: string, appLang: string): string { 298 const translatedName = getLocalizedRegionName(countryCode, appLang) 299 300 if (translatedName) { 301 return translatedName 302 } 303 304 // Fallback: get English name. Needed for i.e. Esperanto 305 const englishName = getLocalizedRegionName(countryCode, 'en') 306 if (englishName) { 307 return englishName 308 } 309 310 // Final fallback: return country code 311 return countryCode 312} 313 314function getLocalizedRegionName( 315 countryCode: string, 316 appLang: string, 317): string | undefined { 318 try { 319 const allNames = new Intl.DisplayNames([appLang], { 320 type: 'region', 321 fallback: 'none', 322 }) 323 324 return allNames.of(countryCode) 325 } catch (err) { 326 console.warn('Error getting localized region name:', err) 327 return undefined 328 } 329}