uniweb 0.5.6 → 0.5.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uniweb",
3
- "version": "0.5.6",
3
+ "version": "0.5.7",
4
4
  "description": "Create structured Vite + React sites with content/code separation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -5,6 +5,7 @@
5
5
  *
6
6
  * Usage:
7
7
  * uniweb i18n extract Extract translatable strings to manifest
8
+ * uniweb i18n init [locales] Generate starter locale files from manifest
8
9
  * uniweb i18n sync Sync manifest with content changes
9
10
  * uniweb i18n status Show translation coverage per locale
10
11
  * uniweb i18n --target <path> Specify site directory explicitly
@@ -99,6 +100,9 @@ export async function i18n(args) {
99
100
  case 'extract':
100
101
  await runExtract(siteRoot, config, effectiveArgs)
101
102
  break
103
+ case 'init':
104
+ await runInit(siteRoot, config, effectiveArgs)
105
+ break
102
106
  case 'sync':
103
107
  await runSync(siteRoot, config, effectiveArgs)
104
108
  break
@@ -313,6 +317,113 @@ async function runExtract(siteRoot, config, args) {
313
317
  }
314
318
  }
315
319
 
320
+ /**
321
+ * Init command - generate starter translation files from manifest
322
+ *
323
+ * Usage:
324
+ * uniweb i18n init es fr Initialize specific locales
325
+ * uniweb i18n init Initialize all configured locales
326
+ * uniweb i18n init --empty Use empty strings instead of source text
327
+ * uniweb i18n init --force Overwrite existing files entirely
328
+ */
329
+ async function runInit(siteRoot, config, args) {
330
+ const useEmpty = args.includes('--empty')
331
+ const force = args.includes('--force')
332
+
333
+ // Collect locale codes from positional args (skip flags)
334
+ const positionalLocales = args.filter(a => !a.startsWith('-'))
335
+
336
+ // Read manifest
337
+ const localesPath = join(siteRoot, config.localesDir)
338
+ const manifestPath = join(localesPath, 'manifest.json')
339
+
340
+ if (!existsSync(manifestPath)) {
341
+ error('No manifest found. Run "uniweb i18n extract" first to generate one.')
342
+ process.exit(1)
343
+ }
344
+
345
+ const manifestRaw = await readFile(manifestPath, 'utf-8')
346
+ const manifest = JSON.parse(manifestRaw)
347
+ const units = manifest.units || {}
348
+ const unitCount = Object.keys(units).length
349
+
350
+ if (unitCount === 0) {
351
+ warn('Manifest has no translatable strings. Nothing to initialize.')
352
+ return
353
+ }
354
+
355
+ // Determine target locales
356
+ let targetLocales = positionalLocales.length > 0
357
+ ? positionalLocales
358
+ : config.locales
359
+
360
+ if (!targetLocales || targetLocales.length === 0) {
361
+ error('No target locales specified.')
362
+ log(`${colors.dim}Specify locales as arguments (e.g., "uniweb i18n init es fr")`)
363
+ log(`or configure them in site.yml under i18n.locales.${colors.reset}`)
364
+ process.exit(1)
365
+ }
366
+
367
+ log(`\n${colors.cyan}Initializing translation files...${colors.reset}\n`)
368
+
369
+ await mkdir(localesPath, { recursive: true })
370
+
371
+ for (const locale of targetLocales) {
372
+ // Skip default locale
373
+ if (locale === config.defaultLocale) {
374
+ warn(`Skipped ${locale} (default locale)`)
375
+ continue
376
+ }
377
+
378
+ const localePath = join(localesPath, `${locale}.json`)
379
+
380
+ if (existsSync(localePath) && !force) {
381
+ // Merge mode: add only missing keys
382
+ const existingRaw = await readFile(localePath, 'utf-8')
383
+ let existing
384
+ try {
385
+ existing = JSON.parse(existingRaw)
386
+ } catch {
387
+ warn(`${locale}.json has invalid JSON, skipping (use --force to overwrite)`)
388
+ continue
389
+ }
390
+
391
+ const existingKeys = new Set(Object.keys(existing))
392
+ let added = 0
393
+
394
+ for (const [hash, unit] of Object.entries(units)) {
395
+ if (!existingKeys.has(hash)) {
396
+ existing[hash] = useEmpty ? '' : unit.source
397
+ added++
398
+ }
399
+ }
400
+
401
+ if (added > 0) {
402
+ await writeFile(localePath, JSON.stringify(existing, null, 2) + '\n')
403
+ const alreadyCount = existingKeys.size
404
+ success(`Updated ${locale}.json (${added} new string${added !== 1 ? 's' : ''} added, ${alreadyCount} already translated)`)
405
+ } else {
406
+ success(`${locale}.json already has all ${unitCount} strings`)
407
+ }
408
+ } else {
409
+ // Create new file (or overwrite with --force)
410
+ const localeData = {}
411
+
412
+ for (const [hash, unit] of Object.entries(units)) {
413
+ localeData[hash] = useEmpty ? '' : unit.source
414
+ }
415
+
416
+ await writeFile(localePath, JSON.stringify(localeData, null, 2) + '\n')
417
+ success(`Created ${locale}.json (${unitCount} string${unitCount !== 1 ? 's' : ''})`)
418
+ }
419
+ }
420
+
421
+ log(`\n${colors.dim}Next steps:`)
422
+ log(` 1. Edit locale files to add translations`)
423
+ log(` 2. Run 'uniweb build' to build with translations`)
424
+ log(` 3. Run 'uniweb i18n status' to check coverage${colors.reset}`)
425
+ }
426
+
316
427
  /**
317
428
  * Sync command - detect changes and update manifest
318
429
  */
