@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.
Files changed (43) hide show
  1. package/README.md +223 -53
  2. package/dist/gobj-ui.cjs.js +1109 -143
  3. package/dist/gobj-ui.es.js +1109 -143
  4. package/package.json +2 -2
  5. package/scripts/validate-locales.mjs +672 -96
  6. package/src/c_yui_form.js +186 -9
  7. package/src/c_yui_gclass.js +2 -0
  8. package/src/c_yui_gobj_tree_js.js +2 -0
  9. package/src/c_yui_json.js +2 -0
  10. package/src/c_yui_json_graph.js +2 -0
  11. package/src/c_yui_nav.js +23 -5
  12. package/src/c_yui_shell.css +16 -0
  13. package/src/c_yui_shell.js +282 -26
  14. package/src/c_yui_treedb_graph.js +26 -8
  15. package/src/c_yui_treedb_topic_with_form.js +40 -18
  16. package/src/c_yui_treedb_topics.js +34 -11
  17. package/src/c_yui_window.js +132 -32
  18. package/src/c_yui_window_manager.js +250 -46
  19. package/src/form_json_editor_names.js +448 -0
  20. package/src/form_json_editor_names.wiring.test.js +595 -0
  21. package/src/form_label_i18n.wiring.test.js +169 -0
  22. package/src/i18n_key_functions.js +14 -6
  23. package/src/i18n_key_functions.test.js +124 -2
  24. package/src/lib_maplibre.js +26 -2
  25. package/src/lib_maplibre_relocalize.test.js +78 -0
  26. package/src/named_controls.wiring.test.js +468 -0
  27. package/src/nav_cards_helpers.js +9 -1
  28. package/src/nav_item_badge.wiring.test.js +182 -1
  29. package/src/shell_route_map.js +25 -8
  30. package/src/table_editor_name.wiring.test.js +201 -0
  31. package/src/treedb_column_title.wiring.test.js +151 -0
  32. package/src/treedb_json_drill.wiring.test.js +41 -1
  33. package/src/window_layer.wiring.test.js +348 -43
  34. package/src/window_show.wiring.test.js +6 -2
  35. package/src/window_stacking.wiring.test.js +334 -0
  36. package/src/yui_badge.js +58 -13
  37. package/src/yui_badge.test.js +40 -3
  38. package/src/yui_dev.js +2 -1
  39. package/src/yui_file_field.js +2 -0
  40. package/src/yui_frontend_view.js +2 -1
  41. package/src/yui_gclass_view.js +9 -6
  42. package/src/yui_tabulator_i18n.js +42 -0
  43. package/src/yui_tabulator_i18n.test.js +48 -1
@@ -59,16 +59,22 @@
59
59
  * - the key given to a TRANSLATOR: t() or tr() -- whatever binds
60
60
  * the name --, `i18next.t()`, i18next's t imported under any
61
61
  * name, an alias of one (`const translate = t`, a `.bind()`,
62
- * a re-export), called `t?.(k)`, `t.call(x, k)` or
62
+ * a destructuring, `const {t: tt} = i18next` or a parameter
63
+ * `{t: tt}`, a re-export), called `t?.(k)`, `t.call(x, k)` or
63
64
  * `t.apply(x, [k])`; an array there is i18next's list of
64
65
  * fallback keys, each one a key (`t([k, "fallback"])`);
66
+ * - each element a translator handed as a CALLBACK walks:
67
+ * `keys.map(t)`, `.forEach(t)` and the other iteration
68
+ * methods, `Array.from(keys, t)` -- and what `.then(t)` /
69
+ * `.catch(t)` settle with, which no source writes;
65
70
  * - the string given to an `i18n` or `code` property, to a
66
71
  * `data-i18n*` attribute (as a property, also computed by a
67
72
  * const, `{[ATTR]: k}`; as a member written, `x.i18n = k`,
68
73
  * `x["data-i18n-title"] = k`, `x[ATTR] = k`,
69
- * `el.dataset.i18nTitle = k`; by setAttribute(), the name a
70
- * literal or a const; or written in markup), and the codes of
71
- * the schema checks (`add("error", "<code>")`);
74
+ * `el.dataset.i18nTitle = k`; by setAttribute() or
75
+ * setAttributeNS(), the name a literal or a const; or written
76
+ * in markup), and the codes of the schema checks
77
+ * (`add("error", "<code>")`);
72
78
  * - the strings given to a gobj-ui function that RECEIVES a key
73
79
  * as an argument (`yui_shell_show_error(shell, "<key>")`,
74
80
  * `icon_button(..., "<key>")`), at the positions its table
@@ -95,8 +101,10 @@
95
101
  * that is a variable, a call, a spread of an import or of a
96
102
  * `let`). So is a list MUTATED, here or where it is imported
97
103
  * (`.push()`, `.splice()`, `.unshift()`, `X[i] = k`,
98
- * `X.length = n`, Object.assign()): what it is given there is
99
- * asked for by nobody.
104
+ * `X.length = n`, Object.assign()), by its name, as a member
105
+ * of a namespace import (`M.X_I18N_KEYS.push(k)`) or through
106
+ * an alias (`const a = X_I18N_KEYS; a.push(k)`): what it is
107
+ * given there is asked for by nobody.
100
108
  *
101
109
  * A string is read where it is written, and FOLLOWED where the
102
110
  * source writes it out: a literal, a template with no `${}`,
@@ -120,9 +128,18 @@
120
128
  * recursion is not followed (a call of a function being read is
121
129
  * a value it cannot follow): up to 7.26.31 a recursive helper
122
130
  * was read again at every call, and three recursive branches
123
- * hung the build. A key built by concatenation, held in a `let`,
124
- * or arriving as data cannot be known: its module publishes it
125
- * in a `<NAME>_I18N_KEYS` list.
131
+ * hung the build. And an UNMARKED plain `let` / `var` whose
132
+ * every value in its scope -- its initializer, every `=`, `||=`,
133
+ * `??=`, `&&=` -- is written as a string literal, a const bound
134
+ * to one, `null`, or a `?:` / `||` / `??` of those (the right one
135
+ * of `&&`), read where t(), an i18n or a data-i18n* value reads
136
+ * a key: it can only ever hold those literals, so each is a key
137
+ * asked for (exact, not a guess; a `let` given anything else --
138
+ * a property of a table, a call, data, a `+=` -- is not
139
+ * followed, and fails nothing). A key built by concatenation,
140
+ * held in a `let` given anything else, or arriving as data
141
+ * cannot be known: its module publishes it in a
142
+ * `<NAME>_I18N_KEYS` list.
126
143
  *
127
144
  * A MARKER at the place a key is read says where the keys of what
