@uniweb/build 0.63.4 → 0.64.0
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 +9 -9
- package/src/i18n/extract.js +24 -7
- package/src/i18n/freeform.js +2 -1
- package/src/i18n/merge.js +22 -17
- package/src/i18n/records.js +389 -161
- package/src/site/content-collector.js +33 -2
- package/src/site/layout-folder.js +15 -0
- package/src/site/query-processor.js +13 -2
- package/src/uwx/asset-map.js +10 -3
- package/src/uwx/backfill.js +5 -5
- package/src/uwx/locale-sync.js +267 -36
- package/src/uwx/project-writer.js +106 -31
- package/src/uwx/records-project.js +108 -21
- package/src/uwx/records.js +19 -16
- package/src/uwx/same-content.js +43 -0
- package/src/uwx/site-project.js +322 -38
- package/src/uwx/site.js +146 -35
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.64.0",
|
|
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/
|
|
61
|
-
"@uniweb/projections": "^0.7.3",
|
|
62
|
-
"@uniweb/schemas": "^0.7.0",
|
|
63
|
-
"@uniweb/content-reader": "^1.2.5",
|
|
60
|
+
"@uniweb/projections": "^0.7.4",
|
|
64
61
|
"@uniweb/semantic-parser": "^1.4.1",
|
|
65
|
-
"@uniweb/
|
|
62
|
+
"@uniweb/schemas": "^0.7.0",
|
|
63
|
+
"@uniweb/theming": "^0.1.15",
|
|
64
|
+
"@uniweb/content-writer": "^0.3.4",
|
|
65
|
+
"@uniweb/content-reader": "^1.2.5"
|
|
66
66
|
},
|
|
67
67
|
"optionalDependencies": {
|
|
68
|
-
"@uniweb/runtime": "^0.27.
|
|
68
|
+
"@uniweb/runtime": "^0.27.5"
|
|
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.0",
|
|
78
|
+
"@uniweb/runtime": "^0.27.5"
|
|
79
79
|
},
|
|
80
80
|
"peerDependenciesMeta": {
|
|
81
81
|
"vite": {
|
package/src/i18n/extract.js
CHANGED
|
@@ -127,6 +127,23 @@ function extractFromPageMeta(page, pageRoute, units) {
|
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Where a translation applies — `{ page, section }`, the key a context-specific override is written
|
|
132
|
+
* under (`"<page>:<section>"`): the page's route and the section's STABLE id. One rule for the
|
|
133
|
+
* extractor, the build (`merge.js`), the push and the pull.
|
|
134
|
+
*
|
|
135
|
+
* ⛔ The section was its build `id` until 2026-09-26 — a numbered file's number (`/about:1` for
|
|
136
|
+
* `1-hero.md`) — so an override moved to another section when files were renumbered, and a clone,
|
|
137
|
+
* whose files are not numbered, could not key it at all.
|
|
138
|
+
*
|
|
139
|
+
* @param {Object} section - a section with `stableId` (and the build `id` as a fallback)
|
|
140
|
+
* @param {string} pageRoute - the route of the page (or layout area) it is on
|
|
141
|
+
* @returns {{ page: string, section: string }}
|
|
142
|
+
*/
|
|
143
|
+
export function translationContext(section, pageRoute) {
|
|
144
|
+
return { page: pageRoute, section: section?.stableId || section?.id || 'unknown' }
|
|
145
|
+
}
|
|
146
|
+
|
|
130
147
|
/**
|
|
131
148
|
* Extract translatable content from a section
|
|
132
149
|
* @param {Object} section - Section data
|
|
@@ -134,8 +151,7 @@ function extractFromPageMeta(page, pageRoute, units) {
|
|
|
134
151
|
* @param {Object} units - Units accumulator
|
|
135
152
|
*/
|
|
136
153
|
function extractFromSection(section, pageRoute, units) {
|
|
137
|
-
const
|
|
138
|
-
const context = { page: pageRoute, section: sectionId }
|
|
154
|
+
const context = translationContext(section, pageRoute)
|
|
139
155
|
|
|
140
156
|
// Extract from parsed semantic content if available
|
|
141
157
|
// The section.content is ProseMirror doc, but we need parsed content
|
|
@@ -173,10 +189,11 @@ function extractFromSection(section, pageRoute, units) {
|
|
|
173
189
|
* than tidy: `deriveStructuralMap` recovers a pulled translation by walking
|
|
174
190
|
* block elements, so a data string translated on the sync wire would be
|
|
175
191
|
* 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
|
-
*
|
|
192
|
+
* would be silently lost on the next pull. ⭐ Since 2026-09-26 both halves exist:
|
|
193
|
+
* the push translates data blocks (`merge.js::resolveDocForLocale`) and the pull
|
|
194
|
+
* reads their strings back pairwise (`uwx/locale-sync.js::deriveStructuralMap`),
|
|
195
|
+
* as the same per-string entries this lane writes — the block-element keying
|
|
196
|
+
* contract is untouched.
|
|
180
197
|
*/
|
|
181
198
|
function extractFromDataBlocks(doc, context, units) {
|
|
182
199
|
for (const node of dataBlockNodes(doc)) {
|
|
@@ -192,7 +209,7 @@ function extractFromDataBlocks(doc, context, units) {
|
|
|
192
209
|
/**
|
|
193
210
|
* Every tagged data block in a doc, including any nested inside a container.
|
|
194
211
|
*/
|
|
195
|
-
function dataBlockNodes(doc) {
|
|
212
|
+
export function dataBlockNodes(doc) {
|
|
196
213
|
const out = []
|
|
197
214
|
const walk = (nodes) => {
|
|
198
215
|
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 } 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
|
|
@@ -268,8 +268,7 @@ function translatePageMeta(page, pageRoute, translations, fallbackToSource) {
|
|
|
268
268
|
* Translate a section's content (synchronous, hash-based only)
|
|
269
269
|
*/
|
|
270
270
|
function translateSectionSync(section, pageRoute, translations, fallbackToSource) {
|
|
271
|
-
const
|
|
272
|
-
const context = { page: pageRoute, section: sectionId }
|
|
271
|
+
const context = translationContext(section, pageRoute)
|
|
273
272
|
|
|
274
273
|
if (section.content?.type === 'doc') {
|
|
275
274
|
translateProseMirrorDoc(section.content, context, translations, fallbackToSource)
|
|
@@ -292,8 +291,7 @@ function translateSectionSync(section, pageRoute, translations, fallbackToSource
|
|
|
292
291
|
async function translateSectionAsync(section, page, translations, options) {
|
|
293
292
|
const { fallbackToSource, locale, localesDir } = options
|
|
294
293
|
const pageRoute = page.route || '/'
|
|
295
|
-
const
|
|
296
|
-
const context = { page: pageRoute, section: sectionId }
|
|
294
|
+
const context = translationContext(section, pageRoute)
|
|
297
295
|
|
|
298
296
|
// Check for free-form translation first
|
|
299
297
|
const freeform = await loadFreeformTranslation(section, page, locale, localesDir)
|
|
@@ -355,23 +353,27 @@ function applyElementTranslation(node, context, translations, fallbackToSource)
|
|
|
355
353
|
* in place. The other half of the fix in `extract.js` — a manifest entry nobody
|
|
356
354
|
* applies is worse than no entry, because a translator has already done the work.
|
|
357
355
|
*
|
|
358
|
-
* Called from the two
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
*
|
|
356
|
+
* Called from the build's two section walks and from `resolveDocForLocale`, the
|
|
357
|
+
* push's. ⛔ The push was left out deliberately until 2026-09-26, because the
|
|
358
|
+
* pull could not read a translated data block back and would have dropped it
|
|
359
|
+
* silently; it now does (`uwx/locale-sync.js::deriveStructuralMap`), so the two
|
|
360
|
+
* halves travel together.
|
|
361
|
+
*
|
|
362
|
+
* @returns {boolean} whether any value was translated
|
|
364
363
|
*/
|
|
365
364
|
function translateDataBlocks(doc, context, translations, fallbackToSource) {
|
|
365
|
+
let changed = false
|
|
366
366
|
const walk = (nodes) => {
|
|
367
367
|
for (const node of nodes || []) {
|
|
368
368
|
if (!node) continue
|
|
369
369
|
if (node.type === 'dataBlock') {
|
|
370
370
|
const data = node.attrs?.data
|
|
371
371
|
if (data && typeof data === 'object') {
|
|
372
|
-
visitDataStrings(data, (value) =>
|
|
373
|
-
lookupTranslation(value, context, translations, fallbackToSource)
|
|
374
|
-
|
|
372
|
+
visitDataStrings(data, (value) => {
|
|
373
|
+
const translated = lookupTranslation(value, context, translations, fallbackToSource)
|
|
374
|
+
if (translated !== value) changed = true
|
|
375
|
+
return translated
|
|
376
|
+
})
|
|
375
377
|
}
|
|
376
378
|
} else if (Array.isArray(node.content)) {
|
|
377
379
|
walk(node.content)
|
|
@@ -379,6 +381,7 @@ function translateDataBlocks(doc, context, translations, fallbackToSource) {
|
|
|
379
381
|
}
|
|
380
382
|
}
|
|
381
383
|
walk(doc?.content)
|
|
384
|
+
return changed
|
|
382
385
|
}
|
|
383
386
|
|
|
384
387
|
/**
|
|
@@ -393,8 +396,9 @@ function translateDataBlocks(doc, context, translations, fallbackToSource) {
|
|
|
393
396
|
export function resolveDocForLocale(sourceDoc, table, context = { page: '', section: '' }) {
|
|
394
397
|
if (!sourceDoc || sourceDoc.type !== 'doc' || !table) return null
|
|
395
398
|
const doc = JSON.parse(JSON.stringify(sourceDoc))
|
|
396
|
-
const
|
|
397
|
-
|
|
399
|
+
const elements = translateProseMirrorDoc(doc, context, table, true)
|
|
400
|
+
const data = translateDataBlocks(doc, context, table, true)
|
|
401
|
+
return elements || data ? doc : null
|
|
398
402
|
}
|
|
399
403
|
|
|
400
404
|
// Parse an inline-markdown translation value into a ProseMirror inline fragment.
|
|
@@ -440,7 +444,8 @@ function lookupTranslation(source, context, translations, fallbackToSource) {
|
|
|
440
444
|
|
|
441
445
|
// Handle translation with overrides
|
|
442
446
|
if (typeof translation === 'object') {
|
|
443
|
-
|
|
447
|
+
// A record's context is its identity (`context.key`, `records.js`); a section's, its page and id.
|
|
448
|
+
const contextKey = context?.key ?? `${context.page}:${context.section}`
|
|
444
449
|
|
|
445
450
|
// Check for context-specific override
|
|
446
451
|
if (translation.overrides?.[contextKey]) {
|