/** * cli:scaffold-component — render/i18n-files.ts * Extracted VERBATIM from the historical generate.ts (pure move — frozen by * the 1.0 corpus hash). Renders nothing when the view is absent from * spec.views. */ import type { GeneratedFile } from '../types.js' import { LOCALES, Locale, buildTranslations, dropI18nPlaceholders, flatToNested, flattenCatalogue, normalizeI18nFieldKeys } from './shared.js' import type { RenderContext } from './context.js' export function renderI18nFiles(rc: RenderContext): GeneratedFile[] { const files: GeneratedFile[] = [] const { spec, ctx, eLower } = rc // ─── i18n JSON per locale ──────────────────────────────────────────────── // The generator ALWAYS emits a COMPLETE floor catalogue (buildTranslations) // that mirrors every bare `t('…')` key the templates render — list / detail / // form / dashboard / home / kanban / reconduction / breadcrumb. In enriched // mode the PRD's business-translated labels (pageSpec.i18nKeys) are layered // ON TOP as overrides: the PRD wins where it supplies a key, the floor // guarantees no rendered key is ever missing (never a raw key on screen). // Without the floor, a thin PRD i18nKeys block leaves structural keys // (breadcrumb.section, list.actionsColumn, form.submitCreate, …) untranslated. // // i18n catalogues land at `src/i18n/locales/{locale}/{module}.json` — // module-level (one JSON per module, NOT per entity), matching the // SmartStack project layout where i18n config.ts loads namespaces by module. // // SELF-SUFFICIENT emission: the existing on-disk module catalogue (injected // via ctx.existingI18n by index.ts / tests) is folded in HERE, so any // verbatim writer produces a correct file. Precedence for THIS entity's // subtree: // floor (gap-filler) < existing (earlier views' PRD text, manual fixes) // < this call's PRD i18nKeys (the authored truth). // The floor-below-existing order is what stops the cross-view clobber: the // pipeline scaffolds one VIEW per call while buildTranslations is // view-agnostic, so without it a later `form` call's floor `list.subtitle` // would revert the `list` call's PRD text (AtlasHub PROJECTS/BUDGETS). // Sibling entity roots are carried VERBATIM. Keys in `existing` that belong // to neither floor nor PRD LINGER on purpose (provenance is unknowable — a // manual key rendered by a @customised page must survive); they are inert. for (const locale of LOCALES) { const floorFlat = flattenCatalogue(buildTranslations(spec, locale as Locale)) // dropI18nPlaceholders: a `"[en] "`-style placeholder must never // override the floor — dropping it here means the floor's real translation // (or humanised label) ships instead of the bracket-tagged raw value. const prdFlat = spec.pageSpec ? normalizeI18nFieldKeys(dropI18nPlaceholders({ ...(spec.pageSpec.i18nKeys[locale as keyof typeof spec.pageSpec.i18nKeys] ?? {}) })) : {} const existingModule = ctx.existingI18n?.[locale] ?? {} const existingEntity = existingModule[eLower] const existingEntityFlat = existingEntity !== null && typeof existingEntity === 'object' && !Array.isArray(existingEntity) ? flattenCatalogue(existingEntity as Record) : {} const i18nFlat = { ...floorFlat, ...existingEntityFlat, ...prdFlat } // Wrap under entity key so multiple entities in the same module coexist // without collision: { contact: { list: {...} }, company: { list: {...} } }. // Spread-override keeps the entity key's original position → stable diffs. const payload = { ...existingModule, [eLower]: flatToNested(i18nFlat) } files.push({ path: `src/i18n/locales/${locale}/${spec.module}.json`, content: JSON.stringify(payload, null, 2) + '\n', }) } return files }