@yuneta/gobj-ui 7.26.28 → 7.26.29
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 +169 -40
- package/dist/gobj-ui.cjs.js +17 -13
- package/dist/gobj-ui.es.js +17 -13
- package/package.json +4 -2
- package/scripts/validate-locales.mjs +565 -177
- package/src/c_yui_form.js +5 -3
- package/src/c_yui_gobj_tree_js.js +1 -1
- package/src/c_yui_nav.js +12 -10
- package/src/c_yui_period.js +3 -3
- package/src/c_yui_schema_editor.js +22 -18
- package/src/c_yui_shell.js +4 -3
- package/src/c_yui_treedb_graph.js +3 -4
- package/src/c_yui_treedb_topic_with_form.js +15 -15
- package/src/c_yui_treedb_topics.js +8 -8
- package/src/c_yui_window_manager.js +5 -2
- package/src/i18n_key_functions.js +112 -23
- package/src/i18n_key_functions.test.js +329 -8
- package/src/shell_route_map.js +2 -2
- package/src/yui_gclass_view.js +1 -1
- package/src/yui_time.js +1 -1
|
@@ -29,30 +29,53 @@
|
|
|
29
29
|
* `label` of each object literal
|
|
30
30
|
* of the array `actions`
|
|
31
31
|
* A position may also hold DOM or nothing: only
|
|
32
|
-
* a string there is a key.
|
|
32
|
+
* a string there is a key. DOM is an array (a
|
|
33
|
+
* createElement2 spec), a call of createElement2(),
|
|
34
|
+
* createOneHtml() or html_markup() of gobj-js, of
|
|
35
|
+
* text_node() of gobj-ui, of document.createElement()
|
|
36
|
+
* and the like, a const bound to one, a plain `let`
|
|
37
|
+
* only ever given one -- known by where it comes
|
|
38
|
+
* from, never by a variable's name.
|
|
33
39
|
*
|
|
34
40
|
* What is read at a position: a string literal, a template with
|
|
35
41
|
* no `${}`, every branch of a `?:`, `||`, `??` (the right one of
|
|
36
42
|
* `&&`), and what the reader follows -- a const to its value, a
|
|
37
43
|
* property of a const object, the element of a const array a
|
|
38
|
-
* `.map()` or a for...of walks
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* is
|
|
44
|
-
*
|
|
45
|
-
*
|
|
44
|
+
* `.map()` or a for...of walks (and what a destructuring reads
|
|
45
|
+
* from it), String(x) and `${x}` as x, the default of a parameter,
|
|
46
|
+
* a call of a function of the same module whose every return is
|
|
47
|
+
* one of these. A `let`, a parameter or data is not read: its keys
|
|
48
|
+
* are published in an exported `<NAME>_I18N_KEYS` array, and the
|
|
49
|
+
* call is MARKED with a comment right before what it hands over,
|
|
50
|
+
* `i18n: <NAME>_I18N_KEYS` (the list) or `i18n: data` (no key a
|
|
51
|
+
* list can hold). A marked `let` is held to its marker: every
|
|
52
|
+
* literal it is given is read and must be in the list, and one
|
|
53
|
+
* given nothing but literals fails (write it as a const, which the
|
|
54
|
+
* reader follows). A `data` marker on what the reader follows to
|
|
55
|
+
* keys and nothing else -- a literal, a const or a branch that is
|
|
56
|
+
* one, a call that returns literals -- fails too: it claims data
|
|
57
|
+
* where the reader reads a key.
|
|
46
58
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
59
|
+
* The rule, per CALL: a key position given anything else than a
|
|
60
|
+
* literal (or DOM, or nothing), a const the reader follows, a key
|
|
61
|
+
* position of the enclosing table function handed on, or a marked
|
|
62
|
+
* value fails, with the file and the line; a TRANSLATED text,
|
|
63
|
+
* t("..."), fails even marked (it would be translated again, and
|
|
64
|
+
* never change language). A test (i18n_key_functions.test.js)
|
|
65
|
+
* holds the library to it, and validate-locales an app's own code.
|
|
66
|
+
* Publishing a list exempts no module.
|
|
52
67
|
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
68
|
+
* The table is COMPLETE, and the same test proves it: it finds
|
|
69
|
+
* every function of the library that hands one of its parameters
|
|
70
|
+
* -- or what it reads from one, `opts.title`, a destructured
|
|
71
|
+
* `{label}`, each `items[].name` -- to t(), to an `i18n`, `code` or
|
|
72
|
+
* `data-i18n*` value, or to a key position of a function of this
|
|
73
|
+
* table (REV-CLOUD V21.4.1: up to 7.26.28 it lacked 39 such
|
|
74
|
+
* functions, and 6 entries lacked positions).
|
|
75
|
+
* One called by name goes here, with that position. One nothing
|
|
76
|
+
* calls by name -- an FSM action, a callback, a method -- has no
|
|
77
|
+
* call to read: it marks what it hands on, where it hands it. What
|
|
78
|
+
* a gobj keeps in its `priv` is its own state, never a position.
|
|
56
79
|
*
|
|
57
80
|
* Copyright (c) 2026, ArtGins.
|
|
58
81
|
* All Rights Reserved.
|
|
@@ -88,6 +111,13 @@ export const I18N_KEY_FUNCTIONS = Object.freeze([
|
|
|
88
111
|
{module: "table_cell_text.js", name: "hook_cell_spec", exported: true, keys: ["4"]},
|
|
89
112
|
/* yui_time.js -- a granularity names itself by its id */
|
|
90
113
|
{module: "yui_time.js", name: "period_name", exported: true, keys: ["0", "0.id"]},
|
|
114
|
+
/* nav_cards_helpers.js -- a nav item's name, tooltip and aria label */
|
|
115
|
+
{module: "nav_cards_helpers.js", name: "card_descriptor", exported: true,
|
|
116
|
+
keys: ["0.name", "0.tooltip", "0.aria_label"]},
|
|
117
|
+
{module: "nav_cards_helpers.js", name: "cards_grid_descriptor", exported: true,
|
|
118
|
+
keys: ["0[].name", "0[].tooltip", "0[].aria_label"]},
|
|
119
|
+
/* yui_gclass_view.js -- the dialog's title is the gclass name */
|
|
120
|
+
{module: "yui_gclass_view.js", name: "open_gclass_view", exported: true, keys: ["1"]},
|
|
91
121
|
|
|
92
122
|
/*
|
|
93
123
|
* Helpers of their own module
|
|
@@ -106,21 +136,36 @@ export const I18N_KEY_FUNCTIONS = Object.freeze([
|
|
|
106
136
|
{module: "c_yui_gclass.js", name: "empty_line", exported: false, keys: ["0"]},
|
|
107
137
|
{module: "c_yui_gclass.js", name: "build_zone", exported: false, keys: ["1.key"]},
|
|
108
138
|
{module: "c_yui_period.js", name: "nav_button", exported: false, keys: ["3"]},
|
|
109
|
-
{module: "c_yui_period.js", name: "mode_name", exported: false,
|
|
139
|
+
{module: "c_yui_period.js", name: "mode_name", exported: false,
|
|
140
|
+
keys: ["0.id", "0.roll.label", "0.spec", "0.spec.id"]},
|
|
141
|
+
{module: "c_yui_period.js", name: "build_mode_button", exported: false,
|
|
142
|
+
keys: ["1.id", "1.roll.label", "1.spec", "1.spec.id"]},
|
|
110
143
|
{module: "c_yui_form.js", name: "cell_icon", exported: false, keys: ["1"]},
|
|
144
|
+
{module: "c_yui_form.js", name: "name_form_control", exported: false, keys: ["1.label_i18n", "1.label"]},
|
|
145
|
+
{module: "c_yui_form.js", name: "create_form_field", exported: false,
|
|
146
|
+
keys: ["4.label_i18n", "4.label", "4.options", "4.options[]"]},
|
|
111
147
|
{module: "c_yui_treedb_graph.js", name: "step_button", exported: false, keys: ["2"]},
|
|
112
148
|
{module: "c_yui_treedb_graph.js", name: "layout_step_button", exported: false, keys: ["3"]},
|
|
113
149
|
{module: "c_yui_treedb_graph.js", name: "answer_print_tranger_refused", exported: false,
|
|
114
|
-
keys: ["2.i18n"]},
|
|
150
|
+
keys: ["2.i18n", "2.error"]},
|
|
115
151
|
{module: "c_yui_treedb_topics.js", name: "answer_print_tranger_refused", exported: false,
|
|
116
|
-
keys: ["2.i18n"]},
|
|
152
|
+
keys: ["2.i18n", "2.error"]},
|
|
117
153
|
{module: "c_yui_treedb_topics.js", name: "card_action_anchor", exported: false, keys: ["2"]},
|
|
118
154
|
{module: "c_yui_treedb_topics.js", name: "push_meta", exported: false, keys: ["0"]},
|
|
119
155
|
{module: "c_yui_treedb_topics.js", name: "relation", exported: false, keys: ["1"]},
|
|
156
|
+
{module: "c_yui_treedb_topics.js", name: "add_tab", exported: false, keys: ["3"]},
|
|
157
|
+
{module: "c_yui_treedb_topics.js", name: "add_topic_card", exported: false, keys: ["2"]},
|
|
158
|
+
{module: "c_yui_treedb_topics.js", name: "show_topic_info", exported: false, keys: ["1"]},
|
|
159
|
+
{module: "c_yui_treedb_topics.js", name: "build_topic_info_panel", exported: false, keys: ["2"]},
|
|
120
160
|
{module: "c_yui_treedb_topic_with_form.js", name: "confirm_then", exported: false, keys: ["1"]},
|
|
121
161
|
{module: "c_yui_treedb_topic_with_form.js", name: "present_json_popup", exported: false,
|
|
122
|
-
keys: ["2.title"]},
|
|
123
|
-
{module: "
|
|
162
|
+
keys: ["1", "2.title"]},
|
|
163
|
+
{module: "c_yui_treedb_topic_with_form.js", name: "col_title_formatter", exported: false, keys: ["1.id"]},
|
|
164
|
+
{module: "c_yui_treedb_topic_with_form.js", name: "open_cell_json_dialog", exported: false, keys: ["2"]},
|
|
165
|
+
{module: "c_yui_treedb_topic_with_form.js", name: "open_cell_file_dialog", exported: false, keys: ["2"]},
|
|
166
|
+
{module: "c_yui_treedb_topic_with_form.js", name: "open_hook_choice_dialog", exported: false,
|
|
167
|
+
keys: ["1[].topic_name"]},
|
|
168
|
+
{module: "c_yui_schema_editor.js", name: "show_notice", exported: false, keys: ["1", "2"]},
|
|
124
169
|
{module: "c_yui_schema_editor.js", name: "toolbar_button", exported: false, keys: ["2"]},
|
|
125
170
|
{module: "c_yui_schema_editor.js", name: "row_icon", exported: false, keys: ["3"]},
|
|
126
171
|
{module: "c_yui_schema_editor.js", name: "name_control", exported: false, keys: ["1"]},
|
|
@@ -130,7 +175,14 @@ export const I18N_KEY_FUNCTIONS = Object.freeze([
|
|
|
130
175
|
{module: "c_yui_schema_editor.js", name: "row", exported: false, keys: ["0"]},
|
|
131
176
|
{module: "c_yui_schema_editor.js", name: "confirm_then", exported: false, keys: ["1"]},
|
|
132
177
|
{module: "c_yui_schema_editor.js", name: "end_writes", exported: false, keys: ["1"]},
|
|
133
|
-
{module: "c_yui_schema_editor.js", name: "open_dialog", exported: false, keys: ["2"]},
|
|
178
|
+
{module: "c_yui_schema_editor.js", name: "open_dialog", exported: false, keys: ["1", "2"]},
|
|
179
|
+
{module: "c_yui_schema_editor.js", name: "refuse_bad_name", exported: false, keys: ["1"]},
|
|
180
|
+
{module: "c_yui_schema_editor.js", name: "open_card", exported: false, keys: ["4[].i18n"]},
|
|
181
|
+
{module: "c_yui_schema_editor.js", name: "open_report", exported: false,
|
|
182
|
+
keys: ["1[].severity", "1[].code"]},
|
|
183
|
+
{module: "c_yui_schema_editor.js", name: "render_import_plan", exported: false,
|
|
184
|
+
keys: ["1.conflicts[].code", "1.writes[].op"]},
|
|
185
|
+
{module: "schema_validate.js", name: "add", exported: false, keys: ["1"]},
|
|
134
186
|
{module: "c_g6_nodes_tree.js", name: "create_floating_icon", exported: false, keys: ["4"]},
|
|
135
187
|
{module: "c_g6_nodes_tree.js", name: "show_dual_icons", exported: false, keys: ["3", "5"]},
|
|
136
188
|
{module: "c_g6_nodes_tree.js", name: "create_form_label", exported: false, keys: ["1"]},
|
|
@@ -143,4 +195,41 @@ export const I18N_KEY_FUNCTIONS = Object.freeze([
|
|
|
143
195
|
{module: "yui_dev.js", name: "mk_dir", exported: false, keys: ["3"]},
|
|
144
196
|
{module: "yui_dev.js", name: "grp", exported: false, keys: ["0"]},
|
|
145
197
|
{module: "yui_dev.js", name: "grp_scroll", exported: false, keys: ["0"]},
|
|
198
|
+
/* c_yui_nav.js -- the items of a menu: the app's nav declaration */
|
|
199
|
+
{module: "c_yui_nav.js", name: "item_li", exported: false, keys: ["1.name", "1.tooltip", "1.aria_label"]},
|
|
200
|
+
{module: "c_yui_nav.js", name: "item_iconbar", exported: false, keys: ["1.name", "1.tooltip", "1.aria_label"]},
|
|
201
|
+
{module: "c_yui_nav.js", name: "render_vertical", exported: false,
|
|
202
|
+
keys: ["1[].name", "1[].tooltip", "1[].aria_label"]},
|
|
203
|
+
{module: "c_yui_nav.js", name: "render_icon_bar", exported: false,
|
|
204
|
+
keys: ["1[].name", "1[].tooltip", "1[].aria_label"]},
|
|
205
|
+
{module: "c_yui_nav.js", name: "render_tabs", exported: false,
|
|
206
|
+
keys: ["1[].name", "1[].tooltip", "1[].aria_label"]},
|
|
207
|
+
{module: "c_yui_nav.js", name: "render_cards", exported: false,
|
|
208
|
+
keys: ["1[].name", "1[].tooltip", "1[].aria_label"]},
|
|
209
|
+
{module: "c_yui_nav.js", name: "render_submenu", exported: false,
|
|
210
|
+
keys: ["1[].name", "1[].tooltip", "1[].aria_label"]},
|
|
211
|
+
{module: "c_yui_nav.js", name: "render_drawer", exported: false,
|
|
212
|
+
keys: ["1[].name", "1[].tooltip", "1[].aria_label"]},
|
|
213
|
+
{module: "c_yui_nav.js", name: "render_breadcrumb", exported: false, keys: ["1[].name", "1[].id"]},
|
|
214
|
+
{module: "c_yui_nav.js", name: "render_accordion", exported: false,
|
|
215
|
+
keys: ["1[].name", "1[].submenu.items[].name", "1[].submenu.items[].tooltip",
|
|
216
|
+
"1[].submenu.items[].aria_label"]},
|
|
217
|
+
/* c_yui_shell.js -- the toolbar items: the app's shell declaration */
|
|
218
|
+
{module: "c_yui_shell.js", name: "build_toolbar", exported: false,
|
|
219
|
+
keys: ["1.toolbar.aria_label", "1.toolbar.items[].name", "1.toolbar.items[].aria_label",
|
|
220
|
+
"1.toolbar.items[].tooltip", "1.toolbar.items[].wordmark"]},
|
|
221
|
+
{module: "c_yui_shell.js", name: "build_toolbar_action_item", exported: false,
|
|
222
|
+
keys: ["1.name", "1.aria_label", "1.tooltip"]},
|
|
223
|
+
{module: "c_yui_shell.js", name: "build_toolbar_brand_item", exported: false,
|
|
224
|
+
keys: ["1.name", "1.aria_label", "1.tooltip", "1.wordmark"]},
|
|
225
|
+
{module: "c_yui_shell.js", name: "build_toolbar_avatar_item", exported: false,
|
|
226
|
+
keys: ["1.name", "1.aria_label", "1.tooltip"]},
|
|
227
|
+
{module: "c_yui_shell.js", name: "build_toolbar_connection_item", exported: false,
|
|
228
|
+
keys: ["1.name", "1.aria_label", "1.tooltip"]},
|
|
229
|
+
{module: "c_yui_shell.js", name: "handle_toolbar_action", exported: false, keys: ["1.name", "1.aria_label"]},
|
|
230
|
+
{module: "c_yui_shell.js", name: "toggle_toolbar_dropdown", exported: false, keys: ["1.name", "1.aria_label"]},
|
|
231
|
+
{module: "c_yui_shell.js", name: "open_toolbar_dropdown", exported: false, keys: ["1.name", "1.aria_label"]},
|
|
232
|
+
{module: "c_yui_shell.js", name: "build_dropdown_row", exported: false, keys: ["1.name", "1.aria_label"]},
|
|
233
|
+
/* shell_route_map.js -- a node of the route map */
|
|
234
|
+
{module: "shell_route_map.js", name: "render_node", exported: false, keys: ["0.label"]},
|
|
146
235
|
]);
|
|
@@ -16,19 +16,27 @@
|
|
|
16
16
|
*
|
|
17
17
|
* - the table names functions that exist where it says, with
|
|
18
18
|
* positions it can follow;
|
|
19
|
+
* - the table is COMPLETE (REV-CLOUD V21.4.1): every function that
|
|
20
|
+
* hands one of its parameters -- or what it reads from one -- to a
|
|
21
|
+
* place a key is read is in it, when something calls it by name;
|
|
22
|
+
* one nothing calls by name (an FSM action, a callback) marks what
|
|
23
|
+
* it hands on. The search runs to a fixpoint, and fails naming the
|
|
24
|
+
* entry to add;
|
|
19
25
|
* - THE RULE, per call, in every module: each value a key position
|
|
20
|
-
* of a key function can take is a string literal (or nothing
|
|
21
|
-
* const the reader follows to one, a key position of the
|
|
26
|
+
* of a key function can take is a string literal (or nothing, or
|
|
27
|
+
* DOM), a const the reader follows to one, a key position of the
|
|
22
28
|
* enclosing table function handed on, or it is MARKED where it is
|
|
23
29
|
* handed -- a comment `i18n: <NAME>_I18N_KEYS` (the module
|
|
24
30
|
* publishes those keys) or `i18n: data` (no key a list can hold).
|
|
25
|
-
* Anything else fails, naming the file and the line;
|
|
31
|
+
* Anything else fails, naming the file and the line; so does a
|
|
32
|
+
* translated text, t("..."), marked or not;
|
|
26
33
|
* - every marker is one and stands where a key is read; the lists
|
|
27
34
|
* it names are published by its module or one it imports; the
|
|
28
35
|
* keys it marks that can be read are in them -- a marked `let`
|
|
29
36
|
* included (or a member or an element read from one), whose
|
|
30
37
|
* every literal is read, and which fails when it holds nothing
|
|
31
|
-
* but literals (a const not written as one);
|
|
38
|
+
* but literals (a const not written as one); a `data` marker on
|
|
39
|
+
* what the reader follows to keys fails (V21.4.2);
|
|
32
40
|
* - every published list is readable (exported, an array of
|
|
33
41
|
* literals), lower-case ASCII, has no duplicate and no stale key;
|
|
34
42
|
* - a list a module derives from its SOURCE (a table, a default, a
|
|
@@ -55,10 +63,15 @@ import {
|
|
|
55
63
|
parse_source,
|
|
56
64
|
walk,
|
|
57
65
|
line_of,
|
|
66
|
+
binding_of,
|
|
58
67
|
source_keys,
|
|
68
|
+
var_leaves,
|
|
59
69
|
leaves_of,
|
|
60
70
|
values_at,
|
|
71
|
+
markers_of,
|
|
72
|
+
marker_of,
|
|
61
73
|
key_calls,
|
|
74
|
+
key_places,
|
|
62
75
|
call_rule_violations,
|
|
63
76
|
published_lists,
|
|
64
77
|
scan_ast,
|
|
@@ -100,11 +113,12 @@ function all_lists()
|
|
|
100
113
|
return out;
|
|
101
114
|
}
|
|
102
115
|
|
|
103
|
-
function rule_report(ast, module)
|
|
116
|
+
function rule_report(ast, module, table)
|
|
104
117
|
{
|
|
105
|
-
return call_rule_violations(ast, module).map((v) =>
|
|
118
|
+
return call_rule_violations(ast, module, table).map((v) =>
|
|
106
119
|
`${module}:${v.line}${v.call_line !== v.line? ` (call at ${v.call_line})` : ""} ` +
|
|
107
|
-
`${v.name}() [${v.path}]: ${v.text.replace(/\s+/g, " ")}`
|
|
120
|
+
`${v.name}() [${v.path}]: ${v.text.replace(/\s+/g, " ")}` +
|
|
121
|
+
(v.why === "translated"? " -- a TRANSLATED text where a key goes: hand the key" : ""));
|
|
108
122
|
}
|
|
109
123
|
|
|
110
124
|
function declares_function(ast, name)
|
|
@@ -579,6 +593,185 @@ function with_key_dropped(lists, name, key)
|
|
|
579
593
|
return copy;
|
|
580
594
|
}
|
|
581
595
|
|
|
596
|
+
/*
|
|
597
|
+
* The FORWARDERS: the functions of the library that hand one of their
|
|
598
|
+
* parameters on to a place a key is read (key_places(): t() / tr(), an
|
|
599
|
+
* `i18n`, `code` or `data-i18n*` value -- a property, setAttribute(),
|
|
600
|
+
* `x.i18n = ...` --, a key position of a function of the table): the
|
|
601
|
+
* parameter itself or what is read from it (`opts.title`, a
|
|
602
|
+
* destructured `{label}`, each `items[].name`), directly or through a
|
|
603
|
+
* `let` it is given to. What their callers give there is a key the
|
|
604
|
+
* validator reads ONLY when the function is in I18N_KEY_FUNCTIONS: up
|
|
605
|
+
* to 7.26.28 39 were not (the review named 17 of them), 6 entries
|
|
606
|
+
* lacked positions, and a literal handed to one would have gone unasked
|
|
607
|
+
* (REV-CLOUD V21.4.1).
|
|
608
|
+
*
|
|
609
|
+
* - A forwarder called by NAME -- in its module, or exported -- goes
|
|
610
|
+
* in the table, with that position.
|
|
611
|
+
* - One that nothing calls by name -- an FSM action (the framework
|
|
612
|
+
* calls it with an event's kw), a callback, a method -- has no call
|
|
613
|
+
* a reader can read: what it hands on is MARKED where it hands it,
|
|
614
|
+
* like any other value the reader cannot follow.
|
|
615
|
+
* - What a gobj keeps in its `priv` is its own state: a caller hands
|
|
616
|
+
* the gobj, never what its priv holds. A path through the priv of
|
|
617
|
+
* a parameter is no position of the function.
|
|
618
|
+
*
|
|
619
|
+
* The search runs to a fixpoint: a function that hands a parameter to
|
|
620
|
+
* a key position of a forwarder it found is a forwarder too.
|
|
621
|
+
*
|
|
622
|
+
* -> {missing: [{module, name, exported, keys, line}],
|
|
623
|
+
* unmarked: [{module, name, how, pos, line}]}
|
|
624
|
+
*
|
|
625
|
+
* `missing`: what the table lacks, each function with the positions it
|
|
626
|
+
* lacks and the first line that hands one. `unmarked`: a forwarder
|
|
627
|
+
* nothing calls by name that hands a parameter on unmarked.
|
|
628
|
+
*/
|
|
629
|
+
const PRIV_PATH_RE = /^\.priv(?:[.[]|$)/;
|
|
630
|
+
|
|
631
|
+
function handed_params(leaf)
|
|
632
|
+
{
|
|
633
|
+
if(leaf.kind === "param") {
|
|
634
|
+
return [leaf];
|
|
635
|
+
}
|
|
636
|
+
if(leaf.kind === "unknown" && leaf.var_root) {
|
|
637
|
+
return var_leaves(leaf.var_root).leaves.filter((l) => l.kind === "param");
|
|
638
|
+
}
|
|
639
|
+
return [];
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/* Is `fn` called by its name in the module (a call that resolves to it)? */
|
|
643
|
+
function is_called_by_name(ast, fn)
|
|
644
|
+
{
|
|
645
|
+
let called = false;
|
|
646
|
+
walk(ast, (n, ancestors) => {
|
|
647
|
+
if(called || n.type !== "CallExpression" || n.callee.type !== "Identifier") {
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
let b = binding_of(n.callee.name, [...ancestors, n]);
|
|
651
|
+
if(!b) {
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
if((b.kind === "function" && b.fn === fn) || (b.kind === "const" && b.init === fn)
|
|
655
|
+
|| (b.kind === "var" && b.declarators.some((d) => d.init === fn))) {
|
|
656
|
+
called = true;
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
return called;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/* How a function is written: one the table can name ("declaration",
|
|
663
|
+
* "variable": bound to a name), or what else it is */
|
|
664
|
+
function declared_as(ast, fn)
|
|
665
|
+
{
|
|
666
|
+
if(fn.type === "FunctionDeclaration") {
|
|
667
|
+
return "declaration";
|
|
668
|
+
}
|
|
669
|
+
let how = "a function expression";
|
|
670
|
+
walk(ast, (n, ancestors) => {
|
|
671
|
+
if(n !== fn) {
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
let parent = ancestors[ancestors.length - 1];
|
|
675
|
+
if(parent.type === "VariableDeclarator" && parent.init === fn && parent.id.type === "Identifier") {
|
|
676
|
+
how = "variable";
|
|
677
|
+
} else if(parent.type === "Property" || parent.type === "MethodDefinition") {
|
|
678
|
+
how = "a method";
|
|
679
|
+
} else if(parent.type === "CallExpression" || parent.type === "NewExpression") {
|
|
680
|
+
how = "a callback";
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
return how;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function find_forwarders(modules = MODULES, asts = ast_of, table = I18N_KEY_FUNCTIONS)
|
|
687
|
+
{
|
|
688
|
+
let virtual = table.map((e) => Object.assign({}, e, {keys: [...e.keys]}));
|
|
689
|
+
let first_line = new Map(); // "<module> <name> <pos>" -> line
|
|
690
|
+
let unmarked = new Map(); // "<module>:<line> <pos>" -> {...}
|
|
691
|
+
let named = new Map(); // a function node -> is it called by name?
|
|
692
|
+
for(let round = 0; round < 32; round++) {
|
|
693
|
+
let grew = false;
|
|
694
|
+
let round_table = virtual.slice(); // a new array: the index is cached per table
|
|
695
|
+
for(let module of modules) {
|
|
696
|
+
let ast = asts(module);
|
|
697
|
+
let markers = markers_of(ast);
|
|
698
|
+
let exported = exported_names(ast);
|
|
699
|
+
for(let place of key_places(ast, {lib_module: module, table: round_table})) {
|
|
700
|
+
for(let leaf of place.leaves) {
|
|
701
|
+
for(let p of handed_params(leaf)) {
|
|
702
|
+
if(PRIV_PATH_RE.test(p.path)) {
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
let b = p.binding;
|
|
706
|
+
let pos = `${b.index}${p.path}`;
|
|
707
|
+
let line = line_of(ast, leaf.node.start); // the place it is handed to
|
|
708
|
+
if(!named.has(b.fn)) {
|
|
709
|
+
let how = declared_as(ast, b.fn);
|
|
710
|
+
let by_name = !!b.name && (how === "declaration" || how === "variable")
|
|
711
|
+
&& (exported.has(b.name) || is_called_by_name(ast, b.fn));
|
|
712
|
+
named.set(b.fn, {by_name, how});
|
|
713
|
+
}
|
|
714
|
+
let {by_name, how} = named.get(b.fn);
|
|
715
|
+
if(!by_name) {
|
|
716
|
+
if(!marker_of(leaf, markers)) {
|
|
717
|
+
unmarked.set(`${module}:${line} ${pos}`,
|
|
718
|
+
{module, name: b.name, how, pos, line});
|
|
719
|
+
}
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
let id = `${module} ${b.name} ${pos}`;
|
|
723
|
+
if(!first_line.has(id) || line < first_line.get(id)) {
|
|
724
|
+
first_line.set(id, line);
|
|
725
|
+
}
|
|
726
|
+
let entry = virtual.find((e) => e.module === module && e.name === b.name);
|
|
727
|
+
if(!entry) {
|
|
728
|
+
entry = {module, name: b.name, exported: exported.has(b.name), keys: []};
|
|
729
|
+
virtual.push(entry);
|
|
730
|
+
}
|
|
731
|
+
if(!entry.keys.includes(pos)) {
|
|
732
|
+
entry.keys.push(pos);
|
|
733
|
+
grew = true;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
if(!grew) {
|
|
740
|
+
break;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
let missing = [];
|
|
745
|
+
for(let entry of virtual) {
|
|
746
|
+
let real = table.find((e) => e.module === entry.module && e.name === entry.name);
|
|
747
|
+
let keys = entry.keys.filter((k) => !real || !real.keys.includes(k));
|
|
748
|
+
if(keys.length === 0) {
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
let lines = keys.map((k) => first_line.get(`${entry.module} ${entry.name} ${k}`) || 0);
|
|
752
|
+
missing.push({module: entry.module, name: entry.name, exported: entry.exported, keys,
|
|
753
|
+
line: Math.min(...lines), in_table: !!real});
|
|
754
|
+
}
|
|
755
|
+
return {missing, unmarked: [...unmarked.values()]};
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function forwarders_report(found)
|
|
759
|
+
{
|
|
760
|
+
let out = [];
|
|
761
|
+
for(let m of found.missing) {
|
|
762
|
+
out.push(`${m.module}:${m.line} ${m.name}() hands [${m.keys.join(", ")}] to a place a key is read: ` +
|
|
763
|
+
(m.in_table? `add the position(s) to its entry of I18N_KEY_FUNCTIONS` :
|
|
764
|
+
`name it in I18N_KEY_FUNCTIONS -- {module: "${m.module}", name: "${m.name}", ` +
|
|
765
|
+
`exported: ${m.exported}, keys: ${JSON.stringify(m.keys)}}`));
|
|
766
|
+
}
|
|
767
|
+
for(let u of found.unmarked) {
|
|
768
|
+
out.push(`${u.module}:${u.line} ${u.name || "a function"} (${u.how}, which nothing calls by name) ` +
|
|
769
|
+
`hands its parameter [${u.pos}] to a place a key is read, unmarked: no caller can be ` +
|
|
770
|
+
`read, so mark what it is where it hands it`);
|
|
771
|
+
}
|
|
772
|
+
return out;
|
|
773
|
+
}
|
|
774
|
+
|
|
582
775
|
|
|
583
776
|
describe("I18N_KEY_FUNCTIONS: the table", () => {
|
|
584
777
|
test("every entry names a function of its module, at positions that can be followed", () => {
|
|
@@ -620,6 +813,63 @@ describe("I18N_KEY_FUNCTIONS: the table", () => {
|
|
|
620
813
|
});
|
|
621
814
|
});
|
|
622
815
|
|
|
816
|
+
describe("REV-CLOUD V21.4.1: every forwarder is in the table", () => {
|
|
817
|
+
test("every function that hands a parameter to a place a key is read, in every module", () => {
|
|
818
|
+
expect(forwarders_report(find_forwarders())).toEqual([]);
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
test("the search finds what it should, and nothing else", () => {
|
|
822
|
+
let code = [
|
|
823
|
+
`import {yui_shell_show_error} from "./shell_modals.js";`,
|
|
824
|
+
`function say(gobj, key) { return ["p", {i18n: key}, t(key)]; }`, // hands 1
|
|
825
|
+
`function told(gobj, opts) { yui_shell_show_error(shell, opts.message); }`, // hands 1.message
|
|
826
|
+
`function relay(gobj, k) { return say(gobj, k); }`, // through say: 1
|
|
827
|
+
`function own(gobj) { let priv = gobj.priv; return t(priv.label); }`, // the gobj's own state
|
|
828
|
+
`function as_dom(gobj, $node) { yui_shell_show_error(shell, $node); }`, // a string there is a key
|
|
829
|
+
`function by_let(gobj, it) { let label = it.name || ""; return {i18n: label}; }`,
|
|
830
|
+
`function by_pattern(gobj, {title, label = "fallback"}) { return t(title); }`,
|
|
831
|
+
`function ac_show(gobj, event, kw, src) { yui_shell_show_error(shell, kw.comment); }`,
|
|
832
|
+
`function ac_marked(gobj, event, kw, src) { return t(/* i18n: data */ kw.comment); }`,
|
|
833
|
+
`say(); told(); relay(); own(); as_dom(); by_let(); by_pattern();`
|
|
834
|
+
].join("\n");
|
|
835
|
+
let ast = parse_as(code, "x.js");
|
|
836
|
+
let found = find_forwarders(["x.js"], () => ast);
|
|
837
|
+
let got = Object.fromEntries(found.missing.map((m) => [m.name, m.keys.sort()]));
|
|
838
|
+
expect(got).toEqual({
|
|
839
|
+
say: ["1"], told: ["1.message"], relay: ["1"], as_dom: ["1"],
|
|
840
|
+
by_let: ["1.name"], by_pattern: ["1.title"]
|
|
841
|
+
});
|
|
842
|
+
expect(found.unmarked.map((u) => `${u.name} ${u.pos}`)).toEqual(["ac_show 2.comment"]);
|
|
843
|
+
|
|
844
|
+
/* ...and named in the table, they are found no more */
|
|
845
|
+
let table = [...I18N_KEY_FUNCTIONS,
|
|
846
|
+
...found.missing.map((m) => ({module: "x.js", name: m.name, exported: false, keys: m.keys}))];
|
|
847
|
+
expect(find_forwarders(["x.js"], () => ast, table).missing).toEqual([]);
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
test("the functions the review named are found, with the table as it was", () => {
|
|
851
|
+
let names = ["name_form_control", "create_form_field", "item_li", "item_iconbar",
|
|
852
|
+
"build_toolbar_action_item", "build_toolbar_brand_item", "build_toolbar_avatar_item",
|
|
853
|
+
"build_toolbar_connection_item", "open_toolbar_dropdown", "build_dropdown_row",
|
|
854
|
+
"col_title_formatter", "add_tab", "add_topic_card", "build_topic_info_panel",
|
|
855
|
+
"card_descriptor", "render_node"];
|
|
856
|
+
let before = I18N_KEY_FUNCTIONS.filter((e) => !names.includes(e.name));
|
|
857
|
+
let found = find_forwarders(MODULES, ast_of, before);
|
|
858
|
+
let missing = new Set(found.missing.map((m) => m.name));
|
|
859
|
+
for(let name of names) {
|
|
860
|
+
expect(missing.has(name), name).toBe(true);
|
|
861
|
+
}
|
|
862
|
+
/* The seventeenth, an FSM action, is not called by name: it is marked where it
|
|
863
|
+
* hands its kw on, and without the marks it is reported */
|
|
864
|
+
let module = "c_yui_window_manager.js";
|
|
865
|
+
let code = readFileSync(join(SRC, module), "utf8");
|
|
866
|
+
expect(find_forwarders([module], () => ast_of(module)).unmarked).toEqual([]);
|
|
867
|
+
let unmarked = parse_as(code.replace(/\/\* i18n: data \*\/ /g, ""), module);
|
|
868
|
+
expect(find_forwarders([module], () => unmarked).unmarked.map((u) => u.name))
|
|
869
|
+
.toContain("ac_register_window");
|
|
870
|
+
});
|
|
871
|
+
});
|
|
872
|
+
|
|
623
873
|
describe("the rule, per call: a key position gets what the validator can read, or a marker", () => {
|
|
624
874
|
test("every call of a key function, in every module of the library", () => {
|
|
625
875
|
let wrong = [];
|
|
@@ -863,6 +1113,70 @@ describe("the reader the rule and the validator share", () => {
|
|
|
863
1113
|
});
|
|
864
1114
|
});
|
|
865
1115
|
|
|
1116
|
+
describe("REV-CLOUD V21.4.2-3: the data marker is a claim; DOM is no key; a translated text is no key", () => {
|
|
1117
|
+
let module = "x.js";
|
|
1118
|
+
let head = [
|
|
1119
|
+
`import {yui_shell_show_info, yui_shell_show_modal} from "./shell_modals.js";`,
|
|
1120
|
+
`import {createElement2} from "@yuneta/gobj-js";`
|
|
1121
|
+
];
|
|
1122
|
+
let scan = (lines) => scan_ast(parse_as([...head, ...lines].join("\n"), module), {lib_module: module});
|
|
1123
|
+
let rule = (lines) => rule_report(parse_as([...head, ...lines].join("\n"), module), module);
|
|
1124
|
+
|
|
1125
|
+
test("the review's repro: a data marker on a call that returns a literal FAILS", () => {
|
|
1126
|
+
let r = scan([
|
|
1127
|
+
`function pick() { return "a literal behind a call"; }`,
|
|
1128
|
+
`yui_shell_show_info(shell, /* i18n: data */ pick());`
|
|
1129
|
+
]);
|
|
1130
|
+
expect(r.marker_errors.map((e) => `${e.line}: ${e.why}`)).toEqual([
|
|
1131
|
+
'4: marked as data, but what it marks is the key "a literal behind a call", which the reader ' +
|
|
1132
|
+
'follows: drop the marker (it is no data)'
|
|
1133
|
+
]);
|
|
1134
|
+
/* ...and the call is READ: unmarked, it passes the rule and its key is asked for */
|
|
1135
|
+
let unmarked = [`function pick() { return "a literal behind a call"; }`,
|
|
1136
|
+
`yui_shell_show_info(shell, pick());`];
|
|
1137
|
+
expect(rule(unmarked)).toEqual([]);
|
|
1138
|
+
expect(scan(unmarked).keys.has("a literal behind a call")).toBe(true);
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
test("a data marker on data, or on data with a literal fallback, is legal", () => {
|
|
1142
|
+
let r = scan([
|
|
1143
|
+
`function f(kw) { yui_shell_show_info(shell, /* i18n: data */ kw.comment || "fallback key"); }`,
|
|
1144
|
+
`function g(kw) { return t(/* i18n: data */ kw.comment); }`,
|
|
1145
|
+
`function h(x) { return x.label; }`,
|
|
1146
|
+
`yui_shell_show_info(shell, /* i18n: data */ h(y));` // returns a parameter: not followed
|
|
1147
|
+
]);
|
|
1148
|
+
expect(r.marker_errors).toEqual([]);
|
|
1149
|
+
expect(r.keys.has("fallback key")).toBe(true);
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
test("DOM at a key position passes the rule; a name proves nothing", () => {
|
|
1153
|
+
expect(rule([
|
|
1154
|
+
`const $box = createElement2(["div"]);`,
|
|
1155
|
+
`yui_shell_show_modal(shell, $box);`,
|
|
1156
|
+
`yui_shell_show_modal(shell, ["div", {}, "a spec"]);`,
|
|
1157
|
+
`let $msg = null;`,
|
|
1158
|
+
`if(x) { $msg = createElement2(["p"]); }`,
|
|
1159
|
+
`yui_shell_show_info(shell, $msg);`
|
|
1160
|
+
])).toEqual([]);
|
|
1161
|
+
expect(rule([
|
|
1162
|
+
`yui_shell_show_modal(shell, $box);`,
|
|
1163
|
+
`let $mixed = createElement2(["p"]);`,
|
|
1164
|
+
`$mixed = "a key in the let";`,
|
|
1165
|
+
`yui_shell_show_info(shell, $mixed);`
|
|
1166
|
+
])).toEqual([`${module}:3 yui_shell_show_modal() [1]: $box`, `${module}:6 yui_shell_show_info() [1]: $mixed`]);
|
|
1167
|
+
});
|
|
1168
|
+
|
|
1169
|
+
test("a translated text at a key position FAILS, marked or not", () => {
|
|
1170
|
+
expect(rule([
|
|
1171
|
+
`yui_shell_show_info(shell, t("saved"));`,
|
|
1172
|
+
`yui_shell_show_info(shell, /* i18n: data */ i18next.t("saved"));`
|
|
1173
|
+
])).toEqual([
|
|
1174
|
+
`${module}:3 yui_shell_show_info() [1]: t("saved") -- a TRANSLATED text where a key goes: hand the key`,
|
|
1175
|
+
`${module}:4 yui_shell_show_info() [1]: i18next.t("saved") -- a TRANSLATED text where a key goes: hand the key`
|
|
1176
|
+
]);
|
|
1177
|
+
});
|
|
1178
|
+
});
|
|
1179
|
+
|
|
866
1180
|
describe("REV-CLOUD V20.5 repro, on the real module", () => {
|
|
867
1181
|
let original = readFileSync(join(SRC, "c_yui_treedb_topics.js"), "utf8");
|
|
868
1182
|
let call = `actions.push(card_action_anchor(\n "TREEDB_CARD_TABLE", "yi-table", "table", fill(routes.table)));`;
|
|
@@ -1066,7 +1380,6 @@ describe("the published *_I18N_KEYS lists", () => {
|
|
|
1066
1380
|
["FORM_JSON_FIELD_I18N_KEYS", "invalid json"],
|
|
1067
1381
|
["SHELL_ROUTE_MAP_I18N_KEYS", "toolbar"],
|
|
1068
1382
|
["SHELL_ROUTE_MAP_I18N_KEYS", "other routes"],
|
|
1069
|
-
["SHELL_ROUTE_MAP_I18N_KEYS", "app"],
|
|
1070
1383
|
["WIZARD_HELPERS_I18N_KEYS", "confirm"],
|
|
1071
1384
|
["WIZARD_HELPERS_I18N_KEYS", "next"],
|
|
1072
1385
|
["C_YUI_SCHEMA_EDITOR_I18N_KEYS", "error"],
|
|
@@ -1077,6 +1390,14 @@ describe("the published *_I18N_KEYS lists", () => {
|
|
|
1077
1390
|
|
|
1078
1391
|
/* ...and with it, the review's repro -- a key dropped from its
|
|
1079
1392
|
* list -- fails the coverage of that list */
|
|
1393
|
+
/* "app" (the route map's root when the app names no brand) is pinned
|
|
1394
|
+
* twice since V21.4.1: the marker moved to the call of render_node()
|
|
1395
|
+
* follows the `let root` it marks, and the list must hold its label */
|
|
1396
|
+
expect(reported("SHELL_ROUTE_MAP_I18N_KEYS", "app")).toBe(false);
|
|
1397
|
+
let route_map_refs = scan_of("shell_route_map.js").marker_refs
|
|
1398
|
+
.filter((ref) => ref.lists.includes("SHELL_ROUTE_MAP_I18N_KEYS"));
|
|
1399
|
+
expect(route_map_refs.flatMap((ref) => ref.keys)).toContain("app");
|
|
1400
|
+
|
|
1080
1401
|
for(let [name, key] of [
|
|
1081
1402
|
["C_YUI_FORM_I18N_KEYS", "this field is required"],
|
|
1082
1403
|
["C_YUI_SCHEMA_EDITOR_I18N_KEYS", "error"],
|
package/src/shell_route_map.js
CHANGED
|
@@ -88,7 +88,7 @@ function render_node(node)
|
|
|
88
88
|
row.push(["span", {class: "icon ROUTEMAP_ICON"},
|
|
89
89
|
[["i", {class: node.icon}]]]);
|
|
90
90
|
}
|
|
91
|
-
row.push(["span", {class: "ROUTEMAP_NAME", i18n:
|
|
91
|
+
row.push(["span", {class: "ROUTEMAP_NAME", i18n: node.label},
|
|
92
92
|
text_node(node.label)]);
|
|
93
93
|
/* Documentation columns: route + implementing gclass + action event. */
|
|
94
94
|
if(node.route) {
|
|
@@ -250,7 +250,7 @@ function build_body(shell, t)
|
|
|
250
250
|
let $tree = createElement2(
|
|
251
251
|
["div", {class: "ROUTEMAP_TREE"}, [
|
|
252
252
|
createElement2(["ul", {class: "ROUTEMAP_UL ROUTEMAP_ROOT"},
|
|
253
|
-
[render_node(root)]])
|
|
253
|
+
[render_node(/* i18n: SHELL_ROUTE_MAP_I18N_KEYS */ root)]])
|
|
254
254
|
]]
|
|
255
255
|
);
|
|
256
256
|
|
package/src/yui_gclass_view.js
CHANGED
|
@@ -197,7 +197,7 @@ function open_gclass_view(host, gclass_name, opts)
|
|
|
197
197
|
dialog: true,
|
|
198
198
|
logical_class: "GCLASS_VIEW_SHEET",
|
|
199
199
|
title_prefix: opts.title_prefix || "",
|
|
200
|
-
title:
|
|
200
|
+
title: gclass_name,
|
|
201
201
|
t: t,
|
|
202
202
|
on_close: on_close,
|
|
203
203
|
});
|
package/src/yui_time.js
CHANGED
|
@@ -544,7 +544,7 @@ function period_name(period, t)
|
|
|
544
544
|
{
|
|
545
545
|
let spec = period_spec(period);
|
|
546
546
|
if(!spec) {
|
|
547
|
-
return tr(t,
|
|
547
|
+
return tr(t, String((period && period.id) || period || ""));
|
|
548
548
|
}
|
|
549
549
|
return tr(t, /* i18n: YUI_TIME_I18N_KEYS */ spec.id);
|
|
550
550
|
}
|