Skip to content

i18n: computePreferredLocale's inner break only exits the inner loop #16598

@hunnyboy1217

Description

@hunnyboy1217

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

  • I am willing to submit a pull request for this issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    - P3: minor bugAn edge case that only affects very specific usage (priority)pkg: astroRelated to the core `astro` package (scope)

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions