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 +2 -0
- package/package.json +2 -2
- package/partials/agents-md.hbs +6 -3
- package/src/commands/i18n.js +70 -103
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.
|
|
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.
|
|
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"
|
package/partials/agents-md.hbs
CHANGED
|
@@ -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
|
|
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
|
|
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 `
|
|
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
|
|
package/src/commands/i18n.js
CHANGED
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
* Commands for managing site content internationalization.
|
|
5
5
|
*
|
|
6
6
|
* Usage:
|
|
7
|
-
* uniweb i18n extract
|
|
8
|
-
* uniweb i18n
|
|
9
|
-
* uniweb i18n
|
|
10
|
-
* uniweb i18n
|
|
11
|
-
*
|
|
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 '
|
|
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('-') ? '
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
337
|
+
* Generate command - generate starter translation files from manifest
|
|
323
338
|
*
|
|
324
339
|
* Usage:
|
|
325
|
-
* uniweb i18n
|
|
326
|
-
* uniweb i18n
|
|
327
|
-
* uniweb i18n
|
|
328
|
-
* uniweb i18n
|
|
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
|
|
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
|
-
|
|
1462
|
-
|
|
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 (
|
|
1479
|
-
--empty (
|
|
1480
|
-
--force (
|
|
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.
|
|
1502
|
-
2.
|
|
1503
|
-
3.
|
|
1504
|
-
4.
|
|
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
|
|
1523
|
-
uniweb i18n extract --
|
|
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
|
|
1526
|
-
uniweb i18n
|
|
1527
|
-
uniweb i18n
|
|
1528
|
-
uniweb i18n
|
|
1529
|
-
uniweb i18n status
|
|
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
|
|
1533
|
-
uniweb i18n audit --clean
|
|
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).
|