@uniweb/core 0.3.7 → 0.3.9

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/website.js +142 -18
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/core",
3
- "version": "0.3.7",
3
+ "version": "0.3.9",
4
4
  "description": "Core classes for the Uniweb platform - Uniweb, Website, Page, Block",
5
5
  "type": "module",
6
6
  "exports": {
package/src/website.js CHANGED
@@ -73,6 +73,12 @@ export default class Website {
73
73
  value: l.code
74
74
  }))
75
75
 
76
+ // Route translations: locale → { forward, reverse } maps
77
+ this._routeTranslations = this._buildRouteTranslations(config)
78
+
79
+ // Deployment base path (set by runtime via setBasePath())
80
+ this.basePath = ''
81
+
76
82
  // Versioned scopes: route → { versions, latestId }
77
83
  // Scopes are routes where versioning starts (e.g., '/docs')
78
84
  this.versionedScopes = versionedScopes
@@ -121,6 +127,73 @@ export default class Website {
121
127
  }))
122
128
  }
123
129
 
130
+ /**
131
+ * Build forward and reverse route translation maps per locale
132
+ * @private
133
+ */
134
+ _buildRouteTranslations(config) {
135
+ const translations = config.i18n?.routeTranslations || {}
136
+ const result = {}
137
+ for (const [locale, routes] of Object.entries(translations)) {
138
+ const forward = new Map() // canonical → translated
139
+ const reverse = new Map() // translated → canonical
140
+ for (const [canonical, translated] of Object.entries(routes)) {
141
+ forward.set(canonical, translated)
142
+ reverse.set(translated, canonical)
143
+ }
144
+ result[locale] = { forward, reverse }
145
+ }
146
+ return result
147
+ }
148
+
149
+ /**
150
+ * Translate a canonical route to a locale-specific display route
151
+ * Supports exact match and prefix match (e.g., /blog → /noticias also applies to /blog/my-post)
152
+ *
153
+ * @param {string} canonicalRoute - Internal route (e.g., '/about')
154
+ * @param {string} [locale] - Target locale (defaults to active locale)
155
+ * @returns {string} Translated route or original if no translation exists
156
+ */
157
+ translateRoute(canonicalRoute, locale = this.activeLocale) {
158
+ if (!locale || locale === this.defaultLocale) return canonicalRoute
159
+ const entry = this._routeTranslations[locale]
160
+ if (!entry) return canonicalRoute
161
+ // Exact match
162
+ const translated = entry.forward.get(canonicalRoute)
163
+ if (translated) return translated
164
+ // Prefix match (e.g., /blog matches /blog/my-post → /noticias/my-post)
165
+ for (const [canonical, trans] of entry.forward) {
166
+ if (canonicalRoute.startsWith(canonical + '/')) {
167
+ return trans + canonicalRoute.slice(canonical.length)
168
+ }
169
+ }
170
+ return canonicalRoute
171
+ }
172
+
173
+ /**
174
+ * Reverse-translate a display route back to the canonical route
175
+ * Used when resolving incoming URLs to find the matching page
176
+ *
177
+ * @param {string} displayRoute - Display route (e.g., '/acerca-de')
178
+ * @param {string} [locale] - Source locale (defaults to active locale)
179
+ * @returns {string} Canonical route or original if no translation exists
180
+ */
181
+ reverseTranslateRoute(displayRoute, locale = this.activeLocale) {
182
+ if (!locale || locale === this.defaultLocale) return displayRoute
183
+ const entry = this._routeTranslations[locale]
184
+ if (!entry) return displayRoute
185
+ // Exact match
186
+ const canonical = entry.reverse.get(displayRoute)
187
+ if (canonical) return canonical
188
+ // Prefix match
189
+ for (const [trans, canon] of entry.reverse) {
190
+ if (displayRoute.startsWith(trans + '/')) {
191
+ return canon + displayRoute.slice(trans.length)
192
+ }
193
+ }
194
+ return displayRoute
195
+ }
196
+
124
197
  /**
125
198
  * Build parent-child relationships between pages based on route structure
126
199
  * E.g., /getting-started/installation is a child of /getting-started
@@ -185,6 +258,9 @@ export default class Website {
185
258
  }
186
259
  }
187
260
 
261
+ // Reverse-translate display route to canonical (e.g., '/acerca-de' → '/about')
262
+ stripped = this.reverseTranslateRoute(stripped)
263
+
188
264
  // Normalize trailing slashes for consistent matching
189
265
  // '/about/' and '/about' should match the same page
190
266
  const normalizedRoute = stripped === '/' ? '/' : stripped.replace(/\/$/, '')
@@ -432,6 +508,20 @@ export default class Website {
432
508
  }
433
509
  }
434
510
 
511
+ /**
512
+ * Set the deployment base path
513
+ * Called by runtime during initialization from Vite's BASE_URL
514
+ *
515
+ * @param {string} path - The base path (e.g., '/templates/international')
516
+ */
517
+ setBasePath(path) {
518
+ if (!path || path === '/') {
519
+ this.basePath = ''
520
+ } else {
521
+ this.basePath = path.endsWith('/') ? path.slice(0, -1) : path
522
+ }
523
+ }
524
+
435
525
  /**
436
526
  * Get remote layout component from foundation config
437
527
  */