128
145
  * follows it come from: `/\* i18n: <NAME>_I18N_KEYS *\/` (that
@@ -135,13 +152,20 @@
135
152
  * published by a module the app mounts (the app's own included),
136
153
  * and the keys it marks that can be read must be in them; a
137
154
  * template with `${}` it marks must have the shape of some key of
138
- * them (`monitor ${control}`: "monitor start"). A `data` marker
155
+ * them (`monitor ${control}`: "monitor start") that no NARROWER
156
+ * marked template builds (`monitor confirm ${control}` builds
157
+ * "monitor confirm start": `monitor ${control}` marked with the
158
+ * list of the confirms fails). A `data` marker
139
159
  * is a claim too: on what the reader follows to keys and nothing
140
160
  * else -- a literal, a const that is one, a `?:` branch that is
141
161
  * one, a call that returns literals, imported or not -- it is a
142
162
  * violation, and so is one that would HIDE a key the reader does
143
163
  * not read -- a call whose returns mix keys with data (split the
144
- * branches), an array of string literals. A marked `let` / `var`
164
+ * branches), an array of string literals. No marker stands on a
165
+ * TRANSLATED text -- `t("k")`, or a string built from one,
166
+ * `t("k") + ":"`, `t("k").toUpperCase()` --: the place keeps the
167
+ * translation as its key, and it never changes language. A
168
+ * marked `let` / `var`
145
169
  * is READ, and so is one a marked member or element is read from
146
170
  * (`LABELS[k]`, `picked = LIST[0]`, the `x` of
147
171
  * `LIST.forEach((x) => ...)`): everything it is given in its
@@ -159,27 +183,45 @@
159
183
  * else -- a `let` given literals, an options variable, a
160
184
  * concatenation, a parameter of a wrapper of the app's own -- is a
161
185
  * violation, with the file and the line. So is a TRANSLATED text
162
- * handed there -- `t("k")`, or any translator above --, even
163
- * marked: the function translates it again and marks it data-i18n
164
- * with the translation, so it never changes language; hand the
165
- * key, not t(key). A function of the app is in no table, so what a
186
+ * handed there -- `t("k")`, any translator above, or a string
187
+ * built from one (`t("k") + ":"`) --, even marked: the function
188
+ * translates it again and marks it data-i18n with the
189
+ * translation, so it never changes language; hand the key, not
190
+ * t(key). A translator handed as a CALLBACK is held to the same
191
+ * rule: each element `keys.map(t)` walks is such a position, and
192
+ * what `.then(t)` is handed, which no source writes, is marked or
193
+ * fails. A function of the app is in no table, so what a
166
194
  * wrapper of its own hands on is marked where it hands it -- and
167
195
  * what its callers give it is read only if the module publishes
168
196
  * it.
169
197
  *
170
198
  * What stays INVISIBLE, and fails nothing: a key handed to t(),
171
- * an i18n or a data-i18n* value by an unmarked `let`, by a call
172
- * the reader does not follow or by a computed string (only the
173
- * key positions of the table are held to the rule per call); a
174
- * translator under another name than t / tr that no const or
175
- * import makes an alias of (a parameter `translate`, a `let`
176
- * alias, `i18next.getFixedT()`); a key function reached through a
177
- * parameter, a `let` given more than one value, a computed
178
- * member (`API[name](...)`); a data-i18n* attribute written
179
- * through an alias of `dataset`, by Object.assign() into it, or
180
- * named by a `let`; an export of a `let`; a module of a package
181
- * (gobj-js, i18next); what is written into a const table after its
182
- * declaration; a window's `title` given as an attr at
199
+ * an i18n or a data-i18n* value by an unmarked `let` given
200
+ * anything but literals (above), by a call the reader does not
201
+ * follow or by a computed string (only the
202
+ * key positions of the table, and a translator handed as a
203
+ * callback, are held to the rule per call); a translator under
204
+ * another name than t / tr that no const or import makes an
205
+ * alias of (a parameter `translate`, a `let` alias,
206
+ * `i18next.getFixedT()`), or called by what is no name of it --
207
+ * `(0, t)(k)`, `[t][0](k)`, `Reflect.apply(t, x, [k])`,
208
+ * `Function.prototype.call.call(t, x, k)` --; a key bound with
209
+ * the translator, `t.bind(null, k)`; a parameter a closure
210
+ * returns (`const inner = () => key; t(inner())`: the search for
211
+ * the functions that forward a key does not see it); a translator
212
+ * handed to a function (`{t: t}`, `yui_tabulator_lang(t)`: it
213
+ * calls it with keys of its own); a translated text held in a
214
+ * marked `let` (`let label = t(k); ... /\* i18n: data *\/ label`:
215
+ * the marker reads the literals of the `let`, not what it
216
+ * translates); a key function reached through a parameter, a
217
+ * `let` given more than one value, a computed member
218
+ * (`API[name](...)`); a data-i18n* attribute written through an
219
+ * alias of `dataset`, by Object.assign() into it, or named by a
220
+ * `let`; an export of a `let`; a published list mutated through a
221
+ * function it is handed to, or by
222
+ * `Array.prototype.push.call(X_I18N_KEYS, k)`; a module of a
223
+ * package (gobj-js, i18next); what is written into a const table
224
+ * after its declaration; a window's `title` given as an attr at
183
225
  * gobj_create().
184
226
 
185
227
  * Options (paths relative to the app's directory):
@@ -1606,7 +1648,8 @@ function callee_of(node, scope, depth = 0)
1606
1648
  return {kind: "fn", fn: b.fn, scope: b.scope, name: b.fn.id.name, url: url_of(b.scope)};
1607
1649
  case "const":
1608
1650
  if(b.steps.length > 0) {
1609
- return named;
1651
+ /* `const {t: tt} = i18next` is `i18next.t`: a member `.t` */
1652
+ return TRANSLATOR_NAMES.has(b.steps[b.steps.length - 1].prop)? TRANSLATOR : named;
1610
1653
  }
1611
1654
  if(FUNCTION_RE.test(b.init.type)) {
1612
1655
  return {kind: "fn", fn: b.init, scope: b.scope, name: b.name, url: url_of(b.scope)};
@@ -1618,6 +1661,9 @@ function callee_of(node, scope, depth = 0)
1618
1661
  }
1619
1662
  case "import":
1620
1663
  return exported_callee(import_target(b), depth) || named;
1664
+ case "param":
1665
+ /* `function f({t: tt})` is `opts.t`, the same member */
1666
+ return /\.(?:t|tr)$/.test(b.path)? TRANSLATOR : named;
1621
1667
  default:
1622
1668
  return named;
1623
1669
  }
@@ -1819,6 +1865,131 @@ function is_translator_call(n, scope)
1819
1865
  return !!c && (c.kind === "t" || TRANSLATOR_NAMES.has(c.target.name));
1820
1866
  }
1821
1867
 
