@yuneta/gobj-ui 7.26.33 → 7.26.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +223 -53
- package/dist/gobj-ui.cjs.js +1053 -144
- package/dist/gobj-ui.es.js +1053 -144
- package/package.json +2 -2
- package/scripts/validate-locales.mjs +672 -96
- package/src/c_yui_form.js +229 -11
- package/src/c_yui_gclass.js +2 -0
- package/src/c_yui_gobj_tree_js.js +2 -0
- package/src/c_yui_json.js +2 -0
- package/src/c_yui_json_graph.js +2 -0
- package/src/c_yui_nav.js +23 -5
- package/src/c_yui_shell.css +16 -0
- package/src/c_yui_shell.js +282 -26
- package/src/c_yui_treedb_graph.js +26 -8
- package/src/c_yui_treedb_topic_with_form.js +40 -18
- package/src/c_yui_treedb_topics.js +34 -11
- package/src/c_yui_window.js +132 -32
- package/src/c_yui_window_manager.js +156 -44
- package/src/form_json_editor_names.js +448 -0
- package/src/form_json_editor_names.wiring.test.js +595 -0
- package/src/form_label_i18n.wiring.test.js +169 -0
- package/src/form_table_validator.wiring.test.js +192 -0
- package/src/form_template_field.wiring.test.js +21 -0
- package/src/i18n_key_functions.js +14 -6
- package/src/i18n_key_functions.test.js +124 -2
- package/src/lib_maplibre.js +26 -2
- package/src/lib_maplibre_relocalize.test.js +78 -0
- package/src/named_controls.wiring.test.js +468 -0
- package/src/nav_cards_helpers.js +9 -1
- package/src/nav_item_badge.wiring.test.js +182 -1
- package/src/shell_route_map.js +25 -8
- package/src/table_editor_name.wiring.test.js +201 -0
- package/src/treedb_column_title.wiring.test.js +151 -0
- package/src/treedb_json_drill.wiring.test.js +41 -1
- package/src/window_layer.wiring.test.js +308 -43
- package/src/window_show.wiring.test.js +6 -2
- package/src/window_stacking.wiring.test.js +334 -0
- package/src/yui_badge.js +58 -13
- package/src/yui_badge.test.js +40 -3
- package/src/yui_dev.js +2 -1
- package/src/yui_file_field.js +2 -0
- package/src/yui_frontend_view.js +2 -1
- package/src/yui_gclass_view.js +9 -6
- package/src/yui_tabulator_i18n.js +42 -0
- package/src/yui_tabulator_i18n.test.js +48 -1
|
@@ -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
|
|
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()
|
|
70
|
-
* literal or a const; or written
|
|
71
|
-
* the schema checks
|
|
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())
|
|
99
|
-
*
|
|
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.
|
|
124
|
-
*
|
|
125
|
-
*
|
|
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")
|
|
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.
|
|
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")`,
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
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
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
* parameter
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
* (
|
|
182
|
-
*
|
|
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
|
-
|
|
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()`)
|
|
3355
|
-
*
|
|
3356
|
-
*
|
|
3357
|
-
* the
|
|
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 &&
|
|
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()
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
3447
|
-
|
|
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
|
|
3646
|
-
*
|
|
3647
|
-
*
|
|
3648
|
-
* `
|
|
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
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
3712
|
-
|
|
3713
|
-
} else if(n.left.type === "
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
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>)
|
|
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
|
|
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
|
-
|
|
3813
|
-
|
|
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
|
-
|
|
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
|
|
4291
|
-
if(!
|
|
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
|
|
4294
|
-
`
|
|
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
|
}
|