Browse Source

ref: reimplement preferred locale resolution (#124)

Sv443 5 months ago
parent
commit
7dc45cc62e
1 changed files with 41 additions and 34 deletions
  1. 41 34
      src/utils/misc.ts

+ 41 - 34
src/utils/misc.ts

@@ -1,4 +1,4 @@
-import { compress, decompress, fetchAdvanced, openInNewTab, pauseFor, randomId, randRange } from "@sv443-network/userutils";
+import { compress, decompress, fetchAdvanced, openInNewTab, pauseFor, randomId, randRange, type Prettify } from "@sv443-network/userutils";
 import { marked } from "marked";
 import { branch, compressionFormat, repo, sessionStorageAvailable } from "../constants.js";
 import { type Domain, type NumberLengthFormat, type ResourceKey, type StringGen } from "../types.js";
@@ -114,18 +114,18 @@ export function sanitizeChannelId(channelId: string) {
 
 /** Tests whether a string is a valid channel ID in the format `@User` or `UC...` */
 export function isValidChannelId(channelId: string) {
-  return channelId.match(/^(UC|@)[\w-]+$/) !== null;
+  return channelId.match(/^(UC|@)[a-zA-Z0-9_-]+$/) !== null;
 }
 
 /** Quality identifier for a thumbnail - from highest to lowest res: `maxresdefault` > `sddefault` > `hqdefault` > `mqdefault` > `default` */
-type ThumbQuality = `${"maxres" | "sd" | "hq" | "mq" | ""}default`;
+type ThumbQuality = `${"maxres" | "sd" | "hq" | "mq"}default` | "default";
 
 /** Returns the thumbnail URL for a video with the given watch ID and quality (defaults to "hqdefault") */
 export function getThumbnailUrl(watchId: string, quality?: ThumbQuality): string
 /** Returns the thumbnail URL for a video with the given watch ID and index (0 is low quality thumbnail, 1-3 are low quality frames from the video) */
-export function getThumbnailUrl(watchId: string, index: 0 | 1 | 2 | 3): string
+export function getThumbnailUrl(watchId: string, index?: 0 | 1 | 2 | 3): string
 /** Returns the thumbnail URL for a video with either a given quality identifier or index */
-export function getThumbnailUrl(watchId: string, qualityOrIndex: ThumbQuality | 0 | 1 | 2 | 3 = "maxresdefault") {
+export function getThumbnailUrl(watchId: string, qualityOrIndex: Prettify<ThumbQuality | 0 | 1 | 2 | 3> = "maxresdefault") {
   return `https://img.youtube.com/vi/${watchId}/${qualityOrIndex}.jpg`;
 }
 
@@ -230,49 +230,56 @@ export function formatNumber(num: number, notation?: NumberLengthFormat): string
  */
 export async function getResourceUrl(name: ResourceKey | "_", uncached = false) {
   let url = !uncached && await GM.getResourceUrl(name);
+
   if(!url || url.length === 0) {
     const resObjOrStr = resourcesJson?.[name as keyof typeof resourcesJson];
+
     if(typeof resObjOrStr === "object" || typeof resObjOrStr === "string") {
-      const pathname = typeof resObjOrStr === "object" && "path" in resObjOrStr ? resObjOrStr.path : resObjOrStr;
-      const ref = typeof resObjOrStr === "object" && "ref" in resObjOrStr ? resObjOrStr.ref : branch;
-
-      if(pathname && pathname.startsWith("/") && pathname.length > 1)
-        return `https://raw.githubusercontent.com/${repo}/${ref}${pathname}`;
-      else if(pathname && pathname.startsWith("http"))
-        return pathname;
-      else if(pathname && pathname.length > 0)
-        return `https://raw.githubusercontent.com/${repo}/${ref}/assets/${pathname}`;
+      const pathName = typeof resObjOrStr === "object" && "path" in resObjOrStr ? resObjOrStr.path : resObjOrStr;
+      const ghRef = typeof resObjOrStr === "object" && "ref" in resObjOrStr ? resObjOrStr.ref : branch;
+
+      if(pathName && pathName.startsWith("/") && pathName.length > 1)
+        return `https://raw.githubusercontent.com/${repo}/${ghRef}${pathName}`;
+      else if(pathName && pathName.startsWith("http"))
+        return pathName;
+      else if(pathName && pathName.length > 0)
+        return `https://raw.githubusercontent.com/${repo}/${ghRef}/assets/${pathName}`;
     }
+
     warn(`Couldn't get blob URL nor external URL for @resource '${name}', trying to use base64-encoded fallback`);
     // @ts-ignore
     url = await GM.getResourceUrl(name, false);
   }
+
   return url;
 }
 
 /**
- * Returns the preferred locale of the user, provided it is supported by the userscript.  
- * Prioritizes `navigator.language`, then `navigator.languages`, then `"en-US"` as a fallback.
+ * Resolves the preferred locale of the user given their browser's language settings, as long as it is supported by the userscript directly or via the `altLocales` prop in `locales.json`  
+ * Prioritizes any supported value of `navigator.language`, then `navigator.languages`, then goes over them again, trimming off the part after the hyphen, then falls back to `"en-US"`
  */
 export function getPreferredLocale(): TrLocale {
-  const nvLangs = navigator.languages
-    .filter(lang => lang.match(/^[a-z]{2}(-|_)[A-Z]$/) !== null);
-
-  if(Object.entries(langMapping).find(([key]) => key === navigator.language))
-    return navigator.language as TrLocale;
-
-  for(const loc of nvLangs) {
-    if(Object.entries(langMapping).find(([key]) => key === loc))
-      return loc as TrLocale;
-  }
-
-  // if navigator.languages has entries that aren't locale codes in the format xx-XX
-  if(navigator.languages.some(lang => lang.match(/^[a-z]{2}$/))) {
-    for(const lang of nvLangs) {
-      const foundLoc = Object.entries(langMapping).find(([ key ]) => key.startsWith(lang))?.[0];
-      if(foundLoc)
-        return foundLoc as TrLocale;
-    }
+  const sanEq = (str1: string, str2: string) => str1.trim().toLowerCase() === str2.trim().toLowerCase();
+
+  const allNvLocs = [...new Set([navigator.language, ...navigator.languages])]
+    .map((v) => v.replace(/_/g, "-"));
+
+  for(const nvLoc of allNvLocs) {
+    const resolvedLoc = Object.entries(langMapping)
+      .find(([key, { altLocales }]) =>
+        sanEq(key, nvLoc) || altLocales.find(al => sanEq(al, nvLoc))
+      )?.[0];
+    if(resolvedLoc)
+      return resolvedLoc.trim() as TrLocale;
+
+    const trimmedNvLoc = nvLoc.split("-")[0];
+    const resolvedFallbackLoc = Object.entries(langMapping)
+      .find(([key, { altLocales }]) =>
+        sanEq(key.split("-")[0], trimmedNvLoc) || altLocales.find(al => sanEq(al.split("-")[0], trimmedNvLoc))
+      )?.[0];
+
+    if(resolvedFallbackLoc)
+      return resolvedFallbackLoc.trim() as TrLocale;
   }
 
   return "en-US";