uniweb 0.13.17 → 0.14.1
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 +1 -1
- package/package.json +7 -7
- package/partials/agents.md +51 -0
- package/src/backend/client.js +173 -40
- package/src/backend/data-bundle.js +15 -3
- package/src/backend/foundation-bring-along.js +76 -21
- package/src/backend/payment-handoff.js +28 -9
- package/src/backend/site-media.js +147 -19
- package/src/backend/site-sync.js +362 -40
- package/src/commands/add.js +575 -273
- package/src/commands/build.js +160 -57
- package/src/commands/clone.js +79 -26
- package/src/commands/content.js +11 -7
- package/src/commands/deploy.js +80 -32
- package/src/commands/dev.js +39 -17
- package/src/commands/docs.js +44 -16
- package/src/commands/doctor.js +338 -82
- package/src/commands/export.js +15 -7
- package/src/commands/handoff.js +6 -2
- package/src/commands/i18n.js +248 -99
- package/src/commands/inspect.js +48 -19
- package/src/commands/invite.js +9 -3
- package/src/commands/org.js +19 -5
- package/src/commands/publish.js +229 -60
- package/src/commands/pull.js +157 -48
- package/src/commands/push.js +144 -42
- package/src/commands/refresh.js +37 -10
- package/src/commands/register.js +235 -64
- package/src/commands/rename.js +126 -46
- package/src/commands/runtime.js +74 -24
- package/src/commands/status.js +48 -15
- package/src/commands/sync.js +7 -2
- package/src/commands/template.js +12 -4
- package/src/commands/update.js +263 -87
- package/src/commands/validate.js +115 -32
- package/src/framework-index.json +15 -13
- package/src/index.js +298 -145
- package/src/templates/fetchers/github.js +7 -7
- package/src/templates/fetchers/npm.js +3 -3
- package/src/templates/fetchers/release.js +21 -18
- package/src/templates/index.js +34 -12
- package/src/templates/processor.js +44 -12
- package/src/templates/resolver.js +42 -15
- package/src/templates/validator.js +14 -7
- package/src/utils/asset-upload.js +90 -22
- package/src/utils/code-upload.js +62 -37
- package/src/utils/config.js +31 -10
- package/src/utils/dep-survey.js +33 -7
- package/src/utils/destination-prompt.js +27 -17
- package/src/utils/git.js +66 -22
- package/src/utils/host-prompt.js +4 -3
- package/src/utils/install-integrity.js +8 -4
- package/src/utils/interactive.js +3 -3
- package/src/utils/json-file.js +6 -2
- package/src/utils/names.js +15 -6
- package/src/utils/placement.js +16 -8
- package/src/utils/registry-auth.js +185 -57
- package/src/utils/registry-orgs.js +142 -53
- package/src/utils/runtime-upload.js +52 -14
- package/src/utils/scaffold.js +55 -14
- package/src/utils/site-content-refs.js +3 -1
- package/src/utils/update-check.js +43 -17
- package/src/utils/workspace.js +13 -7
- package/src/versions.js +18 -7
- package/templates/site/_gitignore +5 -0
package/src/commands/i18n.js
CHANGED
|
@@ -17,14 +17,18 @@
|
|
|
17
17
|
|
|
18
18
|
import { resolve, join, dirname, basename, relative } from 'path'
|
|
19
19
|
import { existsSync } from 'fs'
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
readFile,
|
|
22
|
+
writeFile,
|
|
23
|
+
mkdir,
|
|
24
|
+
readdir,
|
|
25
|
+
unlink,
|
|
26
|
+
rename
|
|
27
|
+
} from 'fs/promises'
|
|
21
28
|
import yaml from 'js-yaml'
|
|
22
29
|
import { resolveDefaultLocale } from '@uniweb/core/locale-config'
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
findSites,
|
|
26
|
-
promptSelect,
|
|
27
|
-
} from '../utils/workspace.js'
|
|
30
|
+
import { DATA_DIR } from '@uniweb/core/data-paths'
|
|
31
|
+
import { isWorkspaceRoot, findSites, promptSelect } from '../utils/workspace.js'
|
|
28
32
|
|
|
29
33
|
// Colors for terminal output
|
|
30
34
|
const colors = {
|
|
@@ -34,7 +38,7 @@ const colors = {
|
|
|
34
38
|
cyan: '\x1b[36m',
|
|
35
39
|
green: '\x1b[32m',
|
|
36
40
|
yellow: '\x1b[33m',
|
|
37
|
-
red: '\x1b[31m'
|
|
41
|
+
red: '\x1b[31m'
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
function log(message) {
|
|
@@ -61,7 +65,7 @@ function parseTargetOption(args) {
|
|
|
61
65
|
if (args[i] === '--target' || args[i] === '-t') {
|
|
62
66
|
return {
|
|
63
67
|
target: args[i + 1],
|
|
64
|
-
remainingArgs: [...args.slice(0, i), ...args.slice(i + 2)]
|
|
68
|
+
remainingArgs: [...args.slice(0, i), ...args.slice(i + 2)]
|
|
65
69
|
}
|
|
66
70
|
}
|
|
67
71
|
}
|
|
@@ -85,13 +89,19 @@ export async function i18n(args) {
|
|
|
85
89
|
|
|
86
90
|
// Default to 'extract' if no subcommand (or if first arg is an option)
|
|
87
91
|
const firstArg = remainingArgs[0]
|
|
88
|
-
const effectiveSubcommand =
|
|
89
|
-
|
|
92
|
+
const effectiveSubcommand =
|
|
93
|
+
!firstArg || firstArg.startsWith('-') ? 'extract' : firstArg
|
|
94
|
+
const effectiveArgs =
|
|
95
|
+
!firstArg || firstArg.startsWith('-')
|
|
96
|
+
? remainingArgs
|
|
97
|
+
: remainingArgs.slice(1)
|
|
90
98
|
|
|
91
99
|
// Find site root
|
|
92
100
|
const siteRoot = await findSiteRoot(target)
|
|
93
101
|
if (!siteRoot) {
|
|
94
|
-
error(
|
|
102
|
+
error(
|
|
103
|
+
'Could not find site root. Make sure you are in a Uniweb site directory.'
|
|
104
|
+
)
|
|
95
105
|
process.exit(1)
|
|
96
106
|
}
|
|
97
107
|
|
|
@@ -162,7 +172,9 @@ async function findSiteRoot(target) {
|
|
|
162
172
|
|
|
163
173
|
if (sites.length === 0) {
|
|
164
174
|
error('No sites found in this workspace.')
|
|
165
|
-
log(
|
|
175
|
+
log(
|
|
176
|
+
`${colors.dim}Sites have @uniweb/runtime in dependencies.${colors.reset}`
|
|
177
|
+
)
|
|
166
178
|
process.exit(1)
|
|
167
179
|
}
|
|
168
180
|
|
|
@@ -220,7 +232,7 @@ async function loadSiteConfig(siteRoot) {
|
|
|
220
232
|
defaultLocale: resolveDefaultLocale(config),
|
|
221
233
|
locales,
|
|
222
234
|
localesDir,
|
|
223
|
-
...config.i18n
|
|
235
|
+
...config.i18n
|
|
224
236
|
}
|
|
225
237
|
}
|
|
226
238
|
|
|
@@ -230,18 +242,22 @@ async function loadSiteConfig(siteRoot) {
|
|
|
230
242
|
async function runExtract(siteRoot, config, args) {
|
|
231
243
|
const verbose = args.includes('--verbose') || args.includes('-v')
|
|
232
244
|
const dryRun = args.includes('--dry-run')
|
|
233
|
-
const collectionsOnly =
|
|
245
|
+
const collectionsOnly =
|
|
246
|
+
args.includes('--collections-only') || args.includes('--collections')
|
|
234
247
|
const noCollections = args.includes('--no-collections')
|
|
235
248
|
// --with-collections is now a no-op (collections are included by default)
|
|
236
249
|
|
|
237
250
|
// Extract page content (unless --collections-only)
|
|
238
251
|
if (!collectionsOnly) {
|
|
239
|
-
log(
|
|
252
|
+
log(
|
|
253
|
+
`\n${colors.cyan}Extracting translatable content${dryRun ? ' (dry run)' : ''}...${colors.reset}\n`
|
|
254
|
+
)
|
|
240
255
|
|
|
241
256
|
try {
|
|
242
257
|
// Collect site content directly from source files (no build required)
|
|
243
258
|
const { collectSiteContent } = await import('@uniweb/build/site')
|
|
244
|
-
const { extractManifest, formatSyncReport } =
|
|
259
|
+
const { extractManifest, formatSyncReport } =
|
|
260
|
+
await import('@uniweb/build/i18n')
|
|
245
261
|
|
|
246
262
|
log(`${colors.dim}Collecting site content...${colors.reset}`)
|
|
247
263
|
const siteContent = await collectSiteContent(siteRoot)
|
|
@@ -250,11 +266,15 @@ async function runExtract(siteRoot, config, args) {
|
|
|
250
266
|
const manifestPath = join(siteRoot, config.localesDir, 'manifest.json')
|
|
251
267
|
const isUpdate = existsSync(manifestPath)
|
|
252
268
|
|
|
253
|
-
const { manifest, report } = await extractManifest(
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
269
|
+
const { manifest, report } = await extractManifest(
|
|
270
|
+
siteRoot,
|
|
271
|
+
siteContent,
|
|
272
|
+
{
|
|
273
|
+
localesDir: config.localesDir,
|
|
274
|
+
verbose,
|
|
275
|
+
dryRun
|
|
276
|
+
}
|
|
277
|
+
)
|
|
258
278
|
|
|
259
279
|
// Show results
|
|
260
280
|
const unitCount = Object.keys(manifest.units).length
|
|
@@ -269,12 +289,18 @@ async function runExtract(siteRoot, config, args) {
|
|
|
269
289
|
if (dryRun) {
|
|
270
290
|
log(`\n${colors.dim}Dry run — no files were modified.${colors.reset}`)
|
|
271
291
|
} else {
|
|
272
|
-
log(
|
|
292
|
+
log(
|
|
293
|
+
`\nManifest written to: ${colors.dim}${config.localesDir}/manifest.json${colors.reset}`
|
|
294
|
+
)
|
|
273
295
|
}
|
|
274
296
|
|
|
275
297
|
if (config.locales.length === 0) {
|
|
276
|
-
log(
|
|
277
|
-
|
|
298
|
+
log(
|
|
299
|
+
`\n${colors.dim}No translation files found in ${config.localesDir}/.`
|
|
300
|
+
)
|
|
301
|
+
log(
|
|
302
|
+
`After translating, create locale files like ${config.localesDir}/es.json${colors.reset}`
|
|
303
|
+
)
|
|
278
304
|
}
|
|
279
305
|
} catch (err) {
|
|
280
306
|
error(`Extraction failed: ${err.message}`)
|
|
@@ -285,10 +311,12 @@ async function runExtract(siteRoot, config, args) {
|
|
|
285
311
|
|
|
286
312
|
// Extract collection content (by default, skip with --no-collections)
|
|
287
313
|
if (!noCollections) {
|
|
288
|
-
log(
|
|
314
|
+
log(
|
|
315
|
+
`\n${colors.cyan}Extracting collection content${dryRun ? ' (dry run)' : ''}...${colors.reset}\n`
|
|
316
|
+
)
|
|
289
317
|
|
|
290
318
|
// Check if collections exist
|
|
291
|
-
const dataDir = join(siteRoot, 'public',
|
|
319
|
+
const dataDir = join(siteRoot, 'public', DATA_DIR)
|
|
292
320
|
if (!existsSync(dataDir)) {
|
|
293
321
|
if (collectionsOnly) {
|
|
294
322
|
error('No collections found. Create collection data in public/data/.')
|
|
@@ -299,14 +327,20 @@ async function runExtract(siteRoot, config, args) {
|
|
|
299
327
|
}
|
|
300
328
|
|
|
301
329
|
try {
|
|
302
|
-
const { extractCollectionManifest, formatSyncReport } =
|
|
303
|
-
|
|
304
|
-
|
|
330
|
+
const { extractCollectionManifest, formatSyncReport } =
|
|
331
|
+
await import('@uniweb/build/i18n')
|
|
332
|
+
|
|
333
|
+
const collectionsManifestPath = join(
|
|
334
|
+
siteRoot,
|
|
335
|
+
config.localesDir,
|
|
336
|
+
'collections',
|
|
337
|
+
'manifest.json'
|
|
338
|
+
)
|
|
305
339
|
const isUpdate = existsSync(collectionsManifestPath)
|
|
306
340
|
|
|
307
341
|
const { manifest, report } = await extractCollectionManifest(siteRoot, {
|
|
308
342
|
localesDir: config.localesDir,
|
|
309
|
-
dryRun
|
|
343
|
+
dryRun
|
|
310
344
|
})
|
|
311
345
|
|
|
312
346
|
const unitCount = Object.keys(manifest.units).length
|
|
@@ -321,10 +355,14 @@ async function runExtract(siteRoot, config, args) {
|
|
|
321
355
|
if (dryRun) {
|
|
322
356
|
log(`\n${colors.dim}Dry run — no files were modified.${colors.reset}`)
|
|
323
357
|
} else {
|
|
324
|
-
log(
|
|
358
|
+
log(
|
|
359
|
+
`\nManifest written to: ${colors.dim}${config.localesDir}/collections/manifest.json${colors.reset}`
|
|
360
|
+
)
|
|
325
361
|
}
|
|
326
362
|
} else {
|
|
327
|
-
log(
|
|
363
|
+
log(
|
|
364
|
+
`${colors.dim}No translatable content found in collections.${colors.reset}`
|
|
365
|
+
)
|
|
328
366
|
}
|
|
329
367
|
} catch (err) {
|
|
330
368
|
error(`Collection extraction failed: ${err.message}`)
|
|
@@ -348,7 +386,7 @@ async function runInit(siteRoot, config, args) {
|
|
|
348
386
|
const force = args.includes('--force')
|
|
349
387
|
|
|
350
388
|
// Collect locale codes from positional args (skip flags)
|
|
351
|
-
const positionalLocales = args.filter(a => !a.startsWith('-'))
|
|
389
|
+
const positionalLocales = args.filter((a) => !a.startsWith('-'))
|
|
352
390
|
|
|
353
391
|
// Read manifest
|
|
354
392
|
const localesPath = join(siteRoot, config.localesDir)
|
|
@@ -370,13 +408,14 @@ async function runInit(siteRoot, config, args) {
|
|
|
370
408
|
}
|
|
371
409
|
|
|
372
410
|
// Determine target locales
|
|
373
|
-
let targetLocales =
|
|
374
|
-
? positionalLocales
|
|
375
|
-
: config.locales
|
|
411
|
+
let targetLocales =
|
|
412
|
+
positionalLocales.length > 0 ? positionalLocales : config.locales
|
|
376
413
|
|
|
377
414
|
if (!targetLocales || targetLocales.length === 0) {
|
|
378
415
|
error('No target locales specified.')
|
|
379
|
-
log(
|
|
416
|
+
log(
|
|
417
|
+
`${colors.dim}Specify locales as arguments (e.g., "uniweb i18n generate es fr")`
|
|
418
|
+
)
|
|
380
419
|
log(`or configure them in site.yml under languages.${colors.reset}`)
|
|
381
420
|
process.exit(1)
|
|
382
421
|
}
|
|
@@ -401,7 +440,9 @@ async function runInit(siteRoot, config, args) {
|
|
|
401
440
|
try {
|
|
402
441
|
existing = JSON.parse(existingRaw)
|
|
403
442
|
} catch {
|
|
404
|
-
warn(
|
|
443
|
+
warn(
|
|
444
|
+
`${locale}.json has invalid JSON, skipping (use --force to overwrite)`
|
|
445
|
+
)
|
|
405
446
|
continue
|
|
406
447
|
}
|
|
407
448
|
|
|
@@ -418,7 +459,9 @@ async function runInit(siteRoot, config, args) {
|
|
|
418
459
|
if (added > 0) {
|
|
419
460
|
await writeFile(localePath, JSON.stringify(existing, null, 2) + '\n')
|
|
420
461
|
const alreadyCount = existingKeys.size
|
|
421
|
-
success(
|
|
462
|
+
success(
|
|
463
|
+
`Updated ${locale}.json (${added} new string${added !== 1 ? 's' : ''} added, ${alreadyCount} already translated)`
|
|
464
|
+
)
|
|
422
465
|
} else {
|
|
423
466
|
success(`${locale}.json already has all ${unitCount} strings`)
|
|
424
467
|
}
|
|
@@ -431,7 +474,9 @@ async function runInit(siteRoot, config, args) {
|
|
|
431
474
|
}
|
|
432
475
|
|
|
433
476
|
await writeFile(localePath, JSON.stringify(localeData, null, 2) + '\n')
|
|
434
|
-
success(
|
|
477
|
+
success(
|
|
478
|
+
`Created ${locale}.json (${unitCount} string${unitCount !== 1 ? 's' : ''})`
|
|
479
|
+
)
|
|
435
480
|
}
|
|
436
481
|
}
|
|
437
482
|
|
|
@@ -445,7 +490,7 @@ async function runInit(siteRoot, config, args) {
|
|
|
445
490
|
* Status command - show translation coverage
|
|
446
491
|
*/
|
|
447
492
|
async function runStatus(siteRoot, config, args) {
|
|
448
|
-
const locale = args.find(a => !a.startsWith('-'))
|
|
493
|
+
const locale = args.find((a) => !a.startsWith('-'))
|
|
449
494
|
const showMissing = args.includes('--missing')
|
|
450
495
|
const showFreeform = args.includes('--freeform')
|
|
451
496
|
const outputJson = args.includes('--json')
|
|
@@ -478,22 +523,31 @@ async function runStatus(siteRoot, config, args) {
|
|
|
478
523
|
|
|
479
524
|
if (config.locales.length === 0) {
|
|
480
525
|
if (outputJson) {
|
|
481
|
-
log(
|
|
526
|
+
log(
|
|
527
|
+
JSON.stringify(
|
|
528
|
+
{ error: 'No translation files found', locales: [] },
|
|
529
|
+
null,
|
|
530
|
+
2
|
|
531
|
+
)
|
|
532
|
+
)
|
|
482
533
|
} else {
|
|
483
534
|
log(`${colors.dim}No translation files found in ${config.localesDir}/.`)
|
|
484
|
-
log(
|
|
535
|
+
log(
|
|
536
|
+
`Create locale files like ${config.localesDir}/es.json to add translations.${colors.reset}`
|
|
537
|
+
)
|
|
485
538
|
}
|
|
486
539
|
return
|
|
487
540
|
}
|
|
488
541
|
|
|
489
542
|
try {
|
|
490
|
-
const { getTranslationStatus, formatTranslationStatus } =
|
|
543
|
+
const { getTranslationStatus, formatTranslationStatus } =
|
|
544
|
+
await import('@uniweb/build/i18n')
|
|
491
545
|
|
|
492
546
|
const localesToCheck = locale ? [locale] : config.locales
|
|
493
547
|
|
|
494
548
|
const status = await getTranslationStatus(siteRoot, {
|
|
495
549
|
localesDir: config.localesDir,
|
|
496
|
-
locales: localesToCheck
|
|
550
|
+
locales: localesToCheck
|
|
497
551
|
})
|
|
498
552
|
|
|
499
553
|
if (outputJson) {
|
|
@@ -504,10 +558,14 @@ async function runStatus(siteRoot, config, args) {
|
|
|
504
558
|
log(formatTranslationStatus(status))
|
|
505
559
|
|
|
506
560
|
// Show next steps if there are missing translations
|
|
507
|
-
const hasMissing = Object.values(status.locales).some(l => l.missing > 0)
|
|
561
|
+
const hasMissing = Object.values(status.locales).some((l) => l.missing > 0)
|
|
508
562
|
if (hasMissing) {
|
|
509
|
-
log(
|
|
510
|
-
|
|
563
|
+
log(
|
|
564
|
+
`\n${colors.dim}To translate missing strings, edit the locale files in ${config.localesDir}/`
|
|
565
|
+
)
|
|
566
|
+
log(
|
|
567
|
+
`Or use: uniweb i18n status --missing --json > missing.json${colors.reset}`
|
|
568
|
+
)
|
|
511
569
|
}
|
|
512
570
|
} catch (err) {
|
|
513
571
|
error(`Status check failed: ${err.message}`)
|
|
@@ -525,9 +583,17 @@ async function runStatusFreeform(siteRoot, config, locale, options = {}) {
|
|
|
525
583
|
|
|
526
584
|
if (!existsSync(freeformPath)) {
|
|
527
585
|
if (outputJson) {
|
|
528
|
-
log(
|
|
586
|
+
log(
|
|
587
|
+
JSON.stringify(
|
|
588
|
+
{ error: 'No free-form translations found', locales: {} },
|
|
589
|
+
null,
|
|
590
|
+
2
|
|
591
|
+
)
|
|
592
|
+
)
|
|
529
593
|
} else {
|
|
530
|
-
log(
|
|
594
|
+
log(
|
|
595
|
+
`${colors.dim}No free-form translations found in ${config.localesDir}/freeform/.${colors.reset}`
|
|
596
|
+
)
|
|
531
597
|
}
|
|
532
598
|
return
|
|
533
599
|
}
|
|
@@ -572,7 +638,7 @@ async function runStatusFreeform(siteRoot, config, locale, options = {}) {
|
|
|
572
638
|
|
|
573
639
|
// Find all locales
|
|
574
640
|
const entries = await readdir(freeformPath, { withFileTypes: true })
|
|
575
|
-
const locales = entries.filter(e => e.isDirectory()).map(e => e.name)
|
|
641
|
+
const locales = entries.filter((e) => e.isDirectory()).map((e) => e.name)
|
|
576
642
|
const localesToCheck = locale ? [locale] : locales
|
|
577
643
|
|
|
578
644
|
const results = {}
|
|
@@ -583,22 +649,33 @@ async function runStatusFreeform(siteRoot, config, locale, options = {}) {
|
|
|
583
649
|
|
|
584
650
|
// Discover translations
|
|
585
651
|
const discovered = await discoverFreeformTranslations(loc, localesPath)
|
|
586
|
-
const allPaths = [
|
|
652
|
+
const allPaths = [
|
|
653
|
+
...discovered.pages,
|
|
654
|
+
...discovered.pageIds,
|
|
655
|
+
...discovered.collections
|
|
656
|
+
]
|
|
587
657
|
|
|
588
658
|
// Check staleness
|
|
589
659
|
const stale = await getStaleTranslations(localeDir, sourceHashes)
|
|
590
660
|
const orphaned = await getOrphanedTranslations(localeDir, validPaths)
|
|
591
661
|
|
|
592
|
-
const upToDate = allPaths.filter(
|
|
593
|
-
|
|
594
|
-
|
|
662
|
+
const upToDate = allPaths.filter(
|
|
663
|
+
(p) =>
|
|
664
|
+
!stale.some((s) => s.path === p) &&
|
|
665
|
+
!orphaned.some((o) => o.path === p)
|
|
595
666
|
)
|
|
596
667
|
|
|
597
668
|
results[loc] = {
|
|
598
669
|
total: allPaths.length,
|
|
599
670
|
upToDate: upToDate.length,
|
|
600
|
-
stale: stale.map(s => ({
|
|
601
|
-
|
|
671
|
+
stale: stale.map((s) => ({
|
|
672
|
+
path: s.path,
|
|
673
|
+
recordedDate: s.recordedDate
|
|
674
|
+
})),
|
|
675
|
+
orphaned: orphaned.map((o) => ({
|
|
676
|
+
path: o.path,
|
|
677
|
+
recordedDate: o.recordedDate
|
|
678
|
+
}))
|
|
602
679
|
}
|
|
603
680
|
}
|
|
604
681
|
|
|
@@ -621,7 +698,9 @@ async function runStatusFreeform(siteRoot, config, locale, options = {}) {
|
|
|
621
698
|
if (info.stale.length > 0) {
|
|
622
699
|
log(` ${colors.yellow}Stale (source changed):${colors.reset}`)
|
|
623
700
|
for (const item of info.stale) {
|
|
624
|
-
log(
|
|
701
|
+
log(
|
|
702
|
+
` ${colors.yellow}⚠${colors.reset} ${item.path} ${colors.dim}(${item.recordedDate})${colors.reset}`
|
|
703
|
+
)
|
|
625
704
|
}
|
|
626
705
|
}
|
|
627
706
|
|
|
@@ -632,19 +711,27 @@ async function runStatusFreeform(siteRoot, config, locale, options = {}) {
|
|
|
632
711
|
}
|
|
633
712
|
}
|
|
634
713
|
|
|
635
|
-
log(
|
|
714
|
+
log(
|
|
715
|
+
` ${colors.dim}Summary: ${info.upToDate} up to date, ${info.stale.length} stale, ${info.orphaned.length} orphaned${colors.reset}`
|
|
716
|
+
)
|
|
636
717
|
log('')
|
|
637
718
|
}
|
|
638
719
|
|
|
639
720
|
// Show next steps
|
|
640
|
-
const hasStale = Object.values(results).some(r => r.stale.length > 0)
|
|
641
|
-
const hasOrphaned = Object.values(results).some(
|
|
721
|
+
const hasStale = Object.values(results).some((r) => r.stale.length > 0)
|
|
722
|
+
const hasOrphaned = Object.values(results).some(
|
|
723
|
+
(r) => r.orphaned.length > 0
|
|
724
|
+
)
|
|
642
725
|
|
|
643
726
|
if (hasStale) {
|
|
644
|
-
log(
|
|
727
|
+
log(
|
|
728
|
+
`${colors.dim}Run 'uniweb i18n update-hash <locale> --all-stale' to update hashes after reviewing.${colors.reset}`
|
|
729
|
+
)
|
|
645
730
|
}
|
|
646
731
|
if (hasOrphaned) {
|
|
647
|
-
log(
|
|
732
|
+
log(
|
|
733
|
+
`${colors.dim}Run 'uniweb i18n prune --freeform' to remove orphaned translations.${colors.reset}`
|
|
734
|
+
)
|
|
648
735
|
}
|
|
649
736
|
} catch (err) {
|
|
650
737
|
error(`Status check failed: ${err.message}`)
|
|
@@ -661,9 +748,17 @@ async function runStatusMissing(siteRoot, config, locale, options = {}) {
|
|
|
661
748
|
|
|
662
749
|
if (config.locales.length === 0) {
|
|
663
750
|
if (outputJson) {
|
|
664
|
-
log(
|
|
751
|
+
log(
|
|
752
|
+
JSON.stringify(
|
|
753
|
+
{ error: 'No translation files found', missing: [] },
|
|
754
|
+
null,
|
|
755
|
+
2
|
|
756
|
+
)
|
|
757
|
+
)
|
|
665
758
|
} else {
|
|
666
|
-
log(
|
|
759
|
+
log(
|
|
760
|
+
`${colors.dim}No translation files found in ${config.localesDir}/.${colors.reset}`
|
|
761
|
+
)
|
|
667
762
|
}
|
|
668
763
|
return
|
|
669
764
|
}
|
|
@@ -683,9 +778,10 @@ async function runStatusMissing(siteRoot, config, locale, options = {}) {
|
|
|
683
778
|
total: result.total,
|
|
684
779
|
translated: result.valid.length,
|
|
685
780
|
missing: result.missing.length,
|
|
686
|
-
coverage:
|
|
687
|
-
|
|
688
|
-
|
|
781
|
+
coverage:
|
|
782
|
+
result.total > 0
|
|
783
|
+
? Math.round((result.valid.length / result.total) * 100)
|
|
784
|
+
: 100
|
|
689
785
|
}
|
|
690
786
|
|
|
691
787
|
// Add locale info to each missing entry
|
|
@@ -746,11 +842,15 @@ async function runStatusMissing(siteRoot, config, locale, options = {}) {
|
|
|
746
842
|
}
|
|
747
843
|
|
|
748
844
|
if (allMissing.length > 20) {
|
|
749
|
-
log(
|
|
845
|
+
log(
|
|
846
|
+
`\n ${colors.dim}... and ${allMissing.length - 20} more${colors.reset}`
|
|
847
|
+
)
|
|
750
848
|
}
|
|
751
849
|
}
|
|
752
850
|
|
|
753
|
-
log(
|
|
851
|
+
log(
|
|
852
|
+
`\n${colors.dim}Use --json to export for translation tools.${colors.reset}`
|
|
853
|
+
)
|
|
754
854
|
} catch (err) {
|
|
755
855
|
error(`Status check failed: ${err.message}`)
|
|
756
856
|
process.exit(1)
|
|
@@ -782,7 +882,7 @@ function truncateString(str, maxLen) {
|
|
|
782
882
|
* Audit command - find stale and missing translations
|
|
783
883
|
*/
|
|
784
884
|
async function runAudit(siteRoot, config, args) {
|
|
785
|
-
const locale = args.find(a => !a.startsWith('-'))
|
|
885
|
+
const locale = args.find((a) => !a.startsWith('-'))
|
|
786
886
|
const clean = args.includes('--clean')
|
|
787
887
|
const verbose = args.includes('--verbose') || args.includes('-v')
|
|
788
888
|
|
|
@@ -798,12 +898,15 @@ async function runAudit(siteRoot, config, args) {
|
|
|
798
898
|
|
|
799
899
|
if (config.locales.length === 0) {
|
|
800
900
|
log(`${colors.dim}No translation files found in ${config.localesDir}/.`)
|
|
801
|
-
log(
|
|
901
|
+
log(
|
|
902
|
+
`Create locale files like ${config.localesDir}/es.json to add translations.${colors.reset}`
|
|
903
|
+
)
|
|
802
904
|
return
|
|
803
905
|
}
|
|
804
906
|
|
|
805
907
|
try {
|
|
806
|
-
const { auditLocale, cleanLocale, formatAuditReport } =
|
|
908
|
+
const { auditLocale, cleanLocale, formatAuditReport } =
|
|
909
|
+
await import('@uniweb/build/i18n')
|
|
807
910
|
|
|
808
911
|
const localesToAudit = locale ? [locale] : config.locales
|
|
809
912
|
const results = []
|
|
@@ -823,10 +926,16 @@ async function runAudit(siteRoot, config, args) {
|
|
|
823
926
|
|
|
824
927
|
for (const result of results) {
|
|
825
928
|
if (result.stale.length > 0) {
|
|
826
|
-
const staleHashes = result.stale.map(s => s.hash)
|
|
827
|
-
const removed = await cleanLocale(
|
|
929
|
+
const staleHashes = result.stale.map((s) => s.hash)
|
|
930
|
+
const removed = await cleanLocale(
|
|
931
|
+
localesPath,
|
|
932
|
+
result.locale,
|
|
933
|
+
staleHashes
|
|
934
|
+
)
|
|
828
935
|
if (removed > 0) {
|
|
829
|
-
success(
|
|
936
|
+
success(
|
|
937
|
+
`Removed ${removed} stale entries from ${result.locale}.json`
|
|
938
|
+
)
|
|
830
939
|
totalRemoved += removed
|
|
831
940
|
}
|
|
832
941
|
}
|
|
@@ -837,19 +946,30 @@ async function runAudit(siteRoot, config, args) {
|
|
|
837
946
|
}
|
|
838
947
|
} else {
|
|
839
948
|
// Suggest --clean if there are stale entries
|
|
840
|
-
const hasStale = results.some(r => r.stale.length > 0)
|
|
949
|
+
const hasStale = results.some((r) => r.stale.length > 0)
|
|
841
950
|
if (hasStale) {
|
|
842
|
-
log(
|
|
951
|
+
log(
|
|
952
|
+
`\n${colors.dim}Run with --clean to remove stale entries.${colors.reset}`
|
|
953
|
+
)
|
|
843
954
|
}
|
|
844
955
|
}
|
|
845
956
|
|
|
846
957
|
// Report entries that need inline tag updates
|
|
847
|
-
const needsTagsTotal = results.reduce(
|
|
958
|
+
const needsTagsTotal = results.reduce(
|
|
959
|
+
(sum, r) => sum + (r.needsTags?.length || 0),
|
|
960
|
+
0
|
|
961
|
+
)
|
|
848
962
|
if (needsTagsTotal > 0) {
|
|
849
|
-
log(
|
|
850
|
-
|
|
963
|
+
log(
|
|
964
|
+
`\n${colors.yellow}${needsTagsTotal} translation(s) have inline marks in the source but not in the translation.`
|
|
965
|
+
)
|
|
966
|
+
log(
|
|
967
|
+
`These translations won't preserve accent/span styling.${colors.reset}`
|
|
968
|
+
)
|
|
851
969
|
if (!verbose) {
|
|
852
|
-
log(
|
|
970
|
+
log(
|
|
971
|
+
`${colors.dim}Run with --verbose to see which entries are affected.${colors.reset}`
|
|
972
|
+
)
|
|
853
973
|
}
|
|
854
974
|
}
|
|
855
975
|
} catch (err) {
|
|
@@ -880,7 +1000,9 @@ async function runInitFreeform(siteRoot, config, args) {
|
|
|
880
1000
|
log(`${colors.dim}Examples:`)
|
|
881
1001
|
log(' uniweb i18n init-freeform es pages/about hero')
|
|
882
1002
|
log(' uniweb i18n init-freeform es page-ids/installation intro')
|
|
883
|
-
log(
|
|
1003
|
+
log(
|
|
1004
|
+
` uniweb i18n init-freeform es collections/articles getting-started${colors.reset}`
|
|
1005
|
+
)
|
|
884
1006
|
process.exit(1)
|
|
885
1007
|
}
|
|
886
1008
|
|
|
@@ -894,7 +1016,9 @@ async function runInitFreeform(siteRoot, config, args) {
|
|
|
894
1016
|
// Check if already exists
|
|
895
1017
|
if (existsSync(targetPath)) {
|
|
896
1018
|
error(`Translation already exists: ${relativePath}`)
|
|
897
|
-
log(
|
|
1019
|
+
log(
|
|
1020
|
+
`${colors.dim}Edit it directly or use 'update-hash' after changes.${colors.reset}`
|
|
1021
|
+
)
|
|
898
1022
|
process.exit(1)
|
|
899
1023
|
}
|
|
900
1024
|
|
|
@@ -936,7 +1060,12 @@ async function runInitFreeform(siteRoot, config, args) {
|
|
|
936
1060
|
} else if (pathType.startsWith('collections/')) {
|
|
937
1061
|
// Find item in collection data
|
|
938
1062
|
const collectionName = pathType.replace('collections/', '')
|
|
939
|
-
const dataPath = join(
|
|
1063
|
+
const dataPath = join(
|
|
1064
|
+
siteRoot,
|
|
1065
|
+
'public',
|
|
1066
|
+
'data',
|
|
1067
|
+
`${collectionName}.json`
|
|
1068
|
+
)
|
|
940
1069
|
|
|
941
1070
|
if (existsSync(dataPath)) {
|
|
942
1071
|
const dataRaw = await readFile(dataPath, 'utf-8')
|
|
@@ -971,7 +1100,9 @@ async function runInitFreeform(siteRoot, config, args) {
|
|
|
971
1100
|
await recordHash(freeformDir, relativePath, sourceHash)
|
|
972
1101
|
|
|
973
1102
|
success(`Created free-form translation: ${relativePath}`)
|
|
974
|
-
log(
|
|
1103
|
+
log(
|
|
1104
|
+
`${colors.dim}Edit the file, then run 'update-hash' when source changes.${colors.reset}`
|
|
1105
|
+
)
|
|
975
1106
|
} catch (err) {
|
|
976
1107
|
error(`Failed to initialize free-form translation: ${err.message}`)
|
|
977
1108
|
process.exit(1)
|
|
@@ -1058,7 +1189,9 @@ async function runUpdateHash(siteRoot, config, args) {
|
|
|
1058
1189
|
|
|
1059
1190
|
if (!locale) {
|
|
1060
1191
|
error('Usage: uniweb i18n update-hash <locale> [path] [section-id]')
|
|
1061
|
-
log(
|
|
1192
|
+
log(
|
|
1193
|
+
`${colors.dim}Or: uniweb i18n update-hash <locale> --all-stale${colors.reset}`
|
|
1194
|
+
)
|
|
1062
1195
|
process.exit(1)
|
|
1063
1196
|
}
|
|
1064
1197
|
|
|
@@ -1089,7 +1222,11 @@ async function runUpdateHash(siteRoot, config, args) {
|
|
|
1089
1222
|
} = await import('@uniweb/build/i18n')
|
|
1090
1223
|
|
|
1091
1224
|
// Build source hashes map
|
|
1092
|
-
const sourceHashes = buildSourceHashMap(
|
|
1225
|
+
const sourceHashes = buildSourceHashMap(
|
|
1226
|
+
siteContent,
|
|
1227
|
+
buildFreeformPath,
|
|
1228
|
+
computeSourceHash
|
|
1229
|
+
)
|
|
1093
1230
|
|
|
1094
1231
|
if (allStale) {
|
|
1095
1232
|
// Update all stale translations
|
|
@@ -1165,7 +1302,9 @@ async function runMove(siteRoot, config, args) {
|
|
|
1165
1302
|
|
|
1166
1303
|
if (!oldPath || !newPath) {
|
|
1167
1304
|
error('Usage: uniweb i18n move <old-path> <new-path>')
|
|
1168
|
-
log(
|
|
1305
|
+
log(
|
|
1306
|
+
`${colors.dim}Example: uniweb i18n move pages/docs/setup pages/getting-started${colors.reset}`
|
|
1307
|
+
)
|
|
1169
1308
|
process.exit(1)
|
|
1170
1309
|
}
|
|
1171
1310
|
|
|
@@ -1182,7 +1321,7 @@ async function runMove(siteRoot, config, args) {
|
|
|
1182
1321
|
|
|
1183
1322
|
// Find all locales with free-form translations
|
|
1184
1323
|
const entries = await readdir(freeformPath, { withFileTypes: true })
|
|
1185
|
-
const locales = entries.filter(e => e.isDirectory()).map(e => e.name)
|
|
1324
|
+
const locales = entries.filter((e) => e.isDirectory()).map((e) => e.name)
|
|
1186
1325
|
|
|
1187
1326
|
let totalMoved = 0
|
|
1188
1327
|
|
|
@@ -1220,7 +1359,9 @@ async function runMove(siteRoot, config, args) {
|
|
|
1220
1359
|
}
|
|
1221
1360
|
|
|
1222
1361
|
if (totalMoved > 0) {
|
|
1223
|
-
success(
|
|
1362
|
+
success(
|
|
1363
|
+
`Moved ${totalMoved} translation file(s) across ${locales.length} locale(s)`
|
|
1364
|
+
)
|
|
1224
1365
|
} else {
|
|
1225
1366
|
log(`${colors.dim}No translations found at: ${oldPath}${colors.reset}`)
|
|
1226
1367
|
}
|
|
@@ -1243,7 +1384,9 @@ async function runRename(siteRoot, config, args) {
|
|
|
1243
1384
|
|
|
1244
1385
|
if (!path || !oldName || !newName) {
|
|
1245
1386
|
error('Usage: uniweb i18n rename <path> <old-name> <new-name>')
|
|
1246
|
-
log(
|
|
1387
|
+
log(
|
|
1388
|
+
`${colors.dim}Example: uniweb i18n rename pages/about hero welcome${colors.reset}`
|
|
1389
|
+
)
|
|
1247
1390
|
process.exit(1)
|
|
1248
1391
|
}
|
|
1249
1392
|
|
|
@@ -1260,7 +1403,7 @@ async function runRename(siteRoot, config, args) {
|
|
|
1260
1403
|
|
|
1261
1404
|
// Find all locales with free-form translations
|
|
1262
1405
|
const entries = await readdir(freeformPath, { withFileTypes: true })
|
|
1263
|
-
const locales = entries.filter(e => e.isDirectory()).map(e => e.name)
|
|
1406
|
+
const locales = entries.filter((e) => e.isDirectory()).map((e) => e.name)
|
|
1264
1407
|
|
|
1265
1408
|
let totalRenamed = 0
|
|
1266
1409
|
|
|
@@ -1285,7 +1428,9 @@ async function runRename(siteRoot, config, args) {
|
|
|
1285
1428
|
if (totalRenamed > 0) {
|
|
1286
1429
|
success(`Renamed translation in ${totalRenamed} locale(s)`)
|
|
1287
1430
|
} else {
|
|
1288
|
-
log(
|
|
1431
|
+
log(
|
|
1432
|
+
`${colors.dim}No translations found: ${path}/${oldName}.md${colors.reset}`
|
|
1433
|
+
)
|
|
1289
1434
|
}
|
|
1290
1435
|
} catch (err) {
|
|
1291
1436
|
error(`Failed to rename translation: ${err.message}`)
|
|
@@ -1316,7 +1461,9 @@ async function runPrune(siteRoot, config, args) {
|
|
|
1316
1461
|
return
|
|
1317
1462
|
}
|
|
1318
1463
|
|
|
1319
|
-
log(
|
|
1464
|
+
log(
|
|
1465
|
+
`\n${colors.cyan}Pruning orphaned free-form translations${dryRun ? ' (dry run)' : ''}...${colors.reset}\n`
|
|
1466
|
+
)
|
|
1320
1467
|
|
|
1321
1468
|
try {
|
|
1322
1469
|
// Load site content
|
|
@@ -1349,7 +1496,7 @@ async function runPrune(siteRoot, config, args) {
|
|
|
1349
1496
|
|
|
1350
1497
|
// Find all locales
|
|
1351
1498
|
const entries = await readdir(freeformPath, { withFileTypes: true })
|
|
1352
|
-
const locales = entries.filter(e => e.isDirectory()).map(e => e.name)
|
|
1499
|
+
const locales = entries.filter((e) => e.isDirectory()).map((e) => e.name)
|
|
1353
1500
|
|
|
1354
1501
|
let totalPruned = 0
|
|
1355
1502
|
|
|
@@ -1379,14 +1526,16 @@ async function runPrune(siteRoot, config, args) {
|
|
|
1379
1526
|
|
|
1380
1527
|
// Update manifest
|
|
1381
1528
|
if (!dryRun && orphaned.length > 0) {
|
|
1382
|
-
const paths = orphaned.map(o => o.path)
|
|
1529
|
+
const paths = orphaned.map((o) => o.path)
|
|
1383
1530
|
await removeManifestEntries(localeDir, paths)
|
|
1384
1531
|
}
|
|
1385
1532
|
}
|
|
1386
1533
|
|
|
1387
1534
|
if (totalPruned > 0) {
|
|
1388
1535
|
if (dryRun) {
|
|
1389
|
-
log(
|
|
1536
|
+
log(
|
|
1537
|
+
`\n${colors.dim}Would remove ${totalPruned} orphaned translation(s). Run without --dry-run to delete.${colors.reset}`
|
|
1538
|
+
)
|
|
1390
1539
|
} else {
|
|
1391
1540
|
success(`\nRemoved ${totalPruned} orphaned translation(s)`)
|
|
1392
1541
|
}
|
|
@@ -1412,7 +1561,7 @@ async function discoverFiles(dir) {
|
|
|
1412
1561
|
for (const entry of entries) {
|
|
1413
1562
|
const fullPath = join(dir, entry.name)
|
|
1414
1563
|
if (entry.isDirectory()) {
|
|
1415
|
-
files.push(...await discoverFiles(fullPath))
|
|
1564
|
+
files.push(...(await discoverFiles(fullPath)))
|
|
1416
1565
|
} else {
|
|
1417
1566
|
files.push(fullPath)
|
|
1418
1567
|
}
|