@yuneta/gobj-ui 7.26.26 → 7.26.27

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
@@ -3625,10 +3625,28 @@ Plurals are compared by language (since `7.26.26`): `songs_one` /
3625
3625
  `songs_few` / `songs_many` / `songs_other` are one key, and each locale must
3626
3626
  carry the categories of ITS language (`Intl.PluralRules`: English `one`,
3627
3627
  `other`; Russian `one`, `few`, `many`, `other`; Chinese `other`). The key with
3628
- no suffix serves `other` and `many`, as i18next falls back to it; `zero`,
3629
- `one`, `two` and `few` need their own form. A value of the shell declaration
3630
- that is not lower-case ASCII (an endonym, `"中文"`, in a language menu) is a
3631
- literal, not a key.
3628
+ no suffix serves `other` and `many`; `zero`, `one`, `two` and `few` need their
3629
+ own form. That is a policy of GRAMMAR, not a need of i18next, which falls back
3630
+ to the bare key for any form that is missing: a Russian count of 3 rendered
3631
+ with the bare key reads wrong, and the check says so. A key that only ends
3632
+ like a form (`pick_one` alone) is a plain key; ordinals (`place_ordinal_one`)
3633
+ are checked with the ordinal categories of each language. A value of the shell
3634
+ declaration is a literal when it is the endonym of one of the app's locales
3635
+ (the `name` its catalog exports: `"中文"`, `"español"`); any other lower-case
3636
+ text is a key, `"configuración"` included.
3637
+
3638
+ **A key that reaches `t()` as data is PUBLISHED by its module** (since
3639
+ `7.26.27`): an exported `<NAME>_I18N_KEYS` array of string literals, which
3640
+ `validate-locales` reads in every module the app mounts and asks the locales
3641
+ for. `yui_dev.js` publishes the 51 keys of the Developer window
3642
+ (`YUI_DEV_I18N_KEYS`) -- they used to be a comment each consumer copied, and
3643
+ one consumer lacked 35 --, `c_g6_nodes_tree.js` those of its shape tables and
3644
+ popover labels (`G6_NODES_TREE_I18N_KEYS`). A module that hands a key to `t()`
3645
+ by variable adds it to such a list:
3646
+
3647
+ ```js
3648
+ export const MY_VIEW_I18N_KEYS = ['shape', 'radius', 'apply to'];
3649
+ ```
3632
3650
 
3633
3651
  Up to `7.26.24` each app carried its own copy, and the copies had drifted into
3634
3652
  three families: only one read the codes of the schema editor, only the other
