Skip to content

i18n: nothing reads TranslationData.flows — a screen flow still renders its authored English in every locale (the runner half #7646 deferred) #11287

Description

@os-zhuang

Summary

TranslationData.flows — the screen-flow copy vocabulary #7646 declared — has no reader anywhere in the shipped platform. An app can author flows.<name>.screens.<node_id>.title today, have it parse, validate and load into the i18n tree, and the wizard still renders the author's source-language strings in every locale.

This is the runner half #7646 explicitly deferred. Filing it because it is not filed anywhere: #7646 is closed, its report named two follow-ups (the objectui runner application, and a FLOW_SCREEN_COPY_KEYS resolver helper Blocked-by: #7634), and neither exists as a card. #7634 closed 2026-08-11, so the resolver half is unblocked.

Measurement (@objectstack/spec / service-automation / console all at 17.1.0)

Real AutomationEngine running HotCRM's real lead_conversion flow, with the real zh-CN bundle carrying the proposed flows section loaded into a real FileI18nAdapter registered as the i18n service on the engine context, and locale: 'zh-CN' on the execution context:

=== 1. carrier: does TranslationDataSchema accept `flows`? ===
safeParse(zh-CN + flows).success = true
=== 2. liveness ledger shipped in @objectstack/spec@17.1.0 ===
flows.status = planned
flows.authorWarn = true
children.label = planned
children.screens = planned
(sibling control) objects.status = live | dashboards.status = live | pages.status = live
=== 3. spec i18n-resolver export surface ===
resolver/translator exports: resolveActionConfirm, resolveActionLabel, resolveActionResultDialog,
resolveActionSuccess, resolveBookClaimedDocs, resolveBookTree, resolveBundleLocale, resolveDocAudiences,
resolveDocLocale, resolveMetadataFormLabels, resolveMetadataTypeDescription, resolveMetadataTypeLabel,
resolveObjectFieldLabels, resolveSettings*, resolveTabLabel, resolveViewDescription, resolveViewLabel,
translateAction, translateApp, translateDashboard, translateMetadataDocument, translateObject,
translatePage, translateView
any naming flow/screen? -> NONE
=== 4. POSITIVE CONTROL — same bundle, a surface that does have a reader ===
translateObject(crm_lead).label = 线索
i18n.t("objects.crm_lead.label","zh-CN") = 线索
i18n.t("flows.lead_conversion.screens.screen_1.title","zh-CN") = 转化详情 <- the string IS in the tree
=== 5. THE MEASUREMENT — the real screen flow, same bundle live ===
run status = paused
screen.nodeId = screen_1
screen.title = "Conversion Details"
screen.fields[].label = ["createOpportunity: Create Opportunity?",
"opportunityName: Opportunity Name",
"opportunityAmount: Opportunity Amount"]

The string is resolvable by key and nothing asks for it.

Where the two halves would land

Server.packages/services/service-automation — the screen executor builds the spec from raw node config with no locale in scope:

screen: {nodeId: node.id,title: interp(cfg.title)??node.label??'Input',description: interp(cfg.description),
fields,// fields[].label copied verbatim from ScreenFieldConfig.label}

Client. The console's screen-flow dialog renders the payload verbatim — no t() on this path:

// @objectstack/console 17.1.0, RecordDetailView chunk(0,J.jsx)(Ge,{children: s.title||`Input`})// dialog title
children:[e.label||e.name,e.required&&]// field label

and the console's metadata-l10n hook enumerates exactly objects / fields / fieldOptions / apps / dashboards / pages / reports / globalActions — it exposes objectLabel, fieldLabel, dashboardLabel, widgetTitle, pageLabel, viewLabel, sectionLabel, actionLabel, … and no flow/screen accessor. Its namespace-detection predicate does not list flows either.

Neither is a drift: the ledger row says so in its own words —

No shipped screen-flow runner reads this group yet — until the objectui half of #7646 lands, a type: 'screen' flow renders the strings authored on the flow (config.title, fields[].label, fields[].placeholder) in every locale.

Also unreachable from the tooling side

  • @objectstack/cli's i18n-coverage.jsCOVERAGE_SOURCE taxonomy has no flow/screen bucket, so os lint's i18n/missing-* family can never report a screen-flow gap, and os i18n extract never scaffolds one. An app whose i18n gate is green is green because the surface is invisible to it, not because it is covered. Measured on HotCRM: pnpm lint:i18n-gate prints 0 i18n/missing-* issues on a tree where six screen dialogs are English in all four locales.
  • packages/lint/src/validate-translation-references.ts skips unrecognised top-level namespaces, so authored flows keys are also unvalidated against the flow they name.
  • The ledger's own authorWarn: true on this row does not reach the author either — measured, and it falsified my expectation rather than confirming it. Injecting a flows: section into a real app's locale bundle and re-running objectstack lint --json produced zero new findings (91 total issues before and after, 0 liveness findings naming flows). Cause: lintLivenessProperties walks stack.translations items as TranslationItems, but that collection is z.array(TranslationBundleSchema) — locale-keyed — so every warned-property lookup misses. Filed separately as lintLivenessProperties walks stack.translations as if each item were a TranslationItem — it is a locale-keyed bundle, so the whole translation ledger warns nobody #11288. Net effect for this card: authoring the group today is silent, not merely inert.

Suggested scope

  1. packages/spec/src/system/i18n-resolver.ts — a FLOW_SCREEN_COPY_KEYS sibling of PAGE_COMPONENT_COPY_KEYS, plus the translateFlow / resolveFlowScreenTitle overlay it drives (i18n: TranslationBundle has no surface for screen-flow UI — wizard screens render source-language only, even in a fully localized app #7646's own recommendation B; Blocked-by: #7634 is discharged — i18n: GET /i18n/locales declares a locale label as "Display name" but every producer sets it to the code #7634 closed 2026-08-11).
  2. The runner application: either server-side (localize ScreenSpec before it goes on the wire, which needs a locale on the execution context) or client-side in the console/objectui screen dialog. Picking the side is the design decision this card owes — the client already holds ScreenSpec.nodeId and ScreenFieldSpec.name, which is what the addressing was measured against, so the client side needs no new identifier.
  3. @objectstack/cli i18n coverage/extract: add a flow/screen bucket so the gap becomes visible to os lint once a reader exists.
  4. Flip the flows ledger row (and its label / screens children) to live with the evidence pointer; drop authorWarn.

Runner chrome stays out of scope, per the #7646 maintainer ruling: Cancel / Submit / Submitting… / Save & Continue / the terminal Done toast are the console's own words in every app (currently hardcoded English literals in the console bundle) and belong in the console's message catalog, not in a per-app bundle. Worth noting the toast separately: FlowSchema.successMessage overrides Done, but successMessage is deliberately not on the flows translation surface, so a localized terminal toast has no channel at all today.

Back-link

Found while working objectstack-ai/hotcrm#1210 — the HotCRM card asking for the four bundle sections. That card cannot be implemented as written: authoring the sections today writes translations nothing renders, and (per the measurement above) nothing warns about it. HotCRM is holding until this lands.

No labels applied — this spans spec + console/objectui, so routing is triage's call.

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions