uniweb 0.6.19 → 0.6.21

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/README.md CHANGED
@@ -85,6 +85,8 @@ my-project/
85
85
 
86
86
  **Pages are folders.** Create `pages/about/` with markdown files inside → visit `/about`. That's the whole routing model.
87
87
 
88
+ **Batteries included:** File-based routing, pre-rendering, localization, dynamic routes, media processing, search indexing, and more. See [Building with Uniweb](https://github.com/uniweb/docs/blob/main/development/building-with-uniweb.md) for the full list.
89
+
88
90
  ### Content as Markdown
89
91
 
90
92
  ```markdown
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uniweb",
3
- "version": "0.6.19",
3
+ "version": "0.6.21",
4
4
  "description": "Create structured Vite + React sites with content/code separation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -40,7 +40,7 @@
40
40
  "js-yaml": "^4.1.0",
41
41
  "prompts": "^2.4.2",
42
42
  "tar": "^7.0.0",
43
- "@uniweb/build": "0.6.16",
43
+ "@uniweb/build": "0.6.18",
44
44
  "@uniweb/core": "0.4.7",
45
45
  "@uniweb/kit": "0.5.10",
46
46
  "@uniweb/runtime": "0.5.21"
@@ -167,16 +167,19 @@ Decimals insert between: `2.5-testimonials.md` goes between `2-` and `3-`.
167
167
  ```yaml
168
168
  title: About Us
169
169
  description: Learn about our company
170
- order: 2 # Navigation sort order
170
+ order: 2 # Navigation sort position
171
+ pages: [team, history, ...] # Child page order (... = rest). Without ... = strict (hides unlisted)
171
172
  index: getting-started # Which child page is the index
172
173
  ```
173
174
 
174
175
  **site.yml:**
175
176
  ```yaml
176
- index: home # Which page folder is the homepage
177
+ index: home # Just set the homepage
178
+ pages: [home, about, ...] # Order pages (... = rest, first = homepage)
179
+ pages: [home, about] # Strict: only listed pages in nav
177
180
  ```
178
181
 
179
- Use `index:` rather than `pages: [...]` listing pages explicitly hides auto-discovered ones.
182
+ Use `pages:` with `...` for ordering, without `...` for strict visibility control. Use `index:` for simple homepage selection.
180
183
 
181
184
  ## Semantic Theming
182
185
 
@@ -4,11 +4,12 @@
4
4
  * Commands for managing site content internationalization.
5
5
  *
6
6
  * Usage:
7
- * uniweb i18n extract Extract translatable strings to manifest
8
- * uniweb i18n init [locales] Generate starter locale files from manifest
9
- * uniweb i18n sync Sync manifest with content changes
10
- * uniweb i18n status Show translation coverage per locale
11
- * uniweb i18n --target <path> Specify site directory explicitly
7
+ * uniweb i18n extract Extract/update translatable strings (default)
8
+ * uniweb i18n generate [locales] Generate starter locale files from manifest
9
+ * uniweb i18n status Show translation coverage per locale
10
+ * uniweb i18n --target <path> Specify site directory explicitly
11
+ *
12
+ * Aliases: sync → extract, init → generate
12
13
  *
13
14
  * When run from workspace root, auto-detects sites. If multiple exist,
14
15
  * prompts for selection.
@@ -81,9 +82,9 @@ export async function i18n(args) {
81
82
  // Parse --target option
82
83
  const { target, remainingArgs } = parseTargetOption(args)
83
84
 
84
- // Default to 'sync' if no subcommand (or if first arg is an option)
85
+ // Default to 'extract' if no subcommand (or if first arg is an option)
85
86
  const firstArg = remainingArgs[0]
86
- const effectiveSubcommand = !firstArg || firstArg.startsWith('-') ? 'sync' : firstArg
87
+ const effectiveSubcommand = !firstArg || firstArg.startsWith('-') ? 'extract' : firstArg
87
88
  const effectiveArgs = !firstArg || firstArg.startsWith('-') ? remainingArgs : remainingArgs.slice(1)
88
89
 
89
90
  // Find site root
@@ -98,14 +99,13 @@ export async function i18n(args) {
98
99
 
99
100
  switch (effectiveSubcommand) {
100
101
  case 'extract':
102
+ case 'sync':
101
103
  await runExtract(siteRoot, config, effectiveArgs)
102
104
  break
105
+ case 'generate':
103
106
  case 'init':
104
107
  await runInit(siteRoot, config, effectiveArgs)
105
108
  break
106
- case 'sync':
107
- await runSync(siteRoot, config, effectiveArgs)
108
- break
109
109
  case 'status':
110
110
  await runStatus(siteRoot, config, effectiveArgs)
111
111
  break
@@ -228,41 +228,48 @@ async function loadSiteConfig(siteRoot) {
228
228
  */
229
229
  async function runExtract(siteRoot, config, args) {
230
230
  const verbose = args.includes('--verbose') || args.includes('-v')
231
+ const dryRun = args.includes('--dry-run')
231
232
  const collectionsOnly = args.includes('--collections-only') || args.includes('--collections')
232
233
  const noCollections = args.includes('--no-collections')
233
234
  // --with-collections is now a no-op (collections are included by default)
234
235
 
235
236
  // Extract page content (unless --collections-only)
236
237
  if (!collectionsOnly) {
237
- log(`\n${colors.cyan}Extracting translatable content...${colors.reset}\n`)
238
-
239
- // Check if site has been built
240
- const siteContentPath = join(siteRoot, 'dist', 'site-content.json')
241
- if (!existsSync(siteContentPath)) {
242
- error('Site content not found. Run "uniweb build" first.')
243
- process.exit(1)
244
- }
238
+ log(`\n${colors.cyan}Extracting translatable content${dryRun ? ' (dry run)' : ''}...${colors.reset}\n`)
245
239
 
246
240
  try {
247
- // Dynamic import to avoid loading at CLI startup
241
+ // Collect site content directly from source files (no build required)
242
+ const { collectSiteContent } = await import('@uniweb/build/site')
248
243
  const { extractManifest, formatSyncReport } = await import('@uniweb/build/i18n')
249
244
 
250
- const { manifest, report } = await extractManifest(siteRoot, {
245
+ log(`${colors.dim}Collecting site content...${colors.reset}`)
246
+ const siteContent = await collectSiteContent(siteRoot)
247
+
248
+ // Check if this is a first-time extract (no previous manifest)
249
+ const manifestPath = join(siteRoot, config.localesDir, 'manifest.json')
250
+ const isUpdate = existsSync(manifestPath)
251
+
252
+ const { manifest, report } = await extractManifest(siteRoot, siteContent, {
251
253
  localesDir: config.localesDir,
252
- siteContentPath,
253
254
  verbose,
255
+ dryRun,
254
256
  })
255
257
 
256
258
  // Show results
257
259
  const unitCount = Object.keys(manifest.units).length
258
260
  success(`Extracted ${unitCount} translatable strings`)
259
261
 
260
- if (report) {
262
+ // Show sync report for updates (skip on first extract — everything would be "added")
263
+ if (report && isUpdate) {
261
264
  log('')
262
265
  log(formatSyncReport(report))
263
266
  }
264
267
 
265
- log(`\nManifest written to: ${colors.dim}${config.localesDir}/manifest.json${colors.reset}`)
268
+ if (dryRun) {
269
+ log(`\n${colors.dim}Dry run — no files were modified.${colors.reset}`)
270
+ } else {
271
+ log(`\nManifest written to: ${colors.dim}${config.localesDir}/manifest.json${colors.reset}`)
272
+ }
266
273
 
267
274
  if (config.locales.length === 0) {
268
275
  log(`\n${colors.dim}No translation files found in ${config.localesDir}/.`)
@@ -277,7 +284,7 @@ async function runExtract(siteRoot, config, args) {
277
284
 
278
285
  // Extract collection content (by default, skip with --no-collections)
279
286
  if (!noCollections) {
280
- log(`\n${colors.cyan}Extracting collection content...${colors.reset}\n`)
287
+ log(`\n${colors.cyan}Extracting collection content${dryRun ? ' (dry run)' : ''}...${colors.reset}\n`)
281
288
 
282
289
  // Check if collections exist
283
290
  const dataDir = join(siteRoot, 'public', 'data')
@@ -293,20 +300,28 @@ async function runExtract(siteRoot, config, args) {
293
300
  try {
294
301
  const { extractCollectionManifest, formatSyncReport } = await import('@uniweb/build/i18n')
295
302
 
303
+ const collectionsManifestPath = join(siteRoot, config.localesDir, 'collections', 'manifest.json')
304
+ const isUpdate = existsSync(collectionsManifestPath)
305
+
296
306
  const { manifest, report } = await extractCollectionManifest(siteRoot, {
297
307
  localesDir: config.localesDir,
308
+ dryRun,
298
309
  })
299
310
 
300
311
  const unitCount = Object.keys(manifest.units).length
301
312
  if (unitCount > 0) {
302
313
  success(`Extracted ${unitCount} translatable strings from collections`)
303
314
 
304
- if (report) {
315
+ if (report && isUpdate) {
305
316
  log('')
306
317
  log(formatSyncReport(report))
307
318
  }
308
319
 
309
- log(`\nManifest written to: ${colors.dim}${config.localesDir}/collections/manifest.json${colors.reset}`)
320
+ if (dryRun) {
321
+ log(`\n${colors.dim}Dry run — no files were modified.${colors.reset}`)
322
+ } else {
323
+ log(`\nManifest written to: ${colors.dim}${config.localesDir}/collections/manifest.json${colors.reset}`)
324
+ }
310
325
  } else {
311
326
  log(`${colors.dim}No translatable content found in collections.${colors.reset}`)
312
327
  }
@@ -319,13 +334,13 @@ async function runExtract(siteRoot, config, args) {
319
334
  }
320
335
 
321
336
  /**
322
- * Init command - generate starter translation files from manifest
337
+ * Generate command - generate starter translation files from manifest
323
338
  *
324
339
  * Usage:
325
- * uniweb i18n init es fr Initialize specific locales
326
- * uniweb i18n init Initialize all configured locales
327
- * uniweb i18n init --empty Use empty strings instead of source text
328
- * uniweb i18n init --force Overwrite existing files entirely
340
+ * uniweb i18n generate es fr Generate specific locales
341
+ * uniweb i18n generate Generate all configured locales
342
+ * uniweb i18n generate --empty Use empty strings instead of source text
343
+ * uniweb i18n generate --force Overwrite existing files entirely
329
344
  */
330
345
  async function runInit(siteRoot, config, args) {
331
346
  const useEmpty = args.includes('--empty')
@@ -360,7 +375,7 @@ async function runInit(siteRoot, config, args) {
360
375
 
361
376
  if (!targetLocales || targetLocales.length === 0) {
362
377
  error('No target locales specified.')
363
- log(`${colors.dim}Specify locales as arguments (e.g., "uniweb i18n init es fr")`)
378
+ log(`${colors.dim}Specify locales as arguments (e.g., "uniweb i18n generate es fr")`)
364
379
  log(`or configure them in site.yml under i18n.locales.${colors.reset}`)
365
380
  process.exit(1)
366
381
  }
@@ -425,55 +440,6 @@ async function runInit(siteRoot, config, args) {
425
440
  log(` 3. Run 'uniweb i18n status' to check coverage${colors.reset}`)
426
441
  }
427
442
 
428
- /**
429
- * Sync command - detect changes and update manifest
430
- */
431
- async function runSync(siteRoot, config, args) {
432
- const verbose = args.includes('--verbose') || args.includes('-v')
433
- const dryRun = args.includes('--dry-run')
434
-
435
- log(`\n${colors.cyan}Syncing i18n manifest...${colors.reset}\n`)
436
-
437
- // Check if site has been built
438
- const siteContentPath = join(siteRoot, 'dist', 'site-content.json')
439
- if (!existsSync(siteContentPath)) {
440
- error('Site content not found. Run "uniweb build" first.')
441
- process.exit(1)
442
- }
443
-
444
- // Check if manifest exists
445
- const manifestPath = join(siteRoot, config.localesDir, 'manifest.json')
446
- if (!existsSync(manifestPath)) {
447
- warn('No existing manifest found. Running extract instead.')
448
- return runExtract(siteRoot, config, args)
449
- }
450
-
451
- try {
452
- const { extractManifest, formatSyncReport } = await import('@uniweb/build/i18n')
453
-
454
- if (dryRun) {
455
- log(`${colors.dim}(dry run - no files will be modified)${colors.reset}\n`)
456
- }
457
-
458
- const { manifest, report } = await extractManifest(siteRoot, {
459
- localesDir: config.localesDir,
460
- siteContentPath,
461
- verbose,
462
- dryRun,
463
- })
464
-
465
- log(formatSyncReport(report))
466
-
467
- if (!dryRun) {
468
- success('\nManifest updated')
469
- }
470
- } catch (err) {
471
- error(`Sync failed: ${err.message}`)
472
- if (verbose) console.error(err)
473
- process.exit(1)
474
- }
475
- }
476
-
477
443
  /**
478
444
  * Status command - show translation coverage
479
445
  */
@@ -1458,10 +1424,8 @@ ${colors.bright}Usage:${colors.reset}
1458
1424
 
1459
1425
  ${colors.bright}Commands:${colors.reset}
1460
1426
  ${colors.dim}# Hash-based (granular) translation${colors.reset}
1461
- (default) Same as sync - extract/update strings (runs if no command given)
1462
- extract Extract translatable strings to locales/manifest.json
1463
- init Generate starter locale files from manifest keys
1464
- sync Update manifest with content changes (detects moved/changed content)
1427
+ extract Extract/update translatable strings (default if no command given)
1428
+ generate Generate starter locale files from manifest keys
1465
1429
  status Show translation coverage per locale
1466
1430
  audit Find stale translations (no longer in manifest) and missing ones
1467
1431
 
@@ -1475,9 +1439,9 @@ ${colors.bright}Commands:${colors.reset}
1475
1439
  ${colors.bright}Options:${colors.reset}
1476
1440
  -t, --target <path> Site directory (auto-detected if not specified)
1477
1441
  --verbose Show detailed output
1478
- --dry-run (sync/prune) Show changes without writing files
1479
- --empty (init) Use empty strings instead of source text
1480
- --force (init) Overwrite existing locale files entirely
1442
+ --dry-run (extract/prune) Preview changes without writing files
1443
+ --empty (generate) Use empty strings instead of source text
1444
+ --force (generate) Overwrite existing locale files entirely
1481
1445
  --clean (audit) Remove stale entries from locale files
1482
1446
  --missing (status) List all missing strings instead of summary
1483
1447
  --freeform (status/prune) Include free-form translation status
@@ -1498,11 +1462,10 @@ ${colors.bright}Configuration:${colors.reset}
1498
1462
  By default, all *.json files in locales/ are treated as translation targets.
1499
1463
 
1500
1464
  ${colors.bright}Workflow:${colors.reset}
1501
- 1. Build your site: uniweb build
1502
- 2. Extract strings: uniweb i18n extract
1503
- 3. Initialize locale files: uniweb i18n init es fr
1504
- 4. Translate locale files: Edit locales/es.json, locales/fr.json, etc.
1505
- 5. Build with translations: uniweb build (generates locale-specific output)
1465
+ 1. Extract strings: uniweb i18n extract
1466
+ 2. Generate locale files: uniweb i18n generate es fr
1467
+ 3. Translate locale files: Edit locales/es.json, locales/fr.json, etc.
1468
+ 4. Build with translations: uniweb build (generates locale-specific output)
1506
1469
 
1507
1470
  ${colors.bright}File Structure:${colors.reset}
1508
1471
  locales/
@@ -1519,18 +1482,18 @@ ${colors.bright}File Structure:${colors.reset}
1519
1482
 
1520
1483
  ${colors.bright}Examples:${colors.reset}
1521
1484
  ${colors.dim}# Hash-based workflow${colors.reset}
1522
- uniweb i18n extract # Extract all translatable strings
1523
- uniweb i18n extract --verbose # Show extracted strings
1485
+ uniweb i18n extract # Extract all translatable strings
1486
+ uniweb i18n extract --dry-run # Preview without writing
1487
+ uniweb i18n extract --verbose # Show extracted strings
1524
1488
  uniweb i18n extract --no-collections # Pages only (skip collections)
1525
- uniweb i18n init es fr # Create starter files for Spanish and French
1526
- uniweb i18n init --empty # Create files with empty values (for translators)
1527
- uniweb i18n init --force # Overwrite existing locale files
1528
- uniweb i18n sync # Update manifest after content changes
1529
- uniweb i18n status # Show coverage for all locales
1530
- uniweb i18n status es # Show coverage for Spanish only
1489
+ uniweb i18n generate es fr # Create starter files for Spanish and French
1490
+ uniweb i18n generate --empty # Create files with empty values (for translators)
1491
+ uniweb i18n generate --force # Overwrite existing locale files
1492
+ uniweb i18n status # Show coverage for all locales
1493
+ uniweb i18n status es # Show coverage for Spanish only
1531
1494
  uniweb i18n status es --missing --json # Export missing for AI translation
1532
- uniweb i18n audit # Find stale and missing translations
1533
- uniweb i18n audit --clean # Remove stale entries
1495
+ uniweb i18n audit # Find stale and missing translations
1496
+ uniweb i18n audit --clean # Remove stale entries
1534
1497
 
1535
1498
  ${colors.dim}# Free-form workflow (complete section replacement)${colors.reset}
1536
1499
  uniweb i18n init-freeform es pages/about hero
@@ -1542,6 +1505,10 @@ ${colors.bright}Examples:${colors.reset}
1542
1505
  uniweb i18n prune --freeform --dry-run # Preview orphan cleanup
1543
1506
  uniweb i18n --target site # Specify site directory explicitly
1544
1507
 
1508
+ ${colors.bright}Aliases:${colors.reset}
1509
+ sync → extract (backward-compatible)
1510
+ init → generate (backward-compatible)
1511
+
1545
1512
  ${colors.bright}Notes:${colors.reset}
1546
1513
  Run from a site directory to operate on that site.
1547
1514
  Run from workspace root to auto-detect sites (prompts if multiple).