| 93 | * Parses the Accept-Language header and returns the best matching locale |
| 94 | */ |
| 95 | export function getAcceptLanguageLocale( |
| 96 | acceptLanguageHeader: string, |
| 97 | locales: string[] |
| 98 | ): string | undefined { |
| 99 | if (!acceptLanguageHeader || !locales.length) { |
| 100 | return undefined |
| 101 | } |
| 102 | |
| 103 | try { |
| 104 | // Parse accept-language header |
| 105 | const languages = acceptLanguageHeader |
| 106 | .split(',') |
| 107 | .map((lang) => { |
| 108 | const parts = lang.trim().split(';') |
| 109 | const locale = parts[0] |
| 110 | let quality = 1 |
| 111 | |
| 112 | if (parts[1]) { |
| 113 | const qMatch = parts[1].match(/q=([0-9.]+)/) |
| 114 | if (qMatch && qMatch[1]) { |
| 115 | quality = parseFloat(qMatch[1]) |
| 116 | } |
| 117 | } |
| 118 | |
| 119 | return { locale, quality } |
| 120 | }) |
| 121 | .filter((lang) => lang.quality > 0) |
| 122 | .sort((a, b) => b.quality - a.quality) |
| 123 | |
| 124 | // Create lowercase lookup for locales |
| 125 | const localeLookup = new Map<string, string>() |
| 126 | for (const locale of locales) { |
| 127 | localeLookup.set(locale.toLowerCase(), locale) |
| 128 | } |
| 129 | |
| 130 | // Try to find exact match first |
| 131 | for (const { locale } of languages) { |
| 132 | const normalized = locale.toLowerCase() |
| 133 | if (localeLookup.has(normalized)) { |
| 134 | return localeLookup.get(normalized) |
| 135 | } |
| 136 | } |
| 137 | |
| 138 | // Try prefix matching (e.g., "en-US" matches "en") |
| 139 | for (const { locale } of languages) { |
| 140 | const prefix = locale.toLowerCase().split('-')[0] |
| 141 | if (localeLookup.has(prefix)) { |
| 142 | return localeLookup.get(prefix) |
| 143 | } |
| 144 | |
| 145 | // Also check if any configured locale starts with this prefix |
| 146 | for (const [key, value] of localeLookup) { |
| 147 | if (key.startsWith(prefix + '-')) { |
| 148 | return value |
| 149 | } |
| 150 | } |
| 151 | } |
| 152 | |