@yuneta/gobj-ui 7.26.26 → 7.26.28

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.
Files changed (44) hide show
  1. package/README.md +473 -19
  2. package/dist/gobj-ui.cjs.js +1100 -231
  3. package/dist/gobj-ui.es.js +1101 -232
  4. package/package.json +1 -1
  5. package/scripts/validate-locales.mjs +2334 -275
  6. package/src/c_g6_nodes_tree.js +1143 -260
  7. package/src/c_yui_form.js +11 -1
  8. package/src/c_yui_fsm_graph.js +5 -0
  9. package/src/c_yui_gclass.js +11 -0
  10. package/src/c_yui_gobj_tree_js.js +17 -0
  11. package/src/c_yui_json_graph.js +5 -0
  12. package/src/c_yui_nav.js +9 -6
  13. package/src/c_yui_pager.js +1 -1
  14. package/src/c_yui_period.js +11 -4
  15. package/src/c_yui_schema_editor.js +19 -7
  16. package/src/c_yui_shell.js +7 -7
  17. package/src/c_yui_treedb_graph.js +15 -6
  18. package/src/c_yui_treedb_schema.js +5 -0
  19. package/src/c_yui_treedb_topic_with_form.js +10 -10
  20. package/src/c_yui_treedb_topics.js +6 -6
  21. package/src/c_yui_window.js +2 -2
  22. package/src/c_yui_wizard.js +8 -1
  23. package/src/form_json_field.js +7 -0
  24. package/src/g6_canvas_tab_order.js +84 -0
  25. package/src/g6_canvas_tab_order.wiring.test.js +232 -0
  26. package/src/g6_nodes_tree_controls.test.js +547 -0
  27. package/src/g6_nodes_tree_focus.wiring.test.js +1360 -0
  28. package/src/g6_nodes_tree_i18n_keys.test.js +275 -0
  29. package/src/i18n_key_functions.js +146 -0
  30. package/src/i18n_key_functions.test.js +1204 -0
  31. package/src/lib_graph.js +38 -1
  32. package/src/nav_backbar_name.wiring.test.js +82 -0
  33. package/src/schema_flags.js +47 -0
  34. package/src/schema_import.js +7 -0
  35. package/src/shell_modals.js +20 -16
  36. package/src/shell_route_map.js +12 -2
  37. package/src/wizard_helpers.js +8 -0
  38. package/src/yui_dev.js +44 -33
  39. package/src/yui_dev_controls.test.js +174 -0
  40. package/src/yui_gclass_view.js +2 -2
  41. package/src/yui_graph_camera.js +1 -1
  42. package/src/yui_install.js +10 -8
  43. package/src/yui_table_select.js +2 -2
  44. package/src/yui_time.js +18 -3
package/src/lib_graph.js CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Utilities for graph, using Bulma
5
5
  *
6
- * Copyright (c) 2025, ArtGins.
6
+ * Copyright (c) 2025-2026, ArtGins.
7
7
  * All Rights Reserved.
8
8
  ***********************************************************************/
