/** * cli:scaffold-component — i18n catalogue disk I/O. * * The generator is pure: index.ts reads the existing on-disk module catalogues * here and injects them via `GenerateContext.existingI18n`, so `generate()` * emits the ALREADY-MERGED full module JSON (sibling entities carried verbatim, * target entity merged with precedence floor < existing < PRD). See the * emission block in generate.ts. * * Fail-closed contract: a locale file that exists but is not a parseable JSON * OBJECT is reported in `malformed[]` — the caller must refuse to write (the * historical silent overwrite-on-parse-error wiped whole modules' manual * translations). * * `writeFileAtomic` mirrors aggregate-component-registry/write.ts: tmp + * rename (atomic on the same volume) so a reader never observes a torn file. * NOTE: atomicity fixes torn writes, NOT lost updates — two concurrent * read-merge-write cycles on the same module can still drop each other's new * entity, which is why same-module scaffolds stay serialized (ba-develop * phases-detail.md). */ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs' import { dirname, resolve } from 'node:path' import { safeJoinPath } from '../../../../../lib/fs.js' export const I18N_LOCALES = ['fr', 'en', 'it', 'de'] as const export interface ExistingI18nRead { /** locale → parsed whole-file catalogue; only locales whose file exists AND parses to a plain object. */ existingI18n: Record> /** Absolute paths of files that exist but are NOT a parseable JSON object (fail-closed input). */ malformed: string[] } /** Read the 4 locale catalogues of `module` under `webRoot`, if present. */ export function readExistingI18n(webRoot: string, module: string): ExistingI18nRead { const existingI18n: Record> = {} const malformed: string[] = [] for (const locale of I18N_LOCALES) { const p = resolve(safeJoinPath(webRoot, `src/i18n/locales/${locale}/${module}.json`)) if (!existsSync(p)) continue try { const parsed = JSON.parse(readFileSync(p, 'utf-8')) as unknown if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) { malformed.push(p) continue } existingI18n[locale] = parsed as Record } catch { malformed.push(p) } } return { existingI18n, malformed } } /** Write atomically (tmp + rename); every observed state is a complete file. */ export function writeFileAtomic(absPath: string, content: string): void { mkdirSync(dirname(absPath), { recursive: true }) const tmp = `${absPath}.tmp-${process.pid}` writeFileSync(tmp, content, 'utf-8') renameSync(tmp, absPath) }