uniweb 1.21.9 → 1.21.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -6
- package/src/commands/i18n.js +126 -79
- package/src/framework-index.json +3 -3
- package/src/index.js +13 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.11",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -43,13 +43,13 @@
|
|
|
43
43
|
"tar": "^7.5.21",
|
|
44
44
|
"@uniweb/content-writer": "~1.3.1",
|
|
45
45
|
"@uniweb/core": "~1.4.9",
|
|
46
|
-
"@uniweb/
|
|
47
|
-
"@uniweb/
|
|
46
|
+
"@uniweb/schemas": "~1.6.5",
|
|
47
|
+
"@uniweb/semantic-parser": "~1.13.0"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"@uniweb/build": "~1.10.
|
|
51
|
-
"@uniweb/
|
|
52
|
-
"@uniweb/
|
|
50
|
+
"@uniweb/build": "~1.10.8",
|
|
51
|
+
"@uniweb/semantic-parser": "~1.13.0",
|
|
52
|
+
"@uniweb/content-reader": "~1.5.7"
|
|
53
53
|
},
|
|
54
54
|
"peerDependenciesMeta": {
|
|
55
55
|
"@uniweb/build": {
|
package/src/commands/i18n.js
CHANGED
|
@@ -600,6 +600,19 @@ async function runStatus(siteRoot, config, args) {
|
|
|
600
600
|
log(`\n${colors.cyan}${scope.records ? 'Record ' : ''}Translation Status${colors.reset}\n`)
|
|
601
601
|
}
|
|
602
602
|
|
|
603
|
+
// ⭐ WHAT IS LEFT INCLUDES THE FREE-FORM TRANSLATIONS. A section translated whole goes stale
|
|
604
|
+
// when its source changes, and the strings counted here never show it. ⛔ Until 2026-10-27
|
|
605
|
+
// this said 100% over a stale one, and only `--freeform` listed it.
|
|
606
|
+
let freeform = null
|
|
607
|
+
if (!scope.records) {
|
|
608
|
+
try {
|
|
609
|
+
freeform = await freeformStatus(siteRoot, config, locale)
|
|
610
|
+
} catch (err) {
|
|
611
|
+
error(`Status check failed: ${err.message}`)
|
|
612
|
+
process.exit(1)
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
603
616
|
if (config.locales.length === 0) {
|
|
604
617
|
if (outputJson) {
|
|
605
618
|
log(
|
|
@@ -614,6 +627,7 @@ async function runStatus(siteRoot, config, args) {
|
|
|
614
627
|
log(
|
|
615
628
|
`Create locale files like ${config.localesDir}/es.json to add translations.${colors.reset}`
|
|
616
629
|
)
|
|
630
|
+
if (freeform) log(freeformSummary(freeform))
|
|
617
631
|
}
|
|
618
632
|
return
|
|
619
633
|
}
|
|
@@ -630,7 +644,7 @@ async function runStatus(siteRoot, config, args) {
|
|
|
630
644
|
})
|
|
631
645
|
|
|
632
646
|
if (outputJson) {
|
|
633
|
-
log(JSON.stringify(status, null, 2))
|
|
647
|
+
log(JSON.stringify(freeform ? { ...status, freeform } : status, null, 2))
|
|
634
648
|
return
|
|
635
649
|
}
|
|
636
650
|
|
|
@@ -646,6 +660,7 @@ async function runStatus(siteRoot, config, args) {
|
|
|
646
660
|
`Or use: uniweb i18n status${scope.records ? ' --records-only' : ''} --missing --json > missing.json${colors.reset}`
|
|
647
661
|
)
|
|
648
662
|
}
|
|
663
|
+
if (freeform) log(freeformSummary(freeform))
|
|
649
664
|
} catch (err) {
|
|
650
665
|
error(`Status check failed: ${err.message}`)
|
|
651
666
|
process.exit(1)
|
|
@@ -653,95 +668,125 @@ async function runStatus(siteRoot, config, args) {
|
|
|
653
668
|
}
|
|
654
669
|
|
|
655
670
|
/**
|
|
656
|
-
*
|
|
671
|
+
* Each language's free-form translations judged against the site's source: what `status
|
|
672
|
+
* --freeform` lists, and plain `status` sums up. Null when the site has none.
|
|
673
|
+
*
|
|
674
|
+
* @returns {Promise<Object<string, { total: number, upToDate: number, stale: Array,
|
|
675
|
+
* orphaned: Array, notChecked: Array }>|null>}
|
|
657
676
|
*/
|
|
658
|
-
async function
|
|
659
|
-
const { outputJson = false } = options
|
|
677
|
+
async function freeformStatus(siteRoot, config, locale) {
|
|
660
678
|
const localesPath = join(siteRoot, config.localesDir)
|
|
661
679
|
const freeformPath = join(localesPath, 'freeform')
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
680
|
+
if (!existsSync(freeformPath)) return null
|
|
681
|
+
|
|
682
|
+
const siteContent = await collectSourceContent(siteRoot)
|
|
683
|
+
|
|
684
|
+
const {
|
|
685
|
+
discoverFreeformTranslations,
|
|
686
|
+
freeformSourceIndex,
|
|
687
|
+
getStaleTranslations,
|
|
688
|
+
getOrphanedTranslations
|
|
689
|
+
} = await import('@uniweb/build/i18n')
|
|
690
|
+
|
|
691
|
+
// What the site's content says about its free-form translations — every path the
|
|
692
|
+
// renderer reads one from, with its source's hash. The build's own check uses this
|
|
693
|
+
// index too, and confirms an orphan against the source as this does, so the translation
|
|
694
|
+
// of a `hidden:` page or section is an orphan to neither.
|
|
695
|
+
const { validPaths, sourceHashes, canJudge } = freeformSourceIndex(siteContent)
|
|
696
|
+
|
|
697
|
+
// Find all locales
|
|
698
|
+
const entries = await readdir(freeformPath, { withFileTypes: true })
|
|
699
|
+
const locales = entries.filter((e) => e.isDirectory()).map((e) => e.name)
|
|
700
|
+
const localesToCheck = locale ? [locale] : locales
|
|
701
|
+
|
|
702
|
+
const results = {}
|
|
703
|
+
|
|
704
|
+
for (const loc of localesToCheck) {
|
|
705
|
+
const localeDir = join(freeformPath, loc)
|
|
706
|
+
if (!existsSync(localeDir)) continue
|
|
707
|
+
|
|
708
|
+
// Discover translations
|
|
709
|
+
const discovered = await discoverFreeformTranslations(loc, localesPath)
|
|
710
|
+
const allPaths = [
|
|
711
|
+
...discovered.pages,
|
|
712
|
+
...discovered.pageIds,
|
|
713
|
+
...discovered.records
|
|
714
|
+
]
|
|
715
|
+
|
|
716
|
+
// Check staleness. Orphaned only when the content can judge it — never a record's
|
|
717
|
+
// translation, nor one for a page whose sections the content does not carry.
|
|
718
|
+
const stale = await getStaleTranslations(localeDir, sourceHashes)
|
|
719
|
+
const orphaned = (await getOrphanedTranslations(localeDir, validPaths)).filter((o) => canJudge(o.path))
|
|
720
|
+
|
|
721
|
+
// ⭐ Up to date only where a source was found to compare with. A translation with none
|
|
722
|
+
// — a record's, whose record the site content does not hold — is NOT CHECKED, and said
|
|
723
|
+
// so. ⛔ Until 2026-10-17 it was counted up to date, like every page translation of a
|
|
724
|
+
// prerendered site while this read the build's split content.
|
|
725
|
+
const judged = (p) => stale.some((s) => s.path === p) || orphaned.some((o) => o.path === p)
|
|
726
|
+
const upToDate = allPaths.filter((p) => !judged(p) && sourceHashes[p])
|
|
727
|
+
const notChecked = allPaths.filter((p) => !judged(p) && !sourceHashes[p])
|
|
728
|
+
|
|
729
|
+
results[loc] = {
|
|
730
|
+
total: allPaths.length,
|
|
731
|
+
upToDate: upToDate.length,
|
|
732
|
+
stale: stale.map((s) => ({
|
|
733
|
+
path: s.path,
|
|
734
|
+
recordedDate: s.recordedDate
|
|
735
|
+
})),
|
|
736
|
+
orphaned: orphaned.map((o) => ({
|
|
737
|
+
path: o.path,
|
|
738
|
+
recordedDate: o.recordedDate
|
|
739
|
+
})),
|
|
740
|
+
notChecked: notChecked.map((path) => ({ path }))
|
|
676
741
|
}
|
|
677
|
-
return
|
|
678
742
|
}
|
|
679
743
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
try {
|
|
685
|
-
const siteContent = await collectSourceContent(siteRoot)
|
|
686
|
-
|
|
687
|
-
const {
|
|
688
|
-
discoverFreeformTranslations,
|
|
689
|
-
freeformSourceIndex,
|
|
690
|
-
getStaleTranslations,
|
|
691
|
-
getOrphanedTranslations
|
|
692
|
-
} = await import('@uniweb/build/i18n')
|
|
693
|
-
|
|
694
|
-
// What the site's content says about its free-form translations — every path the
|
|
695
|
-
// renderer reads one from, with its source's hash. The build's own check uses this
|
|
696
|
-
// index too, so the two judge by one rule; they differ only where the build leaves
|
|
697
|
-
// content out — a `hidden:` page or section, which the build calls orphaned.
|
|
698
|
-
const { validPaths, sourceHashes, canJudge } = freeformSourceIndex(siteContent)
|
|
744
|
+
return results
|
|
745
|
+
}
|
|
699
746
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
747
|
+
/** Plain `status`'s lines for the free-form translations: each language's count, and what is left. */
|
|
748
|
+
function freeformSummary(results) {
|
|
749
|
+
const lines = [`\n${colors.bright}Free-form translations:${colors.reset}`]
|
|
750
|
+
const list = (items) => items.map((i) => i.path).join(', ')
|
|
751
|
+
let left = false
|
|
752
|
+
for (const [loc, info] of Object.entries(results)) {
|
|
753
|
+
const said = [
|
|
754
|
+
info.stale.length > 0 && `${info.stale.length} stale: ${list(info.stale)}`,
|
|
755
|
+
info.orphaned.length > 0 && `${info.orphaned.length} orphaned: ${list(info.orphaned)}`
|
|
756
|
+
].filter(Boolean)
|
|
757
|
+
if (said.length) left = true
|
|
758
|
+
if (info.notChecked.length > 0) said.push(`${info.notChecked.length} not checked`)
|
|
759
|
+
const n = `${info.total} ${info.total === 1 ? 'translation' : 'translations'}`
|
|
760
|
+
lines.push(` ${loc}: ${n}${said.length ? ` — ${said.join('; ')}` : ', all up to date'}`)
|
|
761
|
+
}
|
|
762
|
+
if (left) lines.push(`${colors.dim}Run 'uniweb i18n status --freeform' to see each one, and what to do.${colors.reset}`)
|
|
763
|
+
return lines.join('\n')
|
|
764
|
+
}
|
|
704
765
|
|
|
705
|
-
|
|
766
|
+
/**
|
|
767
|
+
* Status --freeform mode - show free-form translation status
|
|
768
|
+
*/
|
|
769
|
+
async function runStatusFreeform(siteRoot, config, locale, options = {}) {
|
|
770
|
+
const { outputJson = false } = options
|
|
706
771
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
if (!existsSync(localeDir)) continue
|
|
710
|
-
|
|
711
|
-
// Discover translations
|
|
712
|
-
const discovered = await discoverFreeformTranslations(loc, localesPath)
|
|
713
|
-
const allPaths = [
|
|
714
|
-
...discovered.pages,
|
|
715
|
-
...discovered.pageIds,
|
|
716
|
-
...discovered.records
|
|
717
|
-
]
|
|
718
|
-
|
|
719
|
-
// Check staleness. Orphaned only when the content can judge it — never a record's
|
|
720
|
-
// translation, nor one for a page whose sections the content does not carry.
|
|
721
|
-
const stale = await getStaleTranslations(localeDir, sourceHashes)
|
|
722
|
-
const orphaned = (await getOrphanedTranslations(localeDir, validPaths)).filter((o) => canJudge(o.path))
|
|
772
|
+
try {
|
|
773
|
+
const results = await freeformStatus(siteRoot, config, locale)
|
|
723
774
|
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
recordedDate: s.recordedDate
|
|
738
|
-
})),
|
|
739
|
-
orphaned: orphaned.map((o) => ({
|
|
740
|
-
path: o.path,
|
|
741
|
-
recordedDate: o.recordedDate
|
|
742
|
-
})),
|
|
743
|
-
notChecked: notChecked.map((path) => ({ path }))
|
|
775
|
+
if (!results) {
|
|
776
|
+
if (outputJson) {
|
|
777
|
+
log(
|
|
778
|
+
JSON.stringify(
|
|
779
|
+
{ error: 'No free-form translations found', locales: {} },
|
|
780
|
+
null,
|
|
781
|
+
2
|
|
782
|
+
)
|
|
783
|
+
)
|
|
784
|
+
} else {
|
|
785
|
+
log(
|
|
786
|
+
`${colors.dim}No free-form translations found in ${config.localesDir}/freeform/.${colors.reset}`
|
|
787
|
+
)
|
|
744
788
|
}
|
|
789
|
+
return
|
|
745
790
|
}
|
|
746
791
|
|
|
747
792
|
if (outputJson) {
|
|
@@ -749,6 +794,8 @@ async function runStatusFreeform(siteRoot, config, locale, options = {}) {
|
|
|
749
794
|
return
|
|
750
795
|
}
|
|
751
796
|
|
|
797
|
+
log(`\n${colors.cyan}Free-form Translation Status${colors.reset}\n`)
|
|
798
|
+
|
|
752
799
|
// Human-readable output
|
|
753
800
|
for (const [loc, info] of Object.entries(results)) {
|
|
754
801
|
log(`${colors.bright}${loc}:${colors.reset}`)
|
package/src/framework-index.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-10-
|
|
3
|
+
"generatedAt": "2026-10-27T04:18:53.629Z",
|
|
4
4
|
"packages": {
|
|
5
5
|
"@uniweb/api": {
|
|
6
6
|
"version": "1.6.4"
|
|
7
7
|
},
|
|
8
8
|
"@uniweb/build": {
|
|
9
|
-
"version": "1.10.
|
|
9
|
+
"version": "1.10.8"
|
|
10
10
|
},
|
|
11
11
|
"@uniweb/content-reader": {
|
|
12
12
|
"version": "1.5.7"
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"version": "1.2.3"
|
|
37
37
|
},
|
|
38
38
|
"@uniweb/runtime": {
|
|
39
|
-
"version": "1.2.
|
|
39
|
+
"version": "1.2.6"
|
|
40
40
|
},
|
|
41
41
|
"@uniweb/scene": {
|
|
42
42
|
"version": "1.0.1"
|
package/src/index.js
CHANGED
|
@@ -2089,10 +2089,19 @@ ${colors.bright}Options:${colors.reset}
|
|
|
2089
2089
|
i18n: `
|
|
2090
2090
|
${colors.cyan}${colors.bright}uniweb i18n${colors.reset} ${colors.dim}— Internationalization workflow${colors.reset}
|
|
2091
2091
|
|
|
2092
|
-
${colors.bright}
|
|
2093
|
-
i18n extract Extract translatable strings to manifest
|
|
2094
|
-
i18n
|
|
2095
|
-
i18n status
|
|
2092
|
+
${colors.bright}Strings, translated by hash:${colors.reset}
|
|
2093
|
+
i18n extract Extract translatable strings to the manifest (alias: sync)
|
|
2094
|
+
i18n generate Start locale files from the manifest (alias: init)
|
|
2095
|
+
i18n status What is left, per language: strings, and free-form translations
|
|
2096
|
+
i18n audit Find stale and missing string translations
|
|
2097
|
+
|
|
2098
|
+
${colors.bright}Free-form, a whole section translated:${colors.reset}
|
|
2099
|
+
i18n status --freeform Each free-form translation, and what to do
|
|
2100
|
+
i18n init-freeform <locale> <path> <id> Start one from its source section
|
|
2101
|
+
i18n update-hash <locale> --all-stale Mark reviewed translations current
|
|
2102
|
+
i18n move <old-path> <new-path> Move translations when pages move
|
|
2103
|
+
i18n rename <path> <old-id> <new-id> Rename one when its section is renamed
|
|
2104
|
+
i18n prune --freeform Remove orphaned translations
|
|
2096
2105
|
`,
|
|
2097
2106
|
inspect: `
|
|
2098
2107
|
${colors.cyan}${colors.bright}uniweb inspect${colors.reset} ${colors.dim}— Inspect parsed content shape${colors.reset}
|