1868
+ /***************************************************************
1869
+ * Is a value a TRANSLATED text: a call of a translator
1870
+ * (is_translator_call()), or a string BUILT from one -- a
1871
+ * concatenation or a template holding one (`t("k") + ":"`,
1872
+ * `${t("k")}:`), a method called on one (`t("k").toUpperCase()`,
1873
+ * `[t("a"), name].join(" ")`), String() of one, a branch of
1874
+ * `?:` / `||` / `??` that is one? Its text is a translation: a place
1875
+ * that wants a key and gets it keeps the translation as its key.
1876
+ * A call of any other function is not read into (it may build DOM),
1877
+ * nor is a function.
1878
+ ***************************************************************/
1879
+ function is_translated_text(n, scope, depth = 0)
1880
+ {
1881
+ while(n && n.type === "ChainExpression") {
1882
+ n = n.expression;
1883
+ }
1884
+ if(!n || depth > MAX_DEPTH) {
1885
+ return false;
1886
+ }
1887
+ let sub = (x) => is_translated_text(x, scope, depth + 1);
1888
+ switch(n.type) {
1889
+ case "CallExpression": {
1890
+ if(is_translator_call(n, scope)) {
1891
+ return true;
1892
+ }
1893
+ let c = (n.callee.type === "ChainExpression")? n.callee.expression : n.callee;
1894
+ if(c.type === "MemberExpression") {
1895
+ /* A method of a translated text, or of an array that holds one */
1896
+ let o = c.object;
1897
+ if(o.type === "ArrayExpression") {
1898
+ return o.elements.some((e) => !!e && sub((e.type === "SpreadElement")? e.argument : e));
1899
+ }
1900
+ return sub(o);
1901
+ }
1902
+ if(c.type === "Identifier" && c.name === "String" && binding_of("String", scope) === null) {
1903
+ return n.arguments.length > 0 && sub(n.arguments[0]);
1904
+ }
1905
+ return false;
1906
+ }
1907
+ case "BinaryExpression":
1908
+ return n.operator === "+" && (sub(n.left) || sub(n.right));
1909
+ case "TemplateLiteral":
1910
+ return n.expressions.some(sub);
1911
+ case "ConditionalExpression":
1912
+ return sub(n.consequent) || sub(n.alternate);
1913
+ case "LogicalExpression":
1914
+ return (n.operator !== "&&" && sub(n.left)) || sub(n.right);
1915
+ case "SequenceExpression":
1916
+ return sub(n.expressions[n.expressions.length - 1]);
1917
+ case "AwaitExpression":
1918
+ return sub(n.argument);
1919
+ default:
1920
+ return false;
1921
+ }
1922
+ }
1923
+
1924
+ /* The methods of a promise, which call a callback with what it settles
1925
+ * with (ITERATION_METHODS call theirs with each element) */
1926
+ const SETTLE_METHODS = new Set(["then", "catch"]);
1927
+
1928
+ /***************************************************************
1929
+ * A TRANSLATOR handed as a CALLBACK, not called: `keys.map(t)`. The
1930
+ * call it is handed to calls it, and what that call hands it is a
1931
+ * key the source writes nowhere as one:
1932
+ *
1933
+ * {how: "elements", method, receiver, arg}
1934
+ * `keys.map(t)`, `.forEach(t)`, ... (ITERATION_METHODS),
1935
+ * `Array.from(keys, t)`: each ELEMENT of `receiver` is a
1936
+ * key
1937
+ * {how: "settled", method, arg}
1938
+ * `promise.then(t)`, `.catch(t)`: what the promise settles
1939
+ * with, which no source writes
1940
+ * null anything else. A translator handed to a FUNCTION is how
1941
+ * gobj-ui receives one (`{t: t}`, `yui_tabulator_lang(t)`):
1942
+ * that function calls it with keys of its own, read where
1943
+ * it does. A `.bind()` that binds a key too (`t.bind(null,
1944
+ * k)`) is no callback of these.
1945
+ *
1946
+ * `scope` is the chain of ancestors of the call, the call last.
1947
+ ***************************************************************/
1948
+ function translator_callback(call, scope)
1949
+ {
1950
+ let c = (call.callee.type === "ChainExpression")? call.callee.expression : call.callee;
1951
+ if(c.type !== "MemberExpression" || c.computed || c.property.type !== "Identifier") {
1952
+ return null;
1953
+ }
1954
+ let method = c.property.name;
1955
+ let is_translator = (a) => {
1956
+ while(a && a.type === "ChainExpression") {
1957
+ a = a.expression;
1958
+ }
1959
+ if(!a || a.type === "SpreadElement") {
1960
+ return false;
1961
+ }
1962
+ if(a.type === "CallExpression" && a.arguments.length > 1) {
1963
+ return false; // `t.bind(x, k)`: k is the key, not what the callback is handed
1964
+ }
1965
+ let target = callee_of(a, scope);
1966
+ return !!target && (target.kind === "t" || (target.kind === "fn" && TRANSLATOR_NAMES.has(target.name)
1967
+ && !is_key_function(target)));
1968
+ };
1969
+ let arg = call.arguments[0];
1970
+ if(ITERATION_METHODS.has(method) && is_translator(arg)) {
1971
+ return {how: "elements", method, receiver: c.object, arg};
1972
+ }
1973
+ if(SETTLE_METHODS.has(method) && call.arguments.some(is_translator)) {
1974
+ return {how: "settled", method, arg: call.arguments.find(is_translator)};
1975
+ }
1976
+ if(method === "from" && c.object.type === "Identifier" && c.object.name === "Array"
1977
+ && binding_of("Array", scope) === null && call.arguments.length >= 2 && is_translator(call.arguments[1])) {
1978
+ return {how: "elements", method: "Array.from", receiver: call.arguments[0], arg: call.arguments[1]};
1979
+ }
1980
+ return null;
1981
+ }
1982
+
1983
+ /* Is a function the reader reads (callee_of(): {kind: "fn"}) a key
1984
+ * function of I18N_KEY_FUNCTIONS -- a `tr` of the library that takes
1985
+ * its key elsewhere than first? */
1986
+ function is_key_function(target)
1987
+ {
1988
+ let module = lib_module_of(target.url);
1989
+ let fns = module? key_fn_index().by_module.get(module) : null;
1990
+ return !!fns && fns.has(target.name);
1991
+ }
1992
+
1822
1993
 
