@yuneta/gobj-ui 7.26.34 → 7.26.36
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 +223 -53
- package/dist/gobj-ui.cjs.js +1109 -143
- package/dist/gobj-ui.es.js +1109 -143
- package/package.json +2 -2
- package/scripts/validate-locales.mjs +672 -96
- package/src/c_yui_form.js +186 -9
- package/src/c_yui_gclass.js +2 -0
- package/src/c_yui_gobj_tree_js.js +2 -0
- package/src/c_yui_json.js +2 -0
- package/src/c_yui_json_graph.js +2 -0
- package/src/c_yui_nav.js +23 -5
- package/src/c_yui_shell.css +16 -0
- package/src/c_yui_shell.js +282 -26
- package/src/c_yui_treedb_graph.js +26 -8
- package/src/c_yui_treedb_topic_with_form.js +40 -18
- package/src/c_yui_treedb_topics.js +34 -11
- package/src/c_yui_window.js +132 -32
- package/src/c_yui_window_manager.js +250 -46
- package/src/form_json_editor_names.js +448 -0
- package/src/form_json_editor_names.wiring.test.js +595 -0
- package/src/form_label_i18n.wiring.test.js +169 -0
- package/src/i18n_key_functions.js +14 -6
- package/src/i18n_key_functions.test.js +124 -2
- package/src/lib_maplibre.js +26 -2
- package/src/lib_maplibre_relocalize.test.js +78 -0
- package/src/named_controls.wiring.test.js +468 -0
- package/src/nav_cards_helpers.js +9 -1
- package/src/nav_item_badge.wiring.test.js +182 -1
- package/src/shell_route_map.js +25 -8
- package/src/table_editor_name.wiring.test.js +201 -0
- package/src/treedb_column_title.wiring.test.js +151 -0
- package/src/treedb_json_drill.wiring.test.js +41 -1
- package/src/window_layer.wiring.test.js +348 -43
- package/src/window_show.wiring.test.js +6 -2
- package/src/window_stacking.wiring.test.js +334 -0
- package/src/yui_badge.js +58 -13
- package/src/yui_badge.test.js +40 -3
- package/src/yui_dev.js +2 -1
- package/src/yui_file_field.js +2 -0
- package/src/yui_frontend_view.js +2 -1
- package/src/yui_gclass_view.js +9 -6
- package/src/yui_tabulator_i18n.js +42 -0
- package/src/yui_tabulator_i18n.test.js +48 -1
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* form_label_i18n.wiring.test.js
|
|
3
|
+
*
|
|
4
|
+
* The label of a field of a C_YUI_FORM is translated by a
|
|
5
|
+
* CASCADE -- '<topic>.<col>', then '<col>', then the header -- and
|
|
6
|
+
* re-translated on a language change by the key its node carries
|
|
7
|
+
* (data-i18n on the <label>, data-i18n-aria-label / -title on the
|
|
8
|
+
* control). Up to 7.26.34 that key was chosen by asking only the
|
|
9
|
+
* bare '<col>':
|
|
10
|
+
*
|
|
11
|
+
* (a) a label translated by '<topic>.<col>' alone carried its own
|
|
12
|
+
* TRANSLATED text as the key, and never changed language;
|
|
13
|
+
* (b) one translated by both switched to the bare '<col>' text at
|
|
14
|
+
* the first language change.
|
|
15
|
+
*
|
|
16
|
+
* Driven through the real gclass on a document double, switching
|
|
17
|
+
* the language with i18next and refresh_language(), as an app does.
|
|
18
|
+
*
|
|
19
|
+
* Copyright (c) 2026, ArtGins.
|
|
20
|
+
* All Rights Reserved.
|
|
21
|
+
***********************************************************************/
|
|
22
|
+
import {describe, test, expect, beforeAll, afterEach, vi} from "vitest";
|
|
23
|
+
import {install_dom_double} from "../test/dom_double.js";
|
|
24
|
+
|
|
25
|
+
install_dom_double();
|
|
26
|
+
|
|
27
|
+
if(typeof globalThis.ResizeObserver === "undefined") {
|
|
28
|
+
globalThis.ResizeObserver = class {
|
|
29
|
+
observe() {}
|
|
30
|
+
unobserve() {}
|
|
31
|
+
disconnect() {}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* No json field here, but the module imports the editor. */
|
|
36
|
+
vi.mock("vanilla-jsoneditor", () => ({
|
|
37
|
+
createJSONEditor: () => ({
|
|
38
|
+
get() {
|
|
39
|
+
return {text: ""};
|
|
40
|
+
},
|
|
41
|
+
set() {},
|
|
42
|
+
destroy() {},
|
|
43
|
+
}),
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
const {
|
|
47
|
+
SDATA_END, gclass_create,
|
|
48
|
+
gobj_start_up, gobj_create_yuno, gobj_create_pure_child,
|
|
49
|
+
gobj_start, gobj_read_attr, refresh_language,
|
|
50
|
+
} = await import("@yuneta/gobj-js");
|
|
51
|
+
const {register_c_yui_form} = await import("./c_yui_form.js");
|
|
52
|
+
|
|
53
|
+
let i18next = null;
|
|
54
|
+
let yuno = null;
|
|
55
|
+
let seq = 0;
|
|
56
|
+
|
|
57
|
+
beforeAll(async () => {
|
|
58
|
+
i18next = (await import("i18next")).default;
|
|
59
|
+
await i18next.init({
|
|
60
|
+
lng: "en",
|
|
61
|
+
resources: {
|
|
62
|
+
en: {translation: {
|
|
63
|
+
"devices.alias": "Device alias",
|
|
64
|
+
"groups.title": "Group title",
|
|
65
|
+
"title": "Title"
|
|
66
|
+
}},
|
|
67
|
+
es: {translation: {
|
|
68
|
+
"devices.alias": "Alias del equipo",
|
|
69
|
+
"groups.title": "Título del grupo",
|
|
70
|
+
"title": "Título"
|
|
71
|
+
}}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
gobj_start_up(null, null, null, null, null, null, null);
|
|
75
|
+
gclass_create("C_TEST_LABEL_HOST", [], [["ST_IDLE", []]], {}, 0, [SDATA_END()], {}, 0, 0, 0, 0);
|
|
76
|
+
gclass_create(
|
|
77
|
+
"C_TEST_LABEL_APP",
|
|
78
|
+
[["EV_RECORD_CHANGED", 0]],
|
|
79
|
+
[["ST_IDLE", [["EV_RECORD_CHANGED", () => 0, null]]]],
|
|
80
|
+
{}, 0, [SDATA_END()], {}, 0, 0, 0, 0
|
|
81
|
+
);
|
|
82
|
+
register_c_yui_form();
|
|
83
|
+
yuno = gobj_create_yuno("form_label_yuno", "C_TEST_LABEL_HOST", {});
|
|
84
|
+
gobj_start(yuno);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
afterEach(async () => {
|
|
88
|
+
await i18next.changeLanguage("en");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
/* A form of `topic` with one writable column `col`, built in English. */
|
|
92
|
+
function build(topic, col)
|
|
93
|
+
{
|
|
94
|
+
const app = gobj_create_pure_child(`lapp${++seq}`, "C_TEST_LABEL_APP", {}, yuno);
|
|
95
|
+
gobj_start(app);
|
|
96
|
+
const form = gobj_create_pure_child(`lform${seq}`, "C_YUI_FORM", {
|
|
97
|
+
template: [
|
|
98
|
+
{id: "id", header: "id", type: "string", flag: ["persistent", "required"]},
|
|
99
|
+
{id: col, header: col, type: "string", flag: ["persistent", "writable"]},
|
|
100
|
+
],
|
|
101
|
+
topic_name: topic,
|
|
102
|
+
record: {id: "a", [col]: "x"},
|
|
103
|
+
render_mode: "edit",
|
|
104
|
+
editable: true,
|
|
105
|
+
}, app);
|
|
106
|
+
gobj_start(form);
|
|
107
|
+
const $c = gobj_read_attr(form, "$container");
|
|
108
|
+
const $label = $c.querySelector(`label[for="${col}"]`);
|
|
109
|
+
const $input = $c.querySelector(`input[name="${col}"]`);
|
|
110
|
+
expect(!!$label).toBe(true);
|
|
111
|
+
expect(!!$input).toBe(true);
|
|
112
|
+
return {$c, $label, $input};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* What an app does on a language change. */
|
|
116
|
+
async function switch_to_spanish($c)
|
|
117
|
+
{
|
|
118
|
+
await i18next.changeLanguage("es");
|
|
119
|
+
refresh_language($c, i18next.t.bind(i18next));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/* The text refresh_language() writes into a node that carries
|
|
123
|
+
* data-i18n: t(<that key>). Asked directly, because the document
|
|
124
|
+
* double's text nodes have no `nodeValue`, which is what it writes
|
|
125
|
+
* (the attributes -- aria-label, title -- it rewrites in the double
|
|
126
|
+
* too, and those are read from the control). */
|
|
127
|
+
function retranslated($el)
|
|
128
|
+
{
|
|
129
|
+
return i18next.t($el.getAttribute("data-i18n"));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
describe("the label of a field changes language by the key that translated it", () => {
|
|
133
|
+
test("(a) translated by '<topic>.<col>' alone", async () => {
|
|
134
|
+
const {$c, $label, $input} = build("devices", "alias");
|
|
135
|
+
expect($label.textContent).toBe("Device alias");
|
|
136
|
+
expect($label.getAttribute("data-i18n")).toBe("devices.alias");
|
|
137
|
+
expect($input.getAttribute("data-i18n-aria-label")).toBe("devices.alias");
|
|
138
|
+
|
|
139
|
+
await switch_to_spanish($c);
|
|
140
|
+
expect(retranslated($label)).toBe("Alias del equipo");
|
|
141
|
+
expect($input.getAttribute("aria-label")).toBe("Alias del equipo");
|
|
142
|
+
expect($input.getAttribute("title")).toBe("Alias del equipo");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("(b) translated by '<topic>.<col>' and by '<col>': the topic's wins, before and after", async () => {
|
|
146
|
+
const {$c, $label, $input} = build("groups", "title");
|
|
147
|
+
expect($label.textContent).toBe("Group title");
|
|
148
|
+
expect($label.getAttribute("data-i18n")).toBe("groups.title");
|
|
149
|
+
|
|
150
|
+
await switch_to_spanish($c);
|
|
151
|
+
expect(retranslated($label)).toBe("Título del grupo");
|
|
152
|
+
expect($input.getAttribute("aria-label")).toBe("Título del grupo");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("translated by '<col>' alone, as before", async () => {
|
|
156
|
+
const {$c, $label} = build("other", "title");
|
|
157
|
+
expect($label.textContent).toBe("Title");
|
|
158
|
+
expect($label.getAttribute("data-i18n")).toBe("title");
|
|
159
|
+
|
|
160
|
+
await switch_to_spanish($c);
|
|
161
|
+
expect(retranslated($label)).toBe("Título");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("translated by nothing: the header, as before", () => {
|
|
165
|
+
const {$label} = build("other", "zone");
|
|
166
|
+
expect($label.textContent).toBe("zone");
|
|
167
|
+
expect($label.getAttribute("data-i18n")).toBe("zone");
|
|
168
|
+
});
|
|
169
|
+
});
|
|
@@ -71,11 +71,15 @@
|
|
|
71
71
|
* literal (or DOM, or nothing), a const the reader follows, a key
|
|
72
72
|
* position of the enclosing table function handed on, or a marked
|
|
73
73
|
* value fails, with the file and the line; a TRANSLATED text --
|
|
74
|
-
* t("..."), tr(), an alias of t, `t?.()`, `t.call()
|
|
75
|
-
*
|
|
76
|
-
* language). A
|
|
77
|
-
*
|
|
78
|
-
*
|
|
74
|
+
* t("..."), tr(), an alias of t, `t?.()`, `t.call()`, or a string
|
|
75
|
+
* built from one, `t("k") + ":"` -- fails even marked (it would be
|
|
76
|
+
* translated again, and never change language). A translator
|
|
77
|
+
* handed as a callback is held the same way: each element
|
|
78
|
+
* `keys.map(t)` walks is a key position, and what `.then(t)` is
|
|
79
|
+
* handed is one no source writes. A test
|
|
80
|
+
* (i18n_key_functions.test.js) holds the library to it, and
|
|
81
|
+
* validate-locales an app's own code. Publishing a list exempts
|
|
82
|
+
* no module.
|
|
79
83
|
*
|
|
80
84
|
* The table is COMPLETE and CORRECT, and the same test proves both
|
|
81
85
|
* (REV-CLOUD V21.4.1, V22): it finds every function of the library
|
|
@@ -233,7 +237,8 @@ export const I18N_KEY_FUNCTIONS = Object.freeze([
|
|
|
233
237
|
keys: ["1[].name", "1[].tooltip", "1[].aria_label"]},
|
|
234
238
|
{module: "c_yui_nav.js", name: "render_drawer", exported: false,
|
|
235
239
|
keys: ["1[].name", "1[].tooltip", "1[].aria_label"]},
|
|
236
|
-
{module: "c_yui_nav.js", name: "render_breadcrumb", exported: false,
|
|
240
|
+
{module: "c_yui_nav.js", name: "render_breadcrumb", exported: false,
|
|
241
|
+
keys: ["1[].name", "1[].id", "1[].tooltip", "1[].aria_label"]},
|
|
237
242
|
{module: "c_yui_nav.js", name: "render_accordion", exported: false,
|
|
238
243
|
keys: ["1[].name", "1[].submenu.items[].name", "1[].submenu.items[].tooltip",
|
|
239
244
|
"1[].submenu.items[].aria_label"]},
|
|
@@ -253,6 +258,9 @@ export const I18N_KEY_FUNCTIONS = Object.freeze([
|
|
|
253
258
|
{module: "c_yui_shell.js", name: "toggle_toolbar_dropdown", exported: false, keys: ["1.name", "1.aria_label"]},
|
|
254
259
|
{module: "c_yui_shell.js", name: "open_toolbar_dropdown", exported: false, keys: ["1.name", "1.aria_label"]},
|
|
255
260
|
{module: "c_yui_shell.js", name: "build_dropdown_row", exported: false, keys: ["1.name", "1.aria_label"]},
|
|
261
|
+
/* c_yui_shell.js -- the name of a badged item, said with its count:
|
|
262
|
+
* the app's shell declaration */
|
|
263
|
+
{module: "c_yui_shell.js", name: "announce_badge", exported: false, keys: ["1"]},
|
|
256
264
|
/* shell_route_map.js -- a node of the route map */
|
|
257
265
|
{module: "shell_route_map.js", name: "render_node", exported: false, keys: ["0.label"]},
|
|
258
266
|
]);
|
|
@@ -36,8 +36,10 @@
|
|
|
36
36
|
* handed -- a comment `i18n: <NAME>_I18N_KEYS` (the module
|
|
37
37
|
* publishes those keys) or `i18n: data` (no key a list can hold).
|
|
38
38
|
* Anything else fails, naming the file and the line; so does a
|
|
39
|
-
* translated text -- t("..."), tr(), an alias of t
|
|
40
|
-
* not
|
|
39
|
+
* translated text -- t("..."), tr(), an alias of t, a string built
|
|
40
|
+
* from one (`t("k") + ":"`) -- marked or not. A translator handed
|
|
41
|
+
* as a callback (V23.5: `keys.map(t)`, `promise.then(t)`) is held
|
|
42
|
+
* the same way, each element it walks a key position;
|
|
41
43
|
* - every marker is one and stands where a key is read; the lists
|
|
42
44
|
* it names are published by its module or one it imports; the
|
|
43
45
|
* keys it marks that can be read are in them -- a marked `let`
|
|
@@ -1935,3 +1937,123 @@ describe("REV-CLOUD V22.3-4: the data marker hides no key; one notion of a trans
|
|
|
1935
1937
|
[{name: "B_I18N_KEYS", keys: ["b"]}]).map((e) => e.line)).toEqual([2]);
|
|
1936
1938
|
});
|
|
1937
1939
|
});
|
|
1940
|
+
|
|
1941
|
+
describe("REV-CLOUD V23.5: a translator as a callback, by destructuring; setAttributeNS; t() compositions; aliases", () => {
|
|
1942
|
+
let module = "x.js";
|
|
1943
|
+
let head = [`import {yui_shell_show_info} from "./shell_modals.js";`,
|
|
1944
|
+
`import i18next from "i18next";`];
|
|
1945
|
+
let scan = (lines) => scan_ast(parse_as([...head, ...lines].join("\n"), module), {lib_module: module});
|
|
1946
|
+
let rule = (lines) => rule_report(parse_as([...head, ...lines].join("\n"), module), module);
|
|
1947
|
+
let found_in = (lines) => {
|
|
1948
|
+
let ast = parse_as(lines.join("\n"), module);
|
|
1949
|
+
return find_forwarders([module], () => ast, []);
|
|
1950
|
+
};
|
|
1951
|
+
|
|
1952
|
+
test("each element a translator handed to .map() / .forEach() walks is a key; a promise's value is none", () => {
|
|
1953
|
+
let r = scan([`const KINDS = ["kind a", "kind b"];`,
|
|
1954
|
+
`KINDS.map(t);`,
|
|
1955
|
+
`["literal a"].forEach(i18next.t);`,
|
|
1956
|
+
`Array.from(["from a"], t);`]);
|
|
1957
|
+
expect(["kind a", "kind b", "literal a", "from a"].every((k) => r.keys.has(k))).toBe(true);
|
|
1958
|
+
expect(rule([`const KINDS = ["kind a"];`, `KINDS.map(t);`])).toEqual([]);
|
|
1959
|
+
expect(rule([`function f(kw) {`,
|
|
1960
|
+
` let rows = kw.rows;`,
|
|
1961
|
+
` rows.map(t);`,
|
|
1962
|
+
` load().then(t);`,
|
|
1963
|
+
` /* i18n: data */ rows.forEach(t);`,
|
|
1964
|
+
` load().catch(/* i18n: data */ i18next.t);`,
|
|
1965
|
+
` [t("a")].map(t);`,
|
|
1966
|
+
`}`])).toEqual([
|
|
1967
|
+
`${module}:5 t() [map callback]: rows`,
|
|
1968
|
+
`${module}:6 t() [then callback]: load().then(t)`,
|
|
1969
|
+
`${module}:9 t() [map callback]: t("a") -- a TRANSLATED text where a key goes: hand the key`
|
|
1970
|
+
]);
|
|
1971
|
+
/* A translator handed to a FUNCTION is how gobj-ui takes one: no callback of these */
|
|
1972
|
+
expect(rule([`function f(shell) { yui_shell_show_info(shell, "k", {t: t}); build(rows, t); }`])).toEqual([]);
|
|
1973
|
+
});
|
|
1974
|
+
|
|
1975
|
+
test("the search for forwarders sees them: an array walked by t(), setAttributeNS(), a destructured t", () => {
|
|
1976
|
+
let found = found_in([
|
|
1977
|
+
`const ATTR = "data-i18n-title";`,
|
|
1978
|
+
`export function f_map(gobj, keys) { return keys.map(t); }`,
|
|
1979
|
+
`export function f_ns(gobj, el, k) { el.setAttributeNS(null, ATTR, k); }`,
|
|
1980
|
+
`export function f_destr(gobj, {t: translate}, k) { return translate(k); }`,
|
|
1981
|
+
`export function f_const(gobj, k) { const {t: tt} = i18next; return tt(k); }`
|
|
1982
|
+
]);
|
|
1983
|
+
expect(found.opaque).toEqual([]);
|
|
1984
|
+
expect(Object.fromEntries(found.found.map((f) => [f.name, f.keys]))).toEqual({
|
|
1985
|
+
f_map: ["1[]"], f_ns: ["2"], f_destr: ["2"], f_const: ["1"]
|
|
1986
|
+
});
|
|
1987
|
+
});
|
|
1988
|
+
|
|
1989
|
+
test("a destructured translator translates: its key is asked for, and its text is no key", () => {
|
|
1990
|
+
let r = scan([`const {t: tt} = i18next;`, `tt("a destructured key");`]);
|
|
1991
|
+
expect(r.keys.has("a destructured key")).toBe(true);
|
|
1992
|
+
expect(rule([`const {t: tt} = i18next;`, `yui_shell_show_info(shell, /* i18n: data */ tt("k"));`]))
|
|
1993
|
+
.toEqual([`${module}:4 yui_shell_show_info() [1]: tt("k") -- a TRANSLATED text where a key goes: hand the key`]);
|
|
1994
|
+
});
|
|
1995
|
+
|
|
1996
|
+
test("a text BUILT from a translation is translated text: at a key position, and under a marker anywhere", () => {
|
|
1997
|
+
expect(rule([`yui_shell_show_info(shell, /* i18n: data */ t("k") + ":");`,
|
|
1998
|
+
`yui_shell_show_info(shell, /* i18n: data */ t("k").toUpperCase());`,
|
|
1999
|
+
"yui_shell_show_info(shell, /* i18n: data */ `${t(\"k\")}: ${n}`);",
|
|
2000
|
+
`yui_shell_show_info(shell, /* i18n: data */ [t("a"), name].join(" "));`,
|
|
2001
|
+
`yui_shell_show_info(shell, /* i18n: data */ name + ":");`,
|
|
2002
|
+
`yui_shell_show_info(shell, /* i18n: data */ ["p", {}, t("k")]);`])
|
|
2003
|
+
.map((l) => l.replace(/ -- .*/, ""))).toEqual([
|
|
2004
|
+
`${module}:3 yui_shell_show_info() [1]: t("k") + ":"`,
|
|
2005
|
+
`${module}:4 yui_shell_show_info() [1]: t("k").toUpperCase()`,
|
|
2006
|
+
`${module}:5 yui_shell_show_info() [1]: \`\${t("k")}: \${n}\``,
|
|
2007
|
+
`${module}:6 yui_shell_show_info() [1]: [t("a"), name].join(" ")`
|
|
2008
|
+
]);
|
|
2009
|
+
let r = scan([`t(/* i18n: data */ t("k") + "!");`,
|
|
2010
|
+
`x.i18n = /* i18n: data */ t("k").trim();`,
|
|
2011
|
+
`t(/* i18n: data */ kw.comment + "!");`]);
|
|
2012
|
+
expect(r.marker_errors.map((e) => e.line)).toEqual([3, 4]);
|
|
2013
|
+
expect(r.marker_errors[0].why).toMatch(/^marked as data, but `t\("k"\) \+ "!"` is a TRANSLATED text: .*hand the key, not t\(key\)$/);
|
|
2014
|
+
});
|
|
2015
|
+
|
|
2016
|
+
test("an UNMARKED let given nothing but literals is read where a key is read -- at a key position the rule still fails it", () => {
|
|
2017
|
+
let r = scan([`function f(x, el) {`,
|
|
2018
|
+
` let k = x? "a let key" : "another let key";`,
|
|
2019
|
+
` let held = "a held key";`,
|
|
2020
|
+
` held = k;`, // a let given a literal-only let
|
|
2021
|
+
` el.setAttribute("data-i18n", held);`,
|
|
2022
|
+
` return t(k);`,
|
|
2023
|
+
`}`,
|
|
2024
|
+
`function g(kw) {`,
|
|
2025
|
+
` let d = "a data let key";`,
|
|
2026
|
+
` d = kw.comment;`,
|
|
2027
|
+
` return t(d);`,
|
|
2028
|
+
`}`]);
|
|
2029
|
+
expect(["a let key", "another let key", "a held key"].every((k) => r.keys.has(k))).toBe(true);
|
|
2030
|
+
expect(r.keys.has("a data let key")).toBe(false);
|
|
2031
|
+
expect(r.marker_errors).toEqual([]);
|
|
2032
|
+
expect(rule([`function f(x) { let k = x? "a" : "b"; yui_shell_show_info(shell, k); }`]))
|
|
2033
|
+
.toEqual([`${module}:3 yui_shell_show_info() [1]: k`]);
|
|
2034
|
+
});
|
|
2035
|
+
|
|
2036
|
+
test("a published list mutated through a const or let alias, or a namespace member", () => {
|
|
2037
|
+
let ast = parse_source([
|
|
2038
|
+
`import * as M from "./m.js";`,
|
|
2039
|
+
`import {Y_I18N_KEYS} from "./y.js";`,
|
|
2040
|
+
`export const X_I18N_KEYS = ["x"];`,
|
|
2041
|
+
`const a = X_I18N_KEYS;`,
|
|
2042
|
+
`a.push("k");`, // 5
|
|
2043
|
+
`let b = Y_I18N_KEYS;`,
|
|
2044
|
+
`b[0] = "k";`, // 7
|
|
2045
|
+
`M.Z_I18N_KEYS.unshift("k");`, // 8
|
|
2046
|
+
`const z = M.Z_I18N_KEYS;`,
|
|
2047
|
+
`delete z[0];`, // 10
|
|
2048
|
+
`let c = X_I18N_KEYS;`,
|
|
2049
|
+
`c = c.concat(["k"]);`,
|
|
2050
|
+
`c.push("a copy");`,
|
|
2051
|
+
`M.helper.push("not a list");`,
|
|
2052
|
+
`function f(a) { a.push("a parameter"); }`,
|
|
2053
|
+
`let d = X_I18N_KEYS;`,
|
|
2054
|
+
`d = X_I18N_KEYS;`, // the alias pointed again: no mutation
|
|
2055
|
+
`X_I18N_KEYS = [];` // 18: the list itself given another value
|
|
2056
|
+
].join("\n"));
|
|
2057
|
+
expect(list_mutations(ast, [{name: "X_I18N_KEYS", keys: ["x"]}]).map((e) => e.line)).toEqual([5, 7, 8, 10, 18]);
|
|
2058
|
+
});
|
|
2059
|
+
});
|
package/src/lib_maplibre.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Utilities for maplibre
|
|
5
5
|
*
|
|
6
|
-
* Copyright (c) 2025, ArtGins.
|
|
6
|
+
* Copyright (c) 2025-2026, ArtGins.
|
|
7
7
|
* All Rights Reserved.
|
|
8
8
|
***********************************************************************/
|
|
9
9
|
import {
|
|
@@ -283,6 +283,23 @@ function label_ctrl($root, selector, text)
|
|
|
283
283
|
}
|
|
284
284
|
}
|
|
285
285
|
|
|
286
|
+
/* What maplibre wrote ONCE from its locale and never again: the name
|
|
287
|
+
* of the map's canvas (`Map.Title`: a region a keyboard focuses, so a
|
|
288
|
+
* reader announces it) and of a default marker (`Marker.Title`).
|
|
289
|
+
* Rewritten where it still holds the word maplibre wrote -- a name an
|
|
290
|
+
* app gave a marker is its own. */
|
|
291
|
+
function relabel_written($root, selector, was, now)
|
|
292
|
+
{
|
|
293
|
+
if(!$root || !now) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
for(const $el of $root.querySelectorAll(selector)) {
|
|
297
|
+
if($el.getAttribute("aria-label") === was) {
|
|
298
|
+
$el.setAttribute("aria-label", now);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
286
303
|
function yui_maplibre_relocalize(map, t)
|
|
287
304
|
{
|
|
288
305
|
if(!map || typeof map.getContainer !== "function") {
|
|
@@ -291,7 +308,9 @@ function yui_maplibre_relocalize(map, t)
|
|
|
291
308
|
const strings = yui_maplibre_locale(t);
|
|
292
309
|
|
|
293
310
|
/* The map's own table first: a control added later (or a popup
|
|
294
|
-
* opened later) reads it when it builds itself.
|
|
311
|
+
* opened later) reads it when it builds itself. What it said
|
|
312
|
+
* before is kept, to know which names maplibre wrote with it. */
|
|
313
|
+
const was = Object.assign({}, map._locale);
|
|
295
314
|
try {
|
|
296
315
|
map._locale = Object.assign({}, map._locale, strings);
|
|
297
316
|
} catch(e) {
|
|
@@ -308,6 +327,11 @@ function yui_maplibre_relocalize(map, t)
|
|
|
308
327
|
label_ctrl($c, ".maplibregl-ctrl-fullscreen", strings["FullscreenControl.Enter"]);
|
|
309
328
|
label_ctrl($c, ".maplibregl-popup-close-button", strings["Popup.Close"]);
|
|
310
329
|
|
|
330
|
+
/* The canvas said "Mapa" in an English app after a switch, and a
|
|
331
|
+
* default marker "Marcador del mapa": maplibre names them once. */
|
|
332
|
+
relabel_written($c, ".maplibregl-canvas", was["Map.Title"], strings["Map.Title"]);
|
|
333
|
+
relabel_written($c, ".maplibregl-marker", was["Marker.Title"], strings["Marker.Title"]);
|
|
334
|
+
|
|
311
335
|
/* The notice of the cooperative gesture: two lines, one for a
|
|
312
336
|
* pointer and one for a finger, built by `_setupUI()` as
|
|
313
337
|
* `.maplibregl-desktop-message` / `.maplibregl-mobile-message`. */
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* lib_maplibre_relocalize.test.js
|
|
3
|
+
*
|
|
4
|
+
* After a language change, yui_maplibre_relocalize() rewrites the
|
|
5
|
+
* words maplibre drew from its locale. Two of them maplibre writes
|
|
6
|
+
* ONCE and never again: the aria-label of the map's canvas
|
|
7
|
+
* (`Map.Title`, a region a keyboard focuses) and of a default
|
|
8
|
+
* marker (`Marker.Title`). Up to 7.26.34 they stayed in the
|
|
9
|
+
* language the map was built in -- "Mapa" in an English app after
|
|
10
|
+
* a switch (REV-CLOUD V23.5.6). A name an app gave a marker is its
|
|
11
|
+
* own, and is left alone.
|
|
12
|
+
*
|
|
13
|
+
* Copyright (c) 2026, ArtGins.
|
|
14
|
+
* All Rights Reserved.
|
|
15
|
+
***********************************************************************/
|
|
16
|
+
import {describe, test, expect, beforeAll} from "vitest";
|
|
17
|
+
import {install_dom_double} from "../test/dom_double.js";
|
|
18
|
+
|
|
19
|
+
install_dom_double();
|
|
20
|
+
|
|
21
|
+
const {yui_maplibre_locale, yui_maplibre_relocalize} = await import("./lib_maplibre.js");
|
|
22
|
+
|
|
23
|
+
let i18next = null;
|
|
24
|
+
|
|
25
|
+
beforeAll(async () => {
|
|
26
|
+
i18next = (await import("i18next")).default;
|
|
27
|
+
await i18next.init({
|
|
28
|
+
lng: "es",
|
|
29
|
+
resources: {
|
|
30
|
+
es: {translation: {"map": "Mapa", "map marker": "Marcador del mapa"}},
|
|
31
|
+
en: {translation: {"map": "Map", "map marker": "Map marker"}}
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
/* A map built in Spanish, as maplibre leaves it: its locale table, and
|
|
37
|
+
* the names it wrote from it. */
|
|
38
|
+
function spanish_map()
|
|
39
|
+
{
|
|
40
|
+
const $c = document.createElement("div");
|
|
41
|
+
const $canvas = document.createElement("canvas");
|
|
42
|
+
$canvas.className = "maplibregl-canvas";
|
|
43
|
+
$canvas.setAttribute("aria-label", "Mapa");
|
|
44
|
+
$c.appendChild($canvas);
|
|
45
|
+
|
|
46
|
+
const $default = document.createElement("div");
|
|
47
|
+
$default.className = "maplibregl-marker";
|
|
48
|
+
$default.setAttribute("aria-label", "Marcador del mapa");
|
|
49
|
+
$c.appendChild($default);
|
|
50
|
+
|
|
51
|
+
const $own = document.createElement("div");
|
|
52
|
+
$own.className = "maplibregl-marker";
|
|
53
|
+
$own.setAttribute("aria-label", "Sensor 7");
|
|
54
|
+
$c.appendChild($own);
|
|
55
|
+
|
|
56
|
+
const map = {
|
|
57
|
+
_locale: yui_maplibre_locale(i18next.getFixedT("es")),
|
|
58
|
+
getContainer: () => $c,
|
|
59
|
+
};
|
|
60
|
+
return {map, $canvas, $default, $own};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
describe("the names maplibre writes once", () => {
|
|
64
|
+
test("follow a language change: the canvas and a default marker", () => {
|
|
65
|
+
const {map, $canvas, $default, $own} = spanish_map();
|
|
66
|
+
yui_maplibre_relocalize(map, i18next.getFixedT("en"));
|
|
67
|
+
|
|
68
|
+
expect($canvas.getAttribute("aria-label")).toBe("Map");
|
|
69
|
+
expect($default.getAttribute("aria-label")).toBe("Map marker");
|
|
70
|
+
expect($own.getAttribute("aria-label")).toBe("Sensor 7"); // the app's own
|
|
71
|
+
expect(map._locale["Map.Title"]).toBe("Map");
|
|
72
|
+
|
|
73
|
+
/* And back, from what the last switch wrote. */
|
|
74
|
+
yui_maplibre_relocalize(map, i18next.getFixedT("es"));
|
|
75
|
+
expect($canvas.getAttribute("aria-label")).toBe("Mapa");
|
|
76
|
+
expect($default.getAttribute("aria-label")).toBe("Marcador del mapa");
|
|
77
|
+
});
|
|
78
|
+
});
|