Skip to content

cli: the i18n coverage walker never collects objects.<o>._sections — a bundle can ship every section heading in English and lint zero warnings #5405

Description

@yinlianghui

objectstack lint's i18n coverage is driven by a shared walker whose kind union
simply has no entry for an app object's _sections:

  • packages/cli/src/utils/i18n-extract.ts:93-108ExpectedEntry['source'] is
    object | field | option | view | action | globalAction | app | navigation | dashboard | widget | page | metadataType | metadataFormSection | metadataFormField. The two metadataForm* kinds cover Studio metadata
    forms
    (metadataForms.<type>.sections.*, hidden behind
    --include-platform), not app objects.
  • The walker never emits an objects.<o>._sections.* path: the string
    '_sections' does not occur in i18n-extract.ts, and nothing walks
    fieldGroups or a field's group.
  • packages/cli/src/utils/i18n-coverage.ts:190-204 (COVERAGE_SOURCE) mirrors
    the same union, so computeI18nCoverage structurally cannot report a missing
    section heading.

Yet the surface is real and supported end-to-end everywhere else:
ObjectTranslationDataSchema declares _sections
(packages/spec/src/system/translation.zod.ts), the resolver applies them
(record detail pages render translated headings), and os i18n extract will
never scaffold them either — same walker.

Code references pinned to main @ 98369a8.

Measured impact (HotCRM, four locales)

HotCRM's metadata declares 85 sections across 15 objects (union of field
groups and page/view sections[].name). Before objectstack-ai/hotcrm#697,
ja-JP and es-ES each carried 2 of 85 — English headings on essentially
every record page and form — while objectstack lint reported 0 i18n
warnings
for both. The gap also kept growing invisibly: objects added later
(crm_event, crm_event_attendee) shipped with headings untranslated in every
locale, and nothing flagged it. Downstream had to build its own guard
(hotcrm#697) deriving the expected set from metadata.

Suggested fix shape

Add a section kind to the walker: emit
objects.<o>._sections.<key>.label for (a) every distinct group across an
object's fields — the fields are the authority for which sections exist;
fieldGroups[].label supplies the default-locale text — and (b) every
sections[].name under the object's pages/form views. Both sources are needed;
detail pages nest sections inside page:tabs → components → properties.sections, which a shallow walk misses. COVERAGE_SOURCE then maps
it, giving lint an i18n/missing-section category and the extractor the
scaffold for free (they share the walker — that co-derivation is the point of
collectExpectedEntries).

Related: #5164 (the _views key-spelling disagreement between composer,
extractor and lint — adjacent producer-drift problem on the neighbouring group).
Downstream: objectstack-ai/hotcrm#683 (part 2).

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions