@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.
- package/README.md +473 -19
- package/dist/gobj-ui.cjs.js +1100 -231
- package/dist/gobj-ui.es.js +1101 -232
- package/package.json +1 -1
- package/scripts/validate-locales.mjs +2334 -275
- package/src/c_g6_nodes_tree.js +1143 -260
- package/src/c_yui_form.js +11 -1
- package/src/c_yui_fsm_graph.js +5 -0
- package/src/c_yui_gclass.js +11 -0
- package/src/c_yui_gobj_tree_js.js +17 -0
- package/src/c_yui_json_graph.js +5 -0
- package/src/c_yui_nav.js +9 -6
- package/src/c_yui_pager.js +1 -1
- package/src/c_yui_period.js +11 -4
- package/src/c_yui_schema_editor.js +19 -7
- package/src/c_yui_shell.js +7 -7
- package/src/c_yui_treedb_graph.js +15 -6
- package/src/c_yui_treedb_schema.js +5 -0
- package/src/c_yui_treedb_topic_with_form.js +10 -10
- package/src/c_yui_treedb_topics.js +6 -6
- package/src/c_yui_window.js +2 -2
- package/src/c_yui_wizard.js +8 -1
- package/src/form_json_field.js +7 -0
- package/src/g6_canvas_tab_order.js +84 -0
- package/src/g6_canvas_tab_order.wiring.test.js +232 -0
- package/src/g6_nodes_tree_controls.test.js +547 -0
- package/src/g6_nodes_tree_focus.wiring.test.js +1360 -0
- package/src/g6_nodes_tree_i18n_keys.test.js +275 -0
- package/src/i18n_key_functions.js +146 -0
- package/src/i18n_key_functions.test.js +1204 -0
- package/src/lib_graph.js +38 -1
- package/src/nav_backbar_name.wiring.test.js +82 -0
- package/src/schema_flags.js +47 -0
- package/src/schema_import.js +7 -0
- package/src/shell_modals.js +20 -16
- package/src/shell_route_map.js +12 -2
- package/src/wizard_helpers.js +8 -0
- package/src/yui_dev.js +44 -33
- package/src/yui_dev_controls.test.js +174 -0
- package/src/yui_gclass_view.js +2 -2
- package/src/yui_graph_camera.js +1 -1
- package/src/yui_install.js +10 -8
- package/src/yui_table_select.js +2 -2
- 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
|
+
});
|
package/src/schema_flags.js
CHANGED
|
@@ -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"]];
|
package/src/schema_import.js
CHANGED
|
@@ -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
|
package/src/shell_modals.js
CHANGED
|
@@ -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
|
-
*
|
|
628
|
-
* consumer
|
|
629
|
-
*
|
|
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:
|
|
633
|
-
yes:
|
|
634
|
-
no:
|
|
635
|
-
delete:
|
|
636
|
-
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
|
-
|
|
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
|
-
|
|
653
|
-
|
|
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
|
-
|
|
677
|
-
|
|
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
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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"},
|
package/src/shell_route_map.js
CHANGED
|
@@ -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},
|
|
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
|
package/src/wizard_helpers.js
CHANGED
|
@@ -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
|
-
/*
|
|
75
|
-
* `
|
|
76
|
-
* `
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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'
|
|
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'
|
|
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
|
+
});
|
package/src/yui_gclass_view.js
CHANGED
|
@@ -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
|
});
|