9
9
  import {
@@ -55,11 +55,38 @@ function removeChildElements($element)
55
55
 
56
56
  /************************************************************
57
57
  * Disable/Enable elements
58
+ *
59
+ * A disabled control says so to the keyboard and to a screen
60
+ * reader too, not only to the pointer:
61
+ * - `disabled`, what the stylesheets and the click guards
62
+ * read (on a `div` drawn as a button it means nothing to
63
+ * the browser itself);
64
+ * - `aria-disabled="true"`, what a reader announces -- without
65
+ * it a disabled toolbar item was heard as a button that
66
+ * works;
67
+ * - `tabindex="-1"`, so Tab does not stop on a control that
68
+ * does nothing.
69
+ *
70
+ * Enabled again, it gets back the tabindex it had, kept in
71
+ * `data-enabled-tabindex` while it is disabled (empty: it had
72
+ * none, and it gets none). Whoever BUILDS a control disabled
73
+ * writes that attribute itself, with the tabindex the control
74
+ * takes once enabled (see toolbar_item_html in
75
+ * c_g6_nodes_tree.js). An element that keeps the focus while it
76
+ * is disabled keeps it: tabindex -1 still takes a focus, it is
77
+ * only left out of the Tab order.
58
78
  ************************************************************/
79
+ const ENABLED_TABINDEX = 'data-enabled-tabindex';
80
+
59
81
  function disableElements($container, selector)
60
82
  {
61
83
  $container.querySelectorAll(selector).forEach(el => {
62
84
  el.setAttribute('disabled', '');
85
+ el.setAttribute('aria-disabled', 'true');
86
+ if(!el.hasAttribute(ENABLED_TABINDEX)) {
87
+ el.setAttribute(ENABLED_TABINDEX, el.getAttribute('tabindex') ?? '');
88
+ }
89
+ el.setAttribute('tabindex', '-1');
63
90
  });
64
91
  }
65
92
 
@@ -69,6 +96,16 @@ function enableElements($container, selector)
69
96
  if(el.hasAttribute('disabled')) {
70
97
  el.removeAttribute('disabled');
71
98
  }
99
+ el.removeAttribute('aria-disabled');
100
+ if(el.hasAttribute(ENABLED_TABINDEX)) {
101
+ let tabindex = el.getAttribute(ENABLED_TABINDEX);
102
+ el.removeAttribute(ENABLED_TABINDEX);
103
+ if(tabindex === '') {
104
+ el.removeAttribute('tabindex');
105
+ } else {
106
+ el.setAttribute('tabindex', tabindex);
107
+ }
108
+ }
72
109
  });
73
110
  }
74
111
 
@@ -0,0 +1,82 @@
1
+ /***********************************************************************
2
+ * nav_backbar_name.wiring.test.js
3
+ *
4
+ * The back arrow of a C_YUI_NAV `backbar` is a control, and a
5
+ * control is named twice -- `title` and `aria-label` -- each through
6
+ * t() and each with its key. With no `nav_label` it took the literal
7
+ * "back" as its aria-label: no t(), no key, so it read "back" in
8
+ * every language and no locale could change that; with one, it
9
+ * carried the raw key until something re-translated it, and no
10
+ * title at all.
11
+ *
12
+ * Driven through the real gclass on a document double.
13
+ *
14
+ * Copyright (c) 2026, ArtGins.
15
+ * All Rights Reserved.
16
+ ***********************************************************************/
17
+ import {test, expect, beforeAll} from "vitest";
18
+ import {install_dom_double} from "../test/dom_double.js";
19
+
20
+ install_dom_double();
21
+
22
+ const {
23
+ SDATA_END, gclass_create, gobj_start_up, gobj_create_yuno, gobj_create,
24
+ gobj_read_attr, refresh_language,
25
+ } = await import("@yuneta/gobj-js");
26
+ const {register_c_yui_nav} = await import("./c_yui_nav.js");
27
+
28
+ let i18next = null;
29
+ let yuno = null;
30
+ let seq = 0;
31
+
32
+ beforeAll(async () => {
33
+ i18next = (await import("i18next")).default;
34
+ await i18next.init({lng: "es", resources: {
35
+ es: {translation: {"back": "Atrás", "settings": "Ajustes"}},
36
+ en: {translation: {"back": "Back", "settings": "Settings"}},
37
+ }});
38
+ gobj_start_up(null, null, null, null, null, null, null);
39
+ gclass_create("C_TEST_NAV_HOST", [], [["ST_IDLE", []]], {}, 0, [SDATA_END()], {}, 0, 0, 0, 0);
40
+ register_c_yui_nav();
41
+ yuno = gobj_create_yuno("nav_backbar_yuno", "C_TEST_NAV_HOST", {});
42
+ });
43
+
44
+ /* The back arrow of a new backbar */
45
+ function back_arrow(kw)
46
+ {
47
+ const nav = gobj_create(`backbar_${++seq}`, "C_YUI_NAV",
48
+ Object.assign({layout: "backbar", back_route: "/home"}, kw), yuno);
49
+ const $container = gobj_read_attr(nav, "$container");
50
+ return {$container, $a: $container.querySelector(".yui-nav-back")};
51
+ }
52
+
53
+ function names($a)
54
+ {
55
+ return {
56
+ title: $a.getAttribute("title"),
57
+ aria: $a.getAttribute("aria-label"),
58
+ title_key: $a.getAttribute("data-i18n-title"),
59
+ aria_key: $a.getAttribute("data-i18n-aria-label"),
60
+ };
61
+ }
62
+
63
+ test("with no nav_label it is named \"back\", translated, with its keys", () => {
64
+ const {$a} = back_arrow({});
65
+ expect(names($a)).toEqual({title: "Atrás", aria: "Atrás", title_key: "back", aria_key: "back"});
66
+ });
67
+
68
+ test("with a nav_label it is named by it, translated, with its keys", () => {
69
+ const {$a} = back_arrow({nav_label: "settings"});
70
+ expect(names($a)).toEqual({title: "Ajustes", aria: "Ajustes", title_key: "settings", aria_key: "settings"});
71
+ });
72
+
73
+ test("a language change renames it", async () => {
74
+ const {$container, $a} = back_arrow({});
75
+ await i18next.changeLanguage("en");
76
+ try {
77
+ refresh_language($container, i18next.t.bind(i18next));
78
+ expect(names($a)).toEqual({title: "Back", aria: "Back", title_key: "back", aria_key: "back"});
79
+ } finally {
80
+ await i18next.changeLanguage("es");
81
+ }
82
+ });
@@ -114,6 +114,53 @@ const FLAG_CATALOG = [
114
114
  desc: "drawn as a table"},
