@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.
- package/package.json +1 -1
- package/src/website.js +142 -18
package/package.json
CHANGED
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
|
-
|
|
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 (
|
|
686
|
+
if (translatedRoute === '/') {
|
|
581
687
|
return `/${localeCode}/`
|
|
582
688
|
}
|
|
583
689
|
|
|
584
|
-
return `/${localeCode}${
|
|
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
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
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
|
-
|
|
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
|
/**
|