pstream is dead; long live pstream taciturnaxolotl.github.io/pstream-ng/
at main 264 lines 7.4 kB view raw
1import countryLanguages, { LanguageObj } from "@ladjs/country-language"; 2import { getTag } from "@sozialhelden/ietf-language-tags"; 3import { iso6393To1 } from "iso-639-3"; 4 5const languageOrder = ["en", "hi", "fr", "de", "nl", "pt"]; 6 7// mapping of language code to country code. 8// multiple mappings can exist, since languages are spoken in multiple countries. 9// This mapping purely exists to prioritize a country over another in languages where the base language code does 10// not contain a region (i.e. if the language code is zh-Hant where Hant is a script) or if the region in the language code is incorrect 11// iso639_1 -> iso3166 Alpha-2 12const countryPriority: Record<string, string> = { 13 zh: "cn", 14 nv: "us", 15}; 16 17// list of iso639_1 Alpha-2 codes used as default languages 18const defaultLanguageCodes: string[] = [ 19 "ar-SA", 20 "bg-BG", 21 "bn-BD", 22 "cs-CZ", 23 "ca-AD", 24 "da-DK", 25 "de-DE", 26 "de-CH", 27 "el-GR", 28 "en-US", 29 "es-ES", 30 "et-EE", 31 "fa-IR", 32 "fr-FR", 33 "gl-ES", 34 "gu-IN", 35 "he-IL", 36 "id-ID", 37 "it-IT", 38 "ja-JP", 39 "ko-KR", 40 "lv-LV", 41 "ne-NP", 42 "nl-NL", 43 "pl-PL", 44 "pt-BR", 45 "ru-RU", 46 "sl-SI", 47 "sv-SE", 48 "ta-LK", 49 "th-TH", 50 "tr-TR", 51 "vi-VN", 52 "zh-CN", 53 "nv-US", 54]; 55 56export interface LocaleInfo { 57 name: string; 58 nativeName?: string; 59 code: string; 60 isRtl?: boolean; 61} 62 63const extraLanguages: Record<string, LocaleInfo> = { 64 pirate: { 65 code: "pirate", 66 name: "Pirate", 67 nativeName: "Pirate Tongue", 68 }, 69 kitty: { 70 code: "cat", 71 name: "Cat", 72 nativeName: "Kitty Speak", 73 }, 74 uwu: { 75 code: "uwu", 76 name: "Cutsie OwO", 77 nativeName: "UwU", 78 }, 79 minion: { 80 code: "minion", 81 name: "Minion", 82 nativeName: "Minionese", 83 }, 84 tok: { 85 code: "tok", 86 name: "Toki pona", 87 nativeName: "Toki pona", 88 }, 89 futhark: { 90 code: "futhark", 91 name: "Elder Futhark (EN)", 92 nativeName: "ᛖᛚᛞᛖᚱ ᚠᚢᚦᚨᚱᚲ", 93 }, 94}; 95 96function populateLanguageCode(language: string): string { 97 if (language.includes("-")) return language; 98 if (language.length !== 2) return language; 99 return ( 100 defaultLanguageCodes.find((v) => v.startsWith(`${language}-`)) ?? language 101 ); 102} 103 104/** 105 * @param locale idk what kinda code this takes, anything in ietf format I guess 106 * @returns pretty format for language, null if it no info can be found for language 107 */ 108export function getPrettyLanguageNameFromLocale(locale: string): string | null { 109 const tag = 110 locale.length === 3 111 ? getTag(iso6393To1[locale] ?? locale, true) 112 : getTag(locale, true); 113 const lang = tag?.language?.Description?.[0] ?? null; 114 if (!lang) return null; 115 116 const region = tag?.region?.Description?.[0] ?? null; 117 let regionText = ""; 118 if (region) regionText = ` (${region})`; 119 120 return `${lang}${regionText}`; 121} 122 123/** 124 * Sort locale codes by occurrence, rest on alphabetical order 125 * @param langCodes list language codes to sort 126 * @param appLanguage optional app language to prioritize 127 * @returns sorted version of inputted list 128 */ 129export function sortLangCodes(langCodes: string[], appLanguage?: string) { 130 const languagesOrder = [...languageOrder]; 131 if (appLanguage && !languagesOrder.includes(appLanguage)) { 132 languagesOrder.unshift(appLanguage); 133 } 134 const reversedOrder = [...languagesOrder].reverse(); // Reverse is necessary, not sure why 135 136 const results = langCodes.sort((a, b) => { 137 const langOrderA = reversedOrder.findIndex( 138 (v) => a.startsWith(`${v}-`) || a === v, 139 ); 140 const langOrderB = reversedOrder.findIndex( 141 (v) => b.startsWith(`${v}-`) || b === v, 142 ); 143 if (langOrderA !== -1 || langOrderB !== -1) return langOrderB - langOrderA; 144 145 return a.localeCompare(b); 146 }); 147 148 return results; 149} 150 151/** 152 * Get country code for locale 153 * @param locale input locale 154 * @returns country code or null 155 */ 156export function getCountryCodeForLocale(locale: string): string | null { 157 let output: LanguageObj | null = null as any as LanguageObj; 158 const tag = getTag(populateLanguageCode(locale), true); 159 160 if (!tag?.language?.Subtag) return null; 161 // this function isn't async, so its guaranteed to work like this 162 countryLanguages.getLanguage(tag.language.Subtag, (_err, lang) => { 163 if (lang) output = lang; 164 }); 165 166 if (!output) return null; 167 const priority = countryPriority[output.iso639_1.toLowerCase()]; 168 if (output.countries.length === 0) { 169 return priority ?? null; 170 } 171 172 if (priority) { 173 const prioritizedCountry = output.countries.find( 174 (v) => v.code_2.toLowerCase() === priority, 175 ); 176 if (prioritizedCountry) return prioritizedCountry.code_2.toLowerCase(); 177 } 178 179 // If the language contains a region, check that against the countries and 180 // return the region if it matches 181 const regionSubtag = tag?.region?.Subtag.toLowerCase(); 182 if (regionSubtag) { 183 const regionCode = output.countries.find( 184 (c) => 185 c.code_2.toLowerCase() === regionSubtag || 186 c.code_3.toLowerCase() === regionSubtag, 187 ); 188 if (regionCode) return regionCode.code_2.toLowerCase(); 189 } 190 return output.countries[0].code_2.toLowerCase(); 191} 192 193/** 194 * Get information for a specific local 195 * @param locale local code 196 * @returns locale object 197 */ 198export function getLocaleInfo(locale: string): LocaleInfo | null { 199 const realLocale = populateLanguageCode(locale); 200 201 document.body.style.wordSpacing = "normal"; 202 203 const extraLang = extraLanguages[realLocale]; 204 if (extraLang) { 205 if (extraLang.code === "futhark") { 206 document.body.style.wordSpacing = "5px"; 207 } 208 return extraLang; 209 } 210 211 const tag = getTag(realLocale, true); 212 if (!tag?.language?.Subtag) return null; 213 214 let output: LanguageObj | null = null as any as LanguageObj; 215 // this function isnt async, so its garuanteed to work like this 216 countryLanguages.getLanguage(tag.language.Subtag, (_err, lang) => { 217 if (lang) output = lang; 218 }); 219 if (!output) return null; 220 221 const extras = []; 222 if (tag.region?.Description) extras.push(tag.region.Description[0]); 223 if (tag.script?.Description) extras.push(tag.script.Description[0]); 224 const extraStringified = extras.map((v) => `(${v})`).join(" "); 225 226 return { 227 code: tag.parts.langtag ?? realLocale, 228 isRtl: output.direction === "RTL", 229 name: output.name[0] + (extraStringified ? ` ${extraStringified}` : ""), 230 nativeName: output.nativeName[0] ?? undefined, 231 }; 232} 233 234/** 235 * Converts a language code to a TMDB-compatible format (ISO 639-1 with region) 236 * @param language The language code to convert 237 * @returns A TMDB-compatible language code (e.g., "en-US", "el-GR") 238 */ 239export function getTmdbLanguageCode(language: string): string { 240 // Handle empty or undefined 241 if (!language) return "en-US"; 242 243 // If it already has a region code (e.g., "en-US"), use it directly 244 if (language.includes("-")) return language; 245 246 // Handle special/custom languages by defaulting to English 247 if (language.length > 2 || Object.keys(extraLanguages).includes(language)) 248 return "en-US"; 249 250 // For standard language codes, find the appropriate region from the existing defaultLanguageCodes array 251 const defaultCode = defaultLanguageCodes.find((code) => 252 code.startsWith(`${language}-`), 253 ); 254 255 if (defaultCode) return defaultCode; 256 257 // If we can't find a good match, create a standard format like "fr-FR" from "fr" 258 if (language.length === 2) { 259 return `${language}-${language.toUpperCase()}`; 260 } 261 262 // Last resort fallback 263 return "en-US"; 264}