@@ -59,91 +59,61 @@ export function getLanguagesFromNavigator(
5959 return tags . map ( ( t ) => t . trim ( ) ) . filter ( ( x ) => ! ! x ) as BrowserLocaleKey [ ] ;
6060}
6161
62- /**
63- * Case-insensitive lookup of a browser tag in LOCALE_BROWSER_MAP,
64- * with a fallback to its base tag.
65- *
66- * @param tag - Browser tag (any case, e.g., 'Es-Mx')
67- * @returns LocaleValue if found, otherwise undefined
68- */
69- export function mapBrowserTagToLocale ( tag : string ) : LocaleValue | undefined {
70- // normalize language
71- const lc = normalizeBrowserTag ( tag ) ;
72-
73- // direct match if exists
74- if ( lc in LOCALE_BROWSER_MAP_LOWERCASE ) {
75- return LOCALE_BROWSER_MAP_LOWERCASE [ lc ] ;
76- }
77-
78- // otherwise try base prefix
79- const baseLc = baseOf ( lc ) ;
80- if ( baseLc in LOCALE_BROWSER_MAP_LOWERCASE ) {
81- return LOCALE_BROWSER_MAP_LOWERCASE [ baseLc ] ;
82- }
83-
84- // no direct match
85- return undefined ;
86- }
87-
88- /**
89- * Resolve the best supported LocaleValue for a single browser tag.
90- * Rule:
91- * 1) Map via LOCALE_BROWSER_MAP (case-insensitive); if supported, use it.
92- * 2) Otherwise, use the browser tag’s base prefix:
93- * a) if the short code (e.g., 'ar') is supported, use it
94- * b) else pick the first 'ar-*' in supportedLocales (preserves customer order)
95- *
96- * @param browserTag - Browser tag (e.g., 'ar-EG')
97- * @param supportedLocales - Allowed locales (customer-ordered)
98- * @returns Supported LocaleValue or undefined if no base match exists
99- */
100- export function resolveSupportedLocaleForBrowserTag (
101- browserTag : string ,
102- supportedLocales : LocaleValue [ ] ,
103- ) : LocaleValue | undefined {
104- const supportedSet = new Set ( supportedLocales ) ;
105-
106- // look for direct match and accept that if in list
107- const mapped = mapBrowserTagToLocale ( browserTag ) ;
108- if ( mapped && supportedSet . has ( mapped ) ) {
109- return mapped ;
110- }
111-
112- // if no direct match, look for base prefix matches e.g. "ar-EG" -> "ar"
113- const prefixLc = baseOf ( normalizeBrowserTag ( browserTag ) ) ;
114- const shortMatch = supportedLocales . find ( ( l ) => l . toLowerCase ( ) === prefixLc ) ;
115- if ( shortMatch ) {
116- return shortMatch ;
117- }
118-
119- // then first variant with same base
120- const variantMatch = supportedLocales . find (
121- ( l ) => l . includes ( '-' ) && baseOf ( l ) . toLowerCase ( ) === prefixLc ,
122- ) ;
123- return variantMatch ;
124- }
125-
12662/**
12763 * Map an ordered list of browser tags to supported LocaleValues using the resolve rule.
128- * Keeps first-seen order from the browser list and de-duplicates.
129- * Falls back to default if nothing matches .
64+ * De-duplicates and preserves order, but prioritizes **exact LOCALE_BROWSER_MAP hits**
65+ * over fuzzy/base matches across the whole list .
13066 *
13167 * @param browserLocales - Browser tags (ordered by user preference)
13268 * @param supportedLocales - Allowed locales (customer-ordered)
13369 * @param defaultLocale - Fallback when nothing matches (defaults to 'en')
134- * @returns Ordered, unique supported LocaleValues
70+ * @returns Ordered, unique supported LocaleValues with exact hits first
13571 */
13672export function getUserLocalesFromBrowserLanguages (
13773 browserLocales : string [ ] ,
13874 supportedLocales : LocaleValue [ ] ,
13975 defaultLocale : LocaleValue ,
14076) : LocaleValue [ ] {
141- const resolved = browserLocales
142- . map ( ( tag ) => resolveSupportedLocaleForBrowserTag ( tag , supportedLocales ) )
143- . filter ( ( x ) : x is LocaleValue => Boolean ( x ) ) ;
77+ const supportedSet = new Set ( supportedLocales ) ;
78+
79+ const exact : LocaleValue [ ] = [ ] ;
80+ const fuzzy : LocaleValue [ ] = [ ] ;
81+
82+ // eslint-disable-next-line no-restricted-syntax
83+ for ( const tag of browserLocales ) {
84+ const lc = normalizeBrowserTag ( tag ) ;
85+
86+ // 1) Exact LOCALE_BROWSER_MAP match (case-insensitive)
87+ const direct = LOCALE_BROWSER_MAP_LOWERCASE [ lc ] ;
88+ if ( direct && supportedSet . has ( direct ) ) {
89+ exact . push ( direct ) ;
90+ // eslint-disable-next-line no-continue
91+ continue ;
92+ }
93+
94+ // 2) Fuzzy prefix rule against *supportedLocales*:
95+ const prefix = baseOf ( lc ) ;
96+
97+ // 2a) short/base code if supported
98+ const short = supportedLocales . find ( ( l ) => l . toLowerCase ( ) === prefix ) ;
99+ if ( short ) {
100+ fuzzy . push ( short ) ;
101+ // eslint-disable-next-line no-continue
102+ continue ;
103+ }
104+
105+ // 2b) otherwise first variant of same base in customer order
106+ const variant = supportedLocales . find (
107+ ( l ) => l . includes ( '-' ) && baseOf ( l ) . toLowerCase ( ) === prefix ,
108+ ) ;
109+ if ( variant ) {
110+ fuzzy . push ( variant ) ;
111+ }
112+ }
144113
145- const unique = uniqOrdered ( resolved ) ;
146- return unique . length ? unique : [ defaultLocale ] ;
114+ // Exact hits outrank any fuzzy/base matches globally
115+ const ordered = uniqOrdered < LocaleValue > ( [ ...exact , ...fuzzy ] ) ;
116+ return ordered . length ? ordered : [ defaultLocale ] ;
147117}
148118
149119/**
0 commit comments