@@ -8988,7 +8988,7 @@ function destroy_widgets(gobj, $container) {
8988
8988
  * ids would break the association for both. The key is the
8989
8989
  * label's own, so a language change reaches it.
8990
8990
  ************************************************************/
8991
- function name_form_control($field, conf) {
8991
+ function name_form_control$1($field, conf) {
8992
8992
  if (!$field || typeof $field.querySelector !== "function" || !conf) return;
8993
8993
  let key = conf.label_i18n || conf.label || "";
8994
8994
  if (!key) return;
@@ -9059,7 +9059,7 @@ function build_html_form(gobj, $form, prefix, template) {
9059
9059
  if (placeholders[key]) html_field_conf.placeholder = placeholders[key];
9060
9060
  if (html_field_conf.tag) {
9061
9061
  let $field = create_form_field(gobj, $form, prefix, field_desc, html_field_conf);
9062
- name_form_control($field, html_field_conf);
9062
+ name_form_control$1($field, html_field_conf);
9063
9063
  $form.appendChild($field);
9064
9064
  }
9065
9065
  });
@@ -63793,19 +63793,34 @@ function touch_control_css() {
63793
63793
  if (!coarse_pointer()) return "";
63794
63794
  return "min-height:44px;font-size:16px;";
63795
63795
  }
63796
+ var form_control_seq = 0;
63796
63797
  function create_form_label(parent, text) {
63797
63798
  let label = document.createElement("label");
63798
63799
  label.textContent = (0, i18next.t)(text);
63800
+ label.setAttribute("data-i18n", text);
63799
63801
  label.style.cssText = "display:block;margin-bottom:4px;font-weight:500;";
63800
63802
  parent.appendChild(label);
63801
63803
  return label;
63802
63804
  }
63805
+ function name_form_control(parent, control) {
63806
+ let label = parent.lastElementChild;
63807
+ if (!label || label.tagName !== "LABEL") return;
63808
+ let key = label.getAttribute("data-i18n");
63809
+ if (!key) return;
63810
+ control.id = "g6-form-control-" + ++form_control_seq;
63811
+ label.htmlFor = control.id;
63812
+ control.setAttribute("title", (0, i18next.t)(key));
63813
+ control.setAttribute("data-i18n-title", key);
63814
+ control.setAttribute("aria-label", (0, i18next.t)(key));
63815
+ control.setAttribute("data-i18n-aria-label", key);
63816
+ }
63803
63817
  function create_form_color_input(parent, value, onInput) {
63804
63818
  let input = document.createElement("input");
63805
63819
  input.type = "color";
63806
63820
  input.value = value;
63807
63821
  input.style.cssText = "width:100%;height:30px;padding:0;border:1px solid var(--bulma-border-weak, #d9d9d9);border-radius:4px;cursor:pointer;margin-bottom:10px;" + touch_control_css();
63808
63822
  if (onInput) input.addEventListener("input", onInput);
63823
+ name_form_control(parent, input);
63809
63824
  parent.appendChild(input);
63810
63825
  return input;
63811
63826
  }
@@ -63817,6 +63832,7 @@ function create_form_number_input(parent, value, min, max, onInput) {
63817
63832
  input.value = value;
63818
63833
  input.style.cssText = "width:100%;padding:4px 6px;background:var(--bulma-scheme-main-bis, #fff);color:var(--bulma-text-strong, #333);border:1px solid var(--bulma-border-weak, #d9d9d9);border-radius:4px;box-sizing:border-box;margin-bottom:10px;" + touch_control_css();
63819
63834
  if (onInput) input.addEventListener("input", onInput);
63835
+ name_form_control(parent, input);
63820
63836
  parent.appendChild(input);
63821
63837
  return input;
63822
63838
  }
@@ -63826,9 +63842,11 @@ function create_form_select(parent, options, marginBottom) {
63826
63842
  for (let opt of options) {
63827
63843
  let o = document.createElement("option");
63828
63844
  o.value = opt.value;
63829
- o.textContent = opt.label;
63845
+ o.textContent = (0, i18next.t)(opt.i18n);
63846
+ o.setAttribute("data-i18n", opt.i18n);
63830
63847
  select.appendChild(o);
63831
63848
  }
63849
+ name_form_control(parent, select);
63832
63850
  parent.appendChild(select);
63833
63851
  return select;
63834
63852
  }
@@ -64587,7 +64605,7 @@ function show_port_popover(gobj) {
64587
64605
  let shapeSelect = create_form_select(popover, PORT_SHAPES.map((shape) => {
64588
64606
  return {
64589
64607
  value: shape,
64590
- label: (0, i18next.t)(shape)
64608
+ i18n: shape
64591
64609
  };
64592
64610
  }));
64593
64611
  shapeSelect.value = orig_shape;
@@ -64598,15 +64616,15 @@ function show_port_popover(gobj) {
64598
64616
  let scopeSelect = create_form_select(popover, [
64599
64617
  {
64600
64618
  value: "this",
64601
- label: (0, i18next.t)("this port")
64619
+ i18n: "this port"
64602
64620
  },
64603
64621
  {
64604
64622
  value: "same_topic",
64605
- label: (0, i18next.t)("same port in topic")
64623
+ i18n: "same port in topic"
64606
64624
  },
64607
64625
  {
64608
64626
  value: "all",
64609
- label: (0, i18next.t)("all ports")
64627
+ i18n: "all ports"
64610
64628
  }
64611
64629
  ]);
64612
64630
  create_form_button_row(popover, [{
@@ -65294,15 +65312,15 @@ function show_edge_popover(gobj) {
65294
65312
  let scopeSelect = create_form_select(popover, [
65295
65313
  {
65296
65314
  value: "this",
65297
- label: (0, i18next.t)("this edge")
65315
+ i18n: "this edge"
65298
65316
  },
65299
65317
  {
65300
65318
  value: "same_type",
65301
- label: (0, i18next.t)("same type edges")
65319
+ i18n: "same type edges"
65302
65320
  },
65303
65321
  {
65304
65322
  value: "all",
65305
- label: (0, i18next.t)("all edges")
65323
+ i18n: "all edges"
65306
65324
  }
65307
65325
  ]);
65308
65326
  create_form_button_row(popover, [{
@@ -65446,7 +65464,7 @@ function show_node_popover(gobj) {
65446
65464
  let shapeSelect = create_form_select(popover, NODE_SHAPES.map((shape) => {
65447
65465
  return {
65448
65466
  value: shape,
65449
- label: (0, i18next.t)(shape)
65467
+ i18n: shape
65450
65468
  };
65451
65469
  }));
65452
65470
  shapeSelect.value = nodeData.data && nodeData.data.desc ? node_figure_of(gobj, nodeData.data.desc, nodeData.data.graph_props || {}) : DEFAULT_NODE_SHAPE;
@@ -65455,15 +65473,15 @@ function show_node_popover(gobj) {
65455
65473
  let scopeSelect = create_form_select(popover, [
65456
65474
  {
65457
65475
  value: "this",
65458
- label: (0, i18next.t)("this node")
65476
+ i18n: "this node"
65459
65477
  },
65460
65478
  {
65461
65479
  value: "same_topic",
65462
- label: (0, i18next.t)("same topic nodes")
65480
+ i18n: "same topic nodes"
65463
65481
  },
65464
65482
  {
65465
65483
  value: "all",
65466
- label: (0, i18next.t)("all nodes")
65484
+ i18n: "all nodes"
65467
65485
  }
65468
65486
  ]);
65469
65487
  create_form_button_row(popover, [{
@@ -66170,7 +66188,11 @@ function show_create_popover(gobj) {
66170
66188
  });
66171
66189
  let idLabel = document.createElement("div");
66172
66190
  idLabel.style.cssText = "margin-bottom:4px;font-weight:500;";
66173
- idLabel.textContent = "Id:";
66191
+ let idWord = document.createElement("span");
66192
+ idWord.textContent = (0, i18next.t)("node id");
66193
+ idWord.setAttribute("data-i18n", "node id");
66194
+ idLabel.appendChild(idWord);
66195
+ idLabel.appendChild(document.createTextNode(":"));
66174
66196
  popover.appendChild(idLabel);
66175
66197
  let idInput = document.createElement("input");
66176
66198
  idInput.type = "text";
@@ -8983,7 +8983,7 @@ function destroy_widgets(gobj, $container) {
8983
8983
  * ids would break the association for both. The key is the
8984
8984
  * label's own, so a language change reaches it.
8985
8985
  ************************************************************/
8986
- function name_form_control($field, conf) {
8986
+ function name_form_control$1($field, conf) {
8987
8987
  if (!$field || typeof $field.querySelector !== "function" || !conf) return;
8988
8988
  let key = conf.label_i18n || conf.label || "";
8989
8989
  if (!key) return;
@@ -9054,7 +9054,7 @@ function build_html_form(gobj, $form, prefix, template) {
9054
9054
  if (placeholders[key]) html_field_conf.placeholder = placeholders[key];
9055
9055
  if (html_field_conf.tag) {
9056
9056
  let $field = create_form_field(gobj, $form, prefix, field_desc, html_field_conf);
9057
- name_form_control($field, html_field_conf);
9057
+ name_form_control$1($field, html_field_conf);
9058
9058
  $form.appendChild($field);
9059
9059
  }
9060
9060
  });
@@ -63788,19 +63788,34 @@ function touch_control_css() {
63788
63788
  if (!coarse_pointer()) return "";
63789
63789
  return "min-height:44px;font-size:16px;";
63790
63790
  }
63791
+ var form_control_seq = 0;
63791
63792
  function create_form_label(parent, text) {
63792
63793
  let label = document.createElement("label");
63793
63794
  label.textContent = t(text);
63795
+ label.setAttribute("data-i18n", text);
63794
63796
  label.style.cssText = "display:block;margin-bottom:4px;font-weight:500;";
63795
63797
  parent.appendChild(label);
63796
63798
  return label;
63797
63799
  }
63800
+ function name_form_control(parent, control) {
63801
+ let label = parent.lastElementChild;
63802
+ if (!label || label.tagName !== "LABEL") return;
63803
+ let key = label.getAttribute("data-i18n");
63804
+ if (!key) return;
63805
+ control.id = "g6-form-control-" + ++form_control_seq;
63806
+ label.htmlFor = control.id;
63807
+ control.setAttribute("title", t(key));
63808
+ control.setAttribute("data-i18n-title", key);
63809
+ control.setAttribute("aria-label", t(key));
63810
+ control.setAttribute("data-i18n-aria-label", key);
63811
+ }
63798
63812
  function create_form_color_input(parent, value, onInput) {
63799
63813
  let input = document.createElement("input");
63800
63814
  input.type = "color";
63801
63815
  input.value = value;
63802
63816
  input.style.cssText = "width:100%;height:30px;padding:0;border:1px solid var(--bulma-border-weak, #d9d9d9);border-radius:4px;cursor:pointer;margin-bottom:10px;" + touch_control_css();
63803
63817
  if (onInput) input.addEventListener("input", onInput);
63818
+ name_form_control(parent, input);
63804
63819
  parent.appendChild(input);
63805
63820
  return input;
63806
63821
  }
@@ -63812,6 +63827,7 @@ function create_form_number_input(parent, value, min, max, onInput) {
63812
63827
  input.value = value;
63813
63828
  input.style.cssText = "width:100%;padding:4px 6px;background:var(--bulma-scheme-main-bis, #fff);color:var(--bulma-text-strong, #333);border:1px solid var(--bulma-border-weak, #d9d9d9);border-radius:4px;box-sizing:border-box;margin-bottom:10px;" + touch_control_css();
63814
63829
  if (onInput) input.addEventListener("input", onInput);
63830
+ name_form_control(parent, input);
63815
63831
  parent.appendChild(input);
63816
63832
  return input;
63817
63833
  }
@@ -63821,9 +63837,11 @@ function create_form_select(parent, options, marginBottom) {
63821
63837
  for (let opt of options) {
63822
63838
  let o = document.createElement("option");
63823
63839
  o.value = opt.value;
63824
- o.textContent = opt.label;
63840
+ o.textContent = t(opt.i18n);
63841
+ o.setAttribute("data-i18n", opt.i18n);
63825
63842
  select.appendChild(o);
63826
63843
  }
63844
+ name_form_control(parent, select);
63827
63845
  parent.appendChild(select);
63828
63846
  return select;
63829
63847
  }
@@ -64582,7 +64600,7 @@ function show_port_popover(gobj) {
64582
64600
  let shapeSelect = create_form_select(popover, PORT_SHAPES.map((shape) => {
64583
64601
  return {
64584
64602
  value: shape,
64585
- label: t(shape)
64603
+ i18n: shape
64586
64604
  };
64587
64605
  }));
64588
64606
  shapeSelect.value = orig_shape;
@@ -64593,15 +64611,15 @@ function show_port_popover(gobj) {
64593
64611
  let scopeSelect = create_form_select(popover, [
64594
64612
  {
64595
64613
  value: "this",
64596
- label: t("this port")
64614
+ i18n: "this port"
64597
64615
  },
64598
64616
  {
64599
64617
  value: "same_topic",
64600
- label: t("same port in topic")
64618
+ i18n: "same port in topic"
64601
64619
  },
64602
64620
  {
64603
64621
  value: "all",
64604
- label: t("all ports")
64622
+ i18n: "all ports"
64605
64623
  }
64606
64624
  ]);
64607
64625
  create_form_button_row(popover, [{
@@ -65289,15 +65307,15 @@ function show_edge_popover(gobj) {
65289
65307
  let scopeSelect = create_form_select(popover, [
65290
65308
  {
65291
65309
  value: "this",
65292
- label: t("this edge")
65310
+ i18n: "this edge"
65293
65311
  },
65294
65312
  {
65295
65313
  value: "same_type",
65296
- label: t("same type edges")
65314
+ i18n: "same type edges"
65297
65315
  },
65298
65316
  {
65299
65317
  value: "all",
65300
- label: t("all edges")
65318
+ i18n: "all edges"
65301
65319
  }
65302
65320
  ]);
65303
65321
  create_form_button_row(popover, [{
@@ -65441,7 +65459,7 @@ function show_node_popover(gobj) {
65441
65459
  let shapeSelect = create_form_select(popover, NODE_SHAPES.map((shape) => {
65442
65460
  return {
65443
65461
  value: shape,
65444
- label: t(shape)
65462
+ i18n: shape
65445
65463
  };
65446
65464
  }));
65447
65465
  shapeSelect.value = nodeData.data && nodeData.data.desc ? node_figure_of(gobj, nodeData.data.desc, nodeData.data.graph_props || {}) : DEFAULT_NODE_SHAPE;
@@ -65450,15 +65468,15 @@ function show_node_popover(gobj) {
65450
65468
  let scopeSelect = create_form_select(popover, [
65451
65469
  {
65452
65470
  value: "this",
65453
- label: t("this node")
65471
+ i18n: "this node"
65454
65472
  },
65455
65473
  {
65456
65474
  value: "same_topic",
65457
- label: t("same topic nodes")
65475
+ i18n: "same topic nodes"
65458
65476
  },
65459
65477
  {
65460
65478
  value: "all",
65461
- label: t("all nodes")
65479
+ i18n: "all nodes"
65462
65480
  }
65463
65481
  ]);
65464
65482
  create_form_button_row(popover, [{
@@ -66165,7 +66183,11 @@ function show_create_popover(gobj) {
66165
66183
  });
66166
66184
  let idLabel = document.createElement("div");
66167
66185
  idLabel.style.cssText = "margin-bottom:4px;font-weight:500;";
66168
- idLabel.textContent = "Id:";
66186
+ let idWord = document.createElement("span");
66187
+ idWord.textContent = t("node id");
66188
+ idWord.setAttribute("data-i18n", "node id");
66189
+ idLabel.appendChild(idWord);
66190
+ idLabel.appendChild(document.createTextNode(":"));
66169
66191
  popover.appendChild(idLabel);
66170
66192
  let idInput = document.createElement("input");
66171
66193
  idInput.type = "text";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuneta/gobj-ui",
3
- "version": "7.26.26",
3
+ "version": "7.26.27",
4
4
  "type": "module",
5
5
  "main": "dist/gobj-ui.cjs.js",
6
6
  "module": "dist/gobj-ui.es.js",
@@ -22,7 +22,14 @@
22
22
  * `zero`, `one`, `two` and `few` change the grammar and need
23
23
  * their own form when the language has them. A form the
24
24
  * language never uses is a violation; `_zero` is always
25
- * allowed (i18next uses it for a count of 0).
25
+ * allowed (i18next uses it for a count of 0). i18next itself
26
+ * falls back to the bare key for ANY missing form: asking for
27
+ * `one`/`few` is a policy of grammar, not of i18next. A key
28
+ * that only ENDS like a form (`pick_one` alone) is a plain
29
+ * key: a group is a plural when a locale has its bare base,
30
+ * its `_other` or two of its forms. Ordinals
31
+ * (`place_ordinal_one`) are checked with the ordinal
32
+ * categories of each language.
26
33
  * 4. No duplicate key in a locale file (an object literal keeps
27
34
  * the LAST one and says nothing -- a stale entry silently
28
35
  * overrides a new one; it happened to "last" and "loading").
@@ -31,7 +38,10 @@
31
38
  * fine and simply never changes language -- invisible by
32
39
  * construction, and it shipped: a Settings `label` column).
33
40
  *
34
- * Rule 5 reads the app's own source (recursively), the shell
41
+ * Rule 5 reads the app's own source (recursively, .js and .mjs;
42
+ * not tests, not a locales/ directory), the keys a module
43
+ * PUBLISHES as data (an exported `<NAME>_I18N_KEYS` array: the
44
+ * keys it hands to t() as variables, yui_dev.js's 51), the shell
35
45
  * declaration (`src/app_config.json`) and the gobj-ui modules the
36
46
  * app MOUNTS, transitively -- deep, through the barrel, or by a
37
47
  * dynamic import(): the library translates through the APP's
@@ -59,7 +69,8 @@
59
69
  * `attr_keys` names properties whose string value is an i18n key
60
70
  * (`app_title: "central"`), `array_keys` properties whose value is
61
71
  * an array of keys. A key the app hands over that way reaches no
62
- * t() the scan can read.
72
+ * t() the scan can read. A name declared that no source uses is a
73
+ * violation (a typo would turn its check off).
63
74
  *
64
75
  * A locale is a file <code>.js of the locales directory (en.js,
65
76
  * es.js, pt-br.js; flags.js and locales.js are not) exporting
@@ -84,7 +95,7 @@ import {pathToFileURL} from "url";
84
95
 
85
96
  const ASCII_RE = /^[\x20-\x7e]+$/;
86
97
  const LOCALE_FILE_RE = /^([a-z]{2,3}(?:[-_][a-zA-Z]{2,4})?)\.js$/;
87
- const PLURAL_RE = /^(.+)_(zero|one|two|few|many|other)$/;
98
+ const PLURAL_RE = /^(.+?)(_ordinal)?_(zero|one|two|few|many|other)$/;
88
99
 
89
100
  /* This package: the library the app builds with. */
90
101
  const LIB = new URL("../", import.meta.url);
@@ -201,47 +212,64 @@ async function load_locales(locales_dir)
201
212
  codes = ["en", ...codes.filter((c) => c !== "en")];
202
213
  }
203
214
  let locales = {};
215
+ let names = new Set();
204
216
  for(let code of codes) {
205
- let mod = await import(new URL(`${code}.js`, locales_dir).href);
206
- let locale = mod[code] || mod[code.replace(/-/g, "_")] || mod.default;
217
+ let mod;
218
+ try {
219
+ mod = await import(new URL(`${code}.js`, locales_dir).href);
220
+ } catch(e) {
221
+ /* A helper that happens to have a locale's name (app.js), and
222
+ * needs a browser to load: said and skipped, not a crash */
223
+ note(`${code}.js cannot be loaded here (${e.message}): not a locale, skipped`);
224
+ continue;
225
+ }
226
+ let camel = code.replace(/[-_]([a-z])/gi, (m, c) => c.toUpperCase()); // pt-br -> ptBr
227
+ let upper = code.replace(/[-_]([a-z]+)/gi, (m, c) => c.toUpperCase()); // pt-br -> ptBR
228
+ let locale = mod[code] || mod[code.replace(/-/g, "_")] || mod[camel] || mod[upper] || mod.default;
207
229
  if(!locale || !locale.translation || typeof locale.translation !== "object") {
208
230
  note(`${code}.js exports no {${code}} with a translation dict: not a locale, skipped`);
209
231
  continue;
210
232
  }
211
233
  locales[code] = locale.translation;
234
+ if(typeof locale.name === "string" && locale.name) {
235
+ names.add(locale.name); // its endonym: a literal of a language menu
236
+ }
212
237
  }
213
238
  if(Object.keys(locales).length === 0) {
214
239
  abort(`no locale (<code>.js exporting {<code>: {translation}}) in ${locales_dir.pathname}`);
215
240
  }
216
- return locales;
241
+ return {locales, names};
217
242
  }
218
243
 
219
244
  /***************************************************************
220
245
  * The plural categories of a language (Intl.PluralRules)
221
246
  ***************************************************************/
222
- function plural_categories(code)
247
+ function plural_categories(code, ordinal)
223
248
  {
224
249
  try {
225
- return new Set(new Intl.PluralRules(code.replace(/_/g, "-")).resolvedOptions().pluralCategories);
250
+ return new Set(new Intl.PluralRules(code.replace(/_/g, "-"),
251
+ {type: ordinal? "ordinal" : "cardinal"}).resolvedOptions().pluralCategories);
226
252
  } catch(e) {
227
253
  return new Set(["one", "other"]);
228
254
  }
229
255
  }
230
256
 
231
257
  /***************************************************************
232
- * A locale's keys split: plain ones, and {base: Set(categories)}
258
+ * A locale's keys split: plain ones, and {group: Set(categories)}, a
259
+ * group being a base, or "<base>#ordinal" for `<base>_ordinal_<cat>`
233
260
  ***************************************************************/
234
- function split_plurals(keys)
261
+ function split_plurals(keys, plural_groups)
235
262
  {
236
263
  let plain = new Set();
237
264
  let plurals = new Map();
238
265
  for(let k of keys) {
239
266
  let m = k.match(PLURAL_RE);
240
- if(m) {
241
- if(!plurals.has(m[1])) {
242
- plurals.set(m[1], new Set());
267
+ let group = m? m[1] + (m[2]? "#ordinal" : "") : null;
268
+ if(m && plural_groups.has(group)) {
269
+ if(!plurals.has(group)) {
270
+ plurals.set(group, new Set());
243
271
  }
244
- plurals.get(m[1]).add(m[2]);
272
+ plurals.get(group).add(m[3]);
245
273
  } else {
246
274
  plain.add(k);
247
275
  }
@@ -249,20 +277,67 @@ function split_plurals(keys)
249
277
  return {plain, plurals};
250
278
  }
251
279
 
280
+ /***************************************************************
281
+ * Which `<x>_<category>` keys ARE plurals: a key that only ends like one
282
+ * (`pick_one`, `step_two`) is a key. A group is a plural when some locale
283
+ * has its bare base, or its `_other` form, or two of its forms.
284
+ ***************************************************************/
285
+ function find_plural_groups(locales)
286
+ {
287
+ let forms = new Map(); // group -> Set of "code:cat"
288
+ let bare = new Set();
289
+ for(let [code, translation] of Object.entries(locales)) {
290
+ for(let k of Object.keys(translation)) {
291
+ bare.add(k);
292
+ let m = k.match(PLURAL_RE);
293
+ if(m) {
294
+ let group = m[1] + (m[2]? "#ordinal" : "");
295
+ if(!forms.has(group)) {
296
+ forms.set(group, new Map());
297
+ }
298
+ let per = forms.get(group);
299
+ if(!per.has(code)) {
300
+ per.set(code, new Set());
301
+ }
302
+ per.get(code).add(m[3]);
303
+ }
304
+ }
305
+ }
306
+ let groups = new Set();
307
+ for(let [group, per] of forms) {
308
+ let base = group.replace(/#ordinal$/, "");
309
+ let is_plural = bare.has(base) && !group.endsWith("#ordinal");
310
+ for(let cats of per.values()) {
311
+ if(cats.has("other") || cats.size >= 2) {
312
+ is_plural = true;
313
+ }
314
+ }
315
+ if(group.endsWith("#ordinal")) {
316
+ is_plural = true; // `_ordinal_` says it
317
+ }
318
+ if(is_plural) {
319
+ groups.add(group);
320
+ }
321
+ }
322
+ return groups;
323
+ }
324
+
252
325
  /***************************************************************
253
326
  *
254
327
  ***************************************************************/
255
328
  async function main()
256
329
  {
257
330
  let opts = parse_args(process.argv.slice(2));
258
- let locales = await load_locales(opts.locales);
331
+ let {locales, names} = await load_locales(opts.locales);
332
+ opts.language_names = names;
259
333
  let codes = Object.keys(locales);
260
334
 
261
335
  /* Per-locale key checks (ASCII + lower-case) */
336
+ let plural_groups = find_plural_groups(locales);
262
337
  let split = {};
263
338
  for(let code of codes) {
264
339
  let keys = Object.keys(locales[code]);
265
- split[code] = split_plurals(keys);
340
+ split[code] = split_plurals(keys, plural_groups);
266
341
  for(let k of keys) {
267
342
  if(!ASCII_RE.test(k)) {
268
343
  fail(`[${code}] non-ASCII key: ${JSON.stringify(k)}`);
@@ -277,11 +352,9 @@ async function main()
277
352
  * The plural bases, of any locale: a plain key that is one of them
278
353
  * belongs to its plural, and is checked with it
279
354
  */
280
- let bases = new Set();
281
- for(let code of codes) {
282
- for(let base of split[code].plurals.keys()) {
283
- bases.add(base);
284
- }
355
+ let bases = new Set(); // the plain base of each plural group
356
+ for(let group of plural_groups) {
357
+ bases.add(group.replace(/#ordinal$/, ""));
285
358
  }
286
359
 
287
360
  /* Symmetry of the plain keys: every locale carries those of the first */
@@ -309,19 +382,21 @@ async function main()
309
382
  */
310
383
  const SERVED_BY_THE_BASE = new Set(["other", "many"]);
311
384
  for(let code of codes) {
312
- let wanted = plural_categories(code);
313
- for(let base of bases) {
314
- let have = split[code].plurals.get(base) || new Set();
385
+ for(let group of plural_groups) {
386
+ let ordinal = group.endsWith("#ordinal");
387
+ let base = group.replace(/#ordinal$/, "");
388
+ let wanted = plural_categories(code, ordinal);
389
+ let have = split[code].plurals.get(group) || new Set();
315
390
  let has_base = split[code].plain.has(base);
316
391
  for(let c of wanted) {
317
392
  if(!have.has(c) && !(has_base && SERVED_BY_THE_BASE.has(c))) {
318
- fail(`[${code}] plural ${JSON.stringify(base)} lacks the form "${c}" of its language` +
393
+ fail(`[${code}] ${ordinal? "ordinal" : "plural"} ${JSON.stringify(base)} lacks the form "${c}" of its language` +
319
394
  (SERVED_BY_THE_BASE.has(c)? ` (or the key ${JSON.stringify(base)} itself)` : ""));
320
395
  }
321
396
  }
322
397
  for(let c of have) {
323
398
  if(!wanted.has(c) && c !== "zero") {
324
- fail(`[${code}] plural ${JSON.stringify(base)} has a form "${c}" its language never uses`);
399
+ fail(`[${code}] ${ordinal? "ordinal" : "plural"} ${JSON.stringify(base)} has a form "${c}" its language never uses`);
325
400
  }
326
401
  }
327
402
  }
@@ -424,6 +499,11 @@ function collect_used_keys(opts)
424
499
  }
425
500
  let array_re = opts.array_keys.length > 0?
426
501
  re(String.raw`\b(?:` + opts.array_keys.join("|") + String.raw`)\s*:\s*\[([^\]]*)\]`) : null;
502
+ /* A module publishes the keys it hands to t() as DATA (yui_dev.js:
503
+ * YUI_DEV_I18N_KEYS): every key of such an array is asked for */
504
+ let published_re = /\bexport\s+const\s+[A-Z0-9_]+_I18N_KEYS\s*=\s*\[([^\]]*)\]/g;
505
+ let declared = new Set([...opts.attr_keys, ...opts.array_keys]);
506
+ let declared_seen = new Set();
427
507
 
428
508
  let scan = (url) => {
429
509
  /* Comments are not code: the library documents the contract with
@@ -442,15 +522,29 @@ function collect_used_keys(opts)
442
522
  keys.add(k);
443
523
  }
444
524
  }
525
+ let lists = [];
445
526
  if(array_re) {
446
527
  array_re.lastIndex = 0;
447
528
  let f;
448
529
  while((f = array_re.exec(code)) !== null) {
449
- let lit_re = re(STR);
450
- let l;
451
- while((l = lit_re.exec(f[1])) !== null) {
452
- keys.add(literal_of(l));
453
- }
530
+ lists.push(f[1]);
531
+ }
532
+ }
533
+ published_re.lastIndex = 0;
534
+ let pm;
535
+ while((pm = published_re.exec(code)) !== null) {
536
+ lists.push(pm[1]);
537
+ }
538
+ for(let list of lists) {
539
+ let lit_re = re(STR);
540
+ let l;
541
+ while((l = lit_re.exec(list)) !== null) {
542
+ keys.add(literal_of(l));
543
+ }
544
+ }
545
+ for(let name of declared) {
546
+ if(re(String.raw`\b` + name + String.raw`\s*:`).test(code)) {
547
+ declared_seen.add(name);
454
548
  }
455
549
  }
456
550
  return code;
@@ -470,7 +564,14 @@ function collect_used_keys(opts)
470
564
  }
471
565
  }
472
566
 
473
- keys_from_shell_config(opts.config, keys);
567
+ /* A name declared and found in no source is a typo that turns its check off */
568
+ for(let name of declared) {
569
+ if(!declared_seen.has(name)) {
570
+ fail(`package.json "validate-locales" names ${JSON.stringify(name)}, which no source uses: a typo turns its check off`);
571
+ }
572
+ }
573
+
574
+ keys_from_shell_config(opts.config, keys, opts.language_names);
474
575
 
475
576
  /*
476
577
  * ...and the gobj-ui views it MOUNTS. Only the modules actually
@@ -504,10 +605,13 @@ function collect_used_keys(opts)
504
605
  }
505
606
 
506
607
  /***************************************************************
507
- * The .js files of a source directory, recursively: not the tests, not
508
- * the locales (their keys are definitions, not uses), not node_modules
509
- * or a build
608
+ * The .js / .mjs files of a source directory, recursively: not the tests
609
+ * (*.test.js, *.spec.js, __tests__/, test/, tests/), not a locales/
610
+ * directory (its keys are definitions, not uses), not node_modules or a
611
+ * build
510
612
  ***************************************************************/
613
+ const SKIPPED_DIRS = new Set(["node_modules", "dist", "locales", "__tests__", "test", "tests"]);
614
+
511
615
  function source_files(dir, locales_dir)
512
616
  {
513
617
  let out = [];
@@ -519,11 +623,11 @@ function source_files(dir, locales_dir)
519
623
  let url = new URL(f, d);
520
624
  let st = statSync(url);
521
625
  if(st.isDirectory()) {
522
- if(f === "node_modules" || f === "dist" || f.startsWith(".")) {
626
+ if(SKIPPED_DIRS.has(f) || f.startsWith(".")) {
523
627
  continue;
524
628
  }
525
629
  walk(new URL(f + "/", d));
526
- } else if(f.endsWith(".js") && !f.endsWith(".test.js")) {
630
+ } else if(/\.m?js$/.test(f) && !/\.(test|spec)\.m?js$/.test(f)) {
527
631
  out.push(url);
528
632
  }
529
633
  }
@@ -540,15 +644,17 @@ function source_files(dir, locales_dir)
540
644
  * ever -- which is how four toolbar buttons announced themselves as
541
645
  * "toggle language" in Spanish.
542
646
  *
543
- * A value that is not a KEY -- not lower-case ASCII -- is a literal the
544
- * config carries on purpose: "ES/EN" on a language button, an endonym
545
- * ("中文", "Español") in a language menu. `wordmark` and `alt` are
647
+ * A value that is not lower-case is a literal the config carries on
648
+ * purpose ("ES/EN" on a language button); so is the endonym of one of the
649
+ * app's locales (its `name`: "中文", "español") in a language menu. Any
650
+ * other value is a key -- "configuración" is a Spanish literal that forgot
651
+ * its key, and it is reported, as an ASCII one would be. `wordmark` and `alt` are
546
652
  * deliberately out: a brand name is not translated. A key starting with
547
653
  * `_` is a comment.
548
654
  ***************************************************************/
549
655
  const CONFIG_I18N_FIELDS = ["name", "aria_label", "tooltip", "label", "title", "subtitle"];
550
656
 
551
- function keys_from_shell_config(url, keys)
657
+ function keys_from_shell_config(url, keys, language_names)
552
658
  {
553
659
  if(!existsSync(url)) {
554
660
  note(`no ${url.pathname}: no shell keys checked`);
@@ -575,7 +681,7 @@ function keys_from_shell_config(url, keys)
575
681
  continue;
576
682
  }
577
683
  if(CONFIG_I18N_FIELDS.includes(k) && typeof v === "string" && v
578
- && ASCII_RE.test(v) && v === v.toLowerCase()) {
684
+ && v === v.toLowerCase() && !language_names.has(v)) {
579
685
  keys.add(v);
580
686
  }
581
687
  walk(v);
@@ -340,6 +340,19 @@ const SHAPE_SIZE = {
340
340
  * default: a figure is a POINT of the topology, and a circle is
341
341
  * the one figure that says nothing more than that. */
342
342
  const NODE_SHAPES = ['square', 'circle', 'diamond', 'triangle', 'hexagon', 'star'];
343
+
344
+ /* The keys of this view that reach t() as DATA or as an ARGUMENT -- the
345
+ * shape tables, the labels of the property popovers (create_form_label),
346
+ * the confirm button (show_confirm_popover): read by a consumer's
347
+ * validate-locales, which asks the locales for every key of an exported
348
+ * `*_I18N_KEYS` array of a module the app mounts. A shape added to
349
+ * NODE_SHAPES or PORT_SHAPES, or a new label, goes here too (a test checks
350
+ * the shapes). */
351
+ export const G6_NODES_TREE_I18N_KEYS = [
352
+ 'square', 'circle', 'diamond', 'triangle', 'hexagon', 'star',
353
+ 'shape', 'radius', 'apply to', 'line width', 'color', 'fill color', 'stroke color',
354
+ 'delete'
355
+ ];
343
356
  const DEFAULT_NODE_SHAPE = 'circle';
344
357
 
345
358
  /***************************************************************
@@ -4931,15 +4944,43 @@ function touch_control_css()
4931
4944
  * one id.
4932
4945
  */
4933
4946
 
4947
+ /*
4948
+ * A form of the property popovers is a LABEL followed by its control. The
4949
+ * label carries its key (`text` is one) and the control built next is
4950
+ * NAMED by it: a `<label>` beside a control names nothing, so the control
4951
+ * gets the label's `for`, and its own `title` and `aria-label`, with their
4952
+ * keys (REV-CLOUD V19.4.1: 11 controls without a name).
4953
+ */
4954
+ let form_control_seq = 0;
4955
+
4934
4956
  function create_form_label(parent, text)
4935
4957
  {
4936
4958
  let label = document.createElement('label');
4937
4959
  label.textContent = t(text);
4960
+ label.setAttribute('data-i18n', text);
4938
4961
  label.style.cssText = 'display:block;margin-bottom:4px;font-weight:500;';
4939
4962
  parent.appendChild(label);
4940
4963
  return label;
4941
4964
  }
4942
4965
 
4966
+ function name_form_control(parent, control)
4967
+ {
4968
+ let label = parent.lastElementChild;
4969
+ if(!label || label.tagName !== 'LABEL') {
4970
+ return;
4971
+ }
4972
+ let key = label.getAttribute('data-i18n');
4973
+ if(!key) {
4974
+ return;
4975
+ }
4976
+ control.id = 'g6-form-control-' + (++form_control_seq);
4977
+ label.htmlFor = control.id;
4978
+ control.setAttribute('title', t(key));
4979
+ control.setAttribute('data-i18n-title', key);
4980
+ control.setAttribute('aria-label', t(key));
4981
+ control.setAttribute('data-i18n-aria-label', key);
4982
+ }
4983
+
4943
4984
  function create_form_color_input(parent, value, onInput)
4944
4985
  {
4945
4986
  let input = document.createElement('input');
@@ -4952,6 +4993,7 @@ function create_form_color_input(parent, value, onInput)
4952
4993
  if(onInput) {
4953
4994
  input.addEventListener('input', onInput);
4954
4995
  }
4996
+ name_form_control(parent, input);
4955
4997
  parent.appendChild(input);
4956
4998
  return input;
4957
4999
  }
@@ -4976,6 +5018,7 @@ function create_form_number_input(parent, value, min, max, onInput)
4976
5018
  if(onInput) {
4977
5019
  input.addEventListener('input', onInput);
4978
5020
  }
5021
+ name_form_control(parent, input);
4979
5022
  parent.appendChild(input);
4980
5023
  return input;
4981
5024
  }
@@ -4991,11 +5034,14 @@ function create_form_select(parent, options, marginBottom)
4991
5034
  'box-sizing:border-box;margin-bottom:' + (marginBottom || '12px') + ';' +
4992
5035
  touch_control_css();
4993
5036
  for(let opt of options) {
5037
+ /* `i18n` is the option's key: it carries it, and `value` stays explicit */
4994
5038
  let o = document.createElement('option');
4995
5039
  o.value = opt.value;
4996
- o.textContent = opt.label;
5040
+ o.textContent = t(opt.i18n);
5041
+ o.setAttribute('data-i18n', opt.i18n);
4997
5042
  select.appendChild(o);
4998
5043
  }
5044
+ name_form_control(parent, select);
4999
5045
  parent.appendChild(select);
5000
5046
  return select;
5001
5047
  }
@@ -6138,7 +6184,7 @@ function show_port_popover(gobj)
6138
6184
 
6139
6185
  create_form_label(popover, 'shape');
6140
6186
  let shapeSelect = create_form_select(popover, PORT_SHAPES.map((shape) => {
6141
- return {value: shape, label: t(shape)};
6187
+ return {value: shape, i18n: shape};
6142
6188
  }));
6143
6189
  shapeSelect.value = orig_shape;
6144
6190
  shapeSelect.addEventListener('change', preview_port);
@@ -6148,9 +6194,9 @@ function show_port_popover(gobj)
6148
6194
 
6149
6195
  create_form_label(popover, 'apply to');
6150
6196
  let scopeSelect = create_form_select(popover, [
6151
- {value: 'this', label: t('this port')},
6152
- {value: 'same_topic', label: t('same port in topic')},
6153
- {value: 'all', label: t('all ports')},
6197
+ {value: 'this', i18n: 'this port'},
6198
+ {value: 'same_topic', i18n: 'same port in topic'},
6199
+ {value: 'all', i18n: 'all ports'},
6154
6200
  ]);
6155
6201
 
6156
6202
  create_form_button_row(popover, [
@@ -7100,9 +7146,9 @@ function show_edge_popover(gobj)
7100
7146
  // Apply-to scope
7101
7147
  create_form_label(popover, 'apply to');
7102
7148
  let scopeSelect = create_form_select(popover, [
7103
- { value: 'this', label: t('this edge') },
7104
- { value: 'same_type', label: t('same type edges') },
7105
- { value: 'all', label: t('all edges') },
7149
+ { value: 'this', i18n: 'this edge' },
7150
+ { value: 'same_type', i18n: 'same type edges' },
7151
+ { value: 'all', i18n: 'all edges' },
7106
7152
  ]);
7107
7153
 
7108
7154
  create_form_button_row(popover, [
@@ -7339,7 +7385,7 @@ function show_node_popover(gobj)
7339
7385
  * reshape either way. */
7340
7386
  create_form_label(popover, 'shape');
7341
7387
  let shapeSelect = create_form_select(popover, NODE_SHAPES.map((shape) => {
7342
- return {value: shape, label: t(shape)};
7388
+ return {value: shape, i18n: shape};
7343
7389
  }));
7344
7390
  shapeSelect.value = nodeData.data && nodeData.data.desc
7345
7391
  ? node_figure_of(gobj, nodeData.data.desc, nodeData.data.graph_props || {})
@@ -7349,9 +7395,9 @@ function show_node_popover(gobj)
7349
7395
  // Apply-to scope
7350
7396
  create_form_label(popover, 'apply to');
7351
7397
  let scopeSelect = create_form_select(popover, [
7352
- { value: 'this', label: t('this node') },
7353
- { value: 'same_topic', label: t('same topic nodes') },
7354
- { value: 'all', label: t('all nodes') },
7398
+ { value: 'this', i18n: 'this node' },
7399
+ { value: 'same_topic', i18n: 'same topic nodes' },
7400
+ { value: 'all', i18n: 'all nodes' },
7355
7401
  ]);
7356
7402
 
7357
7403
  create_form_button_row(popover, [
@@ -8493,7 +8539,11 @@ function show_create_popover(gobj)
8493
8539
  // Node ID input
8494
8540
  let idLabel = document.createElement('div');
8495
8541
  idLabel.style.cssText = 'margin-bottom:4px;font-weight:500;';
8496
- idLabel.textContent = 'Id:';
8542
+ let idWord = document.createElement('span');
8543
+ idWord.textContent = t('node id');
8544
+ idWord.setAttribute('data-i18n', 'node id');
8545
+ idLabel.appendChild(idWord);
8546
+ idLabel.appendChild(document.createTextNode(':'));
8497
8547
  popover.appendChild(idLabel);
8498
8548
 
8499
8549
  let idInput = document.createElement('input');
@@ -0,0 +1,39 @@
1
+ /***********************************************************************
2
+ * g6_nodes_tree_i18n_keys.test.js
3
+ *
4
+ * The shape tables of c_g6_nodes_tree.js reach t() as DATA, so the
5
+ * view publishes their keys in G6_NODES_TREE_I18N_KEYS, which a
6
+ * consumer's validate-locales asks the locales for. A shape added to
7
+ * a table and not to the list would render raw in every language.
8
+ *
9
+ * Copyright (c) 2026, ArtGins.
10
+ * All Rights Reserved.
11
+ ***********************************************************************/
12
+ import {describe, test, expect} from "vitest";
13
+ import {readFileSync} from "node:fs";
14
+ import {dirname, join} from "node:path";
15
+ import {fileURLToPath} from "node:url";
16
+
17
+ const SRC = readFileSync(join(dirname(fileURLToPath(import.meta.url)), "c_g6_nodes_tree.js"), "utf8");
18
+
19
+ function array_of(name)
20
+ {
21
+ const m = SRC.match(new RegExp(`(?:export\\s+)?const ${name} = \\[([^\\]]*)\\]`));
22
+ expect(m, name).toBeTruthy();
23
+ return [...m[1].matchAll(/'([^']+)'/g)].map((x) => x[1]);
24
+ }
25
+
26
+ describe("c_g6_nodes_tree publishes the keys of its shape tables", () => {
27
+ test("every node and port shape is in G6_NODES_TREE_I18N_KEYS", () => {
28
+ const published = new Set(array_of("G6_NODES_TREE_I18N_KEYS"));
29
+ for(const shape of [...array_of("NODE_SHAPES"), ...array_of("PORT_SHAPES")]) {
30
+ expect(published.has(shape), shape).toBe(true);
31
+ }
32
+ });
33
+
34
+ test("a form control is named by the label before it, and an option carries its key", () => {
35
+ expect(SRC).toMatch(/function name_form_control\(parent, control\)/);
36
+ expect(SRC).toMatch(/o\.setAttribute\('data-i18n', opt\.i18n\)/);
37
+ expect(SRC).not.toMatch(/label: t\('/);
38
+ });
39
+ });
package/src/yui_dev.js CHANGED
@@ -71,36 +71,34 @@ const TRAFFIC_TS_FIELDS = {
71
71
  * repainted from it by `refresh_dev_chrome()` on every toggle, so a
72
72
  * label translated once at build time would come back in English at
73
73
  * the first click. */
74
- /* ⚠️ The keys of this window are a BLIND SPOT for a consumer's
75
- * `validate-locales`: the table below and the `grp` / `mk_view` /
76
- * `mk_expand` / `mk_dir` / `mk_out` helpers pass their key as a
77
- * VARIABLE, and `OUT_TITLES` holds three more in a lookup table --
78
- * so a scan of `t("…")` sees none of them and reports OK with the
79
- * whole window in English. The full set a consumer must define,
80
- * kept here so it can be copied:
81
- *
82
- * automata, both, browser console only, clear,
83
- * clear captured traffic, collapsed, console, copied, copy,
84
- * copy visible traffic to clipboard, creation, data,
85
- * dev window and browser console, dev window only, developer,
86
- * clear the filter, errors, expand, expanded, filter events / payload,
87
- * find, i18n,
88
- * i18next debug output, incoming, log, machine trace shape,
89
- * metadata, outgoing, output, payload, periodic, schema, show,
90
- * show the payload of the traces,
91
- * show this section in the expanded view, simple mach,
92
- * start / stop, subscriptions,
93
- * trace every event of every automaton,
94
- * trace subscriptions and publications,
95
- * trace the creation and destruction of gobjs,
96
- * trace the messages to and from the backend,
97
- * trace the periodic timer event,
98
- * trace the start and stop of gobjs,
99
- * traces, traffic, traffic payload folded,
100
- * traffic payload laid out, view, window, yuno monitor
101
- *
102
- * wattyzer and the yunovatios GUIs carry them; check a new consumer
103
- * against this list, and confirm it by DUMPING the window. */
74
+ /* The keys of this window that reach t() as VARIABLES: the table below,
75
+ * the `grp` / `mk_view` / `mk_expand` / `mk_dir` / `mk_out` helpers and
76
+ * `OUT_TITLES` pass their key by name, so a scan of `t("...")` sees none of
77
+ * them. They are published HERE, as data: a consumer's `validate-locales`
78
+ * (gobj-ui 7.26.27+) reads an exported `*_I18N_KEYS` array of every module
79
+ * the app mounts and asks the locales for each key -- it used to be a
80
+ * comment each consumer copied by hand, and yunomusica lacked 35 of them
81
+ * (REV-CLOUD V19.4.2). A key added to the window goes in this list. */
82
+ export const YUI_DEV_I18N_KEYS = [
83
+ "automata", "both", "browser console only", "clear",
84
+ "clear captured traffic", "collapsed", "console", "copied", "copy",
85
+ "copy visible traffic to clipboard", "creation", "data",
86
+ "dev window and browser console", "dev window only", "developer",
87
+ "clear the filter", "errors", "expand", "expanded",
88
+ "filter events / payload", "find", "i18n", "i18next debug output",
89
+ "incoming", "log", "machine trace shape", "metadata", "outgoing",
90
+ "output", "payload", "periodic", "schema", "show",
91
+ "show the payload of the traces",
92
+ "show this section in the expanded view", "simple mach", "start / stop",
93
+ "subscriptions", "trace every event of every automaton",
94
+ "trace subscriptions and publications",
95
+ "trace the creation and destruction of gobjs",
96
+ "trace the messages to and from the backend",
97
+ "trace the periodic timer event", "trace the start and stop of gobjs",
98
+ "traces", "traffic", "traffic payload folded",
99
+ "traffic payload laid out", "view", "window", "yuno monitor"
100
+ ];
101
+
104
102
  const TRACE_DEFS = [
105
103
  ["automata", "automata", "trace every event of every automaton",
106
104
  automata_state, toggle_automata],