115
115
  ];
116
116
 
117
+ /* The keys the schema editor hands to t() as data: the name of every
118
+ * group (`other` holds the flags the catalogue does not know) and the
119
+ * description of every flag. No scan of the source reads them there, so
120
+ * they are published for a consumer's validate-locales, which asks the
121
+ * app's locales for every key of an exported `*_I18N_KEYS` array. A
122
+ * flag or a group added to the catalogue adds its key here (a test
123
+ * checks it). */
124
+ export const SCHEMA_FLAGS_I18N_KEYS = [
125
+ "storage", "validation", "access", "relation", "key", "stats", "format", "other",
126
+ "saved to disk; a column without it lives only while the yuno runs",
127
+ "convert a value of another type instead of refusing it",
128
+ "copied from the primary record to its other instances",
129
+ "the field must be present when the record is written",
130
+ "the field may be absent, but never null",
131
+ "the value must be one of the `enum` list",
132
+ "the value is built from the `template` field",
133
+ "may be changed after the record is created",
134
+ "may be read back",
135
+ "left out of listings",
136
+ "holds the children this record owns; needs a `hook` mapping",
137
+ "holds the reference to a parent; written by the parent's hook",
138
+ "key generated as a counter when none is given",
139
+ "key generated as a uuid when none is given",
140
+ "key composed as the parent's id plus this record's name",
141
+ "the value is an identifier of something else",
142
+ "a counter, reported in the yuno statistics",
143
+ "a counter read through mt_reading when it is asked for",
144
+ "a counter kept across restarts",
145
+ "the value is a timestamp",
146
+ "stamped with the current time when the record is created",
147
+ "a date",
148
+ "shown masked, and never echoed back",
149
+ "an email address",
150
+ "a url",
151
+ "a telephone number",
152
+ "a colour",
153
+ "an image",
154
+ "a geographic position",
155
+ "an amount of money",
156
+ "a percentage",
157
+ "written in hexadecimal",
158
+ "binary content",
159
+ "encoded in base64",
160
+ "carried as a gbuffer",
161
+ "drawn as a table"
162
+ ];
163
+
117
164
  /* Flags that decide the SHAPE of the record and cannot both be on the
118
165
  * same column: the treedb writes one half of a link, never both. */
119
166
  const EXCLUSIVE = [["hook", "fkey"], ["rowid", "uuid", "qualified"]];