1823
1994
  /***************************
1824
1995
  * What a value can be
@@ -2916,6 +3087,144 @@ function var_leaves(root, seen = new Set(), depth = 0)
2916
3087
  return out;
2917
3088
  }
2918
3089
 
3090
+ /***************************************************************
3091
+ * The keys an UNMARKED plain `let` / `var` holds -- the variable
3092
+ * itself read where a key is read (a leaf of leaves_of() with a
3093
+ * var_root and no step) -- when every value it is given in its scope
3094
+ * (assigned_values(): its value, every `=`, `||=`, `??=`, `&&=`) is,
3095
+ * as WRITTEN, a string literal or nothing (literal_values()). Such a
3096
+ * variable can only ever hold those literals, so they ARE the keys of
3097
+ * the place that reads it: exact, not a guess. Given anything else --
3098
+ * a property of a table (`SHORT_MONTHS[m]`), an element, a call, a
3099
+ * parameter, data --, or written in a way the reader cannot read
3100
+ * (`+=`, a for...of over it, a destructuring), it is not followed and
3101
+ * answers [] -- the invisible case, which fails nothing. (expand()
3102
+ * and var_leaves() read a member of a table as ANY value of it: good
3103
+ * enough for a marker, which names the lists to check against, and
3104
+ * too much to ask the locales for.) One declared by a destructuring,
3105
+ * and a member or an element read from one, are not this: a marker
3106
+ * follows those. Cached per variable.
3107
+ *
3108
+ * -> [key]
3109
+ ***************************************************************/
3110
+ const LITERAL_LETS = new WeakMap(); // the first declarator of a variable -> [string] | null
3111
+
3112
+ function literal_let_keys(leaf)
3113
+ {
3114
+ if(leaf.kind !== "unknown" || !leaf.var_root || leaf.var_root.steps.length > 0) {
3115
+ return [];
3116
+ }
3117
+ return let_literals(leaf.var_root.binding, new Set()) || [];
3118
+ }
3119
+
3120
+ /***************************************************************
3121
+ * The strings a plain `let` / `var` (binding_of(): {kind: "var"})
3122
+ * can hold, when it is only ever given literal_values(): [string],
3123
+ * or null. `seen` holds the variables on the way: one met again adds
3124
+ * nothing of its own (the frame that met it first adds it), and only
3125
+ * an answer found from no other variable's frame is kept.
3126
+ ***************************************************************/
3127
+ function let_literals(b, seen)
3128
+ {
3129
+ if(b.picks.size > 0) {
3130
+ return null; // a part of what a destructuring reads: no plain variable
3131
+ }
3132
+ let first = b.declarators[0];
3133
+ if(LITERAL_LETS.has(first)) {
3134
+ return LITERAL_LETS.get(first);
3135
+ }
3136
+ if(seen.has(first)) {
3137
+ return [];
3138
+ }
3139
+ let outer = (seen.size === 0);
3140
+ seen.add(first);
3141
+ let {values, unknown} = assigned_values(b);
3142
+ let out = (unknown.length === 0)? [] : null;
3143
+ for(let v of values) {
3144
+ if(out === null) {
3145
+ break;
3146
+ }
3147
+ if(!v.node) {
3148
+ continue; // declared with no value: undefined
3149
+ }
3150
+ let lits = literal_values(v.node, v.scope, seen);
3151
+ if(lits === null) {
3152
+ out = null;
3153
+ } else {
3154
+ out.push(...lits);
3155
+ }
3156
+ }
3157
+ let result = (out === null)? null : [...new Set(out)];
3158
+ if(outer) {
3159
+ LITERAL_LETS.set(first, result);
3160
+ }
3161
+ return result;
3162
+ }
3163
+
3164
+ /***************************************************************
3165
+ * The strings a value IS, when it is written as nothing but
3166
+ * literals: a string literal or a template with no `${}` (an empty
3167
+ * one is nothing), `null` and `undefined` (nothing), every branch of
3168
+ * `?:`, `||` and `??` (the right one of `&&`: the left, when it is the
3169
+ * value, is falsy), a const -- of the module or imported from one the
3170
+ * reader reads -- bound to one of these, a plain `let` that is only
3171
+ * ever one of these (let_literals()). [string], or null for anything
3172
+ * else. `scope` is the chain of ancestors of `n`.
3173
+ ***************************************************************/
3174
+ function literal_values(n, scope, seen, depth = 0)
3175
+ {
3176
+ if(!n || depth > MAX_DEPTH) {
3177
+ return null;
3178
+ }
3179
+ let sub = (x, s = scope) => literal_values(x, s, seen, depth + 1);
3180
+ switch(n.type) {
3181
+ case "Literal":
3182
+ if(typeof n.value === "string") {
3183
+ return n.value? [n.value] : [];
3184
+ }
3185
+ return (n.value === null && !n.regex)? [] : null;
3186
+ case "TemplateLiteral": {
3187
+ if(n.expressions.length > 0) {
3188
+ return null;
3189
+ }
3190
+ let s = n.quasis[0].value.cooked;
3191
+ return s? [s] : [];
3192
+ }
3193
+ case "ConditionalExpression": {
3194
+ let a = sub(n.consequent);
3195
+ let c = sub(n.alternate);
3196
+ return (a && c)? [...a, ...c] : null;
3197
+ }
3198
+ case "LogicalExpression": {
3199
+ let r = sub(n.right);
3200
+ if(n.operator === "&&" || r === null) {
3201
+ return r;
3202
+ }
3203
+ let l = sub(n.left);
3204
+ return l? [...l, ...r] : null;
3205
+ }
3206
+ case "Identifier": {
3207
+ let b = binding_of(n.name, scope);
3208
+ if(!b) {
3209
+ return (n.name === "undefined")? [] : null;
3210
+ }
3211
+ if(b.kind === "const" && b.steps.length === 0) {
3212
+ return sub(b.init, b.scope);
3213
+ }
3214
+ if(b.kind === "import") {
3215
+ let x = import_target(b);
3216
+ return (x && x.kind === "const" && x.steps.length === 0)? sub(x.init, x.scope) : null;
3217
+ }
3218
+ if(b.kind === "var") {
3219
+ return let_literals(b, seen);
3220
+ }
3221
+ return null;
3222
+ }
3223
+ default:
3224
+ return null;
3225
+ }
3226
+ }
3227
+
2919
3228
  /***************************************************************
2920
3229
  * What a SOURCE can hold, for a test that derives a published list
2921
3230
  * from it (i18n_key_functions.test.js): the keys of leaves_of(), a
@@ -3351,14 +3660,16 @@ function is_forwarded(leaf, lib_module, table)
3351
3660
  /***************************************************************
3352
3661
  * Is a leaf a TRANSLATED text -- a call of a translator
3353
3662
  * (is_translator_call(): `t("...")`, `i18next.t("...")`, `tr()`, an
3354
- * alias, `t?.()`, `t.call()`)? At a key position it is a bug of its
3355
- * own: the function translates it again and marks it data-i18n with
3356
- * the translation, so it never changes language. What goes there is
3357
- * the key.
3663
+ * alias, `t?.()`, `t.call()`), or a string built from one
3664
+ * (is_translated_text(): `t("k") + ":"`, `t("k").toUpperCase()`)?
3665
+ * At a key position it is a bug of its own: the function translates
3666
+ * it again and marks it data-i18n with the translation, so it never
3667
+ * changes language. What goes there is the key. DOM is no such text,
3668
+ * whatever it holds.
3358
3669
  ***************************************************************/
3359
3670
  function is_translation(leaf)
3360
3671
  {
3361
- return !!leaf.node && is_translator_call(leaf.node, leaf.scope);
3672
+ return !!leaf.node && !KNOWN.has(leaf.kind) && is_translated_text(leaf.node, leaf.scope || []);
3362
3673
  }
3363
3674
 
3364
3675
  /***************************************************************
@@ -3395,38 +3706,55 @@ function holds_only_dom(leaf)
3395
3706
  * (a module publishes those keys) or `/\* i18n: data *\/`.
3396
3707
  *
3397
3708
  * A TRANSLATED text -- a call of any translator, is_translator_call():
3398
- * `t("...")`, `tr()`, an alias, `t?.()`, `t.call()` -- fails even when
3709
+ * `t("...")`, `tr()`, an alias, `t?.()`, `t.call()`, or a string built
3710
+ * from one, is_translated_text(): `t("k") + ":"` -- fails even when
3399
3711
  * marked: what goes there is the key (`why: "translated"`). The calls
3400
3712
  * are found however they reach a key function (key_calls()). `table`
3401
3713
  * is I18N_KEY_FUNCTIONS by default.
3402
3714
  *
3403
- * -> [{line, call_line, name, path, text, why}]
3715
+ * A translator handed as a CALLBACK (translator_callbacks(): `keys.map(t)`,
3716
+ * `promise.then(t)`) is held to the same rule: each element it is
3717
+ * handed is a key position, and so is what a promise settles with --
3718
+ * which no source writes, so it is marked or it fails (`name` "t",
3719
+ * `path` the method it is handed to, `callback` set).
3720
+ *
3721
+ * -> [{line, call_line, name, path, text, why, callback}]
3404
3722
  ***************************************************************/
3405
3723
  function call_rule_violations(ast, lib_module, table)
