Skip to content

i18n: GET /i18n/locales declares a locale label as "Display name" but every producer sets it to the code #7634

Description

@yinlianghui

Found while implementing objectstack-ai/objectui#4039 (the console language menu now reads this endpoint). Filing only; not fixed there — out of that issue's scope, and the consumer side deliberately routes around it.

What

GetLocalesResponseSchema (packages/spec/src/api/protocol.zod.ts) declares each locale descriptor as:

locales: z.array(z.object({
  code: z.string().describe('BCP-47 locale code (e.g., en-US, zh-CN)'),
  label: z.string().describe('Display name of the locale'),
  isDefault: z.boolean().default(false).describe('Whether this is the default locale'),
}))

The only producer of those descriptors is toLocaleDescriptors (packages/spec/src/system/i18n-resolver.ts):

return codes.map((code) => ({ code, label: code, isDefault: code === defaultLocale }));

label is the code echoed back. Both serving surfaces go through that one helper — the runtime dispatcher's /i18n domain and service-i18n's autonomous route — deliberately, and correctly, so there is no second implementation that behaves differently. The field is therefore never a display name on any surface: GET /api/v1/i18n/locales answers { code: 'th', label: 'th' }, never { code: 'th', label: 'ไทย' }.

Why it is worth writing down

label reads as a display name in the schema, in the generated SDK type, and in the OpenAPI surface. A client that trusts the description renders locale codes to users and only finds out by looking. objectui#4039 hit exactly this and had to make the opposite decision explicitly: the console's language menu names locales from its own built-in metadata plus Intl.DisplayNames and ignores the endpoint's label, because using it would have put th in the menu where ไทย belongs.

This is the declared-but-not-enforced shape ADR-0049 is about, one field wide: the vocabulary exists, no implementation stands behind it.

Observation-class, not a live defect

Nothing a user hits today, as far as I can tell — there is no current consumer of the field. The one client that reached for it (the console) now does not. Recording it so triage can grade it rather than leaving it in a PR comment.

Options, not a decision

  • Retire the field — drop label from the descriptor and the schema; code is the whole payload, and naming a locale is the client's job anyway (that is where Intl.DisplayNames and any built-in native names live). Smallest surface, and consistent with what the one real consumer chose.
  • Implement it — have toLocaleDescriptors produce a real display name. Needs a decision on which language it is named in (its own, or the requester's Accept-Language), and puts CLDR data on the server for something every client can already compute.

No pull for the second that I can see; recording both so the call is made rather than inherited.


Generated by Claude Code

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions