stitch2 0.11.0 → 0.12.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/README.md +3 -1
- package/dist/audit.js +9 -0
- package/dist/i18n-report.js +10 -3
- package/dist/i18n.js +19 -3
- package/package.json +1 -1
- package/skills/stitch2-i18n/SKILL.md +10 -0
package/README.md
CHANGED
|
@@ -214,7 +214,9 @@ shows every locale and the screens use the same strings as the product:
|
|
|
214
214
|
Messages are JSON or a JS/TS module (its default export, else its first exported object); nested objects become
|
|
215
215
|
dotted keys, and plurals are objects keyed by CLDR category (`one`, `few`, `other`…). A value with braces is a
|
|
216
216
|
template: each `{key|name=value;…}` becomes its message and the rest is kept, and `{@date|…}` and `{@number|…}`
|
|
217
|
-
format with `Intl` in the screen's language.
|
|
217
|
+
format with `Intl` in the screen's language. Styled parts inside a sentence (a name in bold) are slots:
|
|
218
|
+
`<p data-t="library.showing">At <b data-t-slot="profile">Home gym</b></p>` keeps the `<b>` where the translation puts
|
|
219
|
+
`{profile}`. Two pseudo-languages need
|
|
218
220
|
no translations: **pseudo** makes every text about 40% longer, accented and bracketed, so cut text shows, and
|
|
219
221
|
**pseudo-rtl** lays the page out right to left.
|
|
220
222
|
|
package/dist/audit.js
CHANGED
|
@@ -80,6 +80,15 @@ export async function audit(opts) {
|
|
|
80
80
|
for (const el of all) {
|
|
81
81
|
if (!isLabel(el) || text(el).length > 40 || el.querySelector('p, div, li, ul, br'))
|
|
82
82
|
continue;
|
|
83
|
+
// A control whose text sits in two or more elements (a list row's title and its meta line) is a composite
|
|
84
|
+
// control, not a label: its lines are laid out on purpose.
|
|
85
|
+
const holders = new Set();
|
|
86
|
+
const textWalker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
|
|
87
|
+
for (let n = textWalker.nextNode(); n; n = textWalker.nextNode())
|
|
88
|
+
if (n.textContent.trim() && n.parentElement)
|
|
89
|
+
holders.add(n.parentElement);
|
|
90
|
+
if (holders.size > 1)
|
|
91
|
+
continue;
|
|
83
92
|
// A wrap is one text node breaking across lines; an icon above its label is layout, not a wrap.
|
|
84
93
|
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
|
|
85
94
|
for (let n = walker.nextNode(); n; n = walker.nextNode()) {
|
package/dist/i18n-report.js
CHANGED
|
@@ -21,7 +21,10 @@ const locales = opt('--locale')?.split(',') ?? realLocales();
|
|
|
21
21
|
const suggest = args.includes('--suggest');
|
|
22
22
|
/** Attributes that hold text people read. On components (a dash in the tag) any attribute that reads as words. */
|
|
23
23
|
const TEXT_ATTRS = new Set(['title', 'label', 'placeholder', 'aria-label', 'alt']);
|
|
24
|
-
const NOT_TEXT = /^(class|id|style|href|src|srcset|
|
|
24
|
+
const NOT_TEXT = /^(class|id|style|href|src|srcset|for|role|slot|lang|dir|icon|variant|tone|size|width|height|viewbox|d|fill|stroke|xmlns|rel|target|tabindex|loading|translate)$/;
|
|
25
|
+
// On native elements these are identifiers and form values; on components (<x-row name="API keys">) they are
|
|
26
|
+
// usually what the component shows, so there they count as text when they read as words.
|
|
27
|
+
const NATIVE_ONLY = /^(type|name|value)$/;
|
|
25
28
|
const VOID = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'source', 'track', 'wbr']);
|
|
26
29
|
const words = (s) => /\p{L}{2,}/u.test(s);
|
|
27
30
|
const readsAsText = (s) => words(s) && (/\s/.test(s.trim()) || /\p{Lu}/u.test(s) || /[^\x00-\x7f]/.test(s));
|
|
@@ -67,10 +70,14 @@ export function scan(html) {
|
|
|
67
70
|
uses.push({ key: m[1], line, plural: plural || /(^|;)\s*count\s*=/.test(m[2] ?? '') });
|
|
68
71
|
else if (name === 'data-t')
|
|
69
72
|
uses.push({ key: value, line, plural });
|
|
70
|
-
else if (keyed)
|
|
73
|
+
else if (keyed && name !== 'data-t-slot')
|
|
71
74
|
for (const k of value.split(','))
|
|
72
75
|
uses.push({ key: k.trim(), line, plural });
|
|
73
|
-
else if (!skip &&
|
|
76
|
+
else if (!skip &&
|
|
77
|
+
!name.startsWith('data-') &&
|
|
78
|
+
!NOT_TEXT.test(name) &&
|
|
79
|
+
!(NATIVE_ONLY.test(name) && !tag.includes('-')) &&
|
|
80
|
+
!('data-t-' + name in a)) {
|
|
74
81
|
const textual = TEXT_ATTRS.has(name) ? words(value) : tag.includes('-') && readsAsText(value);
|
|
75
82
|
if (textual)
|
|
76
83
|
loose.push({ text: value, line, attr: name });
|
package/dist/i18n.js
CHANGED
|
@@ -17,6 +17,8 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
17
17
|
* a template: messages in braces, the rest kept
|
|
18
18
|
* <x-row meta="Sun, 4 Oct" data-t-meta="{@date|value=2026-10-04;weekday=short;day=numeric;month=short}">
|
|
19
19
|
* dates and numbers ({@number|value=7920}) by Intl
|
|
20
|
+
* <p data-t="lib.showing">At <b data-t-slot="place">Home gym</b></p>
|
|
21
|
+
* rich text: the slot keeps its markup, at {place}
|
|
20
22
|
*
|
|
21
23
|
* Opened with ?lang=<locale> (the canvas's Language switch, and check), the page's text is replaced from that
|
|
22
24
|
* locale's messages before its components render, and the page gets lang and dir. Two pseudo-languages need no
|
|
@@ -163,7 +165,7 @@ function runtime(p) {
|
|
|
163
165
|
for (const a of Array.from(el.attributes)) {
|
|
164
166
|
if (a.name !== 'data-t' && !a.name.startsWith('data-t-'))
|
|
165
167
|
continue;
|
|
166
|
-
if (a.name === 'data-t-args' || a.name === 'data-t-skip')
|
|
168
|
+
if (a.name === 'data-t-args' || a.name === 'data-t-skip' || a.name === 'data-t-slot')
|
|
167
169
|
continue;
|
|
168
170
|
const args = parseArgs(el.getAttribute('data-t-args'));
|
|
169
171
|
let v;
|
|
@@ -194,8 +196,22 @@ function runtime(p) {
|
|
|
194
196
|
}
|
|
195
197
|
if (v === undefined)
|
|
196
198
|
continue;
|
|
197
|
-
if (a.name === 'data-t')
|
|
198
|
-
|
|
199
|
+
if (a.name === 'data-t') {
|
|
200
|
+
// Rich text: children marked data-t-slot="name" keep their markup and take the place of {name}.
|
|
201
|
+
const slots = new Map();
|
|
202
|
+
for (const s of Array.from(el.querySelectorAll('[data-t-slot]'))) {
|
|
203
|
+
const outer = s.parentElement?.closest('[data-t-slot]');
|
|
204
|
+
if (!outer || !el.contains(outer))
|
|
205
|
+
slots.set(s.getAttribute('data-t-slot') ?? '', s);
|
|
206
|
+
}
|
|
207
|
+
if (!slots.size)
|
|
208
|
+
el.textContent = v;
|
|
209
|
+
else {
|
|
210
|
+
el.textContent = '';
|
|
211
|
+
for (const part of v.split(/(\{\w+\})/))
|
|
212
|
+
el.append(slots.get(part.slice(1, -1)) ?? document.createTextNode(part));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
199
215
|
else
|
|
200
216
|
el.setAttribute(a.name.slice(7), v);
|
|
201
217
|
}
|
package/package.json
CHANGED
|
@@ -53,6 +53,16 @@ Dates and numbers are written by the locale too: `{@date|value=2026-10-04;weekda
|
|
|
53
53
|
`Intl.NumberFormat`'s, and dates are read and shown in UTC. Pair a number with the unit's message:
|
|
54
54
|
`{@number|value=82.5} {unit.kg}`.
|
|
55
55
|
|
|
56
|
+
A sentence with styled parts inside (a name in bold, a highlighted number) keeps them as **slots**: mark each part
|
|
57
|
+
`data-t-slot="<placeholder>"` and key the sentence. The translation decides where each part goes:
|
|
58
|
+
|
|
59
|
+
```html
|
|
60
|
+
<p data-t="library.showing" data-t-args="count=312">Showing what you can do at
|
|
61
|
+
<b data-t-slot="profile" translate="no">Home gym</b>: 312 exercises.</p>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Never split such a sentence into separately keyed pieces: word order differs between languages.
|
|
65
|
+
|
|
56
66
|
- Keep the source text in the markup: the screen reads correctly without stitch2, and a missing key shows it.
|
|
57
67
|
- Key every string the product owns: labels, buttons, headings, empty states, hints, units, tab names, the
|
|
58
68
|
placeholder and aria-label too. Sample data (an exercise name, a user's note, a number) gets `translate="no"`.
|