stitch2 0.10.0 → 0.11.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/README.md CHANGED
@@ -197,6 +197,9 @@ shows every locale and the screens use the same strings as the product:
197
197
  <ex-header title="Settings" data-t-title="settings.title"></ex-header>
198
198
  <p data-t="workout.sets" data-t-args="count=16">16 sets</p> <!-- plural form picked by count -->
199
199
  <p translate="no">Dumbbell Bench Press</p> <!-- sample content stays as written -->
200
+ <ex-row meta="5 exercises · Linear" data-t-meta="{plan.exercises|count=5} · {plan.linear}"></ex-row> <!-- a template -->
201
+ <ex-row meta="Sun, 4 Oct · 7,920 kg"
202
+ data-t-meta="{@date|value=2026-10-04;weekday=short;day=numeric;month=short} · {@number|value=7920} {unit.kg}"></ex-row>
200
203
  ```
201
204
 
202
205
  ```json
@@ -209,7 +212,9 @@ shows every locale and the screens use the same strings as the product:
209
212
  ```
210
213
 
211
214
  Messages are JSON or a JS/TS module (its default export, else its first exported object); nested objects become
212
- dotted keys, and plurals are objects keyed by CLDR category (`one`, `few`, `other`…). Two pseudo-languages need
215
+ dotted keys, and plurals are objects keyed by CLDR category (`one`, `few`, `other`…). A value with braces is a
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
213
218
  no translations: **pseudo** makes every text about 40% longer, accented and bracketed, so cut text shows, and
214
219
  **pseudo-rtl** lays the page out right to left.
215
220
 
package/dist/elements.js CHANGED
@@ -42,7 +42,8 @@ export function defineElements(components, options) {
42
42
  connectedCallback() {
43
43
  const props = {};
44
44
  for (const a of Array.from(this.attributes)) {
45
- if (a.name.startsWith('data-s2-'))
45
+ // stitch2's own markers and the message keys (i18n.ts translated them already) are not props.
46
+ if (a.name.startsWith('data-s2-') || a.name === 'data-t' || a.name.startsWith('data-t-'))
46
47
  continue;
47
48
  props[camel(a.name)] = a.value === '' ? true : a.value;
48
49
  }
@@ -61,9 +61,13 @@ export function scan(html) {
61
61
  const plural = 'data-t-args' in a && /(^|,)\s*count\s*=/.test(a['data-t-args']);
62
62
  const skip = !!top?.skip || 'data-t-skip' in a || a.translate === 'no';
63
63
  for (const [name, value] of Object.entries(a)) {
64
- if (name === 'data-t')
64
+ const keyed = name === 'data-t' || (name.startsWith('data-t-') && name !== 'data-t-args' && name !== 'data-t-skip');
65
+ if (keyed && value.includes('{'))
66
+ for (const m of value.matchAll(/\{([^{}|@][^{}|]*)(?:\|([^}]*))?\}/g))
67
+ uses.push({ key: m[1], line, plural: plural || /(^|;)\s*count\s*=/.test(m[2] ?? '') });
68
+ else if (name === 'data-t')
65
69
  uses.push({ key: value, line, plural });
66
- else if (name.startsWith('data-t-') && name !== 'data-t-args' && name !== 'data-t-skip')
70
+ else if (keyed)
67
71
  for (const k of value.split(','))
68
72
  uses.push({ key: k.trim(), line, plural });
69
73
  else if (!skip && !name.startsWith('data-') && !NOT_TEXT.test(name) && !('data-t-' + name in a)) {
package/dist/i18n.js CHANGED
@@ -13,6 +13,10 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
13
13
  * <span data-t="nav.library">Exercises</span> the element's text
14
14
  * <gg-header title="Settings" data-t-title="settings.title"> an attribute (also comma lists: data-t-options)
15
15
  * <p data-t="count.sets" data-t-args="count=16">16 sets</p> {count} filled in, plural form picked by count
16
+ * <x-row meta="5 exercises · Linear" data-t-meta="{plan.count|count=5} · {plan.linear}">
17
+ * a template: messages in braces, the rest kept
18
+ * <x-row meta="Sun, 4 Oct" data-t-meta="{@date|value=2026-10-04;weekday=short;day=numeric;month=short}">
19
+ * dates and numbers ({@number|value=7920}) by Intl
16
20
  *
17
21
  * Opened with ?lang=<locale> (the canvas's Language switch, and check), the page's text is replaced from that
18
22
  * locale's messages before its components render, and the page gets lang and dir. Two pseudo-languages need no
@@ -105,7 +109,25 @@ export async function pagePayload(locale) {
105
109
  function runtime(p) {
106
110
  const missing = new Set(p.missing);
107
111
  const plural = new Intl.PluralRules(p.lang);
112
+ // {@date|value=…;weekday=short…} and {@number|value=…;maximumFractionDigits=1…}: Intl's options, in the locale.
113
+ const intl = (key, args) => {
114
+ const opts = {};
115
+ for (const [k, v] of Object.entries(args))
116
+ if (k !== 'value')
117
+ opts[k] = v === 'true' ? true : v === 'false' ? false : /^\d+$/.test(v) ? Number(v) : v;
118
+ try {
119
+ return key === '@date'
120
+ ? // As written, whatever the viewer's time zone: the value is read and shown in UTC.
121
+ new Intl.DateTimeFormat(p.lang, { timeZone: 'UTC', ...opts }).format(new Date(/T.*(Z|[+-]\d\d:?\d\d)$/.test(args.value ?? '') || !/T/.test(args.value ?? '') ? (args.value ?? '') : args.value + 'Z'))
122
+ : new Intl.NumberFormat(p.lang, opts).format(Number(args.value));
123
+ }
124
+ catch {
125
+ return undefined;
126
+ }
127
+ };
108
128
  const t = (key, args) => {
129
+ if (key === '@date' || key === '@number')
130
+ return intl(key, args ?? {});
109
131
  let msg;
110
132
  if (args && args.count !== undefined) {
111
133
  const n = Number(args.count);
@@ -116,11 +138,11 @@ function runtime(p) {
116
138
  return undefined;
117
139
  return msg.replace(/\{(\w+)\}/g, (m, k) => (args && args[k] !== undefined ? args[k] : m));
118
140
  };
119
- const known = (key) => key in p.messages || key + '.other' in p.messages;
141
+ const known = (key) => key.startsWith('@') || key in p.messages || key + '.other' in p.messages;
120
142
  const lacks = (key) => missing.has(key) || missing.has(key + '.other') || !known(key);
121
- const parseArgs = (s) => {
143
+ const parseArgs = (s, sep = ',') => {
122
144
  const out = {};
123
- for (const part of (s ?? '').split(',')) {
145
+ for (const part of (s ?? '').split(sep)) {
124
146
  const i = part.indexOf('=');
125
147
  if (i > 0)
126
148
  out[part.slice(0, i).trim()] = part.slice(i + 1).trim();
@@ -141,21 +163,41 @@ function runtime(p) {
141
163
  for (const a of Array.from(el.attributes)) {
142
164
  if (a.name !== 'data-t' && !a.name.startsWith('data-t-'))
143
165
  continue;
166
+ if (a.name === 'data-t-args' || a.name === 'data-t-skip')
167
+ continue;
144
168
  const args = parseArgs(el.getAttribute('data-t-args'));
145
- if (a.name === 'data-t') {
146
- const v = t(a.value, args);
147
- if (v !== undefined)
148
- el.textContent = v;
169
+ let v;
170
+ if (a.value.includes('{')) {
171
+ // A template: each {key} or {key|name=value;name=value} becomes its message, the rest stays as written.
172
+ let whole = true;
173
+ v = a.value.replace(/\{([^{}|]+)(?:\|([^}]*))?\}/g, (_m, key, inline) => {
174
+ keys.push(key);
175
+ const r = t(key, { ...args, ...parseArgs(inline, ';') });
176
+ if (r === undefined)
177
+ whole = false;
178
+ return r ?? '';
179
+ });
180
+ if (!whole)
181
+ v = undefined;
182
+ }
183
+ else if (a.name === 'data-t') {
184
+ v = t(a.value, args);
149
185
  keys.push(a.value);
150
186
  }
151
- else if (a.name !== 'data-t-args' && a.name !== 'data-t-skip') {
152
- const attr = a.name.slice(7);
187
+ else {
188
+ // A list attribute (options="A,B"): one key per item.
153
189
  const parts = a.value.split(',').map((k) => k.trim());
154
190
  const vals = parts.map((k) => t(k, args));
155
- if (vals.every((v) => v !== undefined))
156
- el.setAttribute(attr, vals.join(','));
191
+ if (vals.every((x) => x !== undefined))
192
+ v = vals.join(',');
157
193
  keys.push(...parts);
158
194
  }
195
+ if (v === undefined)
196
+ continue;
197
+ if (a.name === 'data-t')
198
+ el.textContent = v;
199
+ else
200
+ el.setAttribute(a.name.slice(7), v);
159
201
  }
160
202
  if (!keys.length)
161
203
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitch2",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
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",
@@ -40,8 +40,19 @@ Use the app's own files. Never copy messages into the design folder: two copies
40
40
  <input placeholder="Search" data-t-placeholder="exercises.search" />
41
41
  <p data-t="workout.sets" data-t-args="count=16">16 sets</p> <!-- {count} filled in, plural form by count -->
42
42
  <p translate="no">Dumbbell Bench Press</p> <!-- sample content: names, user input, numbers -->
43
+ <s2-row meta="5 exercises · Linear" data-t-meta="{plan.exercises|count=5} · {plan.linear}">
43
44
  ```
44
45
 
46
+ A value with braces is a **template**: each `{key}` or `{key|name=value;name=value}` becomes its message and
47
+ the rest stays as written. Use it for lines the app builds from several messages and data (a meta line, a
48
+ list of `label=url` links: `data-t-links="{site.features}=#features,{site.docs}=/docs"`); sample data inside
49
+ it stays literal. Without braces, `data-t-<attr>` on a comma list takes one key per item.
50
+
51
+ Dates and numbers are written by the locale too: `{@date|value=2026-10-04;weekday=short;day=numeric;month=short}`
52
+ ("Sun, 4 Oct", "вс, 4 окт.") and `{@number|value=7920}` ("7,920", "7 920"); the options are `Intl.DateTimeFormat`'s and
53
+ `Intl.NumberFormat`'s, and dates are read and shown in UTC. Pair a number with the unit's message:
54
+ `{@number|value=82.5} {unit.kg}`.
55
+
45
56
  - Keep the source text in the markup: the screen reads correctly without stitch2, and a missing key shows it.
46
57
  - Key every string the product owns: labels, buttons, headings, empty states, hints, units, tab names, the
47
58
  placeholder and aria-label too. Sample data (an exercise name, a user's note, a number) gets `translate="no"`.