@uniweb/build 0.63.4 → 0.64.1
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 +7 -7
- package/src/i18n/extract.js +43 -7
- package/src/i18n/freeform.js +2 -1
- package/src/i18n/merge.js +36 -17
- package/src/i18n/records.js +393 -161
- package/src/runtime-schema.js +3 -1
- package/src/site/content-collector.js +33 -2
- package/src/site/layout-folder.js +15 -0
- package/src/site/queries-config.js +8 -1
- package/src/site/query-processor.js +13 -2
- package/src/site/registered-foundation.js +47 -0
- package/src/uwx/asset-map.js +10 -3
- package/src/uwx/backfill.js +20 -9
- package/src/uwx/data-schema.js +34 -0
- package/src/uwx/emit-surface.json +6 -0
- package/src/uwx/index.js +9 -0
- package/src/uwx/locale-sync.js +267 -36
- package/src/uwx/open-graph.js +69 -0
- package/src/uwx/project-writer.js +109 -31
- package/src/uwx/records-project.js +156 -28
- package/src/uwx/records.js +67 -18
- package/src/uwx/same-content.js +43 -0
- package/src/uwx/site-project.js +334 -39
- package/src/uwx/site.js +178 -44
- package/src/uwx/sync-package.js +24 -1
- package/src/validate-data.js +15 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.64.1",
|
|
4
4
|
"description": "Build tooling for the Uniweb Component Web Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -57,15 +57,15 @@
|
|
|
57
57
|
"js-yaml": "^4.1.0",
|
|
58
58
|
"sharp": "^0.35.3",
|
|
59
59
|
"yaml": "^2.5.0",
|
|
60
|
-
"@uniweb/content-writer": "^0.3.4",
|
|
61
|
-
"@uniweb/projections": "^0.7.3",
|
|
62
|
-
"@uniweb/schemas": "^0.7.0",
|
|
63
60
|
"@uniweb/content-reader": "^1.2.5",
|
|
61
|
+
"@uniweb/content-writer": "^0.3.4",
|
|
62
|
+
"@uniweb/projections": "^0.7.4",
|
|
64
63
|
"@uniweb/semantic-parser": "^1.4.1",
|
|
64
|
+
"@uniweb/schemas": "^0.7.1",
|
|
65
65
|
"@uniweb/theming": "^0.1.15"
|
|
66
66
|
},
|
|
67
67
|
"optionalDependencies": {
|
|
68
|
-
"@uniweb/runtime": "^0.27.
|
|
68
|
+
"@uniweb/runtime": "^0.27.6"
|
|
69
69
|
},
|
|
70
70
|
"peerDependencies": {
|
|
71
71
|
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
|
|
@@ -74,8 +74,8 @@
|
|
|
74
74
|
"@tailwindcss/vite": "^4.0.0",
|
|
75
75
|
"@vitejs/plugin-react": "^4.0.0 || ^5.0.0",
|
|
76
76
|
"vite-plugin-svgr": "^4.0.0",
|
|
77
|
-
"@uniweb/core": "^0.
|
|
78
|
-
"@uniweb/runtime": "^0.27.
|
|
77
|
+
"@uniweb/core": "^0.30.1",
|
|
78
|
+
"@uniweb/runtime": "^0.27.6"
|
|
79
79
|
},
|
|
80
80
|
"peerDependenciesMeta": {
|
|
81
81
|
"vite": {
|
package/src/i18n/extract.js
CHANGED
|
@@ -30,6 +30,9 @@ try {
|
|
|
30
30
|
export function extractTranslatableContent(siteContent) {
|
|
31
31
|
const units = {}
|
|
32
32
|
|
|
33
|
+
// The site's own Open Graph title and description (`site.yml` `seo:`), in the site's context.
|
|
34
|
+
extractFromSiteMeta(siteContent.config, units)
|
|
35
|
+
|
|
33
36
|
for (const page of siteContent.pages || []) {
|
|
34
37
|
const pageRoute = page.route || '/'
|
|
35
38
|
|
|
@@ -82,6 +85,22 @@ export function extractTranslatableContent(siteContent) {
|
|
|
82
85
|
* @param {string} pageRoute - Page route
|
|
83
86
|
* @param {Object} units - Units accumulator
|
|
84
87
|
*/
|
|
88
|
+
/**
|
|
89
|
+
* The context of the site's own metadata — its Open Graph title and description (`site.yml` `seo:`),
|
|
90
|
+
* which a push sends per language as `settings.og_title` / `og_description` (`uwx/open-graph.js`).
|
|
91
|
+
* A page's is `<route>:_meta`; the site's is this one.
|
|
92
|
+
*/
|
|
93
|
+
export const SITE_META_CONTEXT = Object.freeze({ page: '/', section: '_site' })
|
|
94
|
+
|
|
95
|
+
function extractFromSiteMeta(config, units) {
|
|
96
|
+
const seo = config?.seo
|
|
97
|
+
if (!seo || typeof seo !== 'object') return
|
|
98
|
+
if (typeof seo.ogTitle === 'string' && seo.ogTitle) addUnit(units, seo.ogTitle, 'site.seo.ogTitle', SITE_META_CONTEXT)
|
|
99
|
+
if (typeof seo.ogDescription === 'string' && seo.ogDescription) {
|
|
100
|
+
addUnit(units, seo.ogDescription, 'site.seo.ogDescription', SITE_META_CONTEXT)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
85
104
|
function extractFromPageMeta(page, pageRoute, units) {
|
|
86
105
|
// Use special section identifier for page-level metadata
|
|
87
106
|
const context = { page: pageRoute, section: '_meta' }
|
|
@@ -127,6 +146,23 @@ function extractFromPageMeta(page, pageRoute, units) {
|
|
|
127
146
|
}
|
|
128
147
|
}
|
|
129
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Where a translation applies — `{ page, section }`, the key a context-specific override is written
|
|
151
|
+
* under (`"<page>:<section>"`): the page's route and the section's STABLE id. One rule for the
|
|
152
|
+
* extractor, the build (`merge.js`), the push and the pull.
|
|
153
|
+
*
|
|
154
|
+
* ⛔ The section was its build `id` until 2026-09-26 — a numbered file's number (`/about:1` for
|
|
155
|
+
* `1-hero.md`) — so an override moved to another section when files were renumbered, and a clone,
|
|
156
|
+
* whose files are not numbered, could not key it at all.
|
|
157
|
+
*
|
|
158
|
+
* @param {Object} section - a section with `stableId` (and the build `id` as a fallback)
|
|
159
|
+
* @param {string} pageRoute - the route of the page (or layout area) it is on
|
|
160
|
+
* @returns {{ page: string, section: string }}
|
|
161
|
+
*/
|
|
162
|
+
export function translationContext(section, pageRoute) {
|
|
163
|
+
return { page: pageRoute, section: section?.stableId || section?.id || 'unknown' }
|
|
164
|
+
}
|
|
165
|
+
|
|
130
166
|
/**
|
|
131
167
|
* Extract translatable content from a section
|
|
132
168
|
* @param {Object} section - Section data
|
|
@@ -134,8 +170,7 @@ function extractFromPageMeta(page, pageRoute, units) {
|
|
|
134
170
|
* @param {Object} units - Units accumulator
|
|
135
171
|
*/
|
|
136
172
|
function extractFromSection(section, pageRoute, units) {
|
|
137
|
-
const
|
|
138
|
-
const context = { page: pageRoute, section: sectionId }
|
|
173
|
+
const context = translationContext(section, pageRoute)
|
|
139
174
|
|
|
140
175
|
// Extract from parsed semantic content if available
|
|
141
176
|
// The section.content is ProseMirror doc, but we need parsed content
|
|
@@ -173,10 +208,11 @@ function extractFromSection(section, pageRoute, units) {
|
|
|
173
208
|
* than tidy: `deriveStructuralMap` recovers a pulled translation by walking
|
|
174
209
|
* block elements, so a data string translated on the sync wire would be
|
|
175
210
|
* invisible to it — neither captured in the map nor counted as divergence — and
|
|
176
|
-
* would be silently lost on the next pull.
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
211
|
+
* would be silently lost on the next pull. ⭐ Since 2026-09-26 both halves exist:
|
|
212
|
+
* the push translates data blocks (`merge.js::resolveDocForLocale`) and the pull
|
|
213
|
+
* reads their strings back pairwise (`uwx/locale-sync.js::deriveStructuralMap`),
|
|
214
|
+
* as the same per-string entries this lane writes — the block-element keying
|
|
215
|
+
* contract is untouched.
|
|
180
216
|
*/
|
|
181
217
|
function extractFromDataBlocks(doc, context, units) {
|
|
182
218
|
for (const node of dataBlockNodes(doc)) {
|
|
@@ -192,7 +228,7 @@ function extractFromDataBlocks(doc, context, units) {
|
|
|
192
228
|
/**
|
|
193
229
|
* Every tagged data block in a doc, including any nested inside a container.
|
|
194
230
|
*/
|
|
195
|
-
function dataBlockNodes(doc) {
|
|
231
|
+
export function dataBlockNodes(doc) {
|
|
196
232
|
const out = []
|
|
197
233
|
const walk = (nodes) => {
|
|
198
234
|
for (const node of nodes || []) {
|
package/src/i18n/freeform.js
CHANGED
|
@@ -25,6 +25,7 @@ import yaml from 'js-yaml'
|
|
|
25
25
|
import { poolDirsForSchema, schemaForPoolDirs, RECORDS_DIR } from '../site/entity-pool.js'
|
|
26
26
|
import { parseFrontmatter } from '../utils/frontmatter.js'
|
|
27
27
|
import { computeSourceHash } from './freeform-manifest.js'
|
|
28
|
+
import { layoutAreaRoute } from '../site/layout-folder.js'
|
|
28
29
|
|
|
29
30
|
// Try to import content-reader for markdown → ProseMirror conversion
|
|
30
31
|
let markdownToProseMirror
|
|
@@ -327,7 +328,7 @@ function freeformSections(siteContent) {
|
|
|
327
328
|
if (!areas || typeof areas !== 'object') continue
|
|
328
329
|
for (const [areaKey, layoutPage] of Object.entries(areas)) {
|
|
329
330
|
if (!layoutPage?.sections) continue
|
|
330
|
-
const route = layoutPage.route ||
|
|
331
|
+
const route = layoutPage.route || layoutAreaRoute(layoutName, areaKey)
|
|
331
332
|
visit(layoutPage.sections, { ...layoutPage, route })
|
|
332
333
|
}
|
|
333
334
|
}
|
package/src/i18n/merge.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import { computeHash } from './hash.js'
|
|
16
16
|
import { loadFreeformTranslation } from './freeform.js'
|
|
17
|
-
import { elementText, blockElements } from './extract.js'
|
|
17
|
+
import { elementText, blockElements, translationContext, SITE_META_CONTEXT } from './extract.js'
|
|
18
18
|
import { visitDataStrings } from './data-strings.js'
|
|
19
19
|
|
|
20
20
|
// Inline-markdown → ProseMirror inline fragment, for resolving a whole-element
|
|
@@ -71,6 +71,7 @@ export function mergeTranslations(siteContent, translations, options = {}) {
|
|
|
71
71
|
function mergeTranslationsSync(siteContent, translations, fallbackToSource) {
|
|
72
72
|
// Deep clone to avoid mutating original
|
|
73
73
|
const translated = JSON.parse(JSON.stringify(siteContent))
|
|
74
|
+
translateSiteMeta(translated.config, translations, fallbackToSource)
|
|
74
75
|
|
|
75
76
|
for (const page of translated.pages || []) {
|
|
76
77
|
const pageRoute = page.route || '/'
|
|
@@ -120,6 +121,7 @@ async function mergeTranslationsAsync(siteContent, translations, options) {
|
|
|
120
121
|
|
|
121
122
|
// Deep clone to avoid mutating original
|
|
122
123
|
const translated = JSON.parse(JSON.stringify(siteContent))
|
|
124
|
+
translateSiteMeta(translated.config, translations, fallbackToSource)
|
|
123
125
|
|
|
124
126
|
for (const page of translated.pages || []) {
|
|
125
127
|
const pageRoute = page.route || '/'
|
|
@@ -222,6 +224,18 @@ function toPlainText(value) {
|
|
|
222
224
|
* Every field here is a PLAIN-TEXT slot — `<title>`, a nav label, a meta
|
|
223
225
|
* description — so each value is flattened. See `toPlainText`.
|
|
224
226
|
*/
|
|
227
|
+
// The site's own Open Graph title and description (`config.seo`), in the site's context — as a page's
|
|
228
|
+
// are in its own (`translatePageMeta`).
|
|
229
|
+
function translateSiteMeta(config, translations, fallbackToSource) {
|
|
230
|
+
const seo = config?.seo
|
|
231
|
+
if (!seo || typeof seo !== 'object') return
|
|
232
|
+
for (const key of ['ogTitle', 'ogDescription']) {
|
|
233
|
+
if (typeof seo[key] === 'string' && seo[key]) {
|
|
234
|
+
seo[key] = lookupTranslation(seo[key], SITE_META_CONTEXT, translations, fallbackToSource)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
225
239
|
function translatePageMeta(page, pageRoute, translations, fallbackToSource) {
|
|
226
240
|
const context = { page: pageRoute, section: '_meta' }
|
|
227
241
|
const translate = (value) =>
|
|
@@ -268,8 +282,7 @@ function translatePageMeta(page, pageRoute, translations, fallbackToSource) {
|
|
|
268
282
|
* Translate a section's content (synchronous, hash-based only)
|
|
269
283
|
*/
|
|
270
284
|
function translateSectionSync(section, pageRoute, translations, fallbackToSource) {
|
|
271
|
-
const
|
|
272
|
-
const context = { page: pageRoute, section: sectionId }
|
|
285
|
+
const context = translationContext(section, pageRoute)
|
|
273
286
|
|
|
274
287
|
if (section.content?.type === 'doc') {
|
|
275
288
|
translateProseMirrorDoc(section.content, context, translations, fallbackToSource)
|
|
@@ -292,8 +305,7 @@ function translateSectionSync(section, pageRoute, translations, fallbackToSource
|
|
|
292
305
|
async function translateSectionAsync(section, page, translations, options) {
|
|
293
306
|
const { fallbackToSource, locale, localesDir } = options
|
|
294
307
|
const pageRoute = page.route || '/'
|
|
295
|
-
const
|
|
296
|
-
const context = { page: pageRoute, section: sectionId }
|
|
308
|
+
const context = translationContext(section, pageRoute)
|
|
297
309
|
|
|
298
310
|
// Check for free-form translation first
|
|
299
311
|
const freeform = await loadFreeformTranslation(section, page, locale, localesDir)
|
|
@@ -355,23 +367,27 @@ function applyElementTranslation(node, context, translations, fallbackToSource)
|
|
|
355
367
|
* in place. The other half of the fix in `extract.js` — a manifest entry nobody
|
|
356
368
|
* applies is worse than no entry, because a translator has already done the work.
|
|
357
369
|
*
|
|
358
|
-
* Called from the two
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
*
|
|
370
|
+
* Called from the build's two section walks and from `resolveDocForLocale`, the
|
|
371
|
+
* push's. ⛔ The push was left out deliberately until 2026-09-26, because the
|
|
372
|
+
* pull could not read a translated data block back and would have dropped it
|
|
373
|
+
* silently; it now does (`uwx/locale-sync.js::deriveStructuralMap`), so the two
|
|
374
|
+
* halves travel together.
|
|
375
|
+
*
|
|
376
|
+
* @returns {boolean} whether any value was translated
|
|
364
377
|
*/
|
|
365
378
|
function translateDataBlocks(doc, context, translations, fallbackToSource) {
|
|
379
|
+
let changed = false
|
|
366
380
|
const walk = (nodes) => {
|
|
367
381
|
for (const node of nodes || []) {
|
|
368
382
|
if (!node) continue
|
|
369
383
|
if (node.type === 'dataBlock') {
|
|
370
384
|
const data = node.attrs?.data
|
|
371
385
|
if (data && typeof data === 'object') {
|
|
372
|
-
visitDataStrings(data, (value) =>
|
|
373
|
-
lookupTranslation(value, context, translations, fallbackToSource)
|
|
374
|
-
|
|
386
|
+
visitDataStrings(data, (value) => {
|
|
387
|
+
const translated = lookupTranslation(value, context, translations, fallbackToSource)
|
|
388
|
+
if (translated !== value) changed = true
|
|
389
|
+
return translated
|
|
390
|
+
})
|
|
375
391
|
}
|
|
376
392
|
} else if (Array.isArray(node.content)) {
|
|
377
393
|
walk(node.content)
|
|
@@ -379,6 +395,7 @@ function translateDataBlocks(doc, context, translations, fallbackToSource) {
|
|
|
379
395
|
}
|
|
380
396
|
}
|
|
381
397
|
walk(doc?.content)
|
|
398
|
+
return changed
|
|
382
399
|
}
|
|
383
400
|
|
|
384
401
|
/**
|
|
@@ -393,8 +410,9 @@ function translateDataBlocks(doc, context, translations, fallbackToSource) {
|
|
|
393
410
|
export function resolveDocForLocale(sourceDoc, table, context = { page: '', section: '' }) {
|
|
394
411
|
if (!sourceDoc || sourceDoc.type !== 'doc' || !table) return null
|
|
395
412
|
const doc = JSON.parse(JSON.stringify(sourceDoc))
|
|
396
|
-
const
|
|
397
|
-
|
|
413
|
+
const elements = translateProseMirrorDoc(doc, context, table, true)
|
|
414
|
+
const data = translateDataBlocks(doc, context, table, true)
|
|
415
|
+
return elements || data ? doc : null
|
|
398
416
|
}
|
|
399
417
|
|
|
400
418
|
// Parse an inline-markdown translation value into a ProseMirror inline fragment.
|
|
@@ -440,7 +458,8 @@ function lookupTranslation(source, context, translations, fallbackToSource) {
|
|
|
440
458
|
|
|
441
459
|
// Handle translation with overrides
|
|
442
460
|
if (typeof translation === 'object') {
|
|
443
|
-
|
|
461
|
+
// A record's context is its identity (`context.key`, `records.js`); a section's, its page and id.
|
|
462
|
+
const contextKey = context?.key ?? `${context.page}:${context.section}`
|
|
444
463
|
|
|
445
464
|
// Check for context-specific override
|
|
446
465
|
if (translation.overrides?.[contextKey]) {
|