@@ -569,19 +659,35 @@ export default class Website {
569
659
  * @returns {string}
570
660
  */
571
661
  getLocaleUrl(localeCode, route = null) {
572
- const targetRoute = route || this.activePage?.route || '/'
662
+ let targetRoute = route || this.activePage?.route || '/'
663
+
664
+ // Strip current locale prefix if present in route
665
+ if (this.activeLocale && this.activeLocale !== this.defaultLocale) {
666
+ const prefix = `/${this.activeLocale}`
667
+ if (targetRoute === prefix || targetRoute === `${prefix}/`) {
668
+ targetRoute = '/'
669
+ } else if (targetRoute.startsWith(`${prefix}/`)) {
670
+ targetRoute = targetRoute.slice(prefix.length)
671
+ }
672
+ }
673
+
674
+ // Reverse-translate from current locale to canonical route
675
+ targetRoute = this.reverseTranslateRoute(targetRoute)
573
676
 
574
- // Default locale uses root path (no prefix)
677
+ // Default locale uses root path (no prefix), no translation needed
575
678
  if (localeCode === this.defaultLocale) {
576
679
  return targetRoute
577
680
  }
578
681
 
682
+ // Translate canonical route to target locale's display route
683
+ const translatedRoute = this.translateRoute(targetRoute, localeCode)
684
+
579
685
  // Other locales use /locale/ prefix
580
- if (targetRoute === '/') {
686
+ if (translatedRoute === '/') {
581
687
  return `/${localeCode}/`
582
688
  }
583
689
 
584
- return `/${localeCode}${targetRoute}`
690
+ return `/${localeCode}${translatedRoute}`
585
691
  }
586
692
 
587
693
  /**
@@ -776,19 +882,23 @@ export default class Website {
776
882
  // Already sorted by order in constructor, so no need to re-sort
777
883
 
778
884
  // Build page info objects
779
- const buildPageInfo = (page) => ({
780
- id: page.id,
781
- route: page.getNavRoute(), // Use canonical nav route (e.g., '/' for index pages)
782
- navigableRoute: page.getNavigableRoute(), // First route with content (for links)
783
- title: page.title,
784
- label: page.getLabel(),
785
- description: page.description,
786
- hasContent: page.hasContent(),
787
- version: page.version || null, // Version metadata for filtering by version
788
- children: nested && page.hasChildren()
789
- ? page.children.filter(isPageVisible).map(buildPageInfo)
790
- : []
791
- })
885
+ const buildPageInfo = (page) => {
886
+ const navRoute = page.getNavRoute()
887
+ return {
888
+ id: page.id,
889
+ route: navRoute, // Use canonical nav route (e.g., '/' for index pages)
890
+ navigableRoute: page.getNavigableRoute(), // First route with content (for links)
891
+ translatedRoute: this.translateRoute(navRoute), // Locale-specific display route
892
+ title: page.title,
893
+ label: page.getLabel(),
894
+ description: page.description,
895
+ hasContent: page.hasContent(),
896
+ version: page.version || null, // Version metadata for filtering by version
897
+ children: nested && page.hasChildren()
898
+ ? page.children.filter(isPageVisible).map(buildPageInfo)
899
+ : []
900
+ }
901
+ }
792
902
 
793
903
  return filteredPages.map(buildPageInfo)
794
904
  }
@@ -872,7 +982,21 @@ export default class Website {
872
982
  * website.normalizeRoute('/') // ''
873
983
  */
874
984
  normalizeRoute(route) {
875
- return (route || '').replace(/^\/+/, '').replace(/\/+$/, '')
985
+ let normalized = (route || '').replace(/^\/+/, '').replace(/\/+$/, '')
986
+ // Strip locale prefix so '/es/about' normalizes to 'about'
987
+ if (this.activeLocale && this.activeLocale !== this.defaultLocale) {
988
+ const prefix = this.activeLocale
989
+ if (normalized === prefix) {
990
+ normalized = ''
991
+ } else if (normalized.startsWith(`${prefix}/`)) {
992
+ normalized = normalized.slice(prefix.length + 1)
993
+ }
994
+ }
995
+ // Reverse-translate display route to canonical (e.g., 'acerca-de' → 'about')
996
+ const withSlash = '/' + normalized
997
+ const reversed = this.reverseTranslateRoute(withSlash)
998
+ normalized = reversed.replace(/^\//, '')
999
+ return normalized
876
1000
  }
877
1001
 
878
1002
  /**