uniweb 0.3.7 → 0.4.0
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 +49 -54
- package/package.json +4 -4
- package/src/commands/build.js +25 -0
- package/src/commands/i18n.js +317 -35
package/README.md
CHANGED
|
@@ -6,8 +6,10 @@ Create well-structured Vite + React projects with file-based routing, localizati
|
|
|
6
6
|
|
|
7
7
|
## Quick Start
|
|
8
8
|
|
|
9
|
+
> **Need pnpm?** Run `npm install -g pnpm` or see [pnpm installation](https://pnpm.io/installation).
|
|
10
|
+
|
|
9
11
|
```bash
|
|
10
|
-
|
|
12
|
+
pnpm create uniweb my-site --template marketing
|
|
11
13
|
cd my-site
|
|
12
14
|
pnpm install
|
|
13
15
|
pnpm dev
|
|
@@ -33,16 +35,16 @@ The `marketing` template includes real components (Hero, Features, Pricing, Test
|
|
|
33
35
|
|
|
34
36
|
```bash
|
|
35
37
|
# Multilingual business site (English, Spanish, French)
|
|
36
|
-
|
|
38
|
+
pnpm create uniweb my-site --template international
|
|
37
39
|
|
|
38
40
|
# Academic site (researcher portfolios, lab pages)
|
|
39
|
-
|
|
41
|
+
pnpm create uniweb my-site --template academic
|
|
40
42
|
|
|
41
43
|
# Documentation site
|
|
42
|
-
|
|
44
|
+
pnpm create uniweb my-site --template docs
|
|
43
45
|
|
|
44
46
|
# Minimal starter (build from scratch)
|
|
45
|
-
|
|
47
|
+
pnpm create uniweb my-site
|
|
46
48
|
```
|
|
47
49
|
|
|
48
50
|
Every project is a workspace with two packages:
|
|
@@ -111,7 +113,11 @@ Access the parsed data via `content.data`:
|
|
|
111
113
|
```jsx
|
|
112
114
|
function TeamCard({ content }) {
|
|
113
115
|
const member = content.data['team-member']
|
|
114
|
-
return
|
|
116
|
+
return (
|
|
117
|
+
<div>
|
|
118
|
+
{member.name} — {member.role}
|
|
119
|
+
</div>
|
|
120
|
+
)
|
|
115
121
|
}
|
|
116
122
|
```
|
|
117
123
|
|
|
@@ -245,14 +251,24 @@ Start with local files deployed anywhere. The same foundation works across all t
|
|
|
245
251
|
|
|
246
252
|
---
|
|
247
253
|
|
|
248
|
-
##
|
|
254
|
+
## Create a Project
|
|
249
255
|
|
|
250
256
|
```bash
|
|
251
|
-
#
|
|
252
|
-
|
|
257
|
+
# pnpm (recommended)
|
|
258
|
+
pnpm create uniweb my-site --template marketing
|
|
253
259
|
|
|
254
|
-
#
|
|
260
|
+
# npm (use -- before options)
|
|
261
|
+
npm create uniweb@latest my-site -- --template marketing
|
|
262
|
+
|
|
263
|
+
# npx
|
|
264
|
+
npx uniweb@latest create my-site --template marketing
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Alternatively, install the CLI globally:
|
|
268
|
+
|
|
269
|
+
```bash
|
|
255
270
|
npm install -g uniweb
|
|
271
|
+
uniweb create my-site --template marketing
|
|
256
272
|
```
|
|
257
273
|
|
|
258
274
|
**Requirements:**
|
|
@@ -276,7 +292,7 @@ Or see the [official pnpm installation guide](https://pnpm.io/installation) for
|
|
|
276
292
|
|
|
277
293
|
### `create`
|
|
278
294
|
|
|
279
|
-
Create a new Uniweb project.
|
|
295
|
+
Create a new Uniweb project. See [Create a Project](#create-a-project) for usage examples.
|
|
280
296
|
|
|
281
297
|
```bash
|
|
282
298
|
uniweb create [project-name] [options]
|
|
@@ -298,31 +314,6 @@ uniweb create [project-name] [options]
|
|
|
298
314
|
| GitHub | `github:user/repo` | GitHub repositories |
|
|
299
315
|
| GitHub URL | `https://github.com/user/repo` | Full GitHub URLs |
|
|
300
316
|
|
|
301
|
-
**Examples:**
|
|
302
|
-
|
|
303
|
-
```bash
|
|
304
|
-
# Interactive prompts
|
|
305
|
-
uniweb create
|
|
306
|
-
|
|
307
|
-
# Create with specific name (defaults to single template)
|
|
308
|
-
uniweb create my-project
|
|
309
|
-
|
|
310
|
-
# Single project with site + foundation
|
|
311
|
-
uniweb create my-project --template single
|
|
312
|
-
|
|
313
|
-
# Multi-site/foundation monorepo
|
|
314
|
-
uniweb create my-workspace --template multi
|
|
315
|
-
|
|
316
|
-
# Official marketing template (landing pages, pricing, testimonials)
|
|
317
|
-
uniweb create my-site --template marketing
|
|
318
|
-
|
|
319
|
-
# From npm package
|
|
320
|
-
uniweb create my-site --template @myorg/starter-template
|
|
321
|
-
|
|
322
|
-
# From GitHub repository
|
|
323
|
-
uniweb create my-site --template github:myorg/uniweb-template
|
|
324
|
-
```
|
|
325
|
-
|
|
326
317
|
### `build`
|
|
327
318
|
|
|
328
319
|
Build the current project.
|
|
@@ -425,7 +416,7 @@ Feature-rich templates with real components and sample content.
|
|
|
425
416
|
### Marketing
|
|
426
417
|
|
|
427
418
|
```bash
|
|
428
|
-
|
|
419
|
+
pnpm create uniweb my-site --template marketing
|
|
429
420
|
```
|
|
430
421
|
|
|
431
422
|
**Includes:** Hero, Features, Pricing, Testimonials, CTA, FAQ, Stats, LogoCloud, Video, Gallery, Team
|
|
@@ -435,13 +426,13 @@ Perfect for product launches, SaaS websites, and business landing pages.
|
|
|
435
426
|
**Tailwind v3 variant:**
|
|
436
427
|
|
|
437
428
|
```bash
|
|
438
|
-
|
|
429
|
+
pnpm create uniweb my-site --template marketing --variant tailwind3
|
|
439
430
|
```
|
|
440
431
|
|
|
441
432
|
### Academic
|
|
442
433
|
|
|
443
434
|
```bash
|
|
444
|
-
|
|
435
|
+
pnpm create uniweb my-site --template academic
|
|
445
436
|
```
|
|
446
437
|
|
|
447
438
|
**Includes:** ProfileHero, PublicationList, ResearchAreas, TeamGrid, Timeline, ContactCard, Navbar, Footer
|
|
@@ -451,7 +442,7 @@ Perfect for researcher portfolios, lab websites, and academic department sites.
|
|
|
451
442
|
### Docs
|
|
452
443
|
|
|
453
444
|
```bash
|
|
454
|
-
|
|
445
|
+
pnpm create uniweb my-site --template docs
|
|
455
446
|
```
|
|
456
447
|
|
|
457
448
|
**Includes:** Header, LeftPanel, DocSection, CodeBlock, Footer
|
|
@@ -461,7 +452,7 @@ Perfect for technical documentation, guides, and API references.
|
|
|
461
452
|
### International
|
|
462
453
|
|
|
463
454
|
```bash
|
|
464
|
-
|
|
455
|
+
pnpm create uniweb my-site --template international
|
|
465
456
|
```
|
|
466
457
|
|
|
467
458
|
**Includes:** Hero, Features, Team, CTA, Header (with language switcher), Footer (with language links)
|
|
@@ -489,13 +480,13 @@ Use templates from npm or GitHub:
|
|
|
489
480
|
|
|
490
481
|
```bash
|
|
491
482
|
# npm package
|
|
492
|
-
|
|
483
|
+
pnpm create uniweb my-site --template @myorg/template-name
|
|
493
484
|
|
|
494
485
|
# GitHub repository
|
|
495
|
-
|
|
486
|
+
pnpm create uniweb my-site --template github:user/repo
|
|
496
487
|
|
|
497
488
|
# GitHub with specific branch/tag
|
|
498
|
-
|
|
489
|
+
pnpm create uniweb my-site --template github:user/repo#v1.0.0
|
|
499
490
|
```
|
|
500
491
|
|
|
501
492
|
## Dependency Management
|
|
@@ -665,7 +656,7 @@ Set your homepage with `index:` in `site.yml`:
|
|
|
665
656
|
|
|
666
657
|
```yaml
|
|
667
658
|
# site.yml
|
|
668
|
-
index: home
|
|
659
|
+
index: home # The page folder that becomes /
|
|
669
660
|
```
|
|
670
661
|
|
|
671
662
|
The `index:` option tells the build which page folder becomes the root route (`/`). The page still exists in `pages/home/`, but visitors access it at `/`.
|
|
@@ -685,8 +676,8 @@ Becomes:
|
|
|
685
676
|
|
|
686
677
|
```js
|
|
687
678
|
content.items = [
|
|
688
|
-
{ title:
|
|
689
|
-
{ title:
|
|
679
|
+
{ title: 'First item', paragraphs: [], links: [], imgs: [] },
|
|
680
|
+
{ title: 'Second item', paragraphs: [], links: [], imgs: [] },
|
|
690
681
|
]
|
|
691
682
|
```
|
|
692
683
|
|
|
@@ -708,7 +699,11 @@ Are accessible via `content.data`:
|
|
|
708
699
|
```jsx
|
|
709
700
|
function TeamMember({ content }) {
|
|
710
701
|
const member = content.data['team-member']
|
|
711
|
-
return
|
|
702
|
+
return (
|
|
703
|
+
<div>
|
|
704
|
+
{member.name} - {member.role}
|
|
705
|
+
</div>
|
|
706
|
+
)
|
|
712
707
|
}
|
|
713
708
|
```
|
|
714
709
|
|
|
@@ -718,12 +713,12 @@ The tag name (after the colon) becomes the key in `content.data`.
|
|
|
718
713
|
|
|
719
714
|
In a Uniweb workspace, commands run differently at different levels:
|
|
720
715
|
|
|
721
|
-
| Location
|
|
722
|
-
|
|
723
|
-
| Project root
|
|
724
|
-
| Project root
|
|
725
|
-
| `foundation/` | `uniweb build` | Builds just the foundation
|
|
726
|
-
| `site/`
|
|
716
|
+
| Location | Command | What it does |
|
|
717
|
+
| ------------- | -------------- | --------------------------------------- |
|
|
718
|
+
| Project root | `pnpm build` | Builds all packages (foundation + site) |
|
|
719
|
+
| Project root | `pnpm dev` | Starts dev server for site |
|
|
720
|
+
| `foundation/` | `uniweb build` | Builds just the foundation |
|
|
721
|
+
| `site/` | `uniweb build` | Builds just the site |
|
|
727
722
|
|
|
728
723
|
For day-to-day development, run `pnpm dev` from the project root. The workspace scripts handle the rest.
|
|
729
724
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -37,9 +37,9 @@
|
|
|
37
37
|
"js-yaml": "^4.1.0",
|
|
38
38
|
"prompts": "^2.4.2",
|
|
39
39
|
"tar": "^7.0.0",
|
|
40
|
-
"@uniweb/build": "0.
|
|
40
|
+
"@uniweb/build": "0.3.0",
|
|
41
41
|
"@uniweb/core": "0.2.3",
|
|
42
|
-
"@uniweb/
|
|
43
|
-
"@uniweb/
|
|
42
|
+
"@uniweb/runtime": "0.4.0",
|
|
43
|
+
"@uniweb/kit": "0.3.0"
|
|
44
44
|
}
|
|
45
45
|
}
|
package/src/commands/build.js
CHANGED
|
@@ -407,6 +407,31 @@ async function buildSite(projectDir, options = {}) {
|
|
|
407
407
|
for (const [locale, path] of Object.entries(outputs)) {
|
|
408
408
|
log(` ${colors.dim}dist/${locale}/site-content.json${colors.reset}`)
|
|
409
409
|
}
|
|
410
|
+
|
|
411
|
+
// Translate collections if they exist
|
|
412
|
+
try {
|
|
413
|
+
const { buildLocalizedCollections } = await import('@uniweb/build/i18n')
|
|
414
|
+
|
|
415
|
+
const collectionOutputs = await buildLocalizedCollections(projectDir, {
|
|
416
|
+
locales: i18nConfig.locales,
|
|
417
|
+
outputDir: join(projectDir, 'dist'),
|
|
418
|
+
collectionsLocalesDir: join(projectDir, i18nConfig.localesDir, 'collections')
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
// Count collections translated
|
|
422
|
+
const collectionCount = Object.values(collectionOutputs).reduce(
|
|
423
|
+
(sum, localeOutputs) => sum + Object.keys(localeOutputs).length, 0
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
if (collectionCount > 0) {
|
|
427
|
+
success(`Translated collections for ${Object.keys(collectionOutputs).length} locale(s)`)
|
|
428
|
+
}
|
|
429
|
+
} catch (err) {
|
|
430
|
+
// Collection translation is optional, don't fail build
|
|
431
|
+
if (process.env.DEBUG) {
|
|
432
|
+
console.error('Collection translation:', err.message)
|
|
433
|
+
}
|
|
434
|
+
}
|
|
410
435
|
} catch (err) {
|
|
411
436
|
error(`i18n build failed: ${err.message}`)
|
|
412
437
|
if (process.env.DEBUG) {
|
package/src/commands/i18n.js
CHANGED
|
@@ -105,6 +105,9 @@ export async function i18n(args) {
|
|
|
105
105
|
case 'status':
|
|
106
106
|
await runStatus(siteRoot, config, effectiveArgs)
|
|
107
107
|
break
|
|
108
|
+
case 'audit':
|
|
109
|
+
await runAudit(siteRoot, config, effectiveArgs)
|
|
110
|
+
break
|
|
108
111
|
default:
|
|
109
112
|
error(`Unknown subcommand: ${effectiveSubcommand}`)
|
|
110
113
|
showHelp()
|
|
@@ -205,45 +208,92 @@ async function loadSiteConfig(siteRoot) {
|
|
|
205
208
|
*/
|
|
206
209
|
async function runExtract(siteRoot, config, args) {
|
|
207
210
|
const verbose = args.includes('--verbose') || args.includes('-v')
|
|
211
|
+
const collectionsOnly = args.includes('--collections')
|
|
212
|
+
const withCollections = args.includes('--with-collections')
|
|
208
213
|
|
|
209
|
-
|
|
214
|
+
// Extract page content (unless --collections only)
|
|
215
|
+
if (!collectionsOnly) {
|
|
216
|
+
log(`\n${colors.cyan}Extracting translatable content...${colors.reset}\n`)
|
|
210
217
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
218
|
+
// Check if site has been built
|
|
219
|
+
const siteContentPath = join(siteRoot, 'dist', 'site-content.json')
|
|
220
|
+
if (!existsSync(siteContentPath)) {
|
|
221
|
+
error('Site content not found. Run "uniweb build" first.')
|
|
222
|
+
process.exit(1)
|
|
223
|
+
}
|
|
217
224
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
225
|
+
try {
|
|
226
|
+
// Dynamic import to avoid loading at CLI startup
|
|
227
|
+
const { extractManifest, formatSyncReport } = await import('@uniweb/build/i18n')
|
|
221
228
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
229
|
+
const { manifest, report } = await extractManifest(siteRoot, {
|
|
230
|
+
localesDir: config.localesDir,
|
|
231
|
+
siteContentPath,
|
|
232
|
+
verbose,
|
|
233
|
+
})
|
|
227
234
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
235
|
+
// Show results
|
|
236
|
+
const unitCount = Object.keys(manifest.units).length
|
|
237
|
+
success(`Extracted ${unitCount} translatable strings`)
|
|
231
238
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
239
|
+
if (report) {
|
|
240
|
+
log('')
|
|
241
|
+
log(formatSyncReport(report))
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
log(`\nManifest written to: ${colors.dim}${config.localesDir}/manifest.json${colors.reset}`)
|
|
245
|
+
|
|
246
|
+
if (config.locales.length === 0) {
|
|
247
|
+
log(`\n${colors.dim}No translation files found in ${config.localesDir}/.`)
|
|
248
|
+
log(`After translating, create locale files like ${config.localesDir}/es.json${colors.reset}`)
|
|
249
|
+
}
|
|
250
|
+
} catch (err) {
|
|
251
|
+
error(`Extraction failed: ${err.message}`)
|
|
252
|
+
if (verbose) console.error(err)
|
|
253
|
+
process.exit(1)
|
|
235
254
|
}
|
|
255
|
+
}
|
|
236
256
|
|
|
237
|
-
|
|
257
|
+
// Extract collection content (if --collections or --with-collections)
|
|
258
|
+
if (collectionsOnly || withCollections) {
|
|
259
|
+
log(`\n${colors.cyan}Extracting collection content...${colors.reset}\n`)
|
|
238
260
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
261
|
+
// Check if collections exist
|
|
262
|
+
const dataDir = join(siteRoot, 'public', 'data')
|
|
263
|
+
if (!existsSync(dataDir)) {
|
|
264
|
+
if (collectionsOnly) {
|
|
265
|
+
error('No collections found. Create collection data in public/data/.')
|
|
266
|
+
process.exit(1)
|
|
267
|
+
}
|
|
268
|
+
log(`${colors.dim}No collections found in public/data/.${colors.reset}`)
|
|
269
|
+
return
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
const { extractCollectionManifest, formatSyncReport } = await import('@uniweb/build/i18n')
|
|
274
|
+
|
|
275
|
+
const { manifest, report } = await extractCollectionManifest(siteRoot, {
|
|
276
|
+
localesDir: config.localesDir,
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
const unitCount = Object.keys(manifest.units).length
|
|
280
|
+
if (unitCount > 0) {
|
|
281
|
+
success(`Extracted ${unitCount} translatable strings from collections`)
|
|
282
|
+
|
|
283
|
+
if (report) {
|
|
284
|
+
log('')
|
|
285
|
+
log(formatSyncReport(report))
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
log(`\nManifest written to: ${colors.dim}${config.localesDir}/collections/manifest.json${colors.reset}`)
|
|
289
|
+
} else {
|
|
290
|
+
log(`${colors.dim}No translatable content found in collections.${colors.reset}`)
|
|
291
|
+
}
|
|
292
|
+
} catch (err) {
|
|
293
|
+
error(`Collection extraction failed: ${err.message}`)
|
|
294
|
+
if (verbose) console.error(err)
|
|
295
|
+
if (collectionsOnly) process.exit(1)
|
|
242
296
|
}
|
|
243
|
-
} catch (err) {
|
|
244
|
-
error(`Extraction failed: ${err.message}`)
|
|
245
|
-
if (verbose) console.error(err)
|
|
246
|
-
process.exit(1)
|
|
247
297
|
}
|
|
248
298
|
}
|
|
249
299
|
|
|
@@ -301,19 +351,36 @@ async function runSync(siteRoot, config, args) {
|
|
|
301
351
|
*/
|
|
302
352
|
async function runStatus(siteRoot, config, args) {
|
|
303
353
|
const locale = args.find(a => !a.startsWith('-'))
|
|
304
|
-
|
|
305
|
-
|
|
354
|
+
const showMissing = args.includes('--missing')
|
|
355
|
+
const outputJson = args.includes('--json')
|
|
356
|
+
const byPage = args.includes('--by-page')
|
|
306
357
|
|
|
307
358
|
// Check if manifest exists
|
|
308
|
-
const
|
|
359
|
+
const localesPath = join(siteRoot, config.localesDir)
|
|
360
|
+
const manifestPath = join(localesPath, 'manifest.json')
|
|
309
361
|
if (!existsSync(manifestPath)) {
|
|
310
362
|
error('No manifest found. Run "uniweb i18n extract" first.')
|
|
311
363
|
process.exit(1)
|
|
312
364
|
}
|
|
313
365
|
|
|
366
|
+
// For --missing mode, use auditLocale which returns detailed missing info
|
|
367
|
+
if (showMissing) {
|
|
368
|
+
await runStatusMissing(siteRoot, config, locale, { outputJson, byPage })
|
|
369
|
+
return
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Standard status mode
|
|
373
|
+
if (!outputJson) {
|
|
374
|
+
log(`\n${colors.cyan}Translation Status${colors.reset}\n`)
|
|
375
|
+
}
|
|
376
|
+
|
|
314
377
|
if (config.locales.length === 0) {
|
|
315
|
-
|
|
316
|
-
|
|
378
|
+
if (outputJson) {
|
|
379
|
+
log(JSON.stringify({ error: 'No translation files found', locales: [] }, null, 2))
|
|
380
|
+
} else {
|
|
381
|
+
log(`${colors.dim}No translation files found in ${config.localesDir}/.`)
|
|
382
|
+
log(`Create locale files like ${config.localesDir}/es.json to add translations.${colors.reset}`)
|
|
383
|
+
}
|
|
317
384
|
return
|
|
318
385
|
}
|
|
319
386
|
|
|
@@ -327,13 +394,18 @@ async function runStatus(siteRoot, config, args) {
|
|
|
327
394
|
locales: localesToCheck,
|
|
328
395
|
})
|
|
329
396
|
|
|
397
|
+
if (outputJson) {
|
|
398
|
+
log(JSON.stringify(status, null, 2))
|
|
399
|
+
return
|
|
400
|
+
}
|
|
401
|
+
|
|
330
402
|
log(formatTranslationStatus(status))
|
|
331
403
|
|
|
332
404
|
// Show next steps if there are missing translations
|
|
333
405
|
const hasMissing = Object.values(status.locales).some(l => l.missing > 0)
|
|
334
406
|
if (hasMissing) {
|
|
335
407
|
log(`\n${colors.dim}To translate missing strings, edit the locale files in ${config.localesDir}/`)
|
|
336
|
-
log(`Or use
|
|
408
|
+
log(`Or use: uniweb i18n status --missing --json > missing.json${colors.reset}`)
|
|
337
409
|
}
|
|
338
410
|
} catch (err) {
|
|
339
411
|
error(`Status check failed: ${err.message}`)
|
|
@@ -341,6 +413,202 @@ async function runStatus(siteRoot, config, args) {
|
|
|
341
413
|
}
|
|
342
414
|
}
|
|
343
415
|
|
|
416
|
+
/**
|
|
417
|
+
* Status --missing mode - show detailed missing strings
|
|
418
|
+
*/
|
|
419
|
+
async function runStatusMissing(siteRoot, config, locale, options = {}) {
|
|
420
|
+
const { outputJson = false, byPage = false } = options
|
|
421
|
+
const localesPath = join(siteRoot, config.localesDir)
|
|
422
|
+
|
|
423
|
+
if (config.locales.length === 0) {
|
|
424
|
+
if (outputJson) {
|
|
425
|
+
log(JSON.stringify({ error: 'No translation files found', missing: [] }, null, 2))
|
|
426
|
+
} else {
|
|
427
|
+
log(`${colors.dim}No translation files found in ${config.localesDir}/.${colors.reset}`)
|
|
428
|
+
}
|
|
429
|
+
return
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
try {
|
|
433
|
+
const { auditLocale } = await import('@uniweb/build/i18n')
|
|
434
|
+
|
|
435
|
+
const localesToCheck = locale ? [locale] : config.locales
|
|
436
|
+
|
|
437
|
+
// Collect missing strings from all requested locales
|
|
438
|
+
const allMissing = []
|
|
439
|
+
const byLocale = {}
|
|
440
|
+
|
|
441
|
+
for (const loc of localesToCheck) {
|
|
442
|
+
const result = await auditLocale(localesPath, loc)
|
|
443
|
+
byLocale[loc] = {
|
|
444
|
+
total: result.total,
|
|
445
|
+
translated: result.valid.length,
|
|
446
|
+
missing: result.missing.length,
|
|
447
|
+
coverage: result.total > 0
|
|
448
|
+
? Math.round((result.valid.length / result.total) * 100)
|
|
449
|
+
: 100
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Add locale info to each missing entry
|
|
453
|
+
for (const entry of result.missing) {
|
|
454
|
+
allMissing.push({
|
|
455
|
+
locale: loc,
|
|
456
|
+
...entry
|
|
457
|
+
})
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// JSON output
|
|
462
|
+
if (outputJson) {
|
|
463
|
+
const output = {
|
|
464
|
+
locales: byLocale,
|
|
465
|
+
missing: allMissing
|
|
466
|
+
}
|
|
467
|
+
log(JSON.stringify(output, null, 2))
|
|
468
|
+
return
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Human-readable output
|
|
472
|
+
log(`\n${colors.cyan}Missing Translations${colors.reset}\n`)
|
|
473
|
+
|
|
474
|
+
// Show summary per locale
|
|
475
|
+
for (const [loc, info] of Object.entries(byLocale)) {
|
|
476
|
+
log(`${loc}: ${info.missing} missing (${info.coverage}% coverage)`)
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (allMissing.length === 0) {
|
|
480
|
+
log(`\n${colors.green}All strings are translated!${colors.reset}`)
|
|
481
|
+
return
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
log('')
|
|
485
|
+
|
|
486
|
+
// Group by page if requested
|
|
487
|
+
if (byPage) {
|
|
488
|
+
const grouped = groupByPage(allMissing)
|
|
489
|
+
for (const [page, entries] of Object.entries(grouped)) {
|
|
490
|
+
log(`${colors.bright}${page}${colors.reset}`)
|
|
491
|
+
for (const entry of entries) {
|
|
492
|
+
const preview = truncateString(entry.source, 50)
|
|
493
|
+
log(` ${colors.dim}${entry.hash}${colors.reset} "${preview}"`)
|
|
494
|
+
}
|
|
495
|
+
log('')
|
|
496
|
+
}
|
|
497
|
+
} else {
|
|
498
|
+
// Flat list
|
|
499
|
+
for (const entry of allMissing.slice(0, 20)) {
|
|
500
|
+
const preview = truncateString(entry.source, 60)
|
|
501
|
+
const context = entry.contexts?.[0]
|
|
502
|
+
const location = context ? `${context.page}:${context.section}` : ''
|
|
503
|
+
log(` ${colors.dim}${entry.hash}${colors.reset} "${preview}"`)
|
|
504
|
+
if (location) {
|
|
505
|
+
log(` ${colors.dim}→ ${location}${colors.reset}`)
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (allMissing.length > 20) {
|
|
510
|
+
log(`\n ${colors.dim}... and ${allMissing.length - 20} more${colors.reset}`)
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
log(`\n${colors.dim}Use --json to export for translation tools.${colors.reset}`)
|
|
515
|
+
} catch (err) {
|
|
516
|
+
error(`Status check failed: ${err.message}`)
|
|
517
|
+
process.exit(1)
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Group missing entries by page
|
|
523
|
+
*/
|
|
524
|
+
function groupByPage(entries) {
|
|
525
|
+
const grouped = {}
|
|
526
|
+
for (const entry of entries) {
|
|
527
|
+
const page = entry.contexts?.[0]?.page || 'unknown'
|
|
528
|
+
if (!grouped[page]) grouped[page] = []
|
|
529
|
+
grouped[page].push(entry)
|
|
530
|
+
}
|
|
531
|
+
return grouped
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Truncate string for display
|
|
536
|
+
*/
|
|
537
|
+
function truncateString(str, maxLen) {
|
|
538
|
+
if (str.length <= maxLen) return str
|
|
539
|
+
return str.slice(0, maxLen - 3) + '...'
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Audit command - find stale and missing translations
|
|
544
|
+
*/
|
|
545
|
+
async function runAudit(siteRoot, config, args) {
|
|
546
|
+
const locale = args.find(a => !a.startsWith('-'))
|
|
547
|
+
const clean = args.includes('--clean')
|
|
548
|
+
const verbose = args.includes('--verbose') || args.includes('-v')
|
|
549
|
+
|
|
550
|
+
log(`\n${colors.cyan}Translation Audit${colors.reset}\n`)
|
|
551
|
+
|
|
552
|
+
// Check if manifest exists
|
|
553
|
+
const localesPath = join(siteRoot, config.localesDir)
|
|
554
|
+
const manifestPath = join(localesPath, 'manifest.json')
|
|
555
|
+
if (!existsSync(manifestPath)) {
|
|
556
|
+
error('No manifest found. Run "uniweb i18n extract" first.')
|
|
557
|
+
process.exit(1)
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (config.locales.length === 0) {
|
|
561
|
+
log(`${colors.dim}No translation files found in ${config.localesDir}/.`)
|
|
562
|
+
log(`Create locale files like ${config.localesDir}/es.json to add translations.${colors.reset}`)
|
|
563
|
+
return
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
try {
|
|
567
|
+
const { auditLocale, cleanLocale, formatAuditReport } = await import('@uniweb/build/i18n')
|
|
568
|
+
|
|
569
|
+
const localesToAudit = locale ? [locale] : config.locales
|
|
570
|
+
const results = []
|
|
571
|
+
|
|
572
|
+
for (const loc of localesToAudit) {
|
|
573
|
+
const result = await auditLocale(localesPath, loc)
|
|
574
|
+
results.push(result)
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Show report
|
|
578
|
+
log(formatAuditReport(results, { verbose }))
|
|
579
|
+
|
|
580
|
+
// Clean if requested
|
|
581
|
+
if (clean) {
|
|
582
|
+
log('')
|
|
583
|
+
let totalRemoved = 0
|
|
584
|
+
|
|
585
|
+
for (const result of results) {
|
|
586
|
+
if (result.stale.length > 0) {
|
|
587
|
+
const staleHashes = result.stale.map(s => s.hash)
|
|
588
|
+
const removed = await cleanLocale(localesPath, result.locale, staleHashes)
|
|
589
|
+
if (removed > 0) {
|
|
590
|
+
success(`Removed ${removed} stale entries from ${result.locale}.json`)
|
|
591
|
+
totalRemoved += removed
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (totalRemoved === 0) {
|
|
597
|
+
log(`${colors.dim}No stale entries to remove.${colors.reset}`)
|
|
598
|
+
}
|
|
599
|
+
} else {
|
|
600
|
+
// Suggest --clean if there are stale entries
|
|
601
|
+
const hasStale = results.some(r => r.stale.length > 0)
|
|
602
|
+
if (hasStale) {
|
|
603
|
+
log(`\n${colors.dim}Run with --clean to remove stale entries.${colors.reset}`)
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
} catch (err) {
|
|
607
|
+
error(`Audit failed: ${err.message}`)
|
|
608
|
+
process.exit(1)
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
344
612
|
/**
|
|
345
613
|
* Show help for i18n commands
|
|
346
614
|
*/
|
|
@@ -358,11 +626,18 @@ ${colors.bright}Commands:${colors.reset}
|
|
|
358
626
|
extract Extract translatable strings to locales/manifest.json
|
|
359
627
|
sync Update manifest with content changes (detects moved/changed content)
|
|
360
628
|
status Show translation coverage per locale
|
|
629
|
+
audit Find stale translations (no longer in manifest) and missing ones
|
|
361
630
|
|
|
362
631
|
${colors.bright}Options:${colors.reset}
|
|
363
632
|
-t, --target <path> Site directory (auto-detected if not specified)
|
|
364
633
|
--verbose Show detailed output
|
|
365
634
|
--dry-run (sync) Show changes without writing files
|
|
635
|
+
--clean (audit) Remove stale entries from locale files
|
|
636
|
+
--missing (status) List all missing strings instead of summary
|
|
637
|
+
--json (status) Output as JSON for translation tools
|
|
638
|
+
--by-page (status --missing) Group missing strings by page
|
|
639
|
+
--collections (extract/status/audit) Process only collections
|
|
640
|
+
--with-collections (extract/status/audit) Include collections with pages
|
|
366
641
|
|
|
367
642
|
${colors.bright}Configuration:${colors.reset}
|
|
368
643
|
Optional site.yml settings:
|
|
@@ -390,10 +665,17 @@ ${colors.bright}File Structure:${colors.reset}
|
|
|
390
665
|
${colors.bright}Examples:${colors.reset}
|
|
391
666
|
uniweb i18n extract # Extract all translatable strings
|
|
392
667
|
uniweb i18n extract --verbose # Show extracted strings
|
|
668
|
+
uniweb i18n extract --with-collections # Extract pages + collections
|
|
669
|
+
uniweb i18n extract --collections # Extract collections only
|
|
393
670
|
uniweb i18n sync # Update manifest after content changes
|
|
394
671
|
uniweb i18n sync --dry-run # Preview changes without writing
|
|
395
672
|
uniweb i18n status # Show coverage for all locales
|
|
396
673
|
uniweb i18n status es # Show coverage for Spanish only
|
|
674
|
+
uniweb i18n status --missing # List all missing strings
|
|
675
|
+
uniweb i18n status es --missing --json # Export missing for AI translation
|
|
676
|
+
uniweb i18n status --missing --by-page # Group missing by page
|
|
677
|
+
uniweb i18n audit # Find stale and missing translations
|
|
678
|
+
uniweb i18n audit --clean # Remove stale entries
|
|
397
679
|
uniweb i18n --target site # Specify site directory explicitly
|
|
398
680
|
|
|
399
681
|
${colors.bright}Notes:${colors.reset}
|