@@ -53,6 +53,13 @@ const COL_FIELDS = [
53
53
  /* Read as values whatever they were stored as. */
54
54
  const JSON_FIELDS = ["enum", "hook", "default", "template", "properties", "pkey2s"];
55
55
 
56
+ /* The `op` of a planned write is an i18n key: the schema editor shows
57
+ * each write of a plan by it (`t(write.op)`), as data, where no scan of
58
+ * the source reads it. Published for a consumer's validate-locales,
59
+ * which asks the app's locales for every key of an exported
60
+ * `*_I18N_KEYS` array. */
61
+ export const SCHEMA_IMPORT_I18N_KEYS = ["create", "update", "delete"];
62
+
56
63
 
57
64
  /***************************************************************
58
65
  * Two values, compared the way the store would see them: a flag
@@ -624,21 +624,25 @@ function build_dialog(shell, message, buttons, opts)
624
624
  * "Delete" and "Cancel", keys that no locale carries, so a dialog
625
625
  * that took the defaults read in English in every language.
626
626
  *
627
- * Written as `i18n:` entries so the validate-locales script of a
628
- * consumer finds them when it scans this module: an app that
629
- * mounts these dialogs must define the five keys.
627
+ * An app that mounts these dialogs must define the five keys. A
628
+ * consumer's validate-locales reads them where build_dialog() gets
629
+ * them (a const, `DEFAULT_LABEL.ok`, it follows), and in
630
+ * SHELL_MODALS_I18N_KEYS, which a test holds to this table. (Up to
631
+ * 7.26.27 they were written as `i18n:` entries, which only a regex
632
+ * scan of `i18n: "..."` happened to see.)
630
633
  ***************************************************************/
631
634
  const DEFAULT_LABEL = Object.freeze({
632
- ok: Object.freeze({i18n: "ok"}),
633
- yes: Object.freeze({i18n: "yes"}),
634
- no: Object.freeze({i18n: "no"}),
635
- delete: Object.freeze({i18n: "delete"}),
636
- cancel: Object.freeze({i18n: "cancel"})
635
+ ok: "ok",
636
+ yes: "yes",
637
+ no: "no",
638
+ delete: "delete",
639
+ cancel: "cancel"
637
640
  });
641
+ export const SHELL_MODALS_I18N_KEYS = ["ok", "yes", "no", "delete", "cancel"];
638
642
 
639
643
  export function yui_shell_confirm_ok(shell, message, opts)
640
644
  {
641
- let label = (opts && opts.ok_label) || DEFAULT_LABEL.ok.i18n;
645
+ const label = (opts && opts.ok_label) || DEFAULT_LABEL.ok;
642
646
  if(!opts || !opts.type) {
643
647
  opts = Object.assign({}, opts, {type: "success"});
644
648
  }
@@ -649,8 +653,8 @@ export function yui_shell_confirm_ok(shell, message, opts)
649
653
 
650
654
  export function yui_shell_confirm_yesno(shell, message, opts)
651
655
  {
652
- let yes_label = (opts && opts.yes_label) || DEFAULT_LABEL.yes.i18n;
653
- let no_label = (opts && opts.no_label) || DEFAULT_LABEL.no.i18n;
656
+ const yes_label = (opts && opts.yes_label) || DEFAULT_LABEL.yes;
657
+ const no_label = (opts && opts.no_label) || DEFAULT_LABEL.no;
654
658
  return build_dialog(shell, message, [
655
659
  {label: yes_label, value: "yes", kind: "primary"},
656
660
  {label: no_label, value: "no"}
@@ -673,8 +677,8 @@ export function yui_shell_confirm_yesno(shell, message, opts)
673
677
  ***************************************************************/
674
678
  export function yui_shell_confirm_danger(shell, message, opts)
675
679
  {
676
- let confirm_label = (opts && opts.confirm_label) || DEFAULT_LABEL.delete.i18n;
677
- let cancel_label = (opts && opts.cancel_label) || DEFAULT_LABEL.cancel.i18n;
680
+ const confirm_label = (opts && opts.confirm_label) || DEFAULT_LABEL.delete;
681
+ const cancel_label = (opts && opts.cancel_label) || DEFAULT_LABEL.cancel;
678
682
  if(!opts || !opts.type) {
679
683
  opts = Object.assign({}, opts, {type: "danger"});
680
684
  }
@@ -686,9 +690,9 @@ export function yui_shell_confirm_danger(shell, message, opts)
686
690
 
687
691
  export function yui_shell_confirm_yesnocancel(shell, message, opts)
688
692
  {
689
- let yes_label = (opts && opts.yes_label) || DEFAULT_LABEL.yes.i18n;
690
- let no_label = (opts && opts.no_label) || DEFAULT_LABEL.no.i18n;
691
- let cancel_label = (opts && opts.cancel_label) || DEFAULT_LABEL.cancel.i18n;
693
+ const yes_label = (opts && opts.yes_label) || DEFAULT_LABEL.yes;
694
+ const no_label = (opts && opts.no_label) || DEFAULT_LABEL.no;
695
+ const cancel_label = (opts && opts.cancel_label) || DEFAULT_LABEL.cancel;
692
696
  return build_dialog(shell, message, [
693
697
  {label: yes_label, value: "yes", kind: "primary"},
694
698
  {label: no_label, value: "no"},
@@ -41,6 +41,15 @@ import {text_node} from "./yui_text.js";
41
41
 
42
42
  const WIN_NAME = "shell-route-map-window";
43
43
 
44
+ /* The labels of the map's own nodes -- its two groups, and the root of
45
+ * an app whose brand has no label -- which reach t() as data (a node's
46
+ * `label`), where no scan of the source reads them. Published for a
47
+ * consumer's validate-locales, which asks the app's locales for every
48
+ * key of an exported `*_I18N_KEYS` array; i18n_key_functions.test.js
49
+ * holds it to every `label:` this module writes. The other labels are
50
+ * the app's: its shell declaration. */
51
+ export const SHELL_ROUTE_MAP_I18N_KEYS = ["toolbar", "other routes", "app"];
52
+
44
53
  /* Reference rows — a route's second and third entry point: a drawer
45
54
  * entry, a toolbar SHORTCUT. Shown by default: they are one line
46
55
  * each, and they are exactly what someone auditing the navigation
@@ -79,7 +88,8 @@ function render_node(node)
79
88
  row.push(["span", {class: "icon ROUTEMAP_ICON"},
80
89
  [["i", {class: node.icon}]]]);
81
90
  }
82
- row.push(["span", {class: "ROUTEMAP_NAME", i18n: node.label}, text_node(node.label)]);
91
+ row.push(["span", {class: "ROUTEMAP_NAME", i18n: /* i18n: SHELL_ROUTE_MAP_I18N_KEYS */ node.label},
92
+ text_node(node.label)]);
83
93
  /* Documentation columns: route + implementing gclass + action event. */
84
94
  if(node.route) {
85
95
  row.push(["code", {class: "ROUTEMAP_ROUTE"}, text_node(node.route)]);
@@ -527,7 +537,7 @@ export function yui_shell_show_route_map(shell, opts)
527
537
  /* Fallback: a modal (no C_YUI_WINDOW registered). */
528
538
  let modal_ref = {modal: null};
529
539
  let $body = build_body(shell, t);
530
- modal_ref.modal = yui_shell_show_modal(shell, $body, {
540
+ modal_ref.modal = yui_shell_show_modal(shell, /* i18n: data */ $body, {
531
541
  dialog: true,
532
542
  logical_class: "ROUTEMAP_SHEET",
533
543
  /* The KEY, not t(key): the modal renders `title` with
@@ -44,6 +44,14 @@ function wizard_prev_index(idx, count)
44
44
  return wizard_clamp_index(idx - 1, count);
45
45
  }
46
46
 
47
+ /* The keys of the primary button when the caller names none: they
48
+ * reach t() as data, where no scan of the source reads them, so they
49
+ * are published for a consumer's validate-locales, which asks the app's
50
+ * locales for every key of an exported `*_I18N_KEYS` array.
51
+ * i18n_key_functions.test.js holds it to every value given to
52
+ * `primary_label`. */
53
+ export const WIZARD_HELPERS_I18N_KEYS = ["confirm", "next"];
54
+
47
55
  /***************************************************************
48
56
  * Chrome model for the current step.
49
57
  *
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],
@@ -1546,7 +1544,15 @@ function dev_fallback_copy(text)
1546
1544
  }
1547
1545
 
1548
1546
  /* The control bar: trace toggles, view selector, direction
1549
- * filters, free-text search, copy, clear. Returns an element. */
1547
+ * filters, free-text search, copy, clear. Returns an element.
1548
+ *
1549
+ * Every button is NAMED by the key of the text it shows (`aria-label`)
1550
+ * and described by its `title`, each with its key. Not the other way
1551
+ * round: the title of the three Expand chips is one sentence for the
1552
+ * three, so as a name it would make them one control, and a name has
1553
+ * to hold the words a person sees on the button. A button that shows
1554
+ * only a glyph (the direction filters, the ✕ of the search) is named
1555
+ * by its title. */
1550
1556
  function build_control_bar()
1551
1557
  {
1552
1558
  let trace_chips = TRACE_DEFS.map(([key, label_key, title_key, state, toggle]) => ['button', {
@@ -1568,6 +1574,7 @@ function build_control_bar()
1568
1574
  class: 'YDEV_CHIP', 'data-toggle': 'automata-simple', type: 'button',
1569
1575
  title: t('machine trace shape'), 'data-i18n-title': 'machine trace shape',
1570
1576
  'data-i18n': 'simple mach',
1577
+ 'aria-label': t('simple mach'), 'data-i18n-aria-label': 'simple mach',
1571
1578
  }, t('simple mach'), {
1572
1579
  click: (ev) => {
1573
1580
  ev.stopPropagation();
@@ -1633,6 +1640,7 @@ function build_control_bar()
1633
1640
  class: 'YDEV_SEG_BTN', 'data-output': v, type: 'button',
1634
1641
  title: t(OUT_TITLES[v]), 'data-i18n-title': OUT_TITLES[v],
1635
1642
  'data-i18n': label,
1643
+ 'aria-label': t(label), 'data-i18n-aria-label': label,
1636
1644
  }, t(label), {
1637
1645
  click: (ev) => {
1638
1646
  ev.stopPropagation();
@@ -1656,6 +1664,7 @@ function build_control_bar()
1656
1664
  title: t('show this section in the expanded view'),
1657
1665
  'data-i18n-title': 'show this section in the expanded view',
1658
1666
  'data-i18n': label,
1667
+ 'aria-label': t(label), 'data-i18n-aria-label': label,
1659
1668
  }, t(label), {
1660
1669
  click: (ev) => {
1661
1670
  ev.stopPropagation();
@@ -1722,7 +1731,8 @@ function build_control_bar()
1722
1731
  let copy = ['button', {class: 'YDEV_CHIP', type: 'button',
1723
1732
  title: t('copy visible traffic to clipboard'),
1724
1733
  'data-i18n-title': 'copy visible traffic to clipboard',
1725
- 'data-i18n': 'copy'}, t('copy'), {
1734
+ 'data-i18n': 'copy',
1735
+ 'aria-label': t('copy'), 'data-i18n-aria-label': 'copy'}, t('copy'), {
1726
1736
  click: (ev) => {
1727
1737
  ev.stopPropagation();
1728
1738
  let btn = ev.currentTarget;
@@ -1737,7 +1747,8 @@ function build_control_bar()
1737
1747
  let clear = ['button', {class: 'YDEV_CHIP', type: 'button',
1738
1748
  title: t('clear captured traffic'),
1739
1749
  'data-i18n-title': 'clear captured traffic',
1740
- 'data-i18n': 'clear'}, t('clear'), {
1750
+ 'data-i18n': 'clear',
1751
+ 'aria-label': t('clear'), 'data-i18n-aria-label': 'clear'}, t('clear'), {
1741
1752
  click: (ev) => {
1742
1753
  ev.stopPropagation();
1743
1754
  clear_traffic();
@@ -0,0 +1,174 @@
1
+ /***********************************************************************
2
+ * yui_dev_controls.test.js
3
+ *
4
+ * Every control of the Developer window is NAMED, and every
5
+ * name changes language (REV-CLOUD V20.5.5: nine buttons had a
6
+ * `title` and no `aria-label` -- simple mach, the three of
7
+ * Output, the three of Expand, copy and clear).
8
+ *
9
+ * A button that shows text is named by the key of that text
10
+ * and described by its title; one that shows only a glyph is
11
+ * named by its title. And every key the window asks for, or
12
+ * carries for a language switch, is in YUI_DEV_I18N_KEYS --
13
+ * the list a consumer's validate-locales reads, because most of
14
+ * them reach t() by variable and no scan of the source sees
15
+ * them.
16
+ *
17
+ * Copyright (c) 2026, ArtGins.
18
+ * All Rights Reserved.
19
+ ***********************************************************************/
20
+ import {describe, it, expect, beforeAll} from "vitest";
21
+ import {install_dom_double} from "../test/dom_double.js";
22
+
23
+ let i18next;
24
+ let refresh_language;
25
+ let build_dev_panel;
26
+ let YUI_DEV_I18N_KEYS;
27
+ let $panel;
28
+
29
+ /* Every key t() was asked for: the English catalog is empty, so each
30
+ * one is a miss, and i18next reports it. */
31
+ const asked = new Set();
32
+
33
+ const KEY_ATTRS = ["data-i18n", "data-i18n-title", "data-i18n-aria-label",
34
+ "data-i18n-placeholder", "data-label"];
35
+
36
+ beforeAll(async () => {
37
+ install_dom_double();
38
+ const gobj_js = await import("@yuneta/gobj-js");
39
+ refresh_language = gobj_js.refresh_language;
40
+ ({build_dev_panel, YUI_DEV_I18N_KEYS} = await import("./yui_dev.js"));
41
+
42
+ /* The panel hands the yuno its traffic sink, as in an app. */
43
+ const {SDATA, SDATA_END, data_type_t, gclass_create, gobj_start_up,
44
+ gobj_create_yuno} = gobj_js;
45
+ gobj_start_up(null, null, null, null, null, null, null);
46
+ gclass_create(
47
+ "C_TEST_DEV_YUNO", [], [["ST_IDLE", []]], {}, 0,
48
+ [SDATA(data_type_t.DTP_POINTER, "trace_ievent_callback", 0, null, ""), SDATA_END()],
49
+ {}, 0, 0, 0, 0
50
+ );
51
+ gobj_create_yuno("dev_yuno", "C_TEST_DEV_YUNO", {});
52
+
53
+ /* Spanish is the key with a mark, so a name that did not follow
54
+ * the switch is told apart from one that did. */
55
+ const es = {};
56
+ for(const k of YUI_DEV_I18N_KEYS) {
57
+ es[k] = `«${k}»`;
58
+ }
59
+ i18next = (await import("i18next")).default;
60
+ await i18next.init({
61
+ lng: "en", fallbackLng: false,
62
+ saveMissing: true,
63
+ missingKeyHandler: (lngs, ns, key) => {
64
+ asked.add(key);
65
+ },
66
+ resources: {en: {translation: {}}, es: {translation: es}},
67
+ });
68
+
69
+ $panel = build_dev_panel().$el;
70
+ document.body.appendChild($panel);
71
+ });
72
+
73
+ function controls()
74
+ {
75
+ return $panel.querySelectorAll("button, input, select, textarea");
76
+ }
77
+
78
+ describe("the Developer window's controls", () => {
79
+ it("has the controls this test reads", () => {
80
+ expect(controls().length).toBeGreaterThan(20);
81
+ });
82
+
83
+ it("names every control, and every name carries its key", () => {
84
+ const unnamed = [];
85
+ for(const $c of controls()) {
86
+ const title_key = $c.getAttribute("data-i18n-title");
87
+ const aria_key = $c.getAttribute("data-i18n-aria-label");
88
+ if(!title_key || !aria_key || !$c.getAttribute("title") ||
89
+ !$c.getAttribute("aria-label")) {
90
+ unnamed.push($c.getAttribute("data-i18n") || title_key || $c.tagName);
91
+ }
92
+ }
93
+ expect(unnamed).toEqual([]);
94
+ });
95
+
96
+ it("names a button by the text it shows, a glyph by its title", () => {
97
+ const wrong = [];
98
+ for(const $c of $panel.querySelectorAll("button")) {
99
+ const shown = $c.getAttribute("data-i18n");
100
+ const want = shown || $c.getAttribute("data-i18n-title");
101
+ if($c.getAttribute("data-i18n-aria-label") !== want) {
102
+ wrong.push(`${want}: ${$c.getAttribute("data-i18n-aria-label")}`);
103
+ }
104
+ }
105
+ expect(wrong).toEqual([]);
106
+ });
107
+
108
+ it("names the nine buttons REV-CLOUD V20.5.5 found without one", () => {
109
+ const sel = [
110
+ '[data-toggle="automata-simple"]',
111
+ '[data-output="window"]', '[data-output="console"]', '[data-output="both"]',
112
+ '[data-expand="dev_full_schema"]', '[data-expand="dev_full_data"]',
113
+ '[data-expand="dev_full_meta"]',
114
+ ];
115
+ const $nine = sel.map((s) => $panel.querySelector(s));
116
+ for(const label of ["copy", "clear"]) {
117
+ $nine.push([...$panel.querySelectorAll("button")]
118
+ .find(($b) => $b.getAttribute("data-i18n") === label));
119
+ }
120
+ expect($nine.every(Boolean)).toBe(true);
121
+ for(const $b of $nine) {
122
+ const key = $b.getAttribute("data-i18n");
123
+ expect($b.getAttribute("aria-label"), key).toBe(i18next.t(key));
124
+ expect($b.getAttribute("data-i18n-aria-label"), key).toBe(key);
125
+ expect($b.getAttribute("data-i18n-title"), key).toBeTruthy();
126
+ }
127
+ /* Three chips, three names: their title is one sentence. */
128
+ const expand = $nine.slice(4, 7).map(($b) => $b.getAttribute("aria-label"));
129
+ expect(new Set(expand).size).toBe(3);
130
+ });
131
+
132
+ it("renames every control on a language switch", async () => {
133
+ await i18next.changeLanguage("es");
134
+ try {
135
+ refresh_language($panel, i18next.t);
136
+ const stale = [];
137
+ for(const $c of controls()) {
138
+ const tk = $c.getAttribute("data-i18n-title");
139
+ const ak = $c.getAttribute("data-i18n-aria-label");
140
+ if($c.getAttribute("title") !== `«${tk}»`) {
141
+ stale.push(`title ${tk}`);
142
+ }
143
+ if($c.getAttribute("aria-label") !== `«${ak}»`) {
144
+ stale.push(`aria-label ${ak}`);
145
+ }
146
+ }
147
+ expect(stale).toEqual([]);
148
+ } finally {
149
+ await i18next.changeLanguage("en");
150
+ refresh_language($panel, i18next.t);
151
+ }
152
+ });
153
+
154
+ it("publishes every key it asks for or carries", () => {
155
+ const published = new Set(YUI_DEV_I18N_KEYS);
156
+ const carried = new Set();
157
+ const walk = ($n) => {
158
+ for(const a of KEY_ATTRS) {
159
+ const k = $n.getAttribute(a);
160
+ if(k) {
161
+ carried.add(k);
162
+ }
163
+ }
164
+ for(const $c of $n.children) {
165
+ walk($c);
166
+ }
167
+ };
168
+ walk($panel);
169
+ expect(asked.size).toBeGreaterThan(20);
170
+ const missing = [...new Set([...asked, ...carried])]
171
+ .filter((k) => !published.has(k)).sort();
172
+ expect(missing).toEqual([]);
173
+ });
174
+ });
@@ -193,11 +193,11 @@ function open_gclass_view(host, gclass_name, opts)
193
193
  close_gclass_view(handle);
194
194
  return null;
195
195
  }
196
- handle.modal = yui_shell_show_modal(shell, $box, {
196
+ handle.modal = yui_shell_show_modal(shell, /* i18n: data */ $box, {
197
197
  dialog: true,
198
198
  logical_class: "GCLASS_VIEW_SHEET",
199
199
  title_prefix: opts.title_prefix || "",
200
- title: gclass_name,
200
+ title: /* i18n: data */ gclass_name,
201
201
  t: t,
202
202
  on_close: on_close,
203
203
  });