@uniweb/build 0.4.4 → 0.4.5

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/build",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
4
4
  "description": "Build tooling for the Uniweb Component Web Platform",
5
5
  "type": "module",
6
6
  "exports": {
@@ -50,8 +50,8 @@
50
50
  "sharp": "^0.33.2"
51
51
  },
52
52
  "optionalDependencies": {
53
- "@uniweb/runtime": "0.5.7",
54
- "@uniweb/content-reader": "1.1.2"
53
+ "@uniweb/content-reader": "1.1.2",
54
+ "@uniweb/runtime": "0.5.7"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
@@ -60,7 +60,7 @@
60
60
  "@tailwindcss/vite": "^4.0.0",
61
61
  "@vitejs/plugin-react": "^4.0.0 || ^5.0.0",
62
62
  "vite-plugin-svgr": "^4.0.0",
63
- "@uniweb/core": "0.3.7"
63
+ "@uniweb/core": "0.3.8"
64
64
  },
65
65
  "peerDependenciesMeta": {
66
66
  "vite": {
@@ -178,7 +178,8 @@ function addUnit(units, source, field, context) {
178
178
  const hash = computeHash(source)
179
179
 
180
180
  if (units[hash]) {
181
- const existingContexts = units[hash].contexts
181
+ const existingContexts = units[hash].contexts || []
182
+ units[hash].contexts = existingContexts
182
183
  const contextKey = `${context.collection}:${context.item}`
183
184
  const exists = existingContexts.some(
184
185
  c => `${c.collection}:${c.item}` === contextKey
@@ -27,6 +27,27 @@ export function extractTranslatableContent(siteContent) {
27
27
  }
28
28
  }
29
29
 
30
+ // Extract from 404 page (stored as top-level notFound)
31
+ if (siteContent.notFound) {
32
+ const notFoundPage = siteContent.notFound
33
+ const pageRoute = notFoundPage.route || '/404'
34
+ extractFromPageMeta(notFoundPage, pageRoute, units)
35
+ for (const section of notFoundPage.sections || []) {
36
+ extractFromSection(section, pageRoute, units)
37
+ }
38
+ }
39
+
40
+ // Extract from shared layout pages (header, footer, left, right panels)
41
+ for (const layoutKey of ['header', 'footer', 'left', 'right']) {
42
+ const layoutPage = siteContent[layoutKey]
43
+ if (layoutPage?.sections) {
44
+ const pageRoute = layoutPage.route || `/@${layoutKey}`
45
+ for (const section of layoutPage.sections) {
46
+ extractFromSection(section, pageRoute, units)
47
+ }
48
+ }
49
+ }
50
+
30
51
  return {
31
52
  version: '1.0',
32
53
  defaultLocale: siteContent.config?.defaultLanguage || 'en',
@@ -50,6 +71,11 @@ function extractFromPageMeta(page, pageRoute, units) {
50
71
  addUnit(units, page.title, 'page.title', context)
51
72
  }
52
73
 
74
+ // Page label (short navigation label, distinct from title)
75
+ if (page.label && typeof page.label === 'string') {
76
+ addUnit(units, page.label, 'page.label', context)
77
+ }
78
+
53
79
  // Page description
54
80
  if (page.description && typeof page.description === 'string') {
55
81
  addUnit(units, page.description, 'page.description', context)
package/src/i18n/merge.js CHANGED
@@ -67,6 +67,15 @@ function mergeTranslationsSync(siteContent, translations, fallbackToSource) {
67
67
  }
68
68
  }
69
69
 
70
+ // Translate 404 page (stored as top-level notFound)
71
+ if (translated.notFound) {
72
+ const pageRoute = translated.notFound.route || '/404'
73
+ translatePageMeta(translated.notFound, pageRoute, translations, fallbackToSource)
74
+ for (const section of translated.notFound.sections || []) {
75
+ translateSectionSync(section, pageRoute, translations, fallbackToSource)
76
+ }
77
+ }
78
+
70
79
  return translated
71
80
  }
72
81
 
@@ -95,6 +104,19 @@ async function mergeTranslationsAsync(siteContent, translations, options) {
95
104
  }
96
105
  }
97
106
 
107
+ // Translate 404 page (stored as top-level notFound)
108
+ if (translated.notFound) {
109
+ const pageRoute = translated.notFound.route || '/404'
110
+ translatePageMeta(translated.notFound, pageRoute, translations, fallbackToSource)
111
+ for (const section of translated.notFound.sections || []) {
112
+ await translateSectionAsync(section, translated.notFound, translations, {
113
+ fallbackToSource,
114
+ locale,
115
+ localesDir
116
+ })
117
+ }
118
+ }
119
+
98
120
  return translated
99
121
  }
100
122
 
@@ -109,6 +131,11 @@ function translatePageMeta(page, pageRoute, translations, fallbackToSource) {
109
131
  page.title = lookupTranslation(page.title, context, translations, fallbackToSource)
110
132
  }
111
133
 
134
+ // Translate label (short navigation label)
135
+ if (page.label && typeof page.label === 'string') {
136
+ page.label = lookupTranslation(page.label, context, translations, fallbackToSource)
137
+ }
138
+
112
139
  // Translate description
113
140
  if (page.description && typeof page.description === 'string') {
114
141
  page.description = lookupTranslation(page.description, context, translations, fallbackToSource)
package/src/i18n/sync.js CHANGED
@@ -94,10 +94,12 @@ export function syncManifests(previous, current) {
94
94
  * Check if two context arrays are equal
95
95
  */
96
96
  function contextsEqual(contexts1, contexts2) {
97
- if (contexts1.length !== contexts2.length) return false
97
+ const c1 = contexts1 || []
98
+ const c2 = contexts2 || []
99
+ if (c1.length !== c2.length) return false
98
100
 
99
- const set1 = new Set(contexts1.map(c => `${c.page}:${c.section}`))
100
- const set2 = new Set(contexts2.map(c => `${c.page}:${c.section}`))
101
+ const set1 = new Set(c1.map(c => `${c.page || c.collection}:${c.section || c.item}`))
102
+ const set2 = new Set(c2.map(c => `${c.page || c.collection}:${c.section || c.item}`))
101
103
 
102
104
  if (set1.size !== set2.size) return false
103
105
  for (const key of set1) {
@@ -115,8 +117,9 @@ function findMatchingContext(currentContexts, previousUnits) {
115
117
  const contextKey = `${context.page}:${context.section}`
116
118
 
117
119
  for (const [hash, unit] of Object.entries(previousUnits)) {
118
- const hasContext = unit.contexts.some(
119
- c => `${c.page}:${c.section}` === contextKey
120
+ const unitContexts = unit.contexts || []
121
+ const hasContext = unitContexts.some(
122
+ c => `${c.page || c.collection}:${c.section || c.item}` === contextKey
120
123
  )
121
124
  if (hasContext) {
122
125
  return { hash, source: unit.source, contexts: unit.contexts }
package/src/prerender.js CHANGED
@@ -570,7 +570,9 @@ export async function prerenderSite(siteDir, options = {}) {
570
570
 
571
571
  for (const page of pages) {
572
572
  // Build the output route with locale prefix
573
- const outputRoute = routePrefix + page.route
573
+ // For non-default locales, translate route slugs (e.g., /about → /acerca-de)
574
+ const translatedPageRoute = isDefault ? page.route : website.translateRoute(page.route, locale)
575
+ const outputRoute = routePrefix + translatedPageRoute
574
576
 
575
577
  onProgress(`Rendering ${outputRoute}...`)
576
578
 
@@ -117,10 +117,28 @@ async function processDevSectionFetches(sections, cascadedData, fetchOptions) {
117
117
  import { generateSearchIndex, isSearchEnabled, getSearchIndexFilename } from '../search/index.js'
118
118
  import { mergeTranslations } from '../i18n/merge.js'
119
119
 
120
+ /**
121
+ * Translate a canonical route for a given locale using route translations config
122
+ * Supports exact and prefix matching (e.g., /blog → /noticias also applies to /blog/post)
123
+ */
124
+ function applyRouteTranslation(route, locale, routeTranslations) {
125
+ const localeMap = routeTranslations?.[locale]
126
+ if (!localeMap) return route
127
+ // Exact match
128
+ if (localeMap[route]) return localeMap[route]
129
+ // Prefix match
130
+ for (const [canonical, translated] of Object.entries(localeMap)) {
131
+ if (route.startsWith(canonical + '/')) {
132
+ return translated + route.slice(canonical.length)
133
+ }
134
+ }
135
+ return route
136
+ }
137
+
120
138
  /**
121
139
  * Generate sitemap.xml content
122
140
  */
123
- function generateSitemap(pages, baseUrl, locales = []) {
141
+ function generateSitemap(pages, baseUrl, locales = [], routeTranslations = {}) {
124
142
  const urls = []
125
143
 
126
144
  for (const page of pages) {
@@ -137,7 +155,13 @@ function generateSitemap(pages, baseUrl, locales = []) {
137
155
  // Add hreflang entries for multi-locale sites
138
156
  if (locales.length > 1) {
139
157
  for (const locale of locales) {
140
- const localeLoc = locale.default ? loc : `${baseUrl}/${locale.code}${page.route === '/' ? '' : page.route}`
158
+ let localeLoc
159
+ if (locale.default) {
160
+ localeLoc = loc
161
+ } else {
162
+ const translatedRoute = page.route === '/' ? '' : applyRouteTranslation(page.route, locale.code, routeTranslations)
163
+ localeLoc = `${baseUrl}/${locale.code}${translatedRoute}`
164
+ }
141
165
  urlEntry += `\n <xhtml:link rel="alternate" hreflang="${locale.code}" href="${escapeXml(localeLoc)}" />`
142
166
  }
143
167
  // Add x-default pointing to default locale
@@ -664,7 +688,7 @@ export function siteContentPlugin(options = {}) {
664
688
  // Serve sitemap.xml in dev mode
665
689
  if (req.url === '/sitemap.xml' && seoEnabled && siteContent?.pages) {
666
690
  res.setHeader('Content-Type', 'application/xml')
667
- res.end(generateSitemap(siteContent.pages, seoOptions.baseUrl, seoOptions.locales))
691
+ res.end(generateSitemap(siteContent.pages, seoOptions.baseUrl, seoOptions.locales, siteContent.config?.i18n?.routeTranslations))
668
692
  return
669
693
  }
670
694
 
@@ -851,7 +875,7 @@ export function siteContentPlugin(options = {}) {
851
875
  // Generate SEO files if enabled
852
876
  if (seoEnabled && finalContent?.pages) {
853
877
  // Generate sitemap.xml
854
- const sitemap = generateSitemap(finalContent.pages, seoOptions.baseUrl, seoOptions.locales)
878
+ const sitemap = generateSitemap(finalContent.pages, seoOptions.baseUrl, seoOptions.locales, finalContent.config?.i18n?.routeTranslations)
855
879
  this.emitFile({
856
880
  type: 'asset',
857
881
  fileName: 'sitemap.xml',