@yuneta/gobj-ui 7.26.27 → 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 +458 -22
  2. package/dist/gobj-ui.cjs.js +1063 -216
  3. package/dist/gobj-ui.es.js +1064 -217
  4. package/package.json +1 -1
  5. package/scripts/validate-locales.mjs +2269 -316
  6. package/src/c_g6_nodes_tree.js +1086 -253
  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 +241 -5
  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 +16 -3
  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
@@ -1544,7 +1544,15 @@ function dev_fallback_copy(text)
1544
1544
  }
1545
1545
 
1546
1546
  /* The control bar: trace toggles, view selector, direction
1547
- * 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. */
1548
1556
  function build_control_bar()
1549
1557
  {
1550
1558
  let trace_chips = TRACE_DEFS.map(([key, label_key, title_key, state, toggle]) => ['button', {
@@ -1566,6 +1574,7 @@ function build_control_bar()
1566
1574
  class: 'YDEV_CHIP', 'data-toggle': 'automata-simple', type: 'button',
1567
1575
  title: t('machine trace shape'), 'data-i18n-title': 'machine trace shape',
1568
1576
  'data-i18n': 'simple mach',
1577
+ 'aria-label': t('simple mach'), 'data-i18n-aria-label': 'simple mach',
1569
1578
  }, t('simple mach'), {
1570
1579
  click: (ev) => {
1571
1580
  ev.stopPropagation();
@@ -1631,6 +1640,7 @@ function build_control_bar()
1631
1640
  class: 'YDEV_SEG_BTN', 'data-output': v, type: 'button',
1632
1641
  title: t(OUT_TITLES[v]), 'data-i18n-title': OUT_TITLES[v],
1633
1642
  'data-i18n': label,
1643
+ 'aria-label': t(label), 'data-i18n-aria-label': label,
1634
1644
  }, t(label), {
1635
1645
  click: (ev) => {
1636
1646
  ev.stopPropagation();
@@ -1654,6 +1664,7 @@ function build_control_bar()
1654
1664
  title: t('show this section in the expanded view'),
1655
1665
  'data-i18n-title': 'show this section in the expanded view',
1656
1666
  'data-i18n': label,
1667
+ 'aria-label': t(label), 'data-i18n-aria-label': label,
1657
1668
  }, t(label), {
1658
1669
  click: (ev) => {
1659
1670
  ev.stopPropagation();
@@ -1720,7 +1731,8 @@ function build_control_bar()
1720
1731
  let copy = ['button', {class: 'YDEV_CHIP', type: 'button',
1721
1732
  title: t('copy visible traffic to clipboard'),
1722
1733
  'data-i18n-title': 'copy visible traffic to clipboard',
1723
- 'data-i18n': 'copy'}, t('copy'), {
1734
+ 'data-i18n': 'copy',
1735
+ 'aria-label': t('copy'), 'data-i18n-aria-label': 'copy'}, t('copy'), {
1724
1736
  click: (ev) => {
1725
1737
  ev.stopPropagation();
1726
1738
  let btn = ev.currentTarget;
@@ -1735,7 +1747,8 @@ function build_control_bar()
1735
1747
  let clear = ['button', {class: 'YDEV_CHIP', type: 'button',
1736
1748
  title: t('clear captured traffic'),
1737
1749
  'data-i18n-title': 'clear captured traffic',
1738
- 'data-i18n': 'clear'}, t('clear'), {
1750
+ 'data-i18n': 'clear',
1751
+ 'aria-label': t('clear'), 'data-i18n-aria-label': 'clear'}, t('clear'), {
1739
1752
  click: (ev) => {
1740
1753
  ev.stopPropagation();
1741
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
  });
@@ -326,7 +326,7 @@ export function yui_graph_update_anchor($container, state)
326
326
  $btn.classList.toggle('color_pending_state', state === "arming");
327
327
  $btn.setAttribute("aria-pressed", (state === "on")? "true": "false");
328
328
 
329
- let key = (state === "arming")? "click the element to centre on"
329
+ const key = (state === "arming")? "click the element to centre on"
330
330
  : (state === "on")? "centred: click to release"
331
331
  : "anchor view";
332
332
  $btn.setAttribute("title", t(key));
@@ -197,13 +197,15 @@ function yui_install_start_watch()
197
197
  /* The question and its answers when the app names none. They are i18n
198
198
  * KEYS, lower-case, and the ones every consumer already defines; the
199
199
  * question was "Install this app?" until 7.25.16, a key no locale
200
- * carries. Written as `i18n:` entries so the validate-locales script of
201
- * a consumer finds them when it scans this module. */
200
+ * carries. A consumer's validate-locales reads them where the dialog
201
+ * gets them (`DEFAULT_ASK.message`, a const it follows), and in
202
+ * YUI_INSTALL_I18N_KEYS, which a test holds to this table. */
202
203
  const DEFAULT_ASK = Object.freeze({
203
- message: Object.freeze({i18n: "install this app"}),
204
- yes: Object.freeze({i18n: "install"}),
205
- no: Object.freeze({i18n: "not now"})
204
+ message: "install this app",
205
+ yes: "install",
206
+ no: "not now"
206
207
  });
208
+ export const YUI_INSTALL_I18N_KEYS = ["install this app", "install", "not now"];
207
209
 
208
210
  function yui_install_ask_once(shell, opts)
209
211
  {
@@ -224,10 +226,10 @@ function yui_install_ask_once(shell, opts)
224
226
  S.asking = true;
225
227
  kw_set_local_storage_value(ASKED_KEY, true);
226
228
 
227
- yui_shell_confirm_yesno(shell, opts.message || DEFAULT_ASK.message.i18n, {
229
+ yui_shell_confirm_yesno(shell, opts.message || DEFAULT_ASK.message, {
228
230
  t: opts.t,
229
- yes_label: opts.yes_label || DEFAULT_ASK.yes.i18n,
230
- no_label: opts.no_label || DEFAULT_ASK.no.i18n
231
+ yes_label: opts.yes_label || DEFAULT_ASK.yes,
232
+ no_label: opts.no_label || DEFAULT_ASK.no
231
233
  }).then(function(yes) {
232
234
  S.asking = false;
233
235
  if(yes) {
@@ -234,7 +234,6 @@ function yui_selection_bar(t, opts)
234
234
  }
235
235
  }]
236
236
  );
237
- $clear._yui_label = "clear selection";
238
237
 
239
238
  /* NOT `is-flex` in the class list: Bulma's helpers all carry
240
239
  * `!important`, so `is-flex` and `is-hidden` would fight as equals and
@@ -260,7 +259,8 @@ function yui_selection_bar(t, opts)
260
259
  $count.textContent = t("{{n}} selected", {n: count});
261
260
  for(let $b of $buttons.concat([$clear])) {
262
261
  let $label = label_of($b);
263
- let key = $b._yui_label;
262
+ /* A literal, where validate-locales reads it; an action's key is the HOST's */
263
+ const key = ($b === $clear)? "clear selection" : $b._yui_label;
264
264
  if($label && key) {
265
265
  $label.textContent = t(key);
266
266
  }