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 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. Two pseudo-languages need
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()) {
@@ -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|type|name|value|for|role|slot|lang|dir|icon|variant|tone|size|width|height|viewbox|d|fill|stroke|xmlns|rel|target|tabindex|loading|translate)$/;
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 && !name.startsWith('data-') && !NOT_TEXT.test(name) && !('data-t-' + name in a)) {
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
- el.textContent = v;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitch2",
3
- "version": "0.11.0",
3
+ "version": "0.12.1",
4
4
  "description": "A design canvas, layout and type checks, component consistency and screen versioning for agent-built UI, on Google's DESIGN.md format.",
5
5
  "license": "MIT",
6
6
  "author": "ifokeev",
@@ -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"`.