@uniweb/build 0.8.22 → 0.8.24
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 +3 -3
- package/src/i18n/extract.js +30 -8
- package/src/i18n/hash.js +44 -0
- package/src/i18n/merge.js +77 -16
- package/src/runtime-schema.js +5 -0
- package/src/site/plugin.js +9 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.24",
|
|
4
4
|
"description": "Build tooling for the Uniweb Component Web Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -54,9 +54,9 @@
|
|
|
54
54
|
"@uniweb/theming": "0.1.2"
|
|
55
55
|
},
|
|
56
56
|
"optionalDependencies": {
|
|
57
|
+
"@uniweb/runtime": "0.6.20",
|
|
57
58
|
"@uniweb/content-reader": "1.1.4",
|
|
58
|
-
"@uniweb/schemas": "0.2.1"
|
|
59
|
-
"@uniweb/runtime": "0.6.18"
|
|
59
|
+
"@uniweb/schemas": "0.2.1"
|
|
60
60
|
},
|
|
61
61
|
"peerDependencies": {
|
|
62
62
|
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
|
package/src/i18n/extract.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* strings and building a manifest of translation units.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { computeHash } from './hash.js'
|
|
8
|
+
import { computeHash, stripInlineTags } from './hash.js'
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Extract all translatable units from site content
|
|
@@ -250,15 +250,36 @@ function extractFromList(listNode, context, units) {
|
|
|
250
250
|
}
|
|
251
251
|
|
|
252
252
|
/**
|
|
253
|
-
* Extract all text content from a node
|
|
253
|
+
* Extract all text content from a node.
|
|
254
|
+
* When a node has multiple text children with some carrying span marks
|
|
255
|
+
* (e.g., `[text]{accent}`), wraps the marked spans in XLIFF-style
|
|
256
|
+
* `<N>...</N>` tags so translators can reposition them.
|
|
254
257
|
*/
|
|
255
258
|
function extractTextFromNode(node) {
|
|
256
259
|
if (!node.content) return ''
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
.
|
|
260
|
+
const textChildren = node.content.filter(n => n.type === 'text')
|
|
261
|
+
|
|
262
|
+
// Check for mixed inline marks (span marks from `[text]{class}` syntax)
|
|
263
|
+
const hasInlineMarks = textChildren.length > 1 &&
|
|
264
|
+
textChildren.some(n => n.marks?.some(m => m.type === 'span'))
|
|
265
|
+
|
|
266
|
+
if (!hasInlineMarks) {
|
|
267
|
+
return textChildren.map(n => n.text || '').join('').trim()
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Wrap span-marked text in numbered tags
|
|
271
|
+
let markCounter = 0
|
|
272
|
+
const parts = []
|
|
273
|
+
for (const child of textChildren) {
|
|
274
|
+
const text = child.text || ''
|
|
275
|
+
if (child.marks?.some(m => m.type === 'span')) {
|
|
276
|
+
markCounter++
|
|
277
|
+
parts.push(`<${markCounter}>${text}</${markCounter}>`)
|
|
278
|
+
} else {
|
|
279
|
+
parts.push(text)
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return parts.join('').trim()
|
|
262
283
|
}
|
|
263
284
|
|
|
264
285
|
/**
|
|
@@ -267,7 +288,8 @@ function extractTextFromNode(node) {
|
|
|
267
288
|
function addUnit(units, source, field, context) {
|
|
268
289
|
if (!source || source.length === 0) return
|
|
269
290
|
|
|
270
|
-
|
|
291
|
+
// Hash on plain text (strip inline tags) so keys stay stable
|
|
292
|
+
const hash = computeHash(stripInlineTags(source))
|
|
271
293
|
|
|
272
294
|
if (units[hash]) {
|
|
273
295
|
// Unit exists - add context if not already present
|
package/src/i18n/hash.js
CHANGED
|
@@ -28,3 +28,47 @@ export function normalizeText(text) {
|
|
|
28
28
|
if (typeof text !== 'string') return ''
|
|
29
29
|
return text.trim().replace(/\s+/g, ' ')
|
|
30
30
|
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Strip inline mark tags for hashing: "<1>text</1>" → "text"
|
|
34
|
+
* Used to keep hash keys stable regardless of mark tagging.
|
|
35
|
+
* @param {string} text
|
|
36
|
+
* @returns {string}
|
|
37
|
+
*/
|
|
38
|
+
export function stripInlineTags(text) {
|
|
39
|
+
if (typeof text !== 'string') return ''
|
|
40
|
+
return text.replace(/<\/?(\d+)>/g, '')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Parse tagged translation string into segments.
|
|
45
|
+
* "plain <1>marked</1> more" →
|
|
46
|
+
* { segments: [{ text: "plain " }, { text: "marked", markIndex: 0 }, { text: " more" }], hasMarks: true }
|
|
47
|
+
*
|
|
48
|
+
* Tag numbers are 1-based in the string, markIndex is 0-based in the result.
|
|
49
|
+
* @param {string} text
|
|
50
|
+
* @returns {{ segments: Array<{ text: string, markIndex?: number }>, hasMarks: boolean }}
|
|
51
|
+
*/
|
|
52
|
+
export function parseInlineTags(text) {
|
|
53
|
+
const regex = /<(\d+)>([\s\S]*?)<\/\1>/g
|
|
54
|
+
const segments = []
|
|
55
|
+
let lastIndex = 0
|
|
56
|
+
let hasMarks = false
|
|
57
|
+
let match
|
|
58
|
+
|
|
59
|
+
while ((match = regex.exec(text)) !== null) {
|
|
60
|
+
hasMarks = true
|
|
61
|
+
if (match.index > lastIndex) {
|
|
62
|
+
segments.push({ text: text.slice(lastIndex, match.index) })
|
|
63
|
+
}
|
|
64
|
+
segments.push({ text: match[2], markIndex: parseInt(match[1], 10) - 1 })
|
|
65
|
+
lastIndex = regex.lastIndex
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (lastIndex < text.length) {
|
|
69
|
+
segments.push({ text: text.slice(lastIndex) })
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!hasMarks) return { segments: [{ text }], hasMarks: false }
|
|
73
|
+
return { segments, hasMarks }
|
|
74
|
+
}
|
package/src/i18n/merge.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* when no free-form translation exists.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { computeHash } from './hash.js'
|
|
15
|
+
import { computeHash, stripInlineTags, parseInlineTags } from './hash.js'
|
|
16
16
|
import { loadFreeformTranslation } from './freeform.js'
|
|
17
17
|
|
|
18
18
|
/**
|
|
@@ -264,27 +264,88 @@ function translateProseMirrorDoc(doc, context, translations, fallbackToSource) {
|
|
|
264
264
|
}
|
|
265
265
|
|
|
266
266
|
/**
|
|
267
|
-
* Recursively translate a node and its children
|
|
267
|
+
* Recursively translate a node and its children.
|
|
268
|
+
* For nodes with mixed marked/unmarked text children (inline span marks),
|
|
269
|
+
* translates the node as a whole unit using XLIFF-style inline tags,
|
|
270
|
+
* then rebuilds children with original marks re-applied.
|
|
268
271
|
*/
|
|
269
272
|
function translateNode(node, context, translations, fallbackToSource) {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
273
|
+
if (!node.content) return
|
|
274
|
+
|
|
275
|
+
// Check for inline span marks across multiple text children
|
|
276
|
+
const textChildren = node.content.filter(n => n.type === 'text')
|
|
277
|
+
const hasInlineMarks = textChildren.length > 1 &&
|
|
278
|
+
textChildren.some(n => n.marks?.some(m => m.type === 'span'))
|
|
279
|
+
|
|
280
|
+
if (hasInlineMarks) {
|
|
281
|
+
// Build tagged source string (mirrors extraction)
|
|
282
|
+
let markCounter = 0
|
|
283
|
+
const markMap = [] // tag index → original marks array
|
|
284
|
+
const parts = []
|
|
285
|
+
for (const child of textChildren) {
|
|
286
|
+
const text = child.text || ''
|
|
287
|
+
if (child.marks?.some(m => m.type === 'span')) {
|
|
288
|
+
markCounter++
|
|
289
|
+
markMap.push(child.marks)
|
|
290
|
+
parts.push(`<${markCounter}>${text}</${markCounter}>`)
|
|
283
291
|
} else {
|
|
284
|
-
|
|
292
|
+
parts.push(text)
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const plainSource = stripInlineTags(parts.join('').trim())
|
|
296
|
+
|
|
297
|
+
const translated = lookupTranslation(plainSource, context, translations, fallbackToSource)
|
|
298
|
+
if (translated !== plainSource) {
|
|
299
|
+
// Parse tagged translation and rebuild text children
|
|
300
|
+
const { segments } = parseInlineTags(translated)
|
|
301
|
+
const newTextChildren = segments.map(seg => {
|
|
302
|
+
if (seg.markIndex !== undefined && markMap[seg.markIndex]) {
|
|
303
|
+
return { type: 'text', text: seg.text, marks: [...markMap[seg.markIndex]] }
|
|
304
|
+
}
|
|
305
|
+
return { type: 'text', text: seg.text }
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
// Replace text children in content, preserve non-text children in place
|
|
309
|
+
const result = []
|
|
310
|
+
let textInserted = false
|
|
311
|
+
for (const child of node.content) {
|
|
312
|
+
if (child.type === 'text') {
|
|
313
|
+
if (!textInserted) {
|
|
314
|
+
result.push(...newTextChildren)
|
|
315
|
+
textInserted = true
|
|
316
|
+
}
|
|
317
|
+
// Skip remaining old text children
|
|
318
|
+
} else {
|
|
319
|
+
result.push(child)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
node.content = result
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Recurse into non-text children
|
|
326
|
+
for (const child of node.content) {
|
|
327
|
+
if (child.type !== 'text') {
|
|
285
328
|
translateNode(child, context, translations, fallbackToSource)
|
|
286
329
|
}
|
|
287
330
|
}
|
|
331
|
+
return
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Default: translate each child individually
|
|
335
|
+
for (const child of node.content) {
|
|
336
|
+
if (child.type === 'text' && child.text) {
|
|
337
|
+
const translated = lookupTranslation(
|
|
338
|
+
child.text,
|
|
339
|
+
context,
|
|
340
|
+
translations,
|
|
341
|
+
fallbackToSource
|
|
342
|
+
)
|
|
343
|
+
if (translated !== child.text) {
|
|
344
|
+
child.text = translated
|
|
345
|
+
}
|
|
346
|
+
} else {
|
|
347
|
+
translateNode(child, context, translations, fallbackToSource)
|
|
348
|
+
}
|
|
288
349
|
}
|
|
289
350
|
}
|
|
290
351
|
|
package/src/runtime-schema.js
CHANGED
|
@@ -309,6 +309,7 @@ export function extractAllRuntimeSchemas(componentsMeta) {
|
|
|
309
309
|
* - areas: Array of area names this layout supports
|
|
310
310
|
* - transitions: View transition name mapping (stored but not acted on yet)
|
|
311
311
|
* - defaults: Param default values
|
|
312
|
+
* - scroll: Scroll management mode ('self' or CSS selector)
|
|
312
313
|
*
|
|
313
314
|
* @param {Object} fullMeta - The full meta.js default export for a layout
|
|
314
315
|
* @returns {Object|null} - Lean layout runtime schema or null if empty
|
|
@@ -328,6 +329,10 @@ export function extractLayoutRuntimeSchema(fullMeta) {
|
|
|
328
329
|
runtime.transitions = fullMeta.transitions
|
|
329
330
|
}
|
|
330
331
|
|
|
332
|
+
if (fullMeta.scroll !== undefined) {
|
|
333
|
+
runtime.scroll = fullMeta.scroll
|
|
334
|
+
}
|
|
335
|
+
|
|
331
336
|
const defaults = extractParamDefaults(fullMeta.params)
|
|
332
337
|
if (defaults) {
|
|
333
338
|
runtime.defaults = defaults
|
package/src/site/plugin.js
CHANGED
|
@@ -942,10 +942,17 @@ export function siteContentPlugin(options = {}) {
|
|
|
942
942
|
headInjection += ` <script type="application/json" id="${variableName}">${JSON.stringify(contentToInject).replace(/</g, '\\u003c')}</script>\n`
|
|
943
943
|
}
|
|
944
944
|
|
|
945
|
-
|
|
945
|
+
let result = html
|
|
946
|
+
|
|
947
|
+
// Update <html lang> for non-default locales
|
|
948
|
+
if (activeLocale) {
|
|
949
|
+
result = result.replace(/<html([^>]*)\slang="[^"]*"/, `<html$1 lang="${activeLocale}"`)
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
if (!headInjection) return result
|
|
946
953
|
|
|
947
954
|
// Insert before </head>
|
|
948
|
-
return
|
|
955
|
+
return result.replace('</head>', headInjection + ' </head>')
|
|
949
956
|
},
|
|
950
957
|
|
|
951
958
|
async generateBundle() {
|