All files / app/i18n acceptLanguage.ts

100% Statements 21/21
100% Branches 5/5
100% Functions 7/7
100% Lines 20/20

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48            1x 24x     46x 46x   46x       23x   23x 38x 18x 37x   23x   21x 21x 29x 312x         13x         16x 16x 8x       10x    
interface AcceptLanguageEntry {
  code: string;
  quality: number;
}
 
/** Parses the Accept-Language HTTP header (e.g., "en-US,en;q=0.9,fr;q=0.8"). */
export function parseAcceptLanguage(header: string): AcceptLanguageEntry[] {
  return header
    .split(",")
    .map((item) => {
      const [code, q = "1"] = item.trim().split(";q=");
      return { code: code, quality: parseFloat(q) };
    })
    .filter((entry) => entry.code.length > 0);
}
 
/** Looks up a supported locale based on an Accept-Language HTTP header. */
export function lookupAcceptLanguage(header: string, supportedLocales: string[]): string | undefined {
  // Consider accepted locales by descending quality.
  const acceptLocales = parseAcceptLanguage(header)
    .filter((e) => e.quality > 0) // q=0 should not be matched
    .sort((a, b) => b.quality - a.quality)
    .map((e) => e.code);
 
  for (const acceptLocale of acceptLocales) {
    // RFC 4647 lookup: First check pt-BR, then pt
    let possibleLocale = acceptLocale;
    while (true) {
      for (const supportedLocale of supportedLocales) {
        if (
          possibleLocale.localeCompare(supportedLocale, undefined, {
            sensitivity: "base", // Case-insensitive
          }) === 0
        ) {
          return supportedLocale;
        }
      }
 
      // Strip the last suffix
      const lastDashIndex = possibleLocale.lastIndexOf("-");
      if (lastDashIndex === -1) break;
      possibleLocale = possibleLocale.slice(0, lastDashIndex);
    }
  }
 
  return undefined;
}