@yuneta/gobj-ui 7.26.25 → 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.
@@ -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
  }
@@ -5010,9 +5056,16 @@ function create_form_button_row(parent, buttons)
5010
5056
  {
5011
5057
  let btnRow = document.createElement('div');
5012
5058
  btnRow.style.cssText = 'display:flex;gap:8px;';
5013
- for(let {text, style, onClick} of buttons) {
5059
+ for(let {i18n, style, onClick} of buttons) {
5060
+ /* `i18n` is the key (named so that validate-locales sees a literal
5061
+ * one): the button carries it, and its name */
5014
5062
  let btn = document.createElement('button');
5015
- btn.textContent = t(text);
5063
+ btn.textContent = t(i18n);
5064
+ btn.setAttribute('data-i18n', i18n);
5065
+ btn.setAttribute('title', t(i18n));
5066
+ btn.setAttribute('data-i18n-title', i18n);
5067
+ btn.setAttribute('aria-label', t(i18n));
5068
+ btn.setAttribute('data-i18n-aria-label', i18n);
5016
5069
  btn.style.cssText = style + touch_control_css();
5017
5070
  btn.addEventListener('click', (e) => {
5018
5071
  e.stopPropagation();
@@ -5059,12 +5112,12 @@ function show_confirm_popover(gobj, target_el, message, confirm_text, confirm_co
5059
5112
 
5060
5113
  create_form_button_row(popover, [
5061
5114
  {
5062
- text: 'cancel',
5115
+ i18n: 'cancel',
5063
5116
  style: BTN_STYLE_CANCEL,
5064
5117
  onClick: () => hide_overlay(gobj, priv_key),
5065
5118
  },
5066
5119
  {
5067
- text: confirm_text,
5120
+ i18n: confirm_text,
5068
5121
  style: 'flex:1;padding:6px;background:' + confirm_color + ';color:#fff;border:none;' +
5069
5122
  'border-radius:4px;cursor:pointer;font-size:13px;font-weight:500;',
5070
5123
  onClick: onConfirm,
@@ -6131,7 +6184,7 @@ function show_port_popover(gobj)
6131
6184
 
6132
6185
  create_form_label(popover, 'shape');
6133
6186
  let shapeSelect = create_form_select(popover, PORT_SHAPES.map((shape) => {
6134
- return {value: shape, label: t(shape)};
6187
+ return {value: shape, i18n: shape};
6135
6188
  }));
6136
6189
  shapeSelect.value = orig_shape;
6137
6190
  shapeSelect.addEventListener('change', preview_port);
@@ -6141,21 +6194,21 @@ function show_port_popover(gobj)
6141
6194
 
6142
6195
  create_form_label(popover, 'apply to');
6143
6196
  let scopeSelect = create_form_select(popover, [
6144
- {value: 'this', label: t('this port')},
6145
- {value: 'same_topic', label: t('same port in topic')},
6146
- {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'},
6147
6200
  ]);
6148
6201
 
6149
6202
  create_form_button_row(popover, [
6150
6203
  {
6151
- text: 'cancel',
6204
+ i18n: 'cancel',
6152
6205
  style: BTN_STYLE_CANCEL,
6153
6206
  onClick: () => {
6154
6207
  hide_port_popover(gobj); /* which undoes the preview */
6155
6208
  },
6156
6209
  },
6157
6210
  {
6158
- text: 'apply',
6211
+ i18n: 'apply',
6159
6212
  style: 'flex:1;padding:6px;background:#1890ff;color:#fff;border:none;' +
6160
6213
  'border-radius:4px;cursor:pointer;font-size:13px;font-weight:500;',
6161
6214
  onClick: () => {
@@ -7093,21 +7146,21 @@ function show_edge_popover(gobj)
7093
7146
  // Apply-to scope
7094
7147
  create_form_label(popover, 'apply to');
7095
7148
  let scopeSelect = create_form_select(popover, [
7096
- { value: 'this', label: t('this edge') },
7097
- { value: 'same_type', label: t('same type edges') },
7098
- { 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' },
7099
7152
  ]);
7100
7153
 
7101
7154
  create_form_button_row(popover, [
7102
7155
  {
7103
- text: 'cancel',
7156
+ i18n: 'cancel',
7104
7157
  style: BTN_STYLE_CANCEL,
7105
7158
  onClick: () => {
7106
7159
  deselect_edge(gobj); /* which undoes the preview */
7107
7160
  },
7108
7161
  },
7109
7162
  {
7110
- text: 'apply',
7163
+ i18n: 'apply',
7111
7164
  style: 'flex:1;padding:6px;background:#52c41a;color:#fff;border:none;' +
7112
7165
  'border-radius:4px;cursor:pointer;font-size:13px;font-weight:500;',
7113
7166
  onClick: () => {
@@ -7332,7 +7385,7 @@ function show_node_popover(gobj)
7332
7385
  * reshape either way. */
7333
7386
  create_form_label(popover, 'shape');
7334
7387
  let shapeSelect = create_form_select(popover, NODE_SHAPES.map((shape) => {
7335
- return {value: shape, label: t(shape)};
7388
+ return {value: shape, i18n: shape};
7336
7389
  }));
7337
7390
  shapeSelect.value = nodeData.data && nodeData.data.desc
7338
7391
  ? node_figure_of(gobj, nodeData.data.desc, nodeData.data.graph_props || {})
@@ -7342,21 +7395,21 @@ function show_node_popover(gobj)
7342
7395
  // Apply-to scope
7343
7396
  create_form_label(popover, 'apply to');
7344
7397
  let scopeSelect = create_form_select(popover, [
7345
- { value: 'this', label: t('this node') },
7346
- { value: 'same_topic', label: t('same topic nodes') },
7347
- { 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' },
7348
7401
  ]);
7349
7402
 
7350
7403
  create_form_button_row(popover, [
7351
7404
  {
7352
- text: 'cancel',
7405
+ i18n: 'cancel',
7353
7406
  style: BTN_STYLE_CANCEL,
7354
7407
  onClick: () => {
7355
7408
  hide_node_popover(gobj); /* which undoes the preview */
7356
7409
  },
7357
7410
  },
7358
7411
  {
7359
- text: 'apply',
7412
+ i18n: 'apply',
7360
7413
  style: 'flex:1;padding:6px;background:#1890ff;color:#fff;border:none;' +
7361
7414
  'border-radius:4px;cursor:pointer;font-size:13px;font-weight:500;',
7362
7415
  onClick: () => {
@@ -8431,15 +8484,24 @@ function show_create_popover(gobj)
8431
8484
  let titleEl = document.createElement('div');
8432
8485
  titleEl.style.cssText = 'margin-bottom:10px;font-weight:600;font-size:14px;';
8433
8486
  titleEl.textContent = t('create node');
8487
+ titleEl.setAttribute('data-i18n', 'create node');
8434
8488
  popover.appendChild(titleEl);
8435
8489
 
8436
- // Topic selector
8490
+ // Topic selector: the word carries its key, the colon is not translated
8437
8491
  let topicLabel = document.createElement('div');
8438
8492
  topicLabel.style.cssText = 'margin-bottom:4px;font-weight:500;';
8439
- topicLabel.textContent = t('topic') + ':';
8493
+ let topicWord = document.createElement('span');
8494
+ topicWord.textContent = t('topic');
8495
+ topicWord.setAttribute('data-i18n', 'topic');
8496
+ topicLabel.appendChild(topicWord);
8497
+ topicLabel.appendChild(document.createTextNode(':'));
8440
8498
  popover.appendChild(topicLabel);
8441
8499
 
8442
8500
  let topicSelect = document.createElement('select');
8501
+ topicSelect.setAttribute('title', t('topic'));
8502
+ topicSelect.setAttribute('data-i18n-title', 'topic');
8503
+ topicSelect.setAttribute('aria-label', t('topic'));
8504
+ topicSelect.setAttribute('data-i18n-aria-label', 'topic');
8443
8505
  topicSelect.style.cssText =
8444
8506
  'width:100%;padding:6px;border:1px solid #d9d9d9;border-radius:4px;' +
8445
8507
  'font-size:13px;margin-bottom:8px;' + touch_control_css();
@@ -8477,12 +8539,21 @@ function show_create_popover(gobj)
8477
8539
  // Node ID input
8478
8540
  let idLabel = document.createElement('div');
8479
8541
  idLabel.style.cssText = 'margin-bottom:4px;font-weight:500;';
8480
- 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(':'));
8481
8547
  popover.appendChild(idLabel);
8482
8548
 
8483
8549
  let idInput = document.createElement('input');
8484
8550
  idInput.type = 'text';
8485
8551
  idInput.placeholder = t('node id');
8552
+ idInput.setAttribute('data-i18n-placeholder', 'node id');
8553
+ idInput.setAttribute('title', t('node id'));
8554
+ idInput.setAttribute('data-i18n-title', 'node id');
8555
+ idInput.setAttribute('aria-label', t('node id'));
8556
+ idInput.setAttribute('data-i18n-aria-label', 'node id');
8486
8557
  idInput.style.cssText =
8487
8558
  'width:100%;padding:6px;border:1px solid #d9d9d9;border-radius:4px;' +
8488
8559
  'font-size:13px;margin-bottom:10px;box-sizing:border-box;' +
@@ -8499,18 +8570,19 @@ function show_create_popover(gobj)
8499
8570
  let createBtn; // need reference for Enter key handler
8500
8571
  create_form_button_row(popover, [
8501
8572
  {
8502
- text: 'cancel',
8573
+ i18n: 'cancel',
8503
8574
  style: BTN_STYLE_CANCEL,
8504
8575
  onClick: () => hide_create_popover(gobj),
8505
8576
  },
8506
8577
  {
8507
- text: 'create',
8578
+ i18n: 'create',
8508
8579
  style: 'flex:1;padding:6px;background:#1890ff;color:#fff;border:none;' +
8509
8580
  'border-radius:4px;cursor:pointer;font-size:13px;font-weight:500;',
8510
8581
  onClick: () => {
8511
8582
  let node_id = idInput.value.trim();
8512
8583
  if(!node_id) {
8513
- errorEl.textContent = t('node id') + ' required';
8584
+ errorEl.textContent = t('node id required');
8585
+ errorEl.setAttribute('data-i18n', 'node id required');
8514
8586
  errorEl.style.display = 'block';
8515
8587
  idInput.focus();
8516
8588
  return;
@@ -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],
@@ -328,11 +328,11 @@ function yui_file_control(gobj, {name, value, readonly, accept, on_pick})
328
328
  if(picked) {
329
329
  $state.classList.add("is-picked");
330
330
  $state.setAttribute("data-i18n", "not saved yet");
331
- $state.textContent = "not saved yet";
331
+ $state.textContent = t("not saved yet");
332
332
  } else if(!kept) {
333
333
  $state.classList.add("is-empty");
334
334
  $state.setAttribute("data-i18n", "no file");
335
- $state.textContent = "no file";
335
+ $state.textContent = t("no file");
336
336
  }
337
337
 
338
338
  $clear.hidden = readonly || (!picked && !kept);