3406
3724
  {
3407
3725
  let markers = markers_of(ast);
3408
3726
  let out = [];
3727
+ let check = (leaves, call, name, path, callback) => {
3728
+ for(let leaf of leaves) {
3729
+ let why = null;
3730
+ if(is_translation(leaf)) {
3731
+ why = "translated";
3732
+ } else if(KNOWN.has(leaf.kind) || is_forwarded(leaf, lib_module, table)
3733
+ || marker_of(leaf, markers) || holds_only_dom(leaf)) {
3734
+ continue;
3735
+ }
3736
+ out.push({
3737
+ line: line_of(ast, leaf.node.start),
3738
+ call_line: line_of(ast, call.start),
3739
+ name: name,
3740
+ path: path,
3741
+ text: ast.code.slice(leaf.node.start, leaf.node.end).slice(0, 120),
3742
+ why: why,
3743
+ callback: callback || null
3744
+ });
3745
+ }
3746
+ };
3409
3747
  for(let {entry, call, scope, args} of key_calls(ast, lib_module, table)) {
3410
3748
  for(let path of entry.keys) {
3411
- for(let leaf of values_at(call, path, scope, args)) {
3412
- let why = null;
3413
- if(is_translation(leaf)) {
3414
- why = "translated";
3415
- } else if(KNOWN.has(leaf.kind) || is_forwarded(leaf, lib_module, table)
3416
- || marker_of(leaf, markers) || holds_only_dom(leaf)) {
3417
- continue;
3418
- }
3419
- out.push({
3420
- line: line_of(ast, leaf.node.start),
3421
- call_line: line_of(ast, call.start),
3422
- name: entry.name,
3423
- path: path,
3424
- text: ast.code.slice(leaf.node.start, leaf.node.end).slice(0, 120),
3425
- why: why
3426
- });
3427
- }
3749
+ check(values_at(call, path, scope, args), call, entry.name, path, null);
3428
3750
  }
3429
3751
  }
3752
+ for(let {call, scope, callback} of translator_callbacks(ast, lib_module)) {
3753
+ let arg = ast.code.slice(callback.arg.start, callback.arg.end);
3754
+ let shown = (callback.method === "Array.from")? `Array.from(..., ${arg})` : `.${callback.method}(${arg})`;
3755
+ check(callback_leaves(call, callback, scope), call, "t", `${callback.method} callback`,
3756
+ {how: callback.how, shown});
3757
+ }
3430
3758
  return out;
3431
3759
  }
3432
3760
 
@@ -3437,14 +3765,25 @@ function call_rule_violations(ast, lib_module, table)
3437
3765
  function call_rule_message(label, v)
3438
3766
  {
3439
3767
  let where = `${label}:${v.line}${v.call_line !== v.line? ` (call at line ${v.call_line})` : ""}`;
3440
- let what = `${v.name}() [${v.path}] is given ${v.text.replace(/\s+/g, " ")}`;
3768
+ let text = v.text.replace(/\s+/g, " ");
3769
+ let cb = v.callback;
3441
3770
  if(v.why === "translated") {
3771
+ let what = cb? `${cb.shown} hands t() ${text}` : `${v.name}() [${v.path}] is given ${text}`;
3442
3772
  return `${where}: ${what}: a TRANSLATED text where a KEY goes (translated again and ` +
3443
3773
  `marked data-i18n with the translation, it never changes language, and no marker ` +
3444
3774
  `excuses it): hand the key, not t(key)`;
3445
3775
  }
3446
- return `${where}: ${what}, which the reader cannot follow: hand it a literal or a const, ` +
3447
- `or mark it /* i18n: data */ or /* i18n: <NAME>_I18N_KEYS */`;
3776
+ if(cb && cb.how === "settled") {
3777
+ return `${where}: ${cb.shown} hands t() what \`${text}\` settles with, as a key no source ` +
3778
+ `writes: mark the call /* i18n: data */ or /* i18n: <NAME>_I18N_KEYS */`;
3779
+ }
3780
+ if(cb) {
3781
+ return `${where}: ${cb.shown} hands t() each element it walks as a key, and the reader cannot ` +
3782
+ `follow ${text}: walk a const array (or a published list), or mark the call ` +
3783
+ `/* i18n: data */ or /* i18n: <NAME>_I18N_KEYS */`;
3784
+ }
3785
+ return `${where}: ${v.name}() [${v.path}] is given ${text}, which the reader cannot follow: hand it ` +
3786
+ `a literal or a const, or mark it /* i18n: data */ or /* i18n: <NAME>_I18N_KEYS */`;
3448
3787
  }
3449
3788
 
3450
3789
 
