{"version":3,"file":"localeDetector.cjs","names":["localeResolver"],"sources":["../../../src/localization/localeDetector.ts"],"sourcesContent":["import type { Locale } from '@intlayer/types/allLocales';\nimport { localeResolver } from './localeResolver';\n\n/**\n * Constants\n */\nconst LANGUAGE_FORMAT_REGULAR_EXPRESSION =\n  /^\\s*([^\\s\\-;]+)(?:-([^\\s;]+))?\\s*(?:;(.*))?$/;\nconst DEFAULT_QUALITY_SCORE = 1;\n\n/**\n * Enumeration for specificity weights.\n * Higher values indicate a more precise match.\n */\nenum SpecificityWeight {\n  None = 0,\n  Broad = 1, // Matches prefix (e.g., 'en' matches 'en-US')\n  Prefix = 2, // Matches prefix in reverse (e.g., 'en-US' matches 'en')\n  Exact = 4, // Matches exact string (e.g., 'en-US' matches 'en-US')\n}\n\n/**\n * Represents a parsed language tag from the header.\n */\ntype LanguagePreference = {\n  languageCode: string;\n  regionCode?: string;\n  fullLocale: string;\n  qualityScore: number;\n  originalIndex: number;\n};\n\n/**\n * Represents the result of matching a requested language against an available language.\n */\ntype MatchResult = {\n  providedIndex: number;\n  headerIndex: number;\n  qualityScore: number;\n  specificityScore: number;\n};\n\n/**\n * Parses a single language tag string from the Accept-Language header.\n * Example input: \"en-US;q=0.8\"\n */\nconst parseLanguageTag = (\n  tagString: string,\n  index: number\n): LanguagePreference | null => {\n  const match = LANGUAGE_FORMAT_REGULAR_EXPRESSION.exec(tagString);\n  if (!match) {\n    return null;\n  }\n\n  const languageCode = match[1];\n  const regionCode = match[2];\n  const parameters = match[3];\n\n  // Construct the full locale string (e.g., \"en-US\" or \"en\")\n  const fullLocale = regionCode\n    ? `${languageCode}-${regionCode}`\n    : languageCode;\n\n  let qualityScore = DEFAULT_QUALITY_SCORE;\n\n  // Parse parameters to find the quality score (\"q\")\n  if (parameters) {\n    const parameterList = parameters.split(';');\n    for (const parameter of parameterList) {\n      const [key, value] = parameter.split('=');\n      if (key === 'q') {\n        qualityScore = parseFloat(value);\n      }\n    }\n  }\n\n  return {\n    languageCode,\n    regionCode,\n    qualityScore,\n    originalIndex: index,\n    fullLocale,\n  };\n};\n\n/**\n * Parses the entire Accept-Language header string into a list of preferences.\n */\nconst parseAcceptLanguageHeader = (\n  headerValue: string\n): LanguagePreference[] => {\n  const rawTags = headerValue.split(',');\n  const preferences: LanguagePreference[] = [];\n\n  for (let index = 0; index < rawTags.length; index++) {\n    const tag = rawTags[index].trim();\n    const parsedLanguage = parseLanguageTag(tag, index);\n\n    if (parsedLanguage) {\n      preferences.push(parsedLanguage);\n    }\n  }\n\n  return preferences;\n};\n\n/**\n * Calculates the specificity of a match between a provided language and a requested preference.\n */\nconst calculateMatchSpecificity = (\n  providedLanguage: string,\n  preference: LanguagePreference,\n  providedIndex: number\n): MatchResult | null => {\n  const parsedProvided = parseLanguageTag(providedLanguage, providedIndex);\n  if (!parsedProvided) {\n    return null;\n  }\n\n  let specificityScore = SpecificityWeight.None;\n\n  const preferenceFullLower = preference.fullLocale.toLowerCase();\n  const preferencePrefixLower = preference.languageCode.toLowerCase();\n  const providedFullLower = parsedProvided.fullLocale.toLowerCase();\n  const providedPrefixLower = parsedProvided.languageCode.toLowerCase();\n\n  if (preferenceFullLower === providedFullLower) {\n    specificityScore |= SpecificityWeight.Exact;\n  } else if (preferencePrefixLower === providedFullLower) {\n    specificityScore |= SpecificityWeight.Prefix;\n  } else if (preferenceFullLower === providedPrefixLower) {\n    specificityScore |= SpecificityWeight.Broad;\n  } else if (preference.fullLocale !== '*') {\n    return null;\n  }\n\n  return {\n    providedIndex,\n    headerIndex: preference.originalIndex,\n    qualityScore: preference.qualityScore,\n    specificityScore,\n  };\n};\n\n/**\n * Determines the best match for a specific available language against the list of user accepted languages.\n */\nconst getBestMatchForLanguage = (\n  providedLanguage: string,\n  acceptedPreferences: LanguagePreference[],\n  providedIndex: number\n): MatchResult => {\n  // Initialize with a non-match priority\n  let bestMatch: MatchResult = {\n    headerIndex: -1,\n    qualityScore: 0,\n    specificityScore: 0,\n    providedIndex,\n  };\n\n  for (const preference of acceptedPreferences) {\n    const matchSpec = calculateMatchSpecificity(\n      providedLanguage,\n      preference,\n      providedIndex\n    );\n\n    if (matchSpec) {\n      // Compare current best match with new match\n      const scoreDifference =\n        bestMatch.specificityScore - matchSpec.specificityScore ||\n        bestMatch.qualityScore - matchSpec.qualityScore ||\n        bestMatch.headerIndex - matchSpec.headerIndex;\n\n      // If the new match is better (difference < 0), update priority\n      if (scoreDifference < 0) {\n        bestMatch = matchSpec;\n      }\n    }\n  }\n\n  return bestMatch;\n};\n\n/**\n * Comparator function to sort language matches.\n * Sorting order:\n * 1. Quality Score (Descending)\n * 2. Specificity Score (Descending)\n * 3. Order in Header (Ascending - lower index is better)\n * 4. Order in Provided List (Ascending)\n */\nconst compareMatchResults = (a: MatchResult, b: MatchResult): number => {\n  return (\n    b.qualityScore - a.qualityScore ||\n    b.specificityScore - a.specificityScore ||\n    a.headerIndex - b.headerIndex ||\n    a.providedIndex - b.providedIndex ||\n    0\n  );\n};\n\n/**\n * Derives the list of preferred languages based on the Accept-Language header\n * and an optional list of available languages.\n */\nexport const getPreferredLanguages = (\n  acceptHeader: string | undefined,\n  availableLanguages?: string[]\n): string[] => {\n  // RFC 2616 sec 14.4: no header implies '*'\n  const headerValue = acceptHeader === undefined ? '*' : acceptHeader || '';\n  const acceptedPreferences = parseAcceptLanguageHeader(headerValue);\n\n  // If no specific languages are provided to filter against, return the header languages sorted by quality\n  if (!availableLanguages) {\n    return acceptedPreferences\n      .filter((preference) => preference.qualityScore > 0)\n      .sort((a, b) => b.qualityScore - a.qualityScore) // Simple sort by quality\n      .map((preference) => preference.fullLocale);\n  }\n\n  // Map available languages to their match priority against the header\n  const matchResults = availableLanguages.map((language, index) =>\n    getBestMatchForLanguage(language, acceptedPreferences, index)\n  );\n\n  return matchResults\n    .filter((result) => result.qualityScore > 0)\n    .sort(compareMatchResults)\n    .map((result) => availableLanguages[result.providedIndex]);\n};\n\n/**\n * Detects the locale from the request headers.\n *\n * Headers are provided by the browser/client and can be used to determine the user's preferred language.\n * This function intersects the user's `Accept-Language` header with the application's available locales.\n */\nexport const localeDetector = (\n  headers: Record<string, string | undefined>,\n  availableLocales?: Locale[],\n  defaultLocale?: Locale\n): Locale => {\n  const acceptLanguageHeader = headers['accept-language'];\n\n  const preferredLocaleStrings = getPreferredLanguages(\n    acceptLanguageHeader,\n    availableLocales as string[]\n  );\n\n  return localeResolver(\n    preferredLocaleStrings as Locale[],\n    availableLocales,\n    defaultLocale\n  );\n};\n"],"mappings":";;;;;;;AAMA,MAAM,qCACJ;AACF,MAAM,wBAAwB;;;;;AAsC9B,MAAM,oBACJ,WACA,UAC8B;CAC9B,MAAM,QAAQ,mCAAmC,KAAK,UAAU;AAChE,KAAI,CAAC,MACH,QAAO;CAGT,MAAM,eAAe,MAAM;CAC3B,MAAM,aAAa,MAAM;CACzB,MAAM,aAAa,MAAM;CAGzB,MAAM,aAAa,aACf,GAAG,aAAa,GAAG,eACnB;CAEJ,IAAI,eAAe;AAGnB,KAAI,YAAY;EACd,MAAM,gBAAgB,WAAW,MAAM,IAAI;AAC3C,OAAK,MAAM,aAAa,eAAe;GACrC,MAAM,CAAC,KAAK,SAAS,UAAU,MAAM,IAAI;AACzC,OAAI,QAAQ,IACV,gBAAe,WAAW,MAAM;;;AAKtC,QAAO;EACL;EACA;EACA;EACA,eAAe;EACf;EACD;;;;;AAMH,MAAM,6BACJ,gBACyB;CACzB,MAAM,UAAU,YAAY,MAAM,IAAI;CACtC,MAAM,cAAoC,EAAE;AAE5C,MAAK,IAAI,QAAQ,GAAG,QAAQ,QAAQ,QAAQ,SAAS;EAEnD,MAAM,iBAAiB,iBADX,QAAQ,OAAO,MACgB,EAAE,MAAM;AAEnD,MAAI,eACF,aAAY,KAAK,eAAe;;AAIpC,QAAO;;;;;AAMT,MAAM,6BACJ,kBACA,YACA,kBACuB;CACvB,MAAM,iBAAiB,iBAAiB,kBAAkB,cAAc;AACxE,KAAI,CAAC,eACH,QAAO;CAGT,IAAI;CAEJ,MAAM,sBAAsB,WAAW,WAAW,aAAa;CAC/D,MAAM,wBAAwB,WAAW,aAAa,aAAa;CACnE,MAAM,oBAAoB,eAAe,WAAW,aAAa;CACjE,MAAM,sBAAsB,eAAe,aAAa,aAAa;AAErE,KAAI,wBAAwB,kBAC1B;UACS,0BAA0B,kBACnC;UACS,wBAAwB,oBACjC;UACS,WAAW,eAAe,IACnC,QAAO;AAGT,QAAO;EACL;EACA,aAAa,WAAW;EACxB,cAAc,WAAW;EACzB;EACD;;;;;AAMH,MAAM,2BACJ,kBACA,qBACA,kBACgB;CAEhB,IAAI,YAAyB;EAC3B,aAAa;EACb,cAAc;EACd,kBAAkB;EAClB;EACD;AAED,MAAK,MAAM,cAAc,qBAAqB;EAC5C,MAAM,YAAY,0BAChB,kBACA,YACA,cACD;AAED,MAAI,WAQF;QALE,UAAU,mBAAmB,UAAU,oBACvC,UAAU,eAAe,UAAU,gBACnC,UAAU,cAAc,UAAU,eAGd,EACpB,aAAY;;;AAKlB,QAAO;;;;;;;;;;AAWT,MAAM,uBAAuB,GAAgB,MAA2B;AACtE,QACE,EAAE,eAAe,EAAE,gBACnB,EAAE,mBAAmB,EAAE,oBACvB,EAAE,cAAc,EAAE,eAClB,EAAE,gBAAgB,EAAE,iBACpB;;;;;;AAQJ,MAAa,yBACX,cACA,uBACa;CAGb,MAAM,sBAAsB,0BADR,iBAAiB,SAAY,MAAM,gBAAgB,GACL;AAGlE,KAAI,CAAC,mBACH,QAAO,oBACJ,QAAQ,eAAe,WAAW,eAAe,EAAE,CACnD,MAAM,GAAG,MAAM,EAAE,eAAe,EAAE,aAAa,CAC/C,KAAK,eAAe,WAAW,WAAW;AAQ/C,QAJqB,mBAAmB,KAAK,UAAU,UACrD,wBAAwB,UAAU,qBAAqB,MAAM,CAG5C,CAChB,QAAQ,WAAW,OAAO,eAAe,EAAE,CAC3C,KAAK,oBAAoB,CACzB,KAAK,WAAW,mBAAmB,OAAO,eAAe;;;;;;;;AAS9D,MAAa,kBACX,SACA,kBACA,kBACW;CACX,MAAM,uBAAuB,QAAQ;AAOrC,QAAOA,mDALwB,sBAC7B,sBACA,iBAIsB,EACtB,kBACA,cACD"}