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-108 — ExpectedEntry['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).
objectstack lint's i18n coverage is driven by a shared walker whose kind unionsimply has no entry for an app object's
_sections:packages/cli/src/utils/i18n-extract.ts:93-108—ExpectedEntry['source']isobject | field | option | view | action | globalAction | app | navigation | dashboard | widget | page | metadataType | metadataFormSection | metadataFormField. The twometadataForm*kinds cover Studio metadataforms (
metadataForms.<type>.sections.*, hidden behind--include-platform), not app objects.objects.<o>._sections.*path: the string'_sections'does not occur ini18n-extract.ts, and nothing walksfieldGroupsor a field'sgroup.packages/cli/src/utils/i18n-coverage.ts:190-204(COVERAGE_SOURCE) mirrorsthe same union, so
computeI18nCoveragestructurally cannot report a missingsection heading.
Yet the surface is real and supported end-to-end everywhere else:
ObjectTranslationDataSchemadeclares_sections(
packages/spec/src/system/translation.zod.ts), the resolver applies them(record detail pages render translated headings), and
os i18n extractwillnever 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/viewsections[].name). Before objectstack-ai/hotcrm#697,ja-JPandes-ESeach carried 2 of 85 — English headings on essentiallyevery record page and form — while
objectstack lintreported 0 i18nwarnings for both. The gap also kept growing invisibly: objects added later
(
crm_event,crm_event_attendee) shipped with headings untranslated in everylocale, and nothing flagged it. Downstream had to build its own guard
(hotcrm#697) deriving the expected set from metadata.
Suggested fix shape
Add a
sectionkind to the walker: emitobjects.<o>._sections.<key>.labelfor (a) every distinctgroupacross anobject's fields — the fields are the authority for which sections exist;
fieldGroups[].labelsupplies the default-locale text — and (b) everysections[].nameunder 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_SOURCEthen mapsit, giving lint an
i18n/missing-sectioncategory and the extractor thescaffold for free (they share the walker — that co-derivation is the point of
collectExpectedEntries).Related: #5164 (the
_viewskey-spelling disagreement between composer,extractor and lint — adjacent producer-drift problem on the neighbouring group).
Downstream: objectstack-ai/hotcrm#683 (part 2).