Skip to content

normalizeLanguage only tries the full tag and the bare base language, so de-AT and zh-Hant-TW miss the locale they obviously mean #217

Description

@hotlong

Found while shipping #172. Filed unassigned. Pre-existing on main; #172 did not introduce it and did not change this function.

Notation: this repo's issue-body sanitizer strips angle brackets and decodes HTML numeric character references, including inside code fences. Nothing below needs either.

The reading

apps/docs/middleware.ts:

function normalizeLanguage(lang: string): string {
  if (LANGUAGE_MAPPING[lang]) return LANGUAGE_MAPPING[lang];
  const baseLang = lang.split('-')[0];
  if (LANGUAGE_MAPPING[baseLang]) return LANGUAGE_MAPPING[baseLang];
  return lang;
}

Two lookups: the exact tag, then the substring before the first hyphen. The result is then required to be in i18n.languages.

LANGUAGE_MAPPING holds only Chinese and Korean entries. So for every other language the second lookup misses too, and the function returns the tag unchanged — which is not in i18n.languages when it carries a region.

Consequences, for a browser that sends a region tag without also listing the bare language:

Accept-Language negotiated should be
de-AT en de
fr-CA en fr
es-MX en es
ja-JP en ja
zh-Hant-TW zh-Hans zh-Hant
zh-Hans-CN zh-Hans (by luck, via the bare zh entry) zh-Hans

Most real browsers send de-AT,de;q=0.9, and the bare de on the second pass rescues it — which is why this has not been noticed. The ones that send a single region tag fall through to English.

The zh-Hant-TW row is the one that got worse in kind rather than degree: before #172 every Chinese browser landed on Simplified, so a script-plus-region tag landing on Simplified was indistinguishable from the general policy. Now that Traditional ships, zh-Hant-TW is a wrong answer where zh-TW, zh-HK, zh-MO and zh-Hant are all right ones.

Why it is not just "add more table rows"

Adding de-AT, fr-CA, zh-Hant-TW … enumerates a set that has no end. The shape of the bug is that the fallback chain drops all subtags in one step instead of one at a time. BCP 47 truncation is: try the whole tag, then drop the last subtag, repeat, until something matches — zh-Hant-TW -> zh-Hant -> zh. That is a few lines and closes the class.

Whichever route is taken, the mapping table and the truncation need to agree about which wins, and the result must still be checked against i18n.languages rather than returned raw.

Verification

Against a running server, Accept-Language set to exactly one tag with no q-list, asserting the Location header of /docs/quickstart. Measured today on the #172 branch: zh-TW, zh-HK, zh-MO, zh-Hant all reach /zh-Hant/docs/quickstart, and zh-CN, zh-Hans reach /zh-Hans/...; the region tags in the table above were not covered by that run and are the cases this issue is about.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions