uniweb 1.21.9 → 1.21.10

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uniweb",
3
- "version": "1.21.9",
3
+ "version": "1.21.10",
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/semantic-parser": "~1.13.0",
47
- "@uniweb/schemas": "~1.6.5"
46
+ "@uniweb/schemas": "~1.6.5",
47
+ "@uniweb/semantic-parser": "~1.13.0"
48
48
  },
49
49
  "peerDependencies": {
50
- "@uniweb/build": "~1.10.7",
51
- "@uniweb/content-reader": "~1.5.7",
52
- "@uniweb/semantic-parser": "~1.13.0"
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": {
@@ -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
- * Status --freeform mode - show free-form translation status
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 runStatusFreeform(siteRoot, config, locale, options = {}) {
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
- if (!existsSync(freeformPath)) {
664
- if (outputJson) {
665
- log(
666
- JSON.stringify(
667
- { error: 'No free-form translations found', locales: {} },
668
- null,
669
- 2
670
- )
671
- )
672
- } else {
673
- log(
674
- `${colors.dim}No free-form translations found in ${config.localesDir}/freeform/.${colors.reset}`
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
- if (!outputJson) {
681
- log(`\n${colors.cyan}Free-form Translation Status${colors.reset}\n`)
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
- // Find all locales
701
- const entries = await readdir(freeformPath, { withFileTypes: true })
702
- const locales = entries.filter((e) => e.isDirectory()).map((e) => e.name)
703
- const localesToCheck = locale ? [locale] : locales
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
- const results = {}
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
- for (const loc of localesToCheck) {
708
- const localeDir = join(freeformPath, loc)
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
- // ⭐ Up to date only where a source was found to compare with. A translation with none
725
- // — a record's, whose record the site content does not hold — is NOT CHECKED, and said
726
- // so. ⛔ Until 2026-10-17 it was counted up to date, like every page translation of a
727
- // prerendered site while this read the build's split content.
728
- const judged = (p) => stale.some((s) => s.path === p) || orphaned.some((o) => o.path === p)
729
- const upToDate = allPaths.filter((p) => !judged(p) && sourceHashes[p])
730
- const notChecked = allPaths.filter((p) => !judged(p) && !sourceHashes[p])
731
-
732
- results[loc] = {
733
- total: allPaths.length,
734
- upToDate: upToDate.length,
735
- stale: stale.map((s) => ({
736
- path: s.path,
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}`)
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-10-26T23:21:20.295Z",
3
+ "generatedAt": "2026-10-27T01:25:35.143Z",
4
4
  "packages": {
5
5
  "@uniweb/api": {
6
6
  "version": "1.6.4"
7
7
  },
8
8
  "@uniweb/build": {
9
- "version": "1.10.7"
9
+ "version": "1.10.8"
10
10
  },
11
11
  "@uniweb/content-reader": {
12
12
  "version": "1.5.7"
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}Subcommands:${colors.reset}
2093
- i18n extract Extract translatable strings to manifest
2094
- i18n sync Update manifest with content changes
2095
- i18n status Show translation coverage per locale
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}