Skip to content

i18n: the validation-message bridge matches the locale EXACTLY while every document translator does variant fallback — accept-language: zh refuses in English on the same response whose dataset labels are Chinese #15757

Description

@os-warren

Found on 17.3.0 while wiring the newly-shipped objects.<o>._validations.<r>.message key (from #14381) into a downstream app (objectstack-ai/duly#106). The key itself works — this is about which locales reach it.

The asymmetry, in one server, from one bundle

The app ships exactly one non-source locale, zh-CN. Same running server, same session, only accept-language varying.

Authored validation messagePATCH /api/v1/data/duly_duty/<id> tripping returned_needs_note:

accept-language: zh-CN            -> "请写明打回的原因——负责人需要据此知道该改什么。"   ✅
accept-language: zh-CN,zh;q=0.9   -> "请写明打回的原因——负责人需要据此知道该改什么。"   ✅
accept-language: zh               -> "Say why the duty is being returned — …"        ❌
accept-language: en               -> "Say why the duty is being returned — …"        ✅ correct

Dataset labelsPOST /api/v1/analytics/dataset/query, the same zh:

accept-language: zh    -> field labels: ['形式', '清单内职责']     ✅ translated
accept-language: en    -> field labels: ['Form', 'Duties on the register']

So a client sending zh gets a screen whose dataset/view/object labels are Chinese and whose refusals are English.

Mechanism

@objectstack/spec already implements the negotiation, in resolveBundleLocale:

const base = lower.split("-")[0];
const baseMatch = available.find((code) => code.toLowerCase() === base);
if (baseMatch) return baseMatch;
const variantMatch = available.find((code) => code.toLowerCase().split("-")[0] === base);
if (variantMatch) return variantMatch;          // <- 'zh' resolves to 'zh-CN' here

pickData calls it, and every document translator (translateObject, translateView, translateDataset, …) goes through pickData — which is why the labels above translate on bare zh.

The authored-validation path does not. authoredRuleMessage (@objectstack/objectql 17.3.0) asks the bridged i18n service directly:

const key = objectValidationMessageKey(messages.objectName, rule.name);
const translated = messages.translate(key, messages.locale);
if (typeof translated === "string" && translated.length > 0 && translated !== key) return translated;
return rule.message;

messages.locale is whatever preferredLocaleFromHeader returned — and that helper deliberately returns the header's first tag verbatim (zh), doing no region expansion of its own:

const top = header.split(",")[0]?.split(";")[0]?.trim();
return top && top !== "*" ? top : void 0;

With no zh bundle present the service lookup misses and the English rule.message is returned. Nothing on this path consults resolveBundleLocale.

Why it is worth closing rather than living with

  • Accept-Language: zh is ordinary — Playwright's Chromium sends exactly that for locale: 'zh-CN', which is how this was found; plenty of non-browser clients send a bare language tag too.
  • The failure is the half-translated screen the i18n gates exist to prevent, and it is invisible to an app: the bundle key is present and correct, the coverage gate is green, and the app cannot tell that one consumer negotiates differently from the others.
  • It is a one-line-shaped fix in a place the platform already owns: route the validation-message lookup through the same resolveBundleLocale/pickData seam the document translators use, so there is one negotiation rule rather than two.

Adjacent but distinct: #14882 is about localeChain's fallbackChain defaulting to ['en'] and ignoring i18n.fallbackLocale — that is about which locale wins after a miss; this is about a path that never attempts variant matching at all.

Downstream evidence: objectstack-ai/duly#106.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions