@uniweb/core 0.3.7 → 0.3.8
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 +125 -18
package/package.json
CHANGED
package/src/website.js
CHANGED
|
@@ -73,6 +73,9 @@ 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
|
+
|
|
76
79
|
// Versioned scopes: route → { versions, latestId }
|
|
77
80
|
// Scopes are routes where versioning starts (e.g., '/docs')
|
|
78
81
|
this.versionedScopes = versionedScopes
|
|
@@ -121,6 +124,73 @@ export default class Website {
|
|
|
121
124
|
}))
|
|
122
125
|
}
|
|
123
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Build forward and reverse route translation maps per locale
|
|
129
|
+
* @private
|
|
130
|
+
*/
|
|
131
|
+
_buildRouteTranslations(config) {
|
|
132
|
+
const translations = config.i18n?.routeTranslations || {}
|
|
133
|
+
const result = {}
|
|
134
|
+
for (const [locale, routes] of Object.entries(translations)) {
|
|
135
|
+
const forward = new Map() // canonical → translated
|
|
136
|
+
const reverse = new Map() // translated → canonical
|
|
137
|
+
for (const [canonical, translated] of Object.entries(routes)) {
|
|
138
|
+
forward.set(canonical, translated)
|
|
139
|
+
reverse.set(translated, canonical)
|
|
140
|
+
}
|
|
141
|
+
result[locale] = { forward, reverse }
|
|
142
|
+
}
|
|
143
|
+
return result
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Translate a canonical route to a locale-specific display route
|
|
148
|
+
* Supports exact match and prefix match (e.g., /blog → /noticias also applies to /blog/my-post)
|
|
149
|
+
*
|
|
150
|
+
* @param {string} canonicalRoute - Internal route (e.g., '/about')
|
|
151
|
+
* @param {string} [locale] - Target locale (defaults to active locale)
|
|
152
|
+
* @returns {string} Translated route or original if no translation exists
|
|
153
|
+
*/
|
|
154
|
+
translateRoute(canonicalRoute, locale = this.activeLocale) {
|
|
155
|
+
if (!locale || locale === this.defaultLocale) return canonicalRoute
|
|
156
|
+
const entry = this._routeTranslations[locale]
|
|
157
|
+
if (!entry) return canonicalRoute
|
|
158
|
+
// Exact match
|
|
159
|
+
const translated = entry.forward.get(canonicalRoute)
|
|
160
|
+
if (translated) return translated
|
|
161
|
+
// Prefix match (e.g., /blog matches /blog/my-post → /noticias/my-post)
|
|
162
|
+
for (const [canonical, trans] of entry.forward) {
|
|
163
|
+
if (canonicalRoute.startsWith(canonical + '/')) {
|
|
164
|
+
return trans + canonicalRoute.slice(canonical.length)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return canonicalRoute
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Reverse-translate a display route back to the canonical route
|
|
172
|
+
* Used when resolving incoming URLs to find the matching page
|
|
173
|
+
*
|
|
174
|
+
* @param {string} displayRoute - Display route (e.g., '/acerca-de')
|
|
175
|
+
* @param {string} [locale] - Source locale (defaults to active locale)
|
|
176
|
+
* @returns {string} Canonical route or original if no translation exists
|
|
177
|
+
*/
|
|
178
|
+
reverseTranslateRoute(displayRoute, locale = this.activeLocale) {
|
|
179
|
+
if (!locale || locale === this.defaultLocale) return displayRoute
|
|
180
|
+
const entry = this._routeTranslations[locale]
|
|
181
|
+
if (!entry) return displayRoute
|
|
182
|
+
// Exact match
|
|
183
|
+
const canonical = entry.reverse.get(displayRoute)
|
|
184
|
+
if (canonical) return canonical
|
|
185
|
+
// Prefix match
|
|
186
|
+
for (const [trans, canon] of entry.reverse) {
|
|
187
|
+
if (displayRoute.startsWith(trans + '/')) {
|
|
188
|
+
return canon + displayRoute.slice(trans.length)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return displayRoute
|
|
192
|
+
}
|
|
193
|
+
|
|
124
194
|
/**
|
|
125
195
|
* Build parent-child relationships between pages based on route structure
|
|
126
196
|
* E.g., /getting-started/installation is a child of /getting-started
|
|
@@ -185,6 +255,9 @@ export default class Website {
|
|
|
185
255
|
}
|
|
186
256
|
}
|
|
187
257
|
|
|
258
|
+
// Reverse-translate display route to canonical (e.g., '/acerca-de' → '/about')
|
|
259
|
+
stripped = this.reverseTranslateRoute(stripped)
|
|
260
|
+
|
|
188
261
|
// Normalize trailing slashes for consistent matching
|
|
189
262
|
// '/about/' and '/about' should match the same page
|
|
190
263
|
const normalizedRoute = stripped === '/' ? '/' : stripped.replace(/\/$/, '')
|
|
@@ -569,19 +642,35 @@ export default class Website {
|
|
|
569
642
|
* @returns {string}
|
|
570
643
|
*/
|
|
571
644
|
getLocaleUrl(localeCode, route = null) {
|
|
572
|
-
|
|
645
|
+
let targetRoute = route || this.activePage?.route || '/'
|
|
646
|
+
|
|
647
|
+
// Strip current locale prefix if present in route
|
|
648
|
+
if (this.activeLocale && this.activeLocale !== this.defaultLocale) {
|
|
649
|
+
const prefix = `/${this.activeLocale}`
|
|
650
|
+
if (targetRoute === prefix || targetRoute === `${prefix}/`) {
|
|
651
|
+
targetRoute = '/'
|
|
652
|
+
} else if (targetRoute.startsWith(`${prefix}/`)) {
|
|
653
|
+
targetRoute = targetRoute.slice(prefix.length)
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Reverse-translate from current locale to canonical route
|
|
658
|
+
targetRoute = this.reverseTranslateRoute(targetRoute)
|
|
573
659
|
|
|
574
|
-
// Default locale uses root path (no prefix)
|
|
660
|
+
// Default locale uses root path (no prefix), no translation needed
|
|
575
661
|
if (localeCode === this.defaultLocale) {
|
|
576
662
|
return targetRoute
|
|
577
663
|
}
|
|
578
664
|
|
|
665
|
+
// Translate canonical route to target locale's display route
|
|
666
|
+
const translatedRoute = this.translateRoute(targetRoute, localeCode)
|
|
667
|
+
|
|
579
668
|
// Other locales use /locale/ prefix
|
|
580
|
-
if (
|
|
669
|
+
if (translatedRoute === '/') {
|
|
581
670
|
return `/${localeCode}/`
|
|
582
671
|
}
|
|
583
672
|
|
|
584
|
-
return `/${localeCode}${
|
|
673
|
+
return `/${localeCode}${translatedRoute}`
|
|
585
674
|
}
|
|
586
675
|
|
|
587
676
|
/**
|
|
@@ -776,19 +865,23 @@ export default class Website {
|
|
|
776
865
|
// Already sorted by order in constructor, so no need to re-sort
|
|
777
866
|
|
|
778
867
|
// Build page info objects
|
|
779
|
-
const buildPageInfo = (page) =>
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
:
|
|
791
|
-
|
|
868
|
+
const buildPageInfo = (page) => {
|
|
869
|
+
const navRoute = page.getNavRoute()
|
|
870
|
+
return {
|
|
871
|
+
id: page.id,
|
|
872
|
+
route: navRoute, // Use canonical nav route (e.g., '/' for index pages)
|
|
873
|
+
navigableRoute: page.getNavigableRoute(), // First route with content (for links)
|
|
874
|
+
translatedRoute: this.translateRoute(navRoute), // Locale-specific display route
|
|
875
|
+
title: page.title,
|
|
876
|
+
label: page.getLabel(),
|
|
877
|
+
description: page.description,
|
|
878
|
+
hasContent: page.hasContent(),
|
|
879
|
+
version: page.version || null, // Version metadata for filtering by version
|
|
880
|
+
children: nested && page.hasChildren()
|
|
881
|
+
? page.children.filter(isPageVisible).map(buildPageInfo)
|
|
882
|
+
: []
|
|
883
|
+
}
|
|
884
|
+
}
|
|
792
885
|
|
|
793
886
|
return filteredPages.map(buildPageInfo)
|
|
794
887
|
}
|
|
@@ -872,7 +965,21 @@ export default class Website {
|
|
|
872
965
|
* website.normalizeRoute('/') // ''
|
|
873
966
|
*/
|
|
874
967
|
normalizeRoute(route) {
|
|
875
|
-
|
|
968
|
+
let normalized = (route || '').replace(/^\/+/, '').replace(/\/+$/, '')
|
|
969
|
+
// Strip locale prefix so '/es/about' normalizes to 'about'
|
|
970
|
+
if (this.activeLocale && this.activeLocale !== this.defaultLocale) {
|
|
971
|
+
const prefix = this.activeLocale
|
|
972
|
+
if (normalized === prefix) {
|
|
973
|
+
normalized = ''
|
|
974
|
+
} else if (normalized.startsWith(`${prefix}/`)) {
|
|
975
|
+
normalized = normalized.slice(prefix.length + 1)
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
// Reverse-translate display route to canonical (e.g., 'acerca-de' → 'about')
|
|
979
|
+
const withSlash = '/' + normalized
|
|
980
|
+
const reversed = this.reverseTranslateRoute(withSlash)
|
|
981
|
+
normalized = reversed.replace(/^\//, '')
|
|
982
|
+
return normalized
|
|
876
983
|
}
|
|
877
984
|
|
|
878
985
|
/**
|