@@ -1348,6 +1459,7 @@ ${colors.bright}Commands:${colors.reset}
1348
1459
  ${colors.dim}# Hash-based (granular) translation${colors.reset}
1349
1460
  (default) Same as sync - extract/update strings (runs if no command given)
1350
1461
  extract Extract translatable strings to locales/manifest.json
1462
+ init Generate starter locale files from manifest keys
1351
1463
  sync Update manifest with content changes (detects moved/changed content)
1352
1464
  status Show translation coverage per locale
1353
1465
  audit Find stale translations (no longer in manifest) and missing ones
@@ -1363,6 +1475,8 @@ ${colors.bright}Options:${colors.reset}
1363
1475
  -t, --target <path> Site directory (auto-detected if not specified)
1364
1476
  --verbose Show detailed output
1365
1477
  --dry-run (sync/prune) Show changes without writing files
1478
+ --empty (init) Use empty strings instead of source text
1479
+ --force (init) Overwrite existing locale files entirely
1366
1480
  --clean (audit) Remove stale entries from locale files
1367
1481
  --missing (status) List all missing strings instead of summary
1368
1482
  --freeform (status/prune) Include free-form translation status
@@ -1384,9 +1498,10 @@ ${colors.bright}Configuration:${colors.reset}
1384
1498
 
1385
1499
  ${colors.bright}Workflow:${colors.reset}
1386
1500
  1. Build your site: uniweb build
1387
- 2. Extract strings: uniweb i18n
1388
- 3. Translate locale files: Edit locales/es.json, locales/fr.json, etc.
1389
- 4. Build with translations: uniweb build (generates locale-specific output)
1501
+ 2. Extract strings: uniweb i18n extract
1502
+ 3. Initialize locale files: uniweb i18n init es fr
1503
+ 4. Translate locale files: Edit locales/es.json, locales/fr.json, etc.
1504
+ 5. Build with translations: uniweb build (generates locale-specific output)
1390
1505
 
1391
1506
  ${colors.bright}File Structure:${colors.reset}
1392
1507
  locales/
@@ -1406,6 +1521,9 @@ ${colors.bright}Examples:${colors.reset}
1406
1521
  uniweb i18n extract # Extract all translatable strings
1407
1522
  uniweb i18n extract --verbose # Show extracted strings
1408
1523
  uniweb i18n extract --with-collections # Extract pages + collections
1524
+ uniweb i18n init es fr # Create starter files for Spanish and French
1525
+ uniweb i18n init --empty # Create files with empty values (for translators)
1526
+ uniweb i18n init --force # Overwrite existing locale files
1409
1527
  uniweb i18n sync # Update manifest after content changes
1410
1528
  uniweb i18n status # Show coverage for all locales
1411
1529
  uniweb i18n status es # Show coverage for Spanish only