@yuneta/gobj-ui 7.26.25 → 7.26.26
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 +31 -6
- package/dist/gobj-ui.cjs.js +36 -16
- package/dist/gobj-ui.es.js +36 -16
- package/package.json +1 -1
- package/scripts/validate-locales.mjs +264 -79
- package/src/c_g6_nodes_tree.js +37 -15
- package/src/yui_file_field.js +2 -2
package/README.md
CHANGED
|
@@ -3600,19 +3600,44 @@ directory:
|
|
|
3600
3600
|
}
|
|
3601
3601
|
```
|
|
3602
3602
|
|
|
3603
|
-
It also scans the gobj-ui modules the app mounts
|
|
3604
|
-
**the library translates through the APP's
|
|
3605
|
-
must be defined by the app. The library it
|
|
3606
|
-
package the app builds with.
|
|
3607
|
-
|
|
3603
|
+
It also scans the gobj-ui modules the app mounts -- deep, through the barrel,
|
|
3604
|
+
or by a dynamic `import()`: **the library translates through the APP's
|
|
3605
|
+
i18next**, so every key it asks for must be defined by the app. The library it
|
|
3606
|
+
scans is the one it ships in, the package the app builds with. The app's own
|
|
3607
|
+
source is read recursively. Code and catalogs shared by several apps are named
|
|
3608
|
+
with `--src <dir>` (repeatable) and `--locales <dir>` (yunovatios:
|
|
3608
3609
|
`--src ../gui-common/src --locales ../gui-common/src/locales`); `--config`
|
|
3609
3610
|
names a shell declaration other than `src/app_config.json`.
|
|
3610
3611
|
|
|
3612
|
+
What is particular to an app goes in ITS `package.json`, not in the script:
|
|
3613
|
+
properties whose value is a key, and properties whose value is a list of keys
|
|
3614
|
+
(yunovatios hands its orchestrator `app_title: "central"` and
|
|
3615
|
+
`welcome_features: [...]`):
|
|
3616
|
+
|
|
3617
|
+
```json
|
|
3618
|
+
"validate-locales": {
|
|
3619
|
+
"attr_keys": ["app_title", "app_subtitle", "welcome_title", "welcome_lead"],
|
|
3620
|
+
"array_keys": ["welcome_features"]
|
|
3621
|
+
}
|
|
3622
|
+
```
|
|
3623
|
+
|
|
3624
|
+
Plurals are compared by language (since `7.26.26`): `songs_one` /
|
|
3625
|
+
`songs_few` / `songs_many` / `songs_other` are one key, and each locale must
|
|
3626
|
+
carry the categories of ITS language (`Intl.PluralRules`: English `one`,
|
|
3627
|
+
`other`; Russian `one`, `few`, `many`, `other`; Chinese `other`). The key with
|
|
3628
|
+
no suffix serves `other` and `many`, as i18next falls back to it; `zero`,
|
|
3629
|
+
`one`, `two` and `few` need their own form. A value of the shell declaration
|
|
3630
|
+
that is not lower-case ASCII (an endonym, `"中文"`, in a language menu) is a
|
|
3631
|
+
literal, not a key.
|
|
3632
|
+
|
|
3611
3633
|
Up to `7.26.24` each app carried its own copy, and the copies had drifted into
|
|
3612
3634
|
three families: only one read the codes of the schema editor, only the other
|
|
3613
3635
|
two the barrel imports, and two read `t('topic') + ':'` as a key built by
|
|
3614
3636
|
concatenation and skipped it -- so the gobj tree's *"topic:"* went untranslated
|
|
3615
|
-
in wattyzer and yunovatios.
|
|
3637
|
+
in wattyzer and yunovatios. No app keeps a copy now, but one: `test-app`, this
|
|
3638
|
+
library's own demo, whose catalog has English as the source (a key IS the
|
|
3639
|
+
English text, Title Case included) and so follows other rules, in its own
|
|
3640
|
+
`test-app/scripts/validate-locales.mjs`.
|
|
3616
3641
|
|
|
3617
3642
|
### Dates: never hand-roll them again
|
|
3618
3643
|
|
package/dist/gobj-ui.cjs.js
CHANGED
|
@@ -7181,11 +7181,11 @@ function yui_file_control(gobj, { name, value, readonly, accept, on_pick }) {
|
|
|
7181
7181
|
if (picked) {
|
|
7182
7182
|
$state.classList.add("is-picked");
|
|
7183
7183
|
$state.setAttribute("data-i18n", "not saved yet");
|
|
7184
|
-
$state.textContent = "not saved yet";
|
|
7184
|
+
$state.textContent = (0, i18next.t)("not saved yet");
|
|
7185
7185
|
} else if (!kept) {
|
|
7186
7186
|
$state.classList.add("is-empty");
|
|
7187
7187
|
$state.setAttribute("data-i18n", "no file");
|
|
7188
|
-
$state.textContent = "no file";
|
|
7188
|
+
$state.textContent = (0, i18next.t)("no file");
|
|
7189
7189
|
}
|
|
7190
7190
|
$clear.hidden = readonly || !picked && !kept;
|
|
7191
7191
|
$pick.hidden = !!readonly;
|
|
@@ -63836,9 +63836,14 @@ var BTN_STYLE_CANCEL = "flex:1;padding:6px;background:var(--bulma-scheme-main-bi
|
|
|
63836
63836
|
function create_form_button_row(parent, buttons) {
|
|
63837
63837
|
let btnRow = document.createElement("div");
|
|
63838
63838
|
btnRow.style.cssText = "display:flex;gap:8px;";
|
|
63839
|
-
for (let {
|
|
63839
|
+
for (let { i18n, style, onClick } of buttons) {
|
|
63840
63840
|
let btn = document.createElement("button");
|
|
63841
|
-
btn.textContent = (0, i18next.t)(
|
|
63841
|
+
btn.textContent = (0, i18next.t)(i18n);
|
|
63842
|
+
btn.setAttribute("data-i18n", i18n);
|
|
63843
|
+
btn.setAttribute("title", (0, i18next.t)(i18n));
|
|
63844
|
+
btn.setAttribute("data-i18n-title", i18n);
|
|
63845
|
+
btn.setAttribute("aria-label", (0, i18next.t)(i18n));
|
|
63846
|
+
btn.setAttribute("data-i18n-aria-label", i18n);
|
|
63842
63847
|
btn.style.cssText = style + touch_control_css();
|
|
63843
63848
|
btn.addEventListener("click", (e) => {
|
|
63844
63849
|
e.stopPropagation();
|
|
@@ -63872,11 +63877,11 @@ function show_confirm_popover(gobj, target_el, message, confirm_text, confirm_co
|
|
|
63872
63877
|
msg.textContent = message;
|
|
63873
63878
|
popover.appendChild(msg);
|
|
63874
63879
|
create_form_button_row(popover, [{
|
|
63875
|
-
|
|
63880
|
+
i18n: "cancel",
|
|
63876
63881
|
style: BTN_STYLE_CANCEL,
|
|
63877
63882
|
onClick: () => hide_overlay(gobj, priv_key)
|
|
63878
63883
|
}, {
|
|
63879
|
-
|
|
63884
|
+
i18n: confirm_text,
|
|
63880
63885
|
style: "flex:1;padding:6px;background:" + confirm_color + ";color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:13px;font-weight:500;",
|
|
63881
63886
|
onClick: onConfirm
|
|
63882
63887
|
}]);
|
|
@@ -64605,13 +64610,13 @@ function show_port_popover(gobj) {
|
|
|
64605
64610
|
}
|
|
64606
64611
|
]);
|
|
64607
64612
|
create_form_button_row(popover, [{
|
|
64608
|
-
|
|
64613
|
+
i18n: "cancel",
|
|
64609
64614
|
style: BTN_STYLE_CANCEL,
|
|
64610
64615
|
onClick: () => {
|
|
64611
64616
|
hide_port_popover(gobj);
|
|
64612
64617
|
}
|
|
64613
64618
|
}, {
|
|
64614
|
-
|
|
64619
|
+
i18n: "apply",
|
|
64615
64620
|
style: "flex:1;padding:6px;background:#1890ff;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:13px;font-weight:500;",
|
|
64616
64621
|
onClick: () => {
|
|
64617
64622
|
drop_preview_undo(gobj, "_port_preview_undo");
|
|
@@ -65301,13 +65306,13 @@ function show_edge_popover(gobj) {
|
|
|
65301
65306
|
}
|
|
65302
65307
|
]);
|
|
65303
65308
|
create_form_button_row(popover, [{
|
|
65304
|
-
|
|
65309
|
+
i18n: "cancel",
|
|
65305
65310
|
style: BTN_STYLE_CANCEL,
|
|
65306
65311
|
onClick: () => {
|
|
65307
65312
|
deselect_edge(gobj);
|
|
65308
65313
|
}
|
|
65309
65314
|
}, {
|
|
65310
|
-
|
|
65315
|
+
i18n: "apply",
|
|
65311
65316
|
style: "flex:1;padding:6px;background:#52c41a;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:13px;font-weight:500;",
|
|
65312
65317
|
onClick: () => {
|
|
65313
65318
|
drop_preview_undo(gobj, "_edge_preview_undo");
|
|
@@ -65462,13 +65467,13 @@ function show_node_popover(gobj) {
|
|
|
65462
65467
|
}
|
|
65463
65468
|
]);
|
|
65464
65469
|
create_form_button_row(popover, [{
|
|
65465
|
-
|
|
65470
|
+
i18n: "cancel",
|
|
65466
65471
|
style: BTN_STYLE_CANCEL,
|
|
65467
65472
|
onClick: () => {
|
|
65468
65473
|
hide_node_popover(gobj);
|
|
65469
65474
|
}
|
|
65470
65475
|
}, {
|
|
65471
|
-
|
|
65476
|
+
i18n: "apply",
|
|
65472
65477
|
style: "flex:1;padding:6px;background:#1890ff;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:13px;font-weight:500;",
|
|
65473
65478
|
onClick: () => {
|
|
65474
65479
|
drop_preview_undo(gobj, "_node_preview_undo");
|
|
@@ -66122,12 +66127,21 @@ function show_create_popover(gobj) {
|
|
|
66122
66127
|
let titleEl = document.createElement("div");
|
|
66123
66128
|
titleEl.style.cssText = "margin-bottom:10px;font-weight:600;font-size:14px;";
|
|
66124
66129
|
titleEl.textContent = (0, i18next.t)("create node");
|
|
66130
|
+
titleEl.setAttribute("data-i18n", "create node");
|
|
66125
66131
|
popover.appendChild(titleEl);
|
|
66126
66132
|
let topicLabel = document.createElement("div");
|
|
66127
66133
|
topicLabel.style.cssText = "margin-bottom:4px;font-weight:500;";
|
|
66128
|
-
|
|
66134
|
+
let topicWord = document.createElement("span");
|
|
66135
|
+
topicWord.textContent = (0, i18next.t)("topic");
|
|
66136
|
+
topicWord.setAttribute("data-i18n", "topic");
|
|
66137
|
+
topicLabel.appendChild(topicWord);
|
|
66138
|
+
topicLabel.appendChild(document.createTextNode(":"));
|
|
66129
66139
|
popover.appendChild(topicLabel);
|
|
66130
66140
|
let topicSelect = document.createElement("select");
|
|
66141
|
+
topicSelect.setAttribute("title", (0, i18next.t)("topic"));
|
|
66142
|
+
topicSelect.setAttribute("data-i18n-title", "topic");
|
|
66143
|
+
topicSelect.setAttribute("aria-label", (0, i18next.t)("topic"));
|
|
66144
|
+
topicSelect.setAttribute("data-i18n-aria-label", "topic");
|
|
66131
66145
|
topicSelect.style.cssText = "width:100%;padding:6px;border:1px solid #d9d9d9;border-radius:4px;font-size:13px;margin-bottom:8px;" + touch_control_css();
|
|
66132
66146
|
for (let i = 0; i < topics.length; i++) {
|
|
66133
66147
|
let opt = document.createElement("option");
|
|
@@ -66161,6 +66175,11 @@ function show_create_popover(gobj) {
|
|
|
66161
66175
|
let idInput = document.createElement("input");
|
|
66162
66176
|
idInput.type = "text";
|
|
66163
66177
|
idInput.placeholder = (0, i18next.t)("node id");
|
|
66178
|
+
idInput.setAttribute("data-i18n-placeholder", "node id");
|
|
66179
|
+
idInput.setAttribute("title", (0, i18next.t)("node id"));
|
|
66180
|
+
idInput.setAttribute("data-i18n-title", "node id");
|
|
66181
|
+
idInput.setAttribute("aria-label", (0, i18next.t)("node id"));
|
|
66182
|
+
idInput.setAttribute("data-i18n-aria-label", "node id");
|
|
66164
66183
|
idInput.style.cssText = "width:100%;padding:6px;border:1px solid #d9d9d9;border-radius:4px;font-size:13px;margin-bottom:10px;box-sizing:border-box;" + touch_control_css();
|
|
66165
66184
|
popover.appendChild(idInput);
|
|
66166
66185
|
let errorEl = document.createElement("div");
|
|
@@ -66168,16 +66187,17 @@ function show_create_popover(gobj) {
|
|
|
66168
66187
|
popover.appendChild(errorEl);
|
|
66169
66188
|
let createBtn;
|
|
66170
66189
|
create_form_button_row(popover, [{
|
|
66171
|
-
|
|
66190
|
+
i18n: "cancel",
|
|
66172
66191
|
style: BTN_STYLE_CANCEL,
|
|
66173
66192
|
onClick: () => hide_create_popover(gobj)
|
|
66174
66193
|
}, {
|
|
66175
|
-
|
|
66194
|
+
i18n: "create",
|
|
66176
66195
|
style: "flex:1;padding:6px;background:#1890ff;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:13px;font-weight:500;",
|
|
66177
66196
|
onClick: () => {
|
|
66178
66197
|
let node_id = idInput.value.trim();
|
|
66179
66198
|
if (!node_id) {
|
|
66180
|
-
errorEl.textContent = (0, i18next.t)("node id
|
|
66199
|
+
errorEl.textContent = (0, i18next.t)("node id required");
|
|
66200
|
+
errorEl.setAttribute("data-i18n", "node id required");
|
|
66181
66201
|
errorEl.style.display = "block";
|
|
66182
66202
|
idInput.focus();
|
|
66183
66203
|
return;
|
package/dist/gobj-ui.es.js
CHANGED
|
@@ -7176,11 +7176,11 @@ function yui_file_control(gobj, { name, value, readonly, accept, on_pick }) {
|
|
|
7176
7176
|
if (picked) {
|
|
7177
7177
|
$state.classList.add("is-picked");
|
|
7178
7178
|
$state.setAttribute("data-i18n", "not saved yet");
|
|
7179
|
-
$state.textContent = "not saved yet";
|
|
7179
|
+
$state.textContent = t("not saved yet");
|
|
7180
7180
|
} else if (!kept) {
|
|
7181
7181
|
$state.classList.add("is-empty");
|
|
7182
7182
|
$state.setAttribute("data-i18n", "no file");
|
|
7183
|
-
$state.textContent = "no file";
|
|
7183
|
+
$state.textContent = t("no file");
|
|
7184
7184
|
}
|
|
7185
7185
|
$clear.hidden = readonly || !picked && !kept;
|
|
7186
7186
|
$pick.hidden = !!readonly;
|
|
@@ -63831,9 +63831,14 @@ var BTN_STYLE_CANCEL = "flex:1;padding:6px;background:var(--bulma-scheme-main-bi
|
|
|
63831
63831
|
function create_form_button_row(parent, buttons) {
|
|
63832
63832
|
let btnRow = document.createElement("div");
|
|
63833
63833
|
btnRow.style.cssText = "display:flex;gap:8px;";
|
|
63834
|
-
for (let {
|
|
63834
|
+
for (let { i18n, style, onClick } of buttons) {
|
|
63835
63835
|
let btn = document.createElement("button");
|
|
63836
|
-
btn.textContent = t(
|
|
63836
|
+
btn.textContent = t(i18n);
|
|
63837
|
+
btn.setAttribute("data-i18n", i18n);
|
|
63838
|
+
btn.setAttribute("title", t(i18n));
|
|
63839
|
+
btn.setAttribute("data-i18n-title", i18n);
|
|
63840
|
+
btn.setAttribute("aria-label", t(i18n));
|
|
63841
|
+
btn.setAttribute("data-i18n-aria-label", i18n);
|
|
63837
63842
|
btn.style.cssText = style + touch_control_css();
|
|
63838
63843
|
btn.addEventListener("click", (e) => {
|
|
63839
63844
|
e.stopPropagation();
|
|
@@ -63867,11 +63872,11 @@ function show_confirm_popover(gobj, target_el, message, confirm_text, confirm_co
|
|
|
63867
63872
|
msg.textContent = message;
|
|
63868
63873
|
popover.appendChild(msg);
|
|
63869
63874
|
create_form_button_row(popover, [{
|
|
63870
|
-
|
|
63875
|
+
i18n: "cancel",
|
|
63871
63876
|
style: BTN_STYLE_CANCEL,
|
|
63872
63877
|
onClick: () => hide_overlay(gobj, priv_key)
|
|
63873
63878
|
}, {
|
|
63874
|
-
|
|
63879
|
+
i18n: confirm_text,
|
|
63875
63880
|
style: "flex:1;padding:6px;background:" + confirm_color + ";color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:13px;font-weight:500;",
|
|
63876
63881
|
onClick: onConfirm
|
|
63877
63882
|
}]);
|
|
@@ -64600,13 +64605,13 @@ function show_port_popover(gobj) {
|
|
|
64600
64605
|
}
|
|
64601
64606
|
]);
|
|
64602
64607
|
create_form_button_row(popover, [{
|
|
64603
|
-
|
|
64608
|
+
i18n: "cancel",
|
|
64604
64609
|
style: BTN_STYLE_CANCEL,
|
|
64605
64610
|
onClick: () => {
|
|
64606
64611
|
hide_port_popover(gobj);
|
|
64607
64612
|
}
|
|
64608
64613
|
}, {
|
|
64609
|
-
|
|
64614
|
+
i18n: "apply",
|
|
64610
64615
|
style: "flex:1;padding:6px;background:#1890ff;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:13px;font-weight:500;",
|
|
64611
64616
|
onClick: () => {
|
|
64612
64617
|
drop_preview_undo(gobj, "_port_preview_undo");
|
|
@@ -65296,13 +65301,13 @@ function show_edge_popover(gobj) {
|
|
|
65296
65301
|
}
|
|
65297
65302
|
]);
|
|
65298
65303
|
create_form_button_row(popover, [{
|
|
65299
|
-
|
|
65304
|
+
i18n: "cancel",
|
|
65300
65305
|
style: BTN_STYLE_CANCEL,
|
|
65301
65306
|
onClick: () => {
|
|
65302
65307
|
deselect_edge(gobj);
|
|
65303
65308
|
}
|
|
65304
65309
|
}, {
|
|
65305
|
-
|
|
65310
|
+
i18n: "apply",
|
|
65306
65311
|
style: "flex:1;padding:6px;background:#52c41a;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:13px;font-weight:500;",
|
|
65307
65312
|
onClick: () => {
|
|
65308
65313
|
drop_preview_undo(gobj, "_edge_preview_undo");
|
|
@@ -65457,13 +65462,13 @@ function show_node_popover(gobj) {
|
|
|
65457
65462
|
}
|
|
65458
65463
|
]);
|
|
65459
65464
|
create_form_button_row(popover, [{
|
|
65460
|
-
|
|
65465
|
+
i18n: "cancel",
|
|
65461
65466
|
style: BTN_STYLE_CANCEL,
|
|
65462
65467
|
onClick: () => {
|
|
65463
65468
|
hide_node_popover(gobj);
|
|
65464
65469
|
}
|
|
65465
65470
|
}, {
|
|
65466
|
-
|
|
65471
|
+
i18n: "apply",
|
|
65467
65472
|
style: "flex:1;padding:6px;background:#1890ff;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:13px;font-weight:500;",
|
|
65468
65473
|
onClick: () => {
|
|
65469
65474
|
drop_preview_undo(gobj, "_node_preview_undo");
|
|
@@ -66117,12 +66122,21 @@ function show_create_popover(gobj) {
|
|
|
66117
66122
|
let titleEl = document.createElement("div");
|
|
66118
66123
|
titleEl.style.cssText = "margin-bottom:10px;font-weight:600;font-size:14px;";
|
|
66119
66124
|
titleEl.textContent = t("create node");
|
|
66125
|
+
titleEl.setAttribute("data-i18n", "create node");
|
|
66120
66126
|
popover.appendChild(titleEl);
|
|
66121
66127
|
let topicLabel = document.createElement("div");
|
|
66122
66128
|
topicLabel.style.cssText = "margin-bottom:4px;font-weight:500;";
|
|
66123
|
-
|
|
66129
|
+
let topicWord = document.createElement("span");
|
|
66130
|
+
topicWord.textContent = t("topic");
|
|
66131
|
+
topicWord.setAttribute("data-i18n", "topic");
|
|
66132
|
+
topicLabel.appendChild(topicWord);
|
|
66133
|
+
topicLabel.appendChild(document.createTextNode(":"));
|
|
66124
66134
|
popover.appendChild(topicLabel);
|
|
66125
66135
|
let topicSelect = document.createElement("select");
|
|
66136
|
+
topicSelect.setAttribute("title", t("topic"));
|
|
66137
|
+
topicSelect.setAttribute("data-i18n-title", "topic");
|
|
66138
|
+
topicSelect.setAttribute("aria-label", t("topic"));
|
|
66139
|
+
topicSelect.setAttribute("data-i18n-aria-label", "topic");
|
|
66126
66140
|
topicSelect.style.cssText = "width:100%;padding:6px;border:1px solid #d9d9d9;border-radius:4px;font-size:13px;margin-bottom:8px;" + touch_control_css();
|
|
66127
66141
|
for (let i = 0; i < topics.length; i++) {
|
|
66128
66142
|
let opt = document.createElement("option");
|
|
@@ -66156,6 +66170,11 @@ function show_create_popover(gobj) {
|
|
|
66156
66170
|
let idInput = document.createElement("input");
|
|
66157
66171
|
idInput.type = "text";
|
|
66158
66172
|
idInput.placeholder = t("node id");
|
|
66173
|
+
idInput.setAttribute("data-i18n-placeholder", "node id");
|
|
66174
|
+
idInput.setAttribute("title", t("node id"));
|
|
66175
|
+
idInput.setAttribute("data-i18n-title", "node id");
|
|
66176
|
+
idInput.setAttribute("aria-label", t("node id"));
|
|
66177
|
+
idInput.setAttribute("data-i18n-aria-label", "node id");
|
|
66159
66178
|
idInput.style.cssText = "width:100%;padding:6px;border:1px solid #d9d9d9;border-radius:4px;font-size:13px;margin-bottom:10px;box-sizing:border-box;" + touch_control_css();
|
|
66160
66179
|
popover.appendChild(idInput);
|
|
66161
66180
|
let errorEl = document.createElement("div");
|
|
@@ -66163,16 +66182,17 @@ function show_create_popover(gobj) {
|
|
|
66163
66182
|
popover.appendChild(errorEl);
|
|
66164
66183
|
let createBtn;
|
|
66165
66184
|
create_form_button_row(popover, [{
|
|
66166
|
-
|
|
66185
|
+
i18n: "cancel",
|
|
66167
66186
|
style: BTN_STYLE_CANCEL,
|
|
66168
66187
|
onClick: () => hide_create_popover(gobj)
|
|
66169
66188
|
}, {
|
|
66170
|
-
|
|
66189
|
+
i18n: "create",
|
|
66171
66190
|
style: "flex:1;padding:6px;background:#1890ff;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:13px;font-weight:500;",
|
|
66172
66191
|
onClick: () => {
|
|
66173
66192
|
let node_id = idInput.value.trim();
|
|
66174
66193
|
if (!node_id) {
|
|
66175
|
-
errorEl.textContent = t("node id
|
|
66194
|
+
errorEl.textContent = t("node id required");
|
|
66195
|
+
errorEl.setAttribute("data-i18n", "node id required");
|
|
66176
66196
|
errorEl.style.display = "block";
|
|
66177
66197
|
idInput.focus();
|
|
66178
66198
|
return;
|
package/package.json
CHANGED
|
@@ -11,7 +11,18 @@
|
|
|
11
11
|
*
|
|
12
12
|
* 1. Keys are ASCII (no em-dash, accents, etc.).
|
|
13
13
|
* 2. Keys are lower-case.
|
|
14
|
-
* 3. All locales carry the same key set (symmetric).
|
|
14
|
+
* 3. All locales carry the same key set (symmetric). A PLURAL key
|
|
15
|
+
* (`songs_one`, `songs_few`, ...: i18next's `_zero` `_one`
|
|
16
|
+
* `_two` `_few` `_many` `_other`) is compared by its base, and
|
|
17
|
+
* each locale carries the plural categories of ITS language
|
|
18
|
+
* (Intl.PluralRules): English `one`/`other`, Spanish
|
|
19
|
+
* `one`/`many`/`other`, Russian `one`/`few`/`many`/`other`,
|
|
20
|
+
* Chinese `other`. The key with no suffix (`songs`) serves
|
|
21
|
+
* `other` and `many` -- i18next falls back to it -- but
|
|
22
|
+
* `zero`, `one`, `two` and `few` change the grammar and need
|
|
23
|
+
* their own form when the language has them. A form the
|
|
24
|
+
* language never uses is a violation; `_zero` is always
|
|
25
|
+
* allowed (i18next uses it for a count of 0).
|
|
15
26
|
* 4. No duplicate key in a locale file (an object literal keeps
|
|
16
27
|
* the LAST one and says nothing -- a stale entry silently
|
|
17
28
|
* overrides a new one; it happened to "last" and "loading").
|
|
@@ -20,14 +31,15 @@
|
|
|
20
31
|
* fine and simply never changes language -- invisible by
|
|
21
32
|
* construction, and it shipped: a Settings `label` column).
|
|
22
33
|
*
|
|
23
|
-
* Rule 5 reads the app's own source, the shell
|
|
24
|
-
* (`src/app_config.json`) and the gobj-ui modules the
|
|
25
|
-
* transitively
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
34
|
+
* Rule 5 reads the app's own source (recursively), the shell
|
|
35
|
+
* declaration (`src/app_config.json`) and the gobj-ui modules the
|
|
36
|
+
* app MOUNTS, transitively -- deep, through the barrel, or by a
|
|
37
|
+
* dynamic import(): the library translates through the APP's
|
|
38
|
+
* i18next, so a key it asks for and the app does not define
|
|
39
|
+
* renders raw (that is how the treedb editor shipped reading
|
|
40
|
+
* "edit", "new", "delete" in every language). The library scanned
|
|
41
|
+
* is THIS one -- the package the app installed and builds with,
|
|
42
|
+
* the one this script ships in.
|
|
31
43
|
*
|
|
32
44
|
* Options (paths relative to the app's directory):
|
|
33
45
|
*
|
|
@@ -37,29 +49,42 @@
|
|
|
37
49
|
* --config <file> the shell declaration
|
|
38
50
|
* (default src/app_config.json)
|
|
39
51
|
*
|
|
52
|
+
* And, in the app's package.json, what is particular to it:
|
|
53
|
+
*
|
|
54
|
+
* "validate-locales": {
|
|
55
|
+
* "attr_keys": ["app_title", "welcome_lead"],
|
|
56
|
+
* "array_keys": ["welcome_features"]
|
|
57
|
+
* }
|
|
58
|
+
*
|
|
59
|
+
* `attr_keys` names properties whose string value is an i18n key
|
|
60
|
+
* (`app_title: "central"`), `array_keys` properties whose value is
|
|
61
|
+
* an array of keys. A key the app hands over that way reaches no
|
|
62
|
+
* t() the scan can read.
|
|
63
|
+
*
|
|
40
64
|
* A locale is a file <code>.js of the locales directory (en.js,
|
|
41
|
-
* es.js; flags.js and locales.js are not) exporting
|
|
65
|
+
* es.js, pt-br.js; flags.js and locales.js are not) exporting
|
|
42
66
|
* `{<code>}` with a `translation` dict. It is imported directly,
|
|
43
67
|
* not through locales.js, which drags in gobj-js and i18next and
|
|
44
|
-
* expects a browser.
|
|
68
|
+
* expects a browser. A file with a locale's name and no such
|
|
69
|
+
* export is said and skipped.
|
|
45
70
|
*
|
|
46
71
|
* Exits 1 on any violation, so the build fails loudly.
|
|
47
72
|
*
|
|
48
73
|
* Up to gobj-ui 7.26.24 every app carried its own copy, and the
|
|
49
|
-
* copies drifted into three families with different rules
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* V17.3.4). This one is their union, and an app keeps no copy.
|
|
74
|
+
* copies drifted into three families with different rules
|
|
75
|
+
* (REV-CLOUD V17.3.4). This one is their union, and an app keeps
|
|
76
|
+
* no copy.
|
|
53
77
|
*
|
|
54
78
|
* Copyright (c) 2025-2026, ArtGins.
|
|
55
79
|
* All Rights Reserved.
|
|
56
80
|
***********************************************************************/
|
|
57
|
-
import {readdirSync, readFileSync, existsSync} from "fs";
|
|
81
|
+
import {readdirSync, readFileSync, existsSync, statSync} from "fs";
|
|
58
82
|
import {resolve} from "path";
|
|
59
83
|
import {pathToFileURL} from "url";
|
|
60
84
|
|
|
61
85
|
const ASCII_RE = /^[\x20-\x7e]+$/;
|
|
62
|
-
const LOCALE_FILE_RE = /^([a-z]{2,3})\.js$/;
|
|
86
|
+
const LOCALE_FILE_RE = /^([a-z]{2,3}(?:[-_][a-zA-Z]{2,4})?)\.js$/;
|
|
87
|
+
const PLURAL_RE = /^(.+)_(zero|one|two|few|many|other)$/;
|
|
63
88
|
|
|
64
89
|
/* This package: the library the app builds with. */
|
|
65
90
|
const LIB = new URL("../", import.meta.url);
|
|
@@ -89,6 +114,14 @@ function abort(msg)
|
|
|
89
114
|
process.exit(1);
|
|
90
115
|
}
|
|
91
116
|
|
|
117
|
+
/***************************************************************
|
|
118
|
+
* Said, not a violation
|
|
119
|
+
***************************************************************/
|
|
120
|
+
function note(msg)
|
|
121
|
+
{
|
|
122
|
+
process.stdout.write(`validate-locales: ${msg}\n`);
|
|
123
|
+
}
|
|
124
|
+
|
|
92
125
|
/***************************************************************
|
|
93
126
|
* The directory a path names, as a URL ending in '/'
|
|
94
127
|
***************************************************************/
|
|
@@ -106,7 +139,9 @@ function parse_args(argv)
|
|
|
106
139
|
let opts = {
|
|
107
140
|
src: [new URL("src/", APP_DIR)],
|
|
108
141
|
locales: new URL("src/locales/", APP_DIR),
|
|
109
|
-
config: new URL("src/app_config.json", APP_DIR)
|
|
142
|
+
config: new URL("src/app_config.json", APP_DIR),
|
|
143
|
+
attr_keys: [],
|
|
144
|
+
array_keys: []
|
|
110
145
|
};
|
|
111
146
|
for(let i = 0; i < argv.length; i++) {
|
|
112
147
|
let a = argv[i];
|
|
@@ -127,6 +162,25 @@ function parse_args(argv)
|
|
|
127
162
|
abort(`unknown argument: ${a} (--src <dir>, --locales <dir>, --config <file>)`);
|
|
128
163
|
}
|
|
129
164
|
}
|
|
165
|
+
|
|
166
|
+
/* What is particular to the app, in its package.json */
|
|
167
|
+
let pkg_url = new URL("package.json", APP_DIR);
|
|
168
|
+
if(existsSync(pkg_url)) {
|
|
169
|
+
let pkg;
|
|
170
|
+
try {
|
|
171
|
+
pkg = JSON.parse(readFileSync(pkg_url, "utf8"));
|
|
172
|
+
} catch(e) {
|
|
173
|
+
abort(`${pkg_url.pathname} is not valid json: ${e.message}`);
|
|
174
|
+
}
|
|
175
|
+
let cfg = pkg["validate-locales"] || {};
|
|
176
|
+
for(let name of ["attr_keys", "array_keys"]) {
|
|
177
|
+
let list = cfg[name] || [];
|
|
178
|
+
if(!Array.isArray(list) || !list.every((x) => /^[A-Za-z_$][\w$]*$/.test(x))) {
|
|
179
|
+
abort(`package.json "validate-locales".${name} must be a list of property names`);
|
|
180
|
+
}
|
|
181
|
+
opts[name] = list;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
130
184
|
return opts;
|
|
131
185
|
}
|
|
132
186
|
|
|
@@ -146,21 +200,55 @@ async function load_locales(locales_dir)
|
|
|
146
200
|
if(codes.includes("en")) {
|
|
147
201
|
codes = ["en", ...codes.filter((c) => c !== "en")];
|
|
148
202
|
}
|
|
149
|
-
if(codes.length === 0) {
|
|
150
|
-
abort(`no locale (<code>.js) in ${locales_dir.pathname}`);
|
|
151
|
-
}
|
|
152
203
|
let locales = {};
|
|
153
204
|
for(let code of codes) {
|
|
154
205
|
let mod = await import(new URL(`${code}.js`, locales_dir).href);
|
|
155
|
-
let locale = mod[code] || mod.default;
|
|
206
|
+
let locale = mod[code] || mod[code.replace(/-/g, "_")] || mod.default;
|
|
156
207
|
if(!locale || !locale.translation || typeof locale.translation !== "object") {
|
|
157
|
-
|
|
208
|
+
note(`${code}.js exports no {${code}} with a translation dict: not a locale, skipped`);
|
|
209
|
+
continue;
|
|
158
210
|
}
|
|
159
211
|
locales[code] = locale.translation;
|
|
160
212
|
}
|
|
213
|
+
if(Object.keys(locales).length === 0) {
|
|
214
|
+
abort(`no locale (<code>.js exporting {<code>: {translation}}) in ${locales_dir.pathname}`);
|
|
215
|
+
}
|
|
161
216
|
return locales;
|
|
162
217
|
}
|
|
163
218
|
|
|
219
|
+
/***************************************************************
|
|
220
|
+
* The plural categories of a language (Intl.PluralRules)
|
|
221
|
+
***************************************************************/
|
|
222
|
+
function plural_categories(code)
|
|
223
|
+
{
|
|
224
|
+
try {
|
|
225
|
+
return new Set(new Intl.PluralRules(code.replace(/_/g, "-")).resolvedOptions().pluralCategories);
|
|
226
|
+
} catch(e) {
|
|
227
|
+
return new Set(["one", "other"]);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/***************************************************************
|
|
232
|
+
* A locale's keys split: plain ones, and {base: Set(categories)}
|
|
233
|
+
***************************************************************/
|
|
234
|
+
function split_plurals(keys)
|
|
235
|
+
{
|
|
236
|
+
let plain = new Set();
|
|
237
|
+
let plurals = new Map();
|
|
238
|
+
for(let k of keys) {
|
|
239
|
+
let m = k.match(PLURAL_RE);
|
|
240
|
+
if(m) {
|
|
241
|
+
if(!plurals.has(m[1])) {
|
|
242
|
+
plurals.set(m[1], new Set());
|
|
243
|
+
}
|
|
244
|
+
plurals.get(m[1]).add(m[2]);
|
|
245
|
+
} else {
|
|
246
|
+
plain.add(k);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return {plain, plurals};
|
|
250
|
+
}
|
|
251
|
+
|
|
164
252
|
/***************************************************************
|
|
165
253
|
*
|
|
166
254
|
***************************************************************/
|
|
@@ -171,10 +259,10 @@ async function main()
|
|
|
171
259
|
let codes = Object.keys(locales);
|
|
172
260
|
|
|
173
261
|
/* Per-locale key checks (ASCII + lower-case) */
|
|
174
|
-
let
|
|
262
|
+
let split = {};
|
|
175
263
|
for(let code of codes) {
|
|
176
264
|
let keys = Object.keys(locales[code]);
|
|
177
|
-
|
|
265
|
+
split[code] = split_plurals(keys);
|
|
178
266
|
for(let k of keys) {
|
|
179
267
|
if(!ASCII_RE.test(k)) {
|
|
180
268
|
fail(`[${code}] non-ASCII key: ${JSON.stringify(k)}`);
|
|
@@ -185,23 +273,60 @@ async function main()
|
|
|
185
273
|
}
|
|
186
274
|
}
|
|
187
275
|
|
|
188
|
-
/*
|
|
276
|
+
/*
|
|
277
|
+
* The plural bases, of any locale: a plain key that is one of them
|
|
278
|
+
* belongs to its plural, and is checked with it
|
|
279
|
+
*/
|
|
280
|
+
let bases = new Set();
|
|
281
|
+
for(let code of codes) {
|
|
282
|
+
for(let base of split[code].plurals.keys()) {
|
|
283
|
+
bases.add(base);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/* Symmetry of the plain keys: every locale carries those of the first */
|
|
189
288
|
let ref = codes[0];
|
|
190
|
-
let
|
|
289
|
+
let plain_of = (code) => new Set([...split[code].plain].filter((k) => !bases.has(k)));
|
|
290
|
+
let ref_plain = plain_of(ref);
|
|
191
291
|
for(let code of codes.slice(1)) {
|
|
192
|
-
let cur =
|
|
193
|
-
for(let k of
|
|
292
|
+
let cur = plain_of(code);
|
|
293
|
+
for(let k of ref_plain) {
|
|
194
294
|
if(!cur.has(k)) {
|
|
195
295
|
fail(`[${code}] missing key present in [${ref}]: ${JSON.stringify(k)}`);
|
|
196
296
|
}
|
|
197
297
|
}
|
|
198
298
|
for(let k of cur) {
|
|
199
|
-
if(!
|
|
299
|
+
if(!ref_plain.has(k)) {
|
|
200
300
|
fail(`[${code}] extra key not in [${ref}]: ${JSON.stringify(k)}`);
|
|
201
301
|
}
|
|
202
302
|
}
|
|
203
303
|
}
|
|
204
304
|
|
|
305
|
+
/*
|
|
306
|
+
* The plural keys: every base in every locale, with the categories of
|
|
307
|
+
* ITS language -- not the same suffixes everywhere. The key with no
|
|
308
|
+
* suffix serves `other` and `many`.
|
|
309
|
+
*/
|
|
310
|
+
const SERVED_BY_THE_BASE = new Set(["other", "many"]);
|
|
311
|
+
for(let code of codes) {
|
|
312
|
+
let wanted = plural_categories(code);
|
|
313
|
+
for(let base of bases) {
|
|
314
|
+
let have = split[code].plurals.get(base) || new Set();
|
|
315
|
+
let has_base = split[code].plain.has(base);
|
|
316
|
+
for(let c of wanted) {
|
|
317
|
+
if(!have.has(c) && !(has_base && SERVED_BY_THE_BASE.has(c))) {
|
|
318
|
+
fail(`[${code}] plural ${JSON.stringify(base)} lacks the form "${c}" of its language` +
|
|
319
|
+
(SERVED_BY_THE_BASE.has(c)? ` (or the key ${JSON.stringify(base)} itself)` : ""));
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
for(let c of have) {
|
|
323
|
+
if(!wanted.has(c) && c !== "zero") {
|
|
324
|
+
fail(`[${code}] plural ${JSON.stringify(base)} has a form "${c}" its language never uses`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
205
330
|
/*
|
|
206
331
|
* No DUPLICATE key in a locale file. A JS object literal keeps the LAST
|
|
207
332
|
* of two identical keys and says nothing, so the imported dict looks
|
|
@@ -223,7 +348,7 @@ async function main()
|
|
|
223
348
|
/* Every key the source asks for exists: a missing one is a violation, not a fallback */
|
|
224
349
|
let used = collect_used_keys(opts);
|
|
225
350
|
for(let k of used) {
|
|
226
|
-
if(!
|
|
351
|
+
if(!ref_plain.has(k) && !bases.has(k)) {
|
|
227
352
|
fail(`key used in the source but defined in NO locale: ${JSON.stringify(k)}`);
|
|
228
353
|
}
|
|
229
354
|
}
|
|
@@ -233,66 +358,99 @@ async function main()
|
|
|
233
358
|
process.exit(1);
|
|
234
359
|
}
|
|
235
360
|
|
|
361
|
+
let n_keys = ref_plain.size + bases.size;
|
|
236
362
|
process.stdout.write(
|
|
237
|
-
`validate-locales: OK (${codes.length} locales x ${
|
|
238
|
-
|
|
363
|
+
`validate-locales: OK (${codes.length} locales x ${n_keys} keys` +
|
|
364
|
+
(bases.size? `, ${bases.size} plural` : "") +
|
|
365
|
+
`, ${used.size} used in the source)\n`);
|
|
239
366
|
}
|
|
240
367
|
|
|
241
368
|
/***************************************************************
|
|
242
369
|
* The i18n keys the source asks for
|
|
370
|
+
*
|
|
371
|
+
* A string literal is matched by its own quote: an apostrophe inside
|
|
372
|
+
* double quotes ("don't") is part of the key.
|
|
373
|
+
*
|
|
374
|
+
* The (?!\s*\+) guards skip a key BUILT BY CONCATENATION
|
|
375
|
+
* (`i18n: "kind_" + t.kind`): the literal is only a prefix, the real key
|
|
376
|
+
* cannot be known statically, and demanding "kind_" is a false positive.
|
|
377
|
+
* Such keys are not validated: keep them in sync by hand. The guard is
|
|
378
|
+
* right after the literal, INSIDE the call: `t('topic') + ':'`
|
|
379
|
+
* concatenates the result, and its key is asked for.
|
|
243
380
|
***************************************************************/
|
|
381
|
+
const STR = String.raw`(?:"([^"\n]+)"|'([^'\n]+)')`;
|
|
382
|
+
const STR_OR_TEMPLATE = String.raw`(?:"([^"\n]+)"|'([^'\n]+)'|` + "`" + String.raw`([^` + "`" + String.raw`$\n]+)` + "`" + ")";
|
|
383
|
+
const NOT_CONCAT = String.raw`(?!\s*\+)`;
|
|
384
|
+
|
|
385
|
+
function re(source)
|
|
386
|
+
{
|
|
387
|
+
return new RegExp(source, "g");
|
|
388
|
+
}
|
|
389
|
+
|
|
244
390
|
const PATTERNS = [
|
|
245
|
-
|
|
246
|
-
* Both quote styles: the apps quote with ", the library with '.
|
|
247
|
-
*
|
|
248
|
-
* The (?!\s*\+) guards skip a key BUILT BY CONCATENATION
|
|
249
|
-
* (`i18n: "kind_" + t.kind`): the literal is only a prefix, the real
|
|
250
|
-
* key cannot be known statically, and demanding "kind_" is a false
|
|
251
|
-
* positive. Such keys are not validated: keep them in sync by hand.
|
|
252
|
-
*/
|
|
253
|
-
/\bt\(\s*['"]([^'"]+)['"](?!\s*\+)/g,
|
|
391
|
+
re(String.raw`\bt\(\s*` + STR_OR_TEMPLATE + NOT_CONCAT),
|
|
254
392
|
/* A local ALIAS of `t` is a blind spot: `yui_tabulator_i18n.js` asks
|
|
255
393
|
* for its whole chrome through `tr(key, default)`. */
|
|
256
|
-
|
|
394
|
+
re(String.raw`\btr\(\s*` + STR_OR_TEMPLATE + NOT_CONCAT),
|
|
257
395
|
/* The codes the schema checks hand to `t(finding.code)` /
|
|
258
396
|
* `t(conflict.code)`: an i18n key each, written where it is born. */
|
|
259
|
-
|
|
260
|
-
|
|
397
|
+
re(String.raw`\badd\(\s*['"](?:error|warning)['"]\s*,\s*` + STR),
|
|
398
|
+
re(String.raw`\bcode:\s*` + STR + NOT_CONCAT),
|
|
261
399
|
/* What refresh_language() re-translates */
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
* no t(), no i18n:, so a typo there rendered raw */
|
|
267
|
-
/\b(?:app_title|app_subtitle|welcome_title|welcome_lead)\s*:\s*['"]([^'"]+)['"]/g
|
|
400
|
+
re(String.raw`\bi18n:\s*` + STR + NOT_CONCAT),
|
|
401
|
+
re(String.raw`["']data-i18n(?:-title|-aria-label|-placeholder)?["']:\s*` + STR + NOT_CONCAT),
|
|
402
|
+
re(String.raw`\bsetAttribute\(\s*["']data-i18n(?:-title|-aria-label|-placeholder)?["']\s*,\s*` + STR + NOT_CONCAT),
|
|
403
|
+
/data-i18n(?:-title|-aria-label|-placeholder)?=["']([^"']+)["']/g
|
|
268
404
|
];
|
|
269
405
|
|
|
406
|
+
function literal_of(m)
|
|
407
|
+
{
|
|
408
|
+
for(let i = 1; i < m.length; i++) {
|
|
409
|
+
if(m[i] !== undefined) {
|
|
410
|
+
return m[i];
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return undefined;
|
|
414
|
+
}
|
|
415
|
+
|
|
270
416
|
function collect_used_keys(opts)
|
|
271
417
|
{
|
|
272
418
|
let keys = new Set();
|
|
273
419
|
|
|
420
|
+
let patterns = [...PATTERNS];
|
|
421
|
+
if(opts.attr_keys.length > 0) {
|
|
422
|
+
/* The app's own: properties whose value is a key */
|
|
423
|
+
patterns.push(re(String.raw`\b(?:` + opts.attr_keys.join("|") + String.raw`)\s*:\s*` + STR));
|
|
424
|
+
}
|
|
425
|
+
let array_re = opts.array_keys.length > 0?
|
|
426
|
+
re(String.raw`\b(?:` + opts.array_keys.join("|") + String.raw`)\s*:\s*\[([^\]]*)\]`) : null;
|
|
427
|
+
|
|
274
428
|
let scan = (url) => {
|
|
275
429
|
/* Comments are not code: the library documents the contract with
|
|
276
430
|
* examples like `data-i18n="<key>"`. */
|
|
277
431
|
let code = strip_comments(readFileSync(url, "utf8"));
|
|
278
|
-
for(let
|
|
279
|
-
|
|
432
|
+
for(let r of patterns) {
|
|
433
|
+
r.lastIndex = 0;
|
|
280
434
|
let m;
|
|
281
|
-
while((m =
|
|
435
|
+
while((m = r.exec(code)) !== null) {
|
|
436
|
+
let k = literal_of(m);
|
|
282
437
|
/* A key INTERPOLATED in a template (`data-i18n="${key}"`)
|
|
283
438
|
* cannot be known statically either */
|
|
284
|
-
if(
|
|
439
|
+
if(!k || k.includes("${")) {
|
|
285
440
|
continue;
|
|
286
441
|
}
|
|
287
|
-
keys.add(
|
|
442
|
+
keys.add(k);
|
|
288
443
|
}
|
|
289
444
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
445
|
+
if(array_re) {
|
|
446
|
+
array_re.lastIndex = 0;
|
|
447
|
+
let f;
|
|
448
|
+
while((f = array_re.exec(code)) !== null) {
|
|
449
|
+
let lit_re = re(STR);
|
|
450
|
+
let l;
|
|
451
|
+
while((l = lit_re.exec(f[1])) !== null) {
|
|
452
|
+
keys.add(literal_of(l));
|
|
453
|
+
}
|
|
296
454
|
}
|
|
297
455
|
}
|
|
298
456
|
return code;
|
|
@@ -306,11 +464,8 @@ function collect_used_keys(opts)
|
|
|
306
464
|
if(!existsSync(dir)) {
|
|
307
465
|
abort(`no source directory at ${dir.pathname}`);
|
|
308
466
|
}
|
|
309
|
-
let
|
|
310
|
-
|
|
311
|
-
});
|
|
312
|
-
for(let f of own) {
|
|
313
|
-
let src = scan(new URL(f, dir));
|
|
467
|
+
for(let url of source_files(dir, opts.locales)) {
|
|
468
|
+
let src = scan(url);
|
|
314
469
|
pending.push(...lib_imports(src, barrel, false));
|
|
315
470
|
}
|
|
316
471
|
}
|
|
@@ -348,6 +503,35 @@ function collect_used_keys(opts)
|
|
|
348
503
|
return keys;
|
|
349
504
|
}
|
|
350
505
|
|
|
506
|
+
/***************************************************************
|
|
507
|
+
* The .js files of a source directory, recursively: not the tests, not
|
|
508
|
+
* the locales (their keys are definitions, not uses), not node_modules
|
|
509
|
+
* or a build
|
|
510
|
+
***************************************************************/
|
|
511
|
+
function source_files(dir, locales_dir)
|
|
512
|
+
{
|
|
513
|
+
let out = [];
|
|
514
|
+
let walk = (d) => {
|
|
515
|
+
if(d.href === locales_dir.href) {
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
for(let f of readdirSync(d).sort()) {
|
|
519
|
+
let url = new URL(f, d);
|
|
520
|
+
let st = statSync(url);
|
|
521
|
+
if(st.isDirectory()) {
|
|
522
|
+
if(f === "node_modules" || f === "dist" || f.startsWith(".")) {
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
walk(new URL(f + "/", d));
|
|
526
|
+
} else if(f.endsWith(".js") && !f.endsWith(".test.js")) {
|
|
527
|
+
out.push(url);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
walk(dir);
|
|
532
|
+
return out;
|
|
533
|
+
}
|
|
534
|
+
|
|
351
535
|
/***************************************************************
|
|
352
536
|
* The shell declaration (`app_config.json`) holds i18n keys in DATA:
|
|
353
537
|
* every `name`, `aria_label` and `tooltip` of an item, the `label` of a
|
|
@@ -356,17 +540,18 @@ function collect_used_keys(opts)
|
|
|
356
540
|
* ever -- which is how four toolbar buttons announced themselves as
|
|
357
541
|
* "toggle language" in Spanish.
|
|
358
542
|
*
|
|
359
|
-
* A value that is not
|
|
360
|
-
* carries on purpose
|
|
361
|
-
*
|
|
362
|
-
*
|
|
543
|
+
* A value that is not a KEY -- not lower-case ASCII -- is a literal the
|
|
544
|
+
* config carries on purpose: "ES/EN" on a language button, an endonym
|
|
545
|
+
* ("中文", "Español") in a language menu. `wordmark` and `alt` are
|
|
546
|
+
* deliberately out: a brand name is not translated. A key starting with
|
|
547
|
+
* `_` is a comment.
|
|
363
548
|
***************************************************************/
|
|
364
549
|
const CONFIG_I18N_FIELDS = ["name", "aria_label", "tooltip", "label", "title", "subtitle"];
|
|
365
550
|
|
|
366
551
|
function keys_from_shell_config(url, keys)
|
|
367
552
|
{
|
|
368
553
|
if(!existsSync(url)) {
|
|
369
|
-
|
|
554
|
+
note(`no ${url.pathname}: no shell keys checked`);
|
|
370
555
|
return;
|
|
371
556
|
}
|
|
372
557
|
let config;
|
|
@@ -390,7 +575,7 @@ function keys_from_shell_config(url, keys)
|
|
|
390
575
|
continue;
|
|
391
576
|
}
|
|
392
577
|
if(CONFIG_I18N_FIELDS.includes(k) && typeof v === "string" && v
|
|
393
|
-
&& v === v.toLowerCase()) {
|
|
578
|
+
&& ASCII_RE.test(v) && v === v.toLowerCase()) {
|
|
394
579
|
keys.add(v);
|
|
395
580
|
}
|
|
396
581
|
walk(v);
|
|
@@ -425,9 +610,9 @@ function barrel_export_map()
|
|
|
425
610
|
} catch(e) {
|
|
426
611
|
abort(`cannot read the gobj-ui barrel at ${LIB_INDEX.pathname}: ${e.message}`);
|
|
427
612
|
}
|
|
428
|
-
let
|
|
613
|
+
let r = /export\s*\{([^}]*)\}\s*from\s*["']\.\/src\/([\w.-]+\.js)["']/g;
|
|
429
614
|
let m;
|
|
430
|
-
while((m =
|
|
615
|
+
while((m = r.exec(src)) !== null) {
|
|
431
616
|
let mod = m[2];
|
|
432
617
|
for(let sym of m[1].split(",")) {
|
|
433
618
|
sym = sym.trim();
|
|
@@ -444,8 +629,8 @@ function barrel_export_map()
|
|
|
444
629
|
|
|
445
630
|
/***************************************************************
|
|
446
631
|
* The gobj-ui source modules a file imports:
|
|
447
|
-
* - deep:
|
|
448
|
-
* - barrel:
|
|
632
|
+
* - deep: "@yuneta/gobj-ui/src/x.js", also by a dynamic import()
|
|
633
|
+
* - barrel: "@yuneta/gobj-ui[/index.js]", resolved per imported symbol
|
|
449
634
|
* - inside the library: "./x.js" (in the app it names an app file,
|
|
450
635
|
* scanned already)
|
|
451
636
|
***************************************************************/
|
|
@@ -454,7 +639,7 @@ function lib_imports(src, barrel, inside_lib)
|
|
|
454
639
|
let out = [];
|
|
455
640
|
let m;
|
|
456
641
|
|
|
457
|
-
let from_app = /from\s
|
|
642
|
+
let from_app = /(?:from\s+|import\(\s*)["']@yuneta\/gobj-ui\/src\/([\w.-]+\.js)["']/g;
|
|
458
643
|
while((m = from_app.exec(src)) !== null) {
|
|
459
644
|
out.push(m[1]);
|
|
460
645
|
}
|
|
@@ -471,7 +656,7 @@ function lib_imports(src, barrel, inside_lib)
|
|
|
471
656
|
}
|
|
472
657
|
|
|
473
658
|
if(inside_lib) {
|
|
474
|
-
let inside = /from\s
|
|
659
|
+
let inside = /(?:from\s+|import\(\s*)["']\.\/([\w.-]+\.js)["']/g;
|
|
475
660
|
while((m = inside.exec(src)) !== null) {
|
|
476
661
|
out.push(m[1]);
|
|
477
662
|
}
|
package/src/c_g6_nodes_tree.js
CHANGED
|
@@ -5010,9 +5010,16 @@ function create_form_button_row(parent, buttons)
|
|
|
5010
5010
|
{
|
|
5011
5011
|
let btnRow = document.createElement('div');
|
|
5012
5012
|
btnRow.style.cssText = 'display:flex;gap:8px;';
|
|
5013
|
-
for(let {
|
|
5013
|
+
for(let {i18n, style, onClick} of buttons) {
|
|
5014
|
+
/* `i18n` is the key (named so that validate-locales sees a literal
|
|
5015
|
+
* one): the button carries it, and its name */
|
|
5014
5016
|
let btn = document.createElement('button');
|
|
5015
|
-
btn.textContent = t(
|
|
5017
|
+
btn.textContent = t(i18n);
|
|
5018
|
+
btn.setAttribute('data-i18n', i18n);
|
|
5019
|
+
btn.setAttribute('title', t(i18n));
|
|
5020
|
+
btn.setAttribute('data-i18n-title', i18n);
|
|
5021
|
+
btn.setAttribute('aria-label', t(i18n));
|
|
5022
|
+
btn.setAttribute('data-i18n-aria-label', i18n);
|
|
5016
5023
|
btn.style.cssText = style + touch_control_css();
|
|
5017
5024
|
btn.addEventListener('click', (e) => {
|
|
5018
5025
|
e.stopPropagation();
|
|
@@ -5059,12 +5066,12 @@ function show_confirm_popover(gobj, target_el, message, confirm_text, confirm_co
|
|
|
5059
5066
|
|
|
5060
5067
|
create_form_button_row(popover, [
|
|
5061
5068
|
{
|
|
5062
|
-
|
|
5069
|
+
i18n: 'cancel',
|
|
5063
5070
|
style: BTN_STYLE_CANCEL,
|
|
5064
5071
|
onClick: () => hide_overlay(gobj, priv_key),
|
|
5065
5072
|
},
|
|
5066
5073
|
{
|
|
5067
|
-
|
|
5074
|
+
i18n: confirm_text,
|
|
5068
5075
|
style: 'flex:1;padding:6px;background:' + confirm_color + ';color:#fff;border:none;' +
|
|
5069
5076
|
'border-radius:4px;cursor:pointer;font-size:13px;font-weight:500;',
|
|
5070
5077
|
onClick: onConfirm,
|
|
@@ -6148,14 +6155,14 @@ function show_port_popover(gobj)
|
|
|
6148
6155
|
|
|
6149
6156
|
create_form_button_row(popover, [
|
|
6150
6157
|
{
|
|
6151
|
-
|
|
6158
|
+
i18n: 'cancel',
|
|
6152
6159
|
style: BTN_STYLE_CANCEL,
|
|
6153
6160
|
onClick: () => {
|
|
6154
6161
|
hide_port_popover(gobj); /* which undoes the preview */
|
|
6155
6162
|
},
|
|
6156
6163
|
},
|
|
6157
6164
|
{
|
|
6158
|
-
|
|
6165
|
+
i18n: 'apply',
|
|
6159
6166
|
style: 'flex:1;padding:6px;background:#1890ff;color:#fff;border:none;' +
|
|
6160
6167
|
'border-radius:4px;cursor:pointer;font-size:13px;font-weight:500;',
|
|
6161
6168
|
onClick: () => {
|
|
@@ -7100,14 +7107,14 @@ function show_edge_popover(gobj)
|
|
|
7100
7107
|
|
|
7101
7108
|
create_form_button_row(popover, [
|
|
7102
7109
|
{
|
|
7103
|
-
|
|
7110
|
+
i18n: 'cancel',
|
|
7104
7111
|
style: BTN_STYLE_CANCEL,
|
|
7105
7112
|
onClick: () => {
|
|
7106
7113
|
deselect_edge(gobj); /* which undoes the preview */
|
|
7107
7114
|
},
|
|
7108
7115
|
},
|
|
7109
7116
|
{
|
|
7110
|
-
|
|
7117
|
+
i18n: 'apply',
|
|
7111
7118
|
style: 'flex:1;padding:6px;background:#52c41a;color:#fff;border:none;' +
|
|
7112
7119
|
'border-radius:4px;cursor:pointer;font-size:13px;font-weight:500;',
|
|
7113
7120
|
onClick: () => {
|
|
@@ -7349,14 +7356,14 @@ function show_node_popover(gobj)
|
|
|
7349
7356
|
|
|
7350
7357
|
create_form_button_row(popover, [
|
|
7351
7358
|
{
|
|
7352
|
-
|
|
7359
|
+
i18n: 'cancel',
|
|
7353
7360
|
style: BTN_STYLE_CANCEL,
|
|
7354
7361
|
onClick: () => {
|
|
7355
7362
|
hide_node_popover(gobj); /* which undoes the preview */
|
|
7356
7363
|
},
|
|
7357
7364
|
},
|
|
7358
7365
|
{
|
|
7359
|
-
|
|
7366
|
+
i18n: 'apply',
|
|
7360
7367
|
style: 'flex:1;padding:6px;background:#1890ff;color:#fff;border:none;' +
|
|
7361
7368
|
'border-radius:4px;cursor:pointer;font-size:13px;font-weight:500;',
|
|
7362
7369
|
onClick: () => {
|
|
@@ -8431,15 +8438,24 @@ function show_create_popover(gobj)
|
|
|
8431
8438
|
let titleEl = document.createElement('div');
|
|
8432
8439
|
titleEl.style.cssText = 'margin-bottom:10px;font-weight:600;font-size:14px;';
|
|
8433
8440
|
titleEl.textContent = t('create node');
|
|
8441
|
+
titleEl.setAttribute('data-i18n', 'create node');
|
|
8434
8442
|
popover.appendChild(titleEl);
|
|
8435
8443
|
|
|
8436
|
-
// Topic selector
|
|
8444
|
+
// Topic selector: the word carries its key, the colon is not translated
|
|
8437
8445
|
let topicLabel = document.createElement('div');
|
|
8438
8446
|
topicLabel.style.cssText = 'margin-bottom:4px;font-weight:500;';
|
|
8439
|
-
|
|
8447
|
+
let topicWord = document.createElement('span');
|
|
8448
|
+
topicWord.textContent = t('topic');
|
|
8449
|
+
topicWord.setAttribute('data-i18n', 'topic');
|
|
8450
|
+
topicLabel.appendChild(topicWord);
|
|
8451
|
+
topicLabel.appendChild(document.createTextNode(':'));
|
|
8440
8452
|
popover.appendChild(topicLabel);
|
|
8441
8453
|
|
|
8442
8454
|
let topicSelect = document.createElement('select');
|
|
8455
|
+
topicSelect.setAttribute('title', t('topic'));
|
|
8456
|
+
topicSelect.setAttribute('data-i18n-title', 'topic');
|
|
8457
|
+
topicSelect.setAttribute('aria-label', t('topic'));
|
|
8458
|
+
topicSelect.setAttribute('data-i18n-aria-label', 'topic');
|
|
8443
8459
|
topicSelect.style.cssText =
|
|
8444
8460
|
'width:100%;padding:6px;border:1px solid #d9d9d9;border-radius:4px;' +
|
|
8445
8461
|
'font-size:13px;margin-bottom:8px;' + touch_control_css();
|
|
@@ -8483,6 +8499,11 @@ function show_create_popover(gobj)
|
|
|
8483
8499
|
let idInput = document.createElement('input');
|
|
8484
8500
|
idInput.type = 'text';
|
|
8485
8501
|
idInput.placeholder = t('node id');
|
|
8502
|
+
idInput.setAttribute('data-i18n-placeholder', 'node id');
|
|
8503
|
+
idInput.setAttribute('title', t('node id'));
|
|
8504
|
+
idInput.setAttribute('data-i18n-title', 'node id');
|
|
8505
|
+
idInput.setAttribute('aria-label', t('node id'));
|
|
8506
|
+
idInput.setAttribute('data-i18n-aria-label', 'node id');
|
|
8486
8507
|
idInput.style.cssText =
|
|
8487
8508
|
'width:100%;padding:6px;border:1px solid #d9d9d9;border-radius:4px;' +
|
|
8488
8509
|
'font-size:13px;margin-bottom:10px;box-sizing:border-box;' +
|
|
@@ -8499,18 +8520,19 @@ function show_create_popover(gobj)
|
|
|
8499
8520
|
let createBtn; // need reference for Enter key handler
|
|
8500
8521
|
create_form_button_row(popover, [
|
|
8501
8522
|
{
|
|
8502
|
-
|
|
8523
|
+
i18n: 'cancel',
|
|
8503
8524
|
style: BTN_STYLE_CANCEL,
|
|
8504
8525
|
onClick: () => hide_create_popover(gobj),
|
|
8505
8526
|
},
|
|
8506
8527
|
{
|
|
8507
|
-
|
|
8528
|
+
i18n: 'create',
|
|
8508
8529
|
style: 'flex:1;padding:6px;background:#1890ff;color:#fff;border:none;' +
|
|
8509
8530
|
'border-radius:4px;cursor:pointer;font-size:13px;font-weight:500;',
|
|
8510
8531
|
onClick: () => {
|
|
8511
8532
|
let node_id = idInput.value.trim();
|
|
8512
8533
|
if(!node_id) {
|
|
8513
|
-
errorEl.textContent = t('node id
|
|
8534
|
+
errorEl.textContent = t('node id required');
|
|
8535
|
+
errorEl.setAttribute('data-i18n', 'node id required');
|
|
8514
8536
|
errorEl.style.display = 'block';
|
|
8515
8537
|
idInput.focus();
|
|
8516
8538
|
return;
|
package/src/yui_file_field.js
CHANGED
|
@@ -328,11 +328,11 @@ function yui_file_control(gobj, {name, value, readonly, accept, on_pick})
|
|
|
328
328
|
if(picked) {
|
|
329
329
|
$state.classList.add("is-picked");
|
|
330
330
|
$state.setAttribute("data-i18n", "not saved yet");
|
|
331
|
-
$state.textContent = "not saved yet";
|
|
331
|
+
$state.textContent = t("not saved yet");
|
|
332
332
|
} else if(!kept) {
|
|
333
333
|
$state.classList.add("is-empty");
|
|
334
334
|
$state.setAttribute("data-i18n", "no file");
|
|
335
|
-
$state.textContent = "no file";
|
|
335
|
+
$state.textContent = t("no file");
|
|
336
336
|
}
|
|
337
337
|
|
|
338
338
|
$clear.hidden = readonly || (!picked && !kept);
|