@uniweb/core 0.7.22 → 0.7.23
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 +5 -4
- package/src/index.js +7 -0
- package/src/locale-config.js +202 -0
- package/src/website.js +3 -2
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/core",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.23",
|
|
4
4
|
"description": "Core classes for the Uniweb platform - Uniweb, Website, Page, Block",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
|
-
".": "./src/index.js"
|
|
7
|
+
".": "./src/index.js",
|
|
8
|
+
"./locale-config": "./src/locale-config.js"
|
|
8
9
|
},
|
|
9
10
|
"files": [
|
|
10
11
|
"src"
|
|
@@ -30,8 +31,8 @@
|
|
|
30
31
|
"vitest": "^4.1.7"
|
|
31
32
|
},
|
|
32
33
|
"dependencies": {
|
|
33
|
-
"@uniweb/
|
|
34
|
-
"@uniweb/
|
|
34
|
+
"@uniweb/semantic-parser": "1.1.18",
|
|
35
|
+
"@uniweb/theming": "0.1.9"
|
|
35
36
|
},
|
|
36
37
|
"scripts": {
|
|
37
38
|
"test": "vitest run"
|
package/src/index.js
CHANGED
|
@@ -20,6 +20,13 @@ export { default as ObservableState } from './observable-state.js'
|
|
|
20
20
|
|
|
21
21
|
// Utilities
|
|
22
22
|
export { substitutePlaceholders } from './substitute-placeholders.js'
|
|
23
|
+
export {
|
|
24
|
+
normalizeLanguageList,
|
|
25
|
+
isWildcardLanguages,
|
|
26
|
+
resolveDefaultLocale,
|
|
27
|
+
resolvePublishableLocales,
|
|
28
|
+
validateLanguageConfig
|
|
29
|
+
} from './locale-config.js'
|
|
23
30
|
export { evaluate as evaluateWhere, match as matchWhere } from './where.js'
|
|
24
31
|
export { isRichSchema } from './schemas.js'
|
|
25
32
|
export { resolveStyle as resolveRequestStyle, listStyleNames as listRequestStyleNames } from './request-styles/index.js'
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared locale-config helpers — the ONE home for the language rules that
|
|
3
|
+
* build, sync, runtime, and the CLI all apply to a site's config.
|
|
4
|
+
*
|
|
5
|
+
* Contract (kb/framework/build/uwx-format.md → "Per-locale publish readiness"):
|
|
6
|
+
* - `languages` (site.yml) / `info.languages` (wire) — the DECLARED working
|
|
7
|
+
* set. A plain, strongly-validated string list.
|
|
8
|
+
* - `publishLanguages` (site.yml) / `info.publish_languages` (wire) — publish
|
|
9
|
+
* intent. Publishable = intersection with declared; absent field = all
|
|
10
|
+
* declared publishable; present-but-empty = nothing publishable; dangling
|
|
11
|
+
* codes (listed but not declared) are benign — warn, ignore in the
|
|
12
|
+
* intersection, round-trip verbatim.
|
|
13
|
+
* - Effective default locale = `defaultLanguage || languages[0] || 'en'` —
|
|
14
|
+
* one rule everywhere. (Historically half the call sites skipped the
|
|
15
|
+
* `languages[0]` step; this module exists so that can't drift again.)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Extract a locale code from a declared-language entry. The contract is
|
|
20
|
+
* strings-only; legacy `{ code, label }` objects are tolerated on read
|
|
21
|
+
* (they appeared in older configs and the runtime's buildLocalesList
|
|
22
|
+
* accepted them) but are never produced. The `'*'` wildcard marker
|
|
23
|
+
* (auto-discover from `locales/`) is not a locale code.
|
|
24
|
+
*
|
|
25
|
+
* @param {*} entry - Declared-language entry.
|
|
26
|
+
* @returns {string|null} The locale code, or null when unusable.
|
|
27
|
+
*/
|
|
28
|
+
function codeOf(entry) {
|
|
29
|
+
if (typeof entry === 'string' && entry.trim() && entry.trim() !== '*') return entry.trim()
|
|
30
|
+
if (entry && typeof entry === 'object' && typeof entry.code === 'string' && entry.code.trim()) {
|
|
31
|
+
return entry.code.trim()
|
|
32
|
+
}
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Whether `languages` uses the auto-discover wildcard (`'*'`, or an array
|
|
38
|
+
* containing it). The declared set is then filesystem-derived and unknown
|
|
39
|
+
* to these pure helpers.
|
|
40
|
+
*
|
|
41
|
+
* @param {*} value - The authored `languages` value.
|
|
42
|
+
* @returns {boolean}
|
|
43
|
+
*/
|
|
44
|
+
export function isWildcardLanguages(value) {
|
|
45
|
+
return value === '*' || (Array.isArray(value) && value.includes('*'))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Normalize a language list to validated string codes: invalid entries
|
|
50
|
+
* dropped, duplicates deduped, order preserved.
|
|
51
|
+
*
|
|
52
|
+
* @param {*} value - The authored list (anything; non-arrays yield []).
|
|
53
|
+
* @returns {string[]} Clean locale codes.
|
|
54
|
+
*/
|
|
55
|
+
export function normalizeLanguageList(value) {
|
|
56
|
+
if (!Array.isArray(value)) return []
|
|
57
|
+
const seen = new Set()
|
|
58
|
+
const codes = []
|
|
59
|
+
for (const entry of value) {
|
|
60
|
+
const code = codeOf(entry)
|
|
61
|
+
if (!code || seen.has(code)) continue
|
|
62
|
+
seen.add(code)
|
|
63
|
+
codes.push(code)
|
|
64
|
+
}
|
|
65
|
+
return codes
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The effective default locale: `defaultLanguage || languages[0] || 'en'`.
|
|
70
|
+
* Works on authored site.yml, built site-content config, and served payload
|
|
71
|
+
* config alike (all carry the same camelCase keys).
|
|
72
|
+
*
|
|
73
|
+
* @param {Object} [config] - Site config (`{ defaultLanguage?, languages? }`).
|
|
74
|
+
* @returns {string} The effective default locale code.
|
|
75
|
+
*/
|
|
76
|
+
export function resolveDefaultLocale(config = {}) {
|
|
77
|
+
if (typeof config?.defaultLanguage === 'string' && config.defaultLanguage.trim()) {
|
|
78
|
+
return config.defaultLanguage.trim()
|
|
79
|
+
}
|
|
80
|
+
return normalizeLanguageList(config?.languages)[0] || 'en'
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The publishable set: `publishLanguages ∩ languages`, in declared order.
|
|
85
|
+
*
|
|
86
|
+
* - Absent `publishLanguages` → all declared publishable (`explicit: false`).
|
|
87
|
+
* - Present (even empty) → the intersection (`explicit: true`); an authored
|
|
88
|
+
* empty list means nothing publishable — NOT treated as absent.
|
|
89
|
+
* - Dangling codes are returned for the caller to warn about; they are never
|
|
90
|
+
* silently pruned from the authored/stored list (the verbatim round-trip is
|
|
91
|
+
* what preserves publish intent across a remove + re-add in `languages`).
|
|
92
|
+
*
|
|
93
|
+
* With wildcard `languages` (`'*'`), the declared set is unknown here
|
|
94
|
+
* (filesystem-derived), so the intersection with "all" is the publish list
|
|
95
|
+
* itself and dangling codes cannot exist.
|
|
96
|
+
*
|
|
97
|
+
* @param {Object} [config] - Site config (`{ languages?, publishLanguages? }`).
|
|
98
|
+
* @returns {{ publishable: string[], dangling: string[], explicit: boolean }}
|
|
99
|
+
*/
|
|
100
|
+
export function resolvePublishableLocales(config = {}) {
|
|
101
|
+
const declared = normalizeLanguageList(config?.languages)
|
|
102
|
+
const raw = config?.publishLanguages
|
|
103
|
+
if (raw == null) return { publishable: declared, dangling: [], explicit: false }
|
|
104
|
+
const listed = normalizeLanguageList(raw)
|
|
105
|
+
if (isWildcardLanguages(config?.languages)) {
|
|
106
|
+
return { publishable: listed, dangling: [], explicit: true }
|
|
107
|
+
}
|
|
108
|
+
const declaredSet = new Set(declared)
|
|
109
|
+
const listedSet = new Set(listed)
|
|
110
|
+
return {
|
|
111
|
+
publishable: declared.filter((code) => listedSet.has(code)),
|
|
112
|
+
dangling: listed.filter((code) => !declaredSet.has(code)),
|
|
113
|
+
explicit: true
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Validate a site's language configuration against the contract. Pure — the
|
|
119
|
+
* caller decides how to surface results (build warnings, publish hard error).
|
|
120
|
+
*
|
|
121
|
+
* Errors (producers hard-error at build/deploy/push):
|
|
122
|
+
* - `nothing-publishable` — an explicit publish list intersects declared to ∅.
|
|
123
|
+
* - `default-not-publishable` — the effective default is excluded from the
|
|
124
|
+
* publishable set.
|
|
125
|
+
*
|
|
126
|
+
* Warnings:
|
|
127
|
+
* - `invalid-language-entry` / `invalid-publish-language-entry` — non-string
|
|
128
|
+
* entries (dropped by normalization).
|
|
129
|
+
* - `duplicate-language` — repeated codes (deduped).
|
|
130
|
+
* - `dangling-publish-language` — listed but not declared (ignored at
|
|
131
|
+
* publish, preserved in the file/wire).
|
|
132
|
+
*
|
|
133
|
+
* @param {Object} [config] - Site config.
|
|
134
|
+
* @returns {{ errors: {code: string, message: string}[],
|
|
135
|
+
* warnings: {code: string, message: string}[] }}
|
|
136
|
+
*/
|
|
137
|
+
export function validateLanguageConfig(config = {}) {
|
|
138
|
+
const errors = []
|
|
139
|
+
const warnings = []
|
|
140
|
+
|
|
141
|
+
const inspectList = (value, field, entryCode) => {
|
|
142
|
+
if (value == null) return
|
|
143
|
+
if (value === '*') return // auto-discover wildcard (languages only)
|
|
144
|
+
if (!Array.isArray(value)) {
|
|
145
|
+
warnings.push({
|
|
146
|
+
code: entryCode,
|
|
147
|
+
message: `${field} must be a list of locale codes (got ${typeof value}) — ignored`
|
|
148
|
+
})
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
const seen = new Set()
|
|
152
|
+
for (const entry of value) {
|
|
153
|
+
if (entry === '*') continue // auto-discover wildcard marker, not a code
|
|
154
|
+
const code = codeOf(entry)
|
|
155
|
+
if (!code) {
|
|
156
|
+
warnings.push({
|
|
157
|
+
code: entryCode,
|
|
158
|
+
message: `${field} entry ${JSON.stringify(entry)} is not a locale code string — dropped`
|
|
159
|
+
})
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
if (typeof entry !== 'string') {
|
|
163
|
+
warnings.push({
|
|
164
|
+
code: entryCode,
|
|
165
|
+
message: `${field} entry for '${code}' uses the legacy object form — use the plain string '${code}'`
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
if (seen.has(code)) {
|
|
169
|
+
warnings.push({ code: 'duplicate-language', message: `${field} lists '${code}' more than once — deduped` })
|
|
170
|
+
}
|
|
171
|
+
seen.add(code)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
inspectList(config?.languages, 'languages', 'invalid-language-entry')
|
|
176
|
+
inspectList(config?.publishLanguages, 'publishLanguages', 'invalid-publish-language-entry')
|
|
177
|
+
|
|
178
|
+
const { publishable, dangling, explicit } = resolvePublishableLocales(config)
|
|
179
|
+
for (const code of dangling) {
|
|
180
|
+
warnings.push({
|
|
181
|
+
code: 'dangling-publish-language',
|
|
182
|
+
message: `publishLanguages lists '${code}' but languages does not declare it — ignored at publish (kept in the file so a re-declared language keeps its publish intent)`
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (explicit && publishable.length === 0) {
|
|
187
|
+
errors.push({
|
|
188
|
+
code: 'nothing-publishable',
|
|
189
|
+
message: 'publishLanguages leaves no publishable language (empty list, or nothing it lists is declared) — a publishable default language is required'
|
|
190
|
+
})
|
|
191
|
+
} else if (explicit) {
|
|
192
|
+
const defaultLocale = resolveDefaultLocale(config)
|
|
193
|
+
if (!publishable.includes(defaultLocale)) {
|
|
194
|
+
errors.push({
|
|
195
|
+
code: 'default-not-publishable',
|
|
196
|
+
message: `the default language '${defaultLocale}' is not in publishLanguages — the effective default must be publishable`
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return { errors, warnings }
|
|
202
|
+
}
|
package/src/website.js
CHANGED
|
@@ -10,6 +10,7 @@ import EntityStore from './entity-store.js'
|
|
|
10
10
|
import FetcherDispatcher from './fetcher-dispatcher.js'
|
|
11
11
|
import ObservableState from './observable-state.js'
|
|
12
12
|
import { normalizeSeo } from './seo.js'
|
|
13
|
+
import { resolveDefaultLocale } from './locale-config.js'
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Website — orchestration root for a single site instance.
|
|
@@ -162,7 +163,7 @@ export default class Website {
|
|
|
162
163
|
this.seo = normalizeSeo(config.seo)
|
|
163
164
|
this.keywords = config.keywords || null
|
|
164
165
|
|
|
165
|
-
this.siteDefaultLocale = config
|
|
166
|
+
this.siteDefaultLocale = resolveDefaultLocale(config)
|
|
166
167
|
this.defaultLocale = config.domainLocale || this.siteDefaultLocale
|
|
167
168
|
this.activeLocale = config.activeLocale || this.defaultLocale
|
|
168
169
|
|
|
@@ -229,7 +230,7 @@ export default class Website {
|
|
|
229
230
|
* @private
|
|
230
231
|
*/
|
|
231
232
|
buildLocalesList(config) {
|
|
232
|
-
const defaultLocale = config
|
|
233
|
+
const defaultLocale = resolveDefaultLocale(config)
|
|
233
234
|
const languages = config.languages || []
|
|
234
235
|
|
|
235
236
|
// Normalize input: convert strings to objects, keep objects as-is
|