@uniweb/core 0.3.6 → 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 +139 -19
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
|
|
@@ -172,9 +242,25 @@ export default class Website {
|
|
|
172
242
|
* @returns {Page|undefined}
|
|
173
243
|
*/
|
|
174
244
|
getPage(route) {
|
|
245
|
+
// Strip locale prefix if present (e.g., '/fr/about' → '/about')
|
|
246
|
+
// Pages are stored with non-prefixed routes; the locale is a URL concern,
|
|
247
|
+
// not a page identity concern.
|
|
248
|
+
let stripped = route
|
|
249
|
+
if (this.activeLocale && this.activeLocale !== this.defaultLocale) {
|
|
250
|
+
const prefix = `/${this.activeLocale}`
|
|
251
|
+
if (stripped === prefix || stripped === `${prefix}/`) {
|
|
252
|
+
stripped = '/'
|
|
253
|
+
} else if (stripped.startsWith(`${prefix}/`)) {
|
|
254
|
+
stripped = stripped.slice(prefix.length)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Reverse-translate display route to canonical (e.g., '/acerca-de' → '/about')
|
|
259
|
+
stripped = this.reverseTranslateRoute(stripped)
|
|
260
|
+
|
|
175
261
|
// Normalize trailing slashes for consistent matching
|
|
176
262
|
// '/about/' and '/about' should match the same page
|
|
177
|
-
const normalizedRoute =
|
|
263
|
+
const normalizedRoute = stripped === '/' ? '/' : stripped.replace(/\/$/, '')
|
|
178
264
|
|
|
179
265
|
// Priority 1: Exact match on actual route
|
|
180
266
|
const exactMatch = this.pages.find((page) => page.route === normalizedRoute)
|
|
@@ -556,19 +642,35 @@ export default class Website {
|
|
|
556
642
|
* @returns {string}
|
|
557
643
|
*/
|
|
558
644
|
getLocaleUrl(localeCode, route = null) {
|
|
559
|
-
|
|
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
|
+
}
|
|
560
656
|
|
|
561
|
-
//
|
|
657
|
+
// Reverse-translate from current locale to canonical route
|
|
658
|
+
targetRoute = this.reverseTranslateRoute(targetRoute)
|
|
659
|
+
|
|
660
|
+
// Default locale uses root path (no prefix), no translation needed
|
|
562
661
|
if (localeCode === this.defaultLocale) {
|
|
563
662
|
return targetRoute
|
|
564
663
|
}
|
|
565
664
|
|
|
665
|
+
// Translate canonical route to target locale's display route
|
|
666
|
+
const translatedRoute = this.translateRoute(targetRoute, localeCode)
|
|
667
|
+
|
|
566
668
|
// Other locales use /locale/ prefix
|
|
567
|
-
if (
|
|
669
|
+
if (translatedRoute === '/') {
|
|
568
670
|
return `/${localeCode}/`
|
|
569
671
|
}
|
|
570
672
|
|
|
571
|
-
return `/${localeCode}${
|
|
673
|
+
return `/${localeCode}${translatedRoute}`
|
|
572
674
|
}
|
|
573
675
|
|
|
574
676
|
/**
|
|
@@ -763,19 +865,23 @@ export default class Website {
|
|
|
763
865
|
// Already sorted by order in constructor, so no need to re-sort
|
|
764
866
|
|
|
765
867
|
// Build page info objects
|
|
766
|
-
const buildPageInfo = (page) =>
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
:
|
|
778
|
-
|
|
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
|
+
}
|
|
779
885
|
|
|
780
886
|
return filteredPages.map(buildPageInfo)
|
|
781
887
|
}
|
|
@@ -859,7 +965,21 @@ export default class Website {
|
|
|
859
965
|
* website.normalizeRoute('/') // ''
|
|
860
966
|
*/
|
|
861
967
|
normalizeRoute(route) {
|
|
862
|
-
|
|
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
|
|
863
983
|
}
|
|
864
984
|
|
|
865
985
|
/**
|