Astro Info
Astro v5.x (latest)
Node v22.x
Package manager pnpm
Platform linux
If this issue only occurs in one browser, which browser is a problem?
No response
Describe the Bug
computePreferredLocale() in packages/astro/src/i18n/utils.ts (lines 86–113) violates its documented "first match wins" semantics when the matching config entry is in object form ({ path, codes }). The break inside the codes loop only exits the inner loop, so the outer iteration continues and any later config entry whose code normalizes to the same value overwrites the earlier (correct) match.
for (const currentLocale of locales) {
if (typeof currentLocale === 'string') {
if (normalizeTheLocale(currentLocale) === normalizeTheLocale(firstResult.locale)) {
result = currentLocale;
break; // ✓ breaks outer
}
} else {
for (const currentCode of currentLocale.codes) {
if (normalizeTheLocale(currentCode) === normalizeTheLocale(firstResult.locale)) {
result = currentCode;
break; // ❌ only breaks inner — outer continues
}
}
}
}
The string-entry branch already breaks the outer loop, confirming "first match wins" is the intended semantics. The codes-array branch is missing the same outer break.
normalizeTheLocale() lowercases and replaces _ with - (packages/astro/src/i18n/index.ts:228), so two distinct literal strings can compare equal after normalization. Three cases that demonstrate the divergence (verified in the linked repro):
locales config |
Accept-Language |
buggy returns |
should return |
[{ path: 'us', codes: ['EN-US'] }, 'en-us'] |
en-us |
'en-us' |
'EN-US' |
[{ path: 'us', codes: ['EN'] }, { path: 'gb', codes: ['en'] }] |
en |
'en' |
'EN' |
[{ path: 'us', codes: ['en_US'] }, { path: 'us2', codes: ['en-US'] }] |
en-US |
'en-US' |
'en_US' |
The unit tests at packages/astro/test/units/i18n/i18n-utils.test.ts:59-79 only cover all-string locales arrays, so the object-entry branch is untested and this regression is uncovered.
Practical impact is limited. The buggy and correct return values represent the same locale, just in different casing or punctuation. The function only feeds Astro.preferredLocale / context.preferredLocale; no routing or redirect code paths consume this return value. Triggering also requires a non-typical config where two locale entries have codes that normalize-equal. So while the code is clearly wrong, severity in real-world projects is low — closer to a code-quality / latent-correctness fix than a user-impacting regression.
The fix is a labelled break outer (or a matched flag plus outer break) so the function honors first-match semantics in both branches.
What's the expected result?
computePreferredLocale should return the first matching code from the first matching locales entry, regardless of whether that entry is a string or { path, codes }. For locales: [{ path: 'us', codes: ['EN'] }, { path: 'gb', codes: ['en'] }] and Accept-Language: en, the expected result is 'EN' (the first matching entry's code), not 'en'.
Link to Minimal Reproducible Example
https://github.com/Hunnyboy1217/astro-i18n-preferred-locale-repro
Participation
Astro Info
If this issue only occurs in one browser, which browser is a problem?
No response
Describe the Bug
computePreferredLocale()inpackages/astro/src/i18n/utils.ts(lines 86–113) violates its documented "first match wins" semantics when the matching config entry is in object form ({ path, codes }). Thebreakinside thecodesloop only exits the inner loop, so the outer iteration continues and any later config entry whose code normalizes to the same value overwrites the earlier (correct) match.The string-entry branch already breaks the outer loop, confirming "first match wins" is the intended semantics. The codes-array branch is missing the same outer break.
normalizeTheLocale()lowercases and replaces_with-(packages/astro/src/i18n/index.ts:228), so two distinct literal strings can compare equal after normalization. Three cases that demonstrate the divergence (verified in the linked repro):localesconfigAccept-Language[{ path: 'us', codes: ['EN-US'] }, 'en-us']en-us'en-us''EN-US'[{ path: 'us', codes: ['EN'] }, { path: 'gb', codes: ['en'] }]en'en''EN'[{ path: 'us', codes: ['en_US'] }, { path: 'us2', codes: ['en-US'] }]en-US'en-US''en_US'The unit tests at
packages/astro/test/units/i18n/i18n-utils.test.ts:59-79only cover all-stringlocalesarrays, so the object-entry branch is untested and this regression is uncovered.Practical impact is limited. The buggy and correct return values represent the same locale, just in different casing or punctuation. The function only feeds
Astro.preferredLocale/context.preferredLocale; no routing or redirect code paths consume this return value. Triggering also requires a non-typical config where two locale entries have codes that normalize-equal. So while the code is clearly wrong, severity in real-world projects is low — closer to a code-quality / latent-correctness fix than a user-impacting regression.The fix is a labelled
break outer(or amatchedflag plus outer break) so the function honors first-match semantics in both branches.What's the expected result?
computePreferredLocaleshould return the first matching code from the first matchinglocalesentry, regardless of whether that entry is a string or{ path, codes }. Forlocales: [{ path: 'us', codes: ['EN'] }, { path: 'gb', codes: ['en'] }]andAccept-Language: en, the expected result is'EN'(the first matching entry's code), not'en'.Link to Minimal Reproducible Example
https://github.com/Hunnyboy1217/astro-i18n-preferred-locale-repro
Participation