@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.
- package/README.md +458 -22
- package/dist/gobj-ui.cjs.js +1063 -216
- package/dist/gobj-ui.es.js +1064 -217
- package/package.json +1 -1
- package/scripts/validate-locales.mjs +2269 -316
- package/src/c_g6_nodes_tree.js +1086 -253
- 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 +241 -5
- 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 +16 -3
- 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
|
@@ -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'
|
|
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'
|
|
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
|
+
});
|
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
|
});
|
package/src/yui_graph_camera.js
CHANGED
|
@@ -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
|
-
|
|
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));
|
package/src/yui_install.js
CHANGED
|
@@ -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.
|
|
201
|
-
*
|
|
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:
|
|
204
|
-
yes:
|
|
205
|
-
no:
|
|
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
|
|
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
|
|
230
|
-
no_label: opts.no_label || DEFAULT_ASK.no
|
|
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) {
|
package/src/yui_table_select.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|