@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.
@@ -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. A `let`, a parameter or data is
39
- * not read: its keys are published in an exported
40
- * `<NAME>_I18N_KEYS` array, and the call is MARKED with a comment
41
- * right before what it hands over, `i18n: <NAME>_I18N_KEYS` (the
42
- * list) or `i18n: data` (no key a list can hold). A marked `let`
43
- * is held to its marker: every literal it is given is read and
44
- * must be in the list, and one given nothing but literals fails
45
- * (write it as a const, which the reader follows).
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
- * A test (i18n_key_functions.test.js) holds the library to it,
48
- * per CALL: a key position given anything else than a literal, a
49
- * const the reader follows, a key position of the enclosing
50
- * table function handed on, or a marked value fails, with the
51
- * file and the line. Publishing a list exempts no module.
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
- * A function added to the library that hands one of its
54
- * parameters to t(), to `i18n`, or to a `data-i18n*` attribute
55
- * goes in this table.
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, keys: ["0.id", "0.roll.label"]},
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: "c_yui_schema_editor.js", name: "show_notice", exported: false, keys: ["1"]},
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), a
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"],
@@ -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: /* i18n: SHELL_ROUTE_MAP_I18N_KEYS */ node.label},
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
 
@@ -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: /* i18n: data */ gclass_name,
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, /* i18n: data */ String((period && period.id) || period || ""));
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
  }