@@ -3642,16 +3981,22 @@ const OBJECT_MUTATORS = new Set(["assign", "defineProperty", "defineProperties"]
3642
3981
  /***************************************************************
3643
3982
  * The places a file MUTATES a published list: one of its own
3644
3983
  * (`lists`, published_lists()) or one it imports (an import whose
3645
- * name ends in _I18N_KEYS). A list is read as it is WRITTEN, so a
3646
- * key it is given later -- `.push()`, `.splice()`, `.unshift()`,
3647
- * `.fill()`, `X[i] = k`, `X.length = n`, `Object.assign(X, ...)`,
3648
- * `delete X[i]`, a reassignment -- is asked for by nobody.
3984
+ * name ends in _I18N_KEYS, or such a member of a namespace import:
3985
+ * `import * as M from "./m.js"; M.X_I18N_KEYS`), by its name or
3986
+ * through an ALIAS of it -- a const bound to one (`const a = X`), or
3987
+ * a `let` / `var` given nothing else. A list is read as it is
3988
+ * WRITTEN, so a key it is given later -- `.push()`, `.splice()`,
3989
+ * `.unshift()`, `.fill()`, `X[i] = k`, `X.length = n`,
3990
+ * `Object.assign(X, ...)`, `delete X[i]`, a reassignment -- is asked
3991
+ * for by nobody. (Through a function it is handed to, or by
3992
+ * `Array.prototype.push.call(X, k)`, it is not seen.)
3649
3993
  *
3650
3994
  * -> [{line, why}]
3651
3995
  ***************************************************************/
3652
3996
  function list_mutations(ast, lists = [])
3653
3997
  {
3654
3998
  let names = new Set();
3999
+ let namespaces = false;
3655
4000
  for(let node of ast.body) {
3656
4001
  let decl = (node.type === "ExportNamedDeclaration")? node.declaration : node;
3657
4002
  if(decl && decl.type === "VariableDeclaration") {
@@ -3673,19 +4018,66 @@ function list_mutations(ast, lists = [])
3673
4018
  if(spec.type === "ImportSpecifier" && PUBLISHED_LIST_RE.test(spec.imported.name || spec.imported.value)) {
3674
4019
  names.add(spec.local.name);
3675
4020
  }
4021
+ if(spec.type === "ImportNamespaceSpecifier") {
4022
+ namespaces = true;
4023
+ }
3676
4024
  }
3677
4025
  }
3678
4026
  }
3679
- if(names.size === 0) {
4027
+ if(names.size === 0 && !namespaces) {
3680
4028
  return [];
3681
4029
  }
3682
- /* A use of one of them: the name, bound at the top of the module */
3683
- let is_list = (n, chain) => {
3684
- if(!n || n.type !== "Identifier" || !names.has(n.name)) {
3685
- return false;
4030
+
4031
+ /*
4032
+ * The list a node names, as said in a message, or null: its name
4033
+ * bound at the top of the module, a member of a namespace import,
4034
+ * or an alias of one of those
4035
+ */
4036
+ let list_of = (n, chain, depth = 0) => {
4037
+ while(n && n.type === "ChainExpression") {
4038
+ n = n.expression;
4039
+ }
4040
+ if(!n || depth > MAX_DEPTH) {
4041
+ return null;
4042
+ }
4043
+ if(n.type === "MemberExpression") {
4044
+ let step = member_step(n);
4045
+ if(step.prop === null || !PUBLISHED_LIST_RE.test(step.prop) || n.object.type !== "Identifier") {
4046
+ return null;
4047
+ }
4048
+ let b = binding_of(n.object.name, chain);
4049
+ return (b && b.kind === "import" && b.imported === "*")? `${n.object.name}.${step.prop}` : null;
4050
+ }
4051
+ if(n.type !== "Identifier") {
4052
+ return null;
3686
4053
  }
3687
4054
  let b = binding_of(n.name, chain);
3688
- return !!b && !!b.scope && b.scope.length === 1;
4055
+ if(!b || !b.scope) {
4056
+ return null;
4057
+ }
4058
+ if(names.has(n.name) && b.scope.length === 1) {
4059
+ return n.name;
4060
+ }
4061
+ if(b.kind === "const" && b.steps.length === 0) {
4062
+ let inner = list_of(b.init, b.scope, depth + 1);
4063
+ return inner? `${inner} (through its alias \`${n.name}\`)` : null;
4064
+ }
4065
+ if(b.kind === "var" && b.picks.size === 0) {
4066
+ /* A `let` that is only ever given the list is an alias of it (the
4067
+ * first value asked first: `let out = []` is no alias, and costs
4068
+ * no walk of its scope) */
4069
+ let first = b.declarators[0];
4070
+ let inner = (first && first.init)? list_of(first.init, b.scope, depth + 1) : null;
4071
+ if(!inner) {
4072
+ return null;
4073
+ }
4074
+ let {values, unknown} = assigned_values(b);
4075
+ if(unknown.length > 0 || values.some((v) => !v.node || list_of(v.node, v.scope, depth + 1) !== inner)) {
4076
+ return null;
4077
+ }
4078
+ return `${inner} (through its alias \`${n.name}\`)`;
4079
+ }
4080
+ return null;
3689
4081
  };
3690
4082
  let out = [];
3691
4083
  let say = (n, name) => {
@@ -3695,30 +4087,36 @@ function list_mutations(ast, lists = [])
3695
4087
  `write every key in its array literal`});
3696
4088
  };
3697
4089
  walk(ast, (n, ancestors) => {
3698
- let chain = [...ancestors, n];
4090
+ let chain = null;
4091
+ let list = (x) => {
4092
+ chain = chain || [...ancestors, n];
4093
+ return list_of(x, chain);
4094
+ };
4095
+ let name = null;
3699
4096
  if(n.type === "CallExpression") {
3700
4097
  let c = n.callee;
3701
- if(c.type === "MemberExpression" && !c.computed && ARRAY_MUTATORS.has(c.property.name)
3702
- && is_list(c.object, chain)) {
3703
- say(n, c.object.name);
4098
+ if(c.type === "MemberExpression" && !c.computed && ARRAY_MUTATORS.has(c.property.name)) {
4099
+ name = list(c.object);
3704
4100
  } else if(c.type === "MemberExpression" && !c.computed && c.object.type === "Identifier"
3705
4101
  && (c.object.name === "Object" || c.object.name === "Reflect")
3706
- && (OBJECT_MUTATORS.has(c.property.name) || c.property.name === "set")
3707
- && is_list(n.arguments[0], chain)) {
3708
- say(n, n.arguments[0].name);
4102
+ && (OBJECT_MUTATORS.has(c.property.name) || c.property.name === "set")) {
4103
+ name = list(n.arguments[0]);
3709
4104
  }
3710
4105
  } else if(n.type === "AssignmentExpression") {
3711
- if(is_list(n.left, chain)) {
3712
- say(n, n.left.name);
3713
- } else if(n.left.type === "MemberExpression" && is_list(n.left.object, chain)) {
3714
- say(n, n.left.object.name);
3715
- }
3716
- } else if(n.type === "UpdateExpression" && n.argument.type === "MemberExpression"
3717
- && is_list(n.argument.object, chain)) {
3718
- say(n, n.argument.object.name);
3719
- } else if(n.type === "UnaryExpression" && n.operator === "delete"
3720
- && n.argument.type === "MemberExpression" && is_list(n.argument.object, chain)) {
3721
- say(n, n.argument.object.name);
4106
+ if(n.left.type === "MemberExpression") {
4107
+ name = list(n.left) || list(n.left.object);
4108
+ } else if(n.left.type === "Identifier" && names.has(n.left.name)) {
4109
+ /* The list itself given another value: an alias given one is no mutation */
4110
+ let x = list(n.left);
4111
+ name = (x === n.left.name)? x : null;
4112
+ }
4113
+ } else if(n.type === "UpdateExpression" && n.argument.type === "MemberExpression") {
4114
+ name = list(n.argument.object);
4115
+ } else if(n.type === "UnaryExpression" && n.operator === "delete" && n.argument.type === "MemberExpression") {
4116
+ name = list(n.argument.object);
4117
+ }
4118
+ if(name) {
4119
+ say(n, name);
3722
4120
  }
3723
4121
  });
3724
4122
  return out;
@@ -3743,7 +4141,8 @@ function list_mutations(ast, lists = [])
3743
4141
  * `t.call(x, key)`); an array there is the list of
3744
4142
  * keys i18next tries in turn, each one a key
3745
4143
  * "code" the code of a schema check, add("error", <code>)
3746
- * "attribute" setAttribute("data-i18n*", <key>), the name a
4144
+ * "attribute" setAttribute("data-i18n*", <key>) and
4145
+ * setAttributeNS(ns, "data-i18n*", <key>), the name a
3747
4146
  * literal or a const the reader follows to one
3748
4147
  * "property" an `i18n`, `code` or `data-i18n*` property of an
3749
4148
  * object literal (`{[ATTR]: k}` with ATTR a const
@@ -3754,6 +4153,10 @@ function list_mutations(ast, lists = [])
3754
4153
  * (`dataset.i18nX` IS the attribute data-i18n-x)
3755
4154
  * "call" a key position of a key function: `entry` and
3756
4155
  * `path` say which
4156
+ * "callback" a translator handed as a CALLBACK
4157
+ * (translator_callback(), in `callback`): each element
4158
+ * of what `keys.map(t)` walks, or what `.then(t)` is
4159
+ * handed (callback_leaves())
3757
4160
  *
3758
4161
  * `node` is the node of the place (the call, the property, the
3759
4162
  * assignment), where a marker of the whole place stands.
@@ -3766,9 +4169,12 @@ function key_places(ast, ctx = {})
3766
4169
  let array_keys = ctx.array_keys || new Set();
3767
4170
  let table = ctx.table || I18N_KEY_FUNCTIONS;
3768
4171
  program_url(ast, ctx.lib_module || null);
3769
- /* One walk: the key calls are found on the way (key_calls() gets them) */
4172
+ /* One walk: the key calls and the translators handed as callbacks
4173
+ * are found on the way (key_calls(), translator_callbacks() get them) */
3770
4174
  let known_calls = key_calls_cached(ast, table);
3771
4175
  let calls = known_calls || [];
4176
+ let known_callbacks = CALLBACKS.get(ast) || null;
4177
+ let callbacks = known_callbacks || [];
3772
4178
  let out = [];
3773
4179
  let read = (what, node, scope, root) => {
3774
4180
  if(node) {
@@ -3807,10 +4213,20 @@ function key_places(ast, ctx = {})
3807
4213
  } else if(name === "add" && is_string_literal(n.arguments[0], /^(error|warning)$/)) {
3808
4214
  /* The codes the schema checks hand to `t(finding.code)` */
3809
4215
  read("code", n.arguments[1], [...ancestors, n], n);
3810
- } else if(name === "setAttribute" && n.arguments.length >= 2) {
4216
+ } else if((name === "setAttribute" && n.arguments.length >= 2)
4217
+ || (name === "setAttributeNS" && n.arguments.length >= 3)) {
4218
+ /* setAttributeNS(ns, name, value): the same, one argument on */
4219
+ let at = (name === "setAttributeNS")? 1 : 0;
4220
+ let scope = [...ancestors, n];
4221
+ if(names_of(n.arguments[at], scope).some((x) => DATA_I18N_ATTR_RE.test(x))) {
4222
+ read("attribute", n.arguments[at + 1], scope, n);
4223
+ }
4224
+ }
4225
+ if(!known_callbacks && may_take_a_callback(n)) {
3811
4226
  let scope = [...ancestors, n];
3812
- if(names_of(n.arguments[0], scope).some((x) => DATA_I18N_ATTR_RE.test(x))) {
3813
- read("attribute", n.arguments[1], scope, n);
4227
+ let cb = translator_callback(n, scope);
4228
+ if(cb) {
4229
+ callbacks.push({call: n, scope, callback: cb});
3814
4230
  }
3815
4231
  }
3816
4232
  break;
@@ -3869,14 +4285,80 @@ function key_places(ast, ctx = {})
3869
4285
  if(!known_calls) {
3870
4286
  set_key_calls(ast, table, calls);
3871
4287
  }
4288
+ if(!known_callbacks) {
4289
+ CALLBACKS.set(ast, callbacks);
4290
+ }
3872
4291
  for(let {entry, call, scope, args} of calls) {
3873
4292
  for(let path of entry.keys) {
3874
4293
  out.push({what: "call", node: call, entry, path, leaves: values_at(call, path, scope, args)});
3875
4294
  }
3876
4295
  }
4296
+ for(let {call, scope, callback} of callbacks) {
4297
+ out.push({what: "callback", node: call, callback, leaves: callback_leaves(call, callback, scope)});
4298
+ }
3877
4299
  return out;
3878
4300
  }
3879
4301
 
4302
+ /***************************************************************
4303
+ * The translators a parsed file hands as CALLBACKS
4304
+ * (translator_callback()), found by key_places() on its walk or by
4305
+ * a walk of their own, and kept:
4306
+ *
4307
+ * -> [{call, scope, callback}], `scope` the ancestors of the call,
4308
+ * the call last
4309
+ ***************************************************************/
4310
+ const CALLBACKS = new WeakMap(); // a parsed file -> its translator callbacks
4311
+
4312
+ function translator_callbacks(ast, lib_module = null)
4313
+ {
4314
+ program_url(ast, lib_module);
4315
+ if(CALLBACKS.has(ast)) {
4316
+ return CALLBACKS.get(ast);
4317
+ }
4318
+ let out = [];
4319
+ walk(ast, (n, ancestors) => {
4320
+ if(n.type === "CallExpression" && may_take_a_callback(n)) {
4321
+ let scope = [...ancestors, n];
4322
+ let cb = translator_callback(n, scope);
4323
+ if(cb) {
4324
+ out.push({call: n, scope, callback: cb});
4325
+ }
4326
+ }
4327
+ });
4328
+ CALLBACKS.set(ast, out);
4329
+ return out;
4330
+ }
4331
+
4332
+ /* Can a call be one translator_callback() looks at: a method of
4333
+ * ITERATION_METHODS, `then` / `catch`, or `Array.from()`? */
4334
+ function may_take_a_callback(n)
4335
+ {
4336
+ let c = (n.callee.type === "ChainExpression")? n.callee.expression : n.callee;
4337
+ if(c.type !== "MemberExpression" || c.computed || c.property.type !== "Identifier" || n.arguments.length === 0) {
4338
+ return false;
4339
+ }
4340
+ let m = c.property.name;
4341
+ return ITERATION_METHODS.has(m) || SETTLE_METHODS.has(m) || m === "from";
4342
+ }
4343
+
4344
+ /***************************************************************
4345
+ * The leaves of what a translator handed as a callback is called
4346
+ * with (translator_callback()): each element of the array, as
4347
+ * leaves_of() reads it, or -- for a promise -- one value the reader
4348
+ * cannot follow, the call. Their `via` starts at the call and at the
4349
+ * translator handed: a marker stands before the call (and so before
4350
+ * the array: `/\* i18n: data *\/ keys.map(t)`) or before the
4351
+ * translator.
4352
+ ***************************************************************/
4353
+ function callback_leaves(call, callback, scope)
4354
+ {
4355
+ let via = [call, callback.arg];
4356
+ if(callback.how === "elements") {
4357
+ return elements_of(callback.receiver, {scope, via, off: false}, 0).map(classify);
4358
+ }
4359
+ return [{kind: "unknown", node: call, scope: scope.slice(0, -1), via, off: false}];
4360
+ }
4361
+
3880
4362
  /***************************************************************
3881
4363
  * The leaves of the key of a translator call: leaves_of(), but an
3882
4364
  * array of keys is read as the keys it holds -- `t([k, "fallback"])`,
@@ -3919,7 +4401,11 @@ function key_list_leaves(node, scope, root)
3919
4401
  * string literals. Under a LIST marker those keys are asked for and
3920
4402
  * must be in the lists, and a template with `${}` must have the
3921
4403
  * shape of a key of them (`shapes`, checked where the lists are
3922
- * known).
4404
+ * known: shape_owner_check()). No marker stands on a TRANSLATED text
4405
+ * (is_translation()) where it is read as a key -- at a key position
4406
+ * of a key function and at a translator handed as a callback the
4407
+ * rule per call says so (call_rule_violations()), anywhere else the
4408
+ * marker does.
3923
4409
  ***************************************************************/
3924
4410
  function scan_ast(ast, ctx)
3925
4411
  {
@@ -3955,7 +4441,8 @@ function scan_ast(ast, ctx)
3955
4441
  }
3956
4442
  if(m.kind === "lists" && leaf.kind === "unknown" && leaf.node && leaf.node.type === "TemplateLiteral"
3957
4443
  && leaf.node.expressions.length > 0) {
3958
- m.shapes.push({text: text_of(leaf.node), source: template_shape(leaf.node)});
4444
+ m.shapes.push({text: text_of(leaf.node), source: template_shape(leaf.node),
4445
+ bottom: template_bottom(leaf.node)});
3959
4446
  }
3960
4447
  };
3961
4448
 
@@ -4009,7 +4496,11 @@ function scan_ast(ast, ctx)
4009
4496
  }
4010
4497
  };
4011
4498
 
4012
- let take = (leaves) => {
4499
+ /* `what`: the kind of place (key_places()). At a key position of a key
4500
+ * function and at a translator handed as a callback, a TRANSLATED
4501
+ * text is the rule's (call_rule_violations()); anywhere else a marker
4502
+ * standing on one answers for it. */
4503
+ let take = (leaves, what) => {
4013
4504
  for(let leaf of leaves) {
4014
4505
  if(leaf.kind === "key") {
4015
4506
  keys.add(leaf.value);
@@ -4022,6 +4513,7 @@ function scan_ast(ast, ctx)
4022
4513
  m.leaves = [];
4023
4514
  m.hidden = [];
4024
4515
  m.shapes = [];
4516
+ m.translated = [];
4025
4517
  }
4026
4518
  m.leaves.push(leaf);
4027
4519
  if(leaf.kind === "key") {
@@ -4031,6 +4523,14 @@ function scan_ast(ast, ctx)
4031
4523
  check_marked_var(leaf, m);
4032
4524
  }
4033
4525
  answer(leaf, m);
4526
+ if(what !== "call" && what !== "callback" && is_translation(leaf)) {
4527
+ m.translated.push(leaf);
4528
+ }
4529
+ } else if(what !== "call" && what !== "callback") {
4530
+ /* An unmarked `let` that can only hold literals holds keys */
4531
+ for(let k of literal_let_keys(leaf)) {
4532
+ keys.add(k);
4533
+ }
4034
4534
  }
4035
4535
  }
4036
4536
  };
@@ -4048,7 +4548,7 @@ function scan_ast(ast, ctx)
4048
4548
  };
4049
4549
 
4050
4550
  for(let place of key_places(ast, ctx)) {
4051
- take(place.leaves);
4551
+ take(place.leaves, place.what);
4052
4552
  }
4053
4553
  walk(ast, (n) => {
4054
4554
  if(n.type === "Property") {
@@ -4080,6 +4580,21 @@ function scan_ast(ast, ctx)
4080
4580
  marker_errors.push({line: m.line, why: marker_misplaced(ast, m)});
4081
4581
  continue;
4082
4582
  }
4583
+ let seen_translated = new Set();
4584
+ for(let leaf of m.translated) {
4585
+ if(seen_translated.has(leaf.node)) {
4586
+ continue;
4587
+ }
4588
+ seen_translated.add(leaf.node);
4589
+ /* A const imported from another file: its text is that file's */
4590
+ let program = (leaf.scope && leaf.scope.length > 0 && typeof leaf.scope[0].code === "string")?
4591
+ leaf.scope[0] : ast;
4592
+ let text = program.code.slice(leaf.node.start, leaf.node.end).replace(/\s+/g, " ").slice(0, 80);
4593
+ marker_errors.push({line: m.line, why: `marked as ` +
4594
+ `${m.kind === "data"? "data" : "keys of " + m.lists.join(", ")}, but \`${text}\` ` +
4595
+ `is a TRANSLATED text: where a key is read, the translation is kept as its key and never ` +
4596
+ `changes language, and no marker excuses it: hand the key, not t(key)`});
4597
+ }
4083
4598
  if(m.kind === "lists") {
4084
4599
  marker_refs.push({lists: m.lists, keys: [...m.keys], line: m.line, shapes: m.shapes});
4085
4600
  continue;
@@ -4126,6 +4641,59 @@ function template_shape(node)
4126
4641
  return "^" + node.quasis.map(esc).join("[\\s\\S]*") + "$";
4127
4642
  }
4128
4643
 
4644
+ /***************************************************************
4645
+ * A template with each `${}` replaced by a character no literal
4646
+ * part holds (`monitor confirm ${control}` -> "monitor confirm \0").
4647
+ * Every string template A builds has the shape of template B exactly
4648
+ * when B's shape matches A's bottom: B's `${}`s must take the \0s,
4649
+ * and what takes a \0 takes any string.
4650
+ ***************************************************************/
4651
+ const TEMPLATE_HOLE = "\u0000";
4652
+
4653
+ function template_bottom(node)
4654
+ {
4655
+ return node.quasis.map((q) => q.value.cooked).join(TEMPLATE_HOLE);
4656
+ }
4657
+
4658
+ /***************************************************************
4659
+ * A template marked with lists (`shape`: {text, source, bottom}),
4660
+ * checked against the keys of those lists and every other marked
4661
+ * template (`shapes`, each with its `label` and `line`):
4662
+ *
4663
+ * {narrower: null} some key of the lists has its shape and
4664
+ * no NARROWER template builds it -- a
4665
+ * template whose shape lies strictly
4666
+ * inside this one's
4667
+ * {narrower: shape} every key of its shape is one of that
4668
+ * narrower template (`monitor confirm
4669
+ * ${control}` inside `monitor ${control}`):
4670
+ * the marker names the other's list
4671
+ * null no key of the lists has its shape
4672
+ *
4673
+ * A key that a narrower template builds is that template's: the
4674
+ * wider one builds it only when what fills its `${}` carries a
4675
+ * literal part of the narrower one ("confirm start" in `monitor
4676
+ * ${control}`), which code that writes both templates does not do.
4677
+ * So a marker that names the list of the other template fails at
4678
+ * BOTH templates, where up to 7.26.34 only the narrower one failed
4679
+ * ("monitor confirm start" has the shape `^monitor .*$`).
4680
+ ***************************************************************/
4681
+ function shape_owner_check(shape, keys, shapes)
4682
+ {
4683
+ let re = new RegExp(shape.source);
4684
+ let own = keys.filter((k) => re.test(k));
4685
+ if(own.length === 0) {
4686
+ return null;
4687
+ }
4688
+ let narrower = shapes.filter((u) => u.source !== shape.source && re.test(u.bottom)
4689
+ && !new RegExp(u.source).test(shape.bottom));
4690
+ let free = own.filter((k) => !narrower.some((u) => new RegExp(u.source).test(k)));
4691
+ if(free.length > 0) {
4692
+ return {narrower: null};
4693
+ }
4694
+ return {narrower: narrower.find((u) => new RegExp(u.source).test(own[0]))};
4695
+ }
4696
+
4129
4697
  /***************************************************************
4130
4698
  * The i18n keys the source asks for.
4131
4699
  *
@@ -4269,6 +4837,8 @@ function collect_used_keys(opts)
4269
4837
  }
4270
4838
 
4271
4839
  /* A marker names lists that were read, and what it marks is in them */
4840
+ let all_shapes = refs.flatMap((ref) => (ref.shapes || []).map((s) =>
4841
+ Object.assign({label: ref.label, line: ref.line}, s)));
4272
4842
  for(let ref of refs) {
4273
4843
  let names = ref.lists.filter((name) => published.has(name));
4274
4844
  for(let name of ref.lists) {
@@ -4286,12 +4856,18 @@ function collect_used_keys(opts)
4286
4856
  `${ref.lists.join(", ")}, and is not in it`);
4287
4857
  }
4288
4858
  }
4859
+ let them = (ref.lists.length > 1)? "them" : "it";
4289
4860
  for(let shape of ref.shapes || []) {
4290
- let re = new RegExp(shape.source);
4291
- if(!names.some((name) => [...published.get(name)].some((k) => re.test(k)))) {
4861
+ let r = shape_owner_check(shape, names.flatMap((name) => [...published.get(name)]), all_shapes);
4862
+ if(!r) {
4863
+ fail(`${ref.label}:${ref.line}: the i18n marker names ${ref.lists.join(", ")} for ` +
4864
+ `${shape.text}, and no key of ${them} has that shape: publish the keys the template builds`);
4865
+ } else if(r.narrower) {
4866
+ let u = r.narrower;
4292
4867
  fail(`${ref.label}:${ref.line}: the i18n marker names ${ref.lists.join(", ")} for ` +
4293
- `${shape.text}, and no key of ${ref.lists.length > 1? "them" : "it"} has that shape: ` +
4294
- `publish the keys the template builds`);
4868
+ `${shape.text}, and every key of ${them} with that shape has the shape of a narrower ` +
4869
+ `template, ${u.text} (${u.label}:${u.line}), which builds those keys: name the list ` +
4870
+ `of the keys ${shape.text} builds`);
4295
4871
  }
4296
4872
  }
4297
4873
  }