@yuneta/gobj-ui 7.26.26 → 7.26.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +473 -19
  2. package/dist/gobj-ui.cjs.js +1100 -231
  3. package/dist/gobj-ui.es.js +1101 -232
  4. package/package.json +1 -1
  5. package/scripts/validate-locales.mjs +2334 -275
  6. package/src/c_g6_nodes_tree.js +1143 -260
  7. package/src/c_yui_form.js +11 -1
  8. package/src/c_yui_fsm_graph.js +5 -0
  9. package/src/c_yui_gclass.js +11 -0
  10. package/src/c_yui_gobj_tree_js.js +17 -0
  11. package/src/c_yui_json_graph.js +5 -0
  12. package/src/c_yui_nav.js +9 -6
  13. package/src/c_yui_pager.js +1 -1
  14. package/src/c_yui_period.js +11 -4
  15. package/src/c_yui_schema_editor.js +19 -7
  16. package/src/c_yui_shell.js +7 -7
  17. package/src/c_yui_treedb_graph.js +15 -6
  18. package/src/c_yui_treedb_schema.js +5 -0
  19. package/src/c_yui_treedb_topic_with_form.js +10 -10
  20. package/src/c_yui_treedb_topics.js +6 -6
  21. package/src/c_yui_window.js +2 -2
  22. package/src/c_yui_wizard.js +8 -1
  23. package/src/form_json_field.js +7 -0
  24. package/src/g6_canvas_tab_order.js +84 -0
  25. package/src/g6_canvas_tab_order.wiring.test.js +232 -0
  26. package/src/g6_nodes_tree_controls.test.js +547 -0
  27. package/src/g6_nodes_tree_focus.wiring.test.js +1360 -0
  28. package/src/g6_nodes_tree_i18n_keys.test.js +275 -0
  29. package/src/i18n_key_functions.js +146 -0
  30. package/src/i18n_key_functions.test.js +1204 -0
  31. package/src/lib_graph.js +38 -1
  32. package/src/nav_backbar_name.wiring.test.js +82 -0
  33. package/src/schema_flags.js +47 -0
  34. package/src/schema_import.js +7 -0
  35. package/src/shell_modals.js +20 -16
  36. package/src/shell_route_map.js +12 -2
  37. package/src/wizard_helpers.js +8 -0
  38. package/src/yui_dev.js +44 -33
  39. package/src/yui_dev_controls.test.js +174 -0
  40. package/src/yui_gclass_view.js +2 -2
  41. package/src/yui_graph_camera.js +1 -1
  42. package/src/yui_install.js +10 -8
  43. package/src/yui_table_select.js +2 -2
  44. package/src/yui_time.js +18 -3
package/README.md CHANGED
@@ -1513,11 +1513,130 @@ clicked":
1513
1513
  | **Delete** / **Backspace** | deletes the selection, after a confirmation that counts what it takes |
1514
1514
  | **Esc**, or a click on the canvas | clears it |
1515
1515
 
1516
- The keys reach the graph only while the **graph has focus** — G6 gives its
1517
- canvas a `tabIndex` of its own — which is what keeps `ctrl+A` inside the find
1518
- box a selection of the TEXT and not of every node: the focus is in the input,
1519
- and the input is not inside the canvas. They arrive as `EV_KEY_DOWN` and the
1520
- action decides, so a key is as visible in the `machine` trace as a click.
1516
+ The keys reach the graph only while the **graph has focus** — G6 hears a key
1517
+ inside its own container, and its main canvas can take the focus — which is
1518
+ what keeps `ctrl+A` inside the find box a selection of the TEXT and not of
1519
+ every node: the focus is in the input, and the input is not inside the graph.
1520
+ They arrive as `EV_KEY_DOWN` and the action decides, so a key is as visible in
1521
+ the `machine` trace as a click.
1522
+
1523
+ **Tab goes from the canvas to the graph's own buttons** (since `7.26.28`). G6
1524
+ gives each of its four canvases `tabIndex = 1`, and a positive tabindex goes
1525
+ before every other control of the page: Tab from the graph went to the other
1526
+ three canvases, then to the top of the page and through every control there,
1527
+ and only then to the buttons beside the selection. Each time G6 writes that
1528
+ `1` — when it creates the canvases (`aftercanvasinit`) and after a renderer
1529
+ change (`afterrendererchange`) — the view gives the main canvas `0` and the
1530
+ other three `-1`. Tab now goes from the canvas to the toolbars and then to the
1531
+ round buttons, in DOM order. The same rule holds in every view of the library
1532
+ that draws a G6 graph (`C_G6_NODES_TREE`, `C_YUI_JSON_GRAPH`,
1533
+ `C_YUI_GOBJ_TREE_JS`, `C_YUI_FSM_GRAPH`, `C_YUI_TREEDB_SCHEMA`): each one calls
1534
+ the shared `keep_canvas_tab_order()` (`src/g6_canvas_tab_order.js`) right after
1535
+ `new Graph()`:
1536
+
1537
+ ```js
1538
+ import {keep_canvas_tab_order} from "./g6_canvas_tab_order.js";
1539
+
1540
+ const graph = priv.graph = new Graph({container: priv.$container /* ... */});
1541
+ keep_canvas_tab_order(gobj, graph); // main canvas 0, the other layers -1
1542
+ ```
1543
+
1544
+ The buttons of the graph are buttons for the keyboard too: the items of both
1545
+ toolbars, the round buttons beside a selection — the gear and the trash of a
1546
+ node or an edge, the link and the gear of a port — and the ✕ of a node's
1547
+ detail popover. Each one has `role="button"`, `tabindex="0"`, and a `title`
1548
+ and an `aria-label` that carry their key (`data-i18n-title`,
1549
+ `data-i18n-aria-label`). **Enter** or **Space** presses the focused one,
1550
+ exactly as a click does, and that key stops at the button: it is not the
1551
+ graph's. Any other key goes on, so **Esc** and **Delete** still clear or
1552
+ delete the selection while one of those buttons has the focus.
1553
+
1554
+ A toolbar item says its STATE as well as its name:
1555
+
1556
+ - **A disabled item** (save, undo and redo with nothing to do, zoom to
1557
+ selection with nothing selected) has `aria-disabled="true"` and is out of
1558
+ the Tab order (`tabindex="-1"`), from its first render and on every change.
1559
+ `disableElements()` / `enableElements()` (`lib_graph.js`) write the three
1560
+ together, and keep the tabindex the item takes back in
1561
+ `data-enabled-tabindex`. A disabled item that still has the focus takes the
1562
+ key and does nothing.
1563
+ - **A toggle** — the anchor, the selection mode — has `aria-pressed`, `true`
1564
+ only while it is ON, the same state its `pressed_state` look shows.
1565
+ - **A rebuild keeps the state** (since `7.26.28`). G6 rebuilds a toolbar
1566
+ whole — on a fold, a language change, full screen, any plugin added or
1567
+ removed — from its `getItems()`, read at that moment, and the new
1568
+ `innerHTML` lands later, over anything told to the old items. So each
1569
+ state is computed in `getItems()` as well as told to the live item. Before,
1570
+ zoom to selection came back disabled over a selection, and save, undo and
1571
+ redo came back disabled over history still pending.
1572
+
1573
+ ```js
1574
+ // An item of a getItems() list, its state computed HERE: a rebuild
1575
+ // draws the item from this list.
1576
+ {id: 'g6-icon-save', value: 'save',
1577
+ className: 'EV_SAVE_GRAPH' + (lit.save ? ' color_submit_state' : ''),
1578
+ title: t('save'), title_key: 'save', disabled: !lit.save},
1579
+ {id: 'g6-icon-select-mode', value: 'selection-mode',
1580
+ className: 'EV_TOGGLE_SELECTION_MODE', pressed: priv.selection_mode,
1581
+ title: t('selection mode'), title_key: 'selection mode'},
1582
+
1583
+ // Between two rebuilds, on the live toolbar:
1584
+ enableElements($container, ".EV_SAVE_GRAPH"); // tabindex 0, no aria-disabled
1585
+ ```
1586
+
1587
+ A press, from the pointer or from a key, does one thing: the button SENDS an
1588
+ input event to the view, with the identity of what it belongs to, and the
1589
+ action does the work. So every press is in the `machine` trace:
1590
+
1591
+ | button | event | kw |
1592
+ |---|---|---|
1593
+ | gear of a node | `EV_TOGGLE_NODE_POPOVER` | `{node_id}` |
1594
+ | trash of a node | `EV_REQUEST_DELETE_NODE` | `{node_id}` |
1595
+ | gear of an edge | `EV_TOGGLE_EDGE_POPOVER` | `{edge_id}` |
1596
+ | trash of an edge | `EV_REQUEST_UNLINK_EDGE` | `{edge_id}` |
1597
+ | gear of a port | `EV_TOGGLE_PORT_POPOVER` | `{node_id, port_key}` |
1598
+ | link of an fkey port (a click; a drag starts the link itself) | `EV_ENTER_LINKING_MODE` | `{node_id, port_key}` |
1599
+ | a press on that link, which may be a drag | `EV_START_LINK_DRAG` | `{node_id, port_key, client_x, client_y}` |
1600
+ | ...its release, and a pointer the browser took | `EV_LINK_DRAG_END` / `EV_LINK_DRAG_CANCEL` | `{client_x, client_y, dragged}` / `{}` |
1601
+ | ✕ of the detail popover | `EV_CLOSE_NODE_DETAIL` | `{node_id}` |
1602
+ | create of the edit toolbar | `EV_TOGGLE_CREATE_POPOVER` | `{}` |
1603
+ | cancel of the create popover (or Escape in its id) / create | `EV_CLOSE_CREATE_POPOVER` / `EV_SUBMIT_NEW_NODE` | `{}` / `{topic_name, id}` |
1604
+ | cancel / apply of a node's popover | `EV_CANCEL_NODE_POPOVER` / `EV_APPLY_NODE_POPOVER` | `{node_id}` / `{node_id, shape_before, fill, stroke, line_width, scope, shape}` |
1605
+ | cancel / apply of an edge's popover | `EV_CANCEL_EDGE_POPOVER` / `EV_APPLY_EDGE_POPOVER` | `{edge_id}` / `{edge_id, line_width, color, scope}` |
1606
+ | cancel / apply of a port's popover | `EV_CANCEL_PORT_POPOVER` / `EV_APPLY_PORT_POPOVER` | `{node_id, port_key}` / `{node_id, port_key, r_before, shape, r, scope}` |
1607
+ | cancel / confirm of a delete or an unlink | `EV_CANCEL_CONFIRM` / `EV_CONFIRMED` | `{what, id}` or `{what, ids}` |
1608
+
1609
+ A popover's apply carries what its form held when it was pressed, each value
1610
+ as the control gives it (a string: the action parses it), so the trace says
1611
+ what was applied, not only that something was:
1612
+
1613
+ ```
1614
+ EV_APPLY_EDGE_POPOVER {"edge_id": "e1", "line_width": "3", "color": "#52c41a", "scope": "this"}
1615
+ ```
1616
+
1617
+ The action refuses, with an error, an identity that is not the current
1618
+ selection (or, for the ✕, the node the popover shows; for a popover's
1619
+ buttons, a popover that is not open; for a cancel of a confirmation, a
1620
+ question that is not the one open). The buttons go away with their selection,
1621
+ so a mismatch is a defect, and to act on it would edit or delete something the
1622
+ reader does not look at. An empty id in the create popover is not a defect:
1623
+ the action says *node id required* in the popover, and puts the focus back in
1624
+ the id.
1625
+
1626
+ Only the MOVES of a link drag are not events: they draw the line and the
1627
+ cursor, which is the gesture's own picture, and one event per move would bury
1628
+ the trace.
1629
+
1630
+ **The keyboard stays with the graph.** A pointer press on one of these buttons
1631
+ does not take the focus: it cancels the `pointerdown`, the click still comes,
1632
+ and the focus stays on the canvas. And before the view removes an element that
1633
+ has the focus — a round button that Escape deselects, a popover that closes
1634
+ with its ✕ or a field in it, a toolbar it rebuilds or takes away — it gives
1635
+ the focus back: after a rebuild to the item of the same CONTROL (its
1636
+ `data-control`: Enter on *enter full screen* lands on *exit full screen*, the
1637
+ same control in its other state), in every other case to the main canvas. Firefox fires no `focusout` when the
1638
+ focused element is removed, so without this the focus fell to `<body>` and the
1639
+ graph heard no key until the next click on it.
1521
1640
 
1522
1641
  Three decisions are worth knowing, because each one is where this could have
1523
1642
  gone wrong:
@@ -2837,8 +2956,9 @@ Consumer i18n keys: `json viewer`, `paste json here`, `invalid json`,
2837
2956
  `collapsed in the source`, `first json`, `second json`, `compare`, `added`,
2838
2957
  `removed`, `changed`, `path`, `no differences`, `compare needs two json`,
2839
2958
  `cannot compare`, `json not kept` (plus `view` and `clear`, which every app
2840
- has). `compare`, `view` and `clear` reach `t()` as variables, so a consumer's
2841
- `validate-locales` does not see them.
2959
+ has). `compare`, `view` and `clear` reach `t()` through the pad's own button
2960
+ helper, which `I18N_KEY_FUNCTIONS` names, so a consumer's `validate-locales`
2961
+ asks for them (since `7.26.28`; it did not see them before).
2842
2962
 
2843
2963
  ### Frontend view — `setup_frontend_view`
2844
2964
 
@@ -3505,6 +3625,31 @@ A `<label>` that **wraps** its control is the one shape that needs no
3505
3625
  `aria-label` next to a visible `i18n` label is worse than none: it OVERRIDES
3506
3626
  the translated text for a reader.
3507
3627
 
3628
+ A control drawn as something other than a `<button>` — a `div` holding an
3629
+ icon or a glyph — is a button for the KEYBOARD too: `role="button"`,
3630
+ `tabindex="0"`, and Enter or Space **click** it, so the key runs the very
3631
+ handler the pointer does:
3632
+
3633
+ ```js
3634
+ el.setAttribute('role', 'button');
3635
+ el.setAttribute('tabindex', '0');
3636
+ el.addEventListener('keydown', (e) => {
3637
+ if(e.key === 'Enter' || e.key === ' ') {
3638
+ e.preventDefault(); // Space would scroll the page
3639
+ e.stopPropagation(); // a key that pressed a button is not the host's
3640
+ el.click();
3641
+ }
3642
+ });
3643
+ ```
3644
+
3645
+ `C_G6_NODES_TREE` builds its floating buttons and the ✕ of its detail popover
3646
+ this way, and the items of its G6 toolbars too (the listener is on the
3647
+ toolbar element, because G6 rebuilds the items). The `stopPropagation`
3648
+ matters there: the graph's container forwards every keydown to G6, so an
3649
+ Enter that pressed a button would also reach the graph's shortcuts and
3650
+ `EV_KEY_DOWN`. What else a button needs inside a graph is in
3651
+ [Selecting several nodes](#selecting-several-nodes-and-moving-them-together).
3652
+
3508
3653
  Two things carry no attribute anyone can set, and are named after the render
3509
3654
  instead — and again on every rebuild, because a name written onto a widget's
3510
3655
  DOM is a race with the next one:
@@ -3522,8 +3667,9 @@ DOM is a race with the next one:
3522
3667
  DEPLOYED DOM, resolve each control's name the way a reader does (`aria-label`
3523
3668
  → `label[for]` → a wrapping `<label>` → the text → `title` → `placeholder`),
3524
3669
  then switch language and diff. A key that arrives as a VARIABLE — from a data
3525
- table, a helper's argument, a local alias of `t()` — is invisible to
3526
- `validate-locales` and shows up only there.
3670
+ table, a local alias of `t()`, a helper `I18N_KEY_FUNCTIONS` does not name — is
3671
+ invisible to `validate-locales` unless its module publishes it (below), and
3672
+ shows up only there.
3527
3673
 
3528
3674
  ### i18n: a string must be able to CHANGE language, not just be translated once
3529
3675
 
@@ -3625,19 +3771,324 @@ Plurals are compared by language (since `7.26.26`): `songs_one` /
3625
3771
  `songs_few` / `songs_many` / `songs_other` are one key, and each locale must
3626
3772
  carry the categories of ITS language (`Intl.PluralRules`: English `one`,
3627
3773
  `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.
3774
+ no suffix serves `other` and `many`; `zero`, `one`, `two` and `few` need their
3775
+ own form. That is a policy of GRAMMAR, not a need of i18next, which falls back
3776
+ to the bare key for any form that is missing: a Russian count of 3 rendered
3777
+ with the bare key reads wrong, and the check says so. A group is a plural only
3778
+ when some locale carries its bare base (`songs`) or its `_other` form — every
3779
+ language has `other`, so a real plural always has one of the two. Keys that
3780
+ only END like forms are plain keys: `pick_one` alone, or `step_one` /
3781
+ `step_two` / `step_three`, an enumeration (up to `7.26.27` two forms were
3782
+ enough to make a plural, and that enumeration gave seven violations). A form
3783
+ asked for by its own name — `t("pick_one")` beside a bare `pick` — is a key
3784
+ like any other. Ordinals (`place_ordinal_one`) are checked with the ordinal
3785
+ categories of each language; the `_ordinal_` infix says they are forms. A value of the shell
3786
+ declaration is a literal when it is the endonym of one of the app's locales
3787
+ (the `name` its catalog exports: `"中文"`, `"español"`); any other lower-case
3788
+ text is a key, `"configuración"` included.
3789
+
3790
+ **The source is PARSED, never run** (since `7.26.28`, with `acorn`, which
3791
+ every app declares already for `check-markup-strings.mjs`). Comments are not
3792
+ code. A key is read where it is WRITTEN, and FOLLOWED where the source writes
3793
+ it out:
3794
+
3795
+ - a string literal, a template with no `${}`, and every branch of `?:`, `||`
3796
+ and `??` (the right one of `&&`), so
3797
+ `"data-i18n": pending ? "loading" : "click to load"` asks for both, and
3798
+ `comment || "print-tranger failed"` for its fallback;
3799
+ - a `const`, to its value, in the scope that declares it (a parameter or a
3800
+ `let` of the same name hides it);
3801
+ - a property of a const object literal: `DEFAULT_LABEL.ok`, and for a
3802
+ computed one, `NOTES[verdict]`, every value the object holds;
3803
+ - the element of a const array literal that a `.map()`, `.forEach()`, ...
3804
+ callback or a `for...of` walks.
3805
+
3806
+ ```js
3807
+ const KEY = "save";
3808
+ t(KEY); // asks for "save"
3809
+ const NOTES = {found: "values found", none: "no values"};
3810
+ t(NOTES[verdict]); // asks for both
3811
+ for(const zone of ZONES) {
3812
+ build_zone(gobj, zone); // ZONES[].key, at build_zone's key position
3813
+ }
3814
+ SHAPES.map((s) => ({value: s, i18n: s})); // every element of SHAPES
3815
+ ```
3816
+
3817
+ What it cannot follow cannot be known: a `let`, a parameter, an import, a
3818
+ call, a concatenation, data from a backend. A key that arrives that way is
3819
+ PUBLISHED by its module (below), and the place it is read is MARKED; a marked
3820
+ `let` -- or a marked member or element of one -- is the one exception to "not
3821
+ followed" (below). A source file that cannot be parsed fails the build, because
3822
+ its keys would go unchecked.
3823
+
3824
+ **A key handed to a FUNCTION is read where it is handed.**
3825
+ `yui_shell_show_error(shell, "some records were gone before the delete")` asks
3826
+ `t()` for its key INSIDE the function, where no scan of `t("...")` sees it, and
3827
+ up to `7.26.27` such keys shipped raw with the validator green (`expand
3828
+ loaded`, `click to load`, `please select some row`, `accept`, ...).
3829
+ `I18N_KEY_FUNCTIONS` (`src/i18n_key_functions.js`) names every gobj-ui function
3830
+ that receives a key, and where it is:
3831
+
3832
+ ```js
3833
+ {module: "shell_modals.js", name: "yui_shell_confirm_danger", exported: true,
3834
+ keys: ["1", "2.title", "2.confirm_label", "2.cancel_label"]},
3835
+ {module: "yui_table_select.js", name: "yui_selection_bar", exported: true,
3836
+ keys: ["1.actions[].label"]},
3837
+ {module: "c_yui_json.js", name: "icon_button", exported: false, keys: ["3"]},
3838
+ ```
3839
+
3840
+ A position is an argument (`"1"`), a property of the object given there
3841
+ (`"2.title"`, also through `Object.assign()` or a spread of a const object),
3842
+ or a property of each object of an array (`"1.actions[].label"`). What is
3843
+ given at those positions is read like any other key (literals, consts,
3844
+ branches): in the app's own code, on a call bound to an import of that
3845
+ function from gobj-ui (an app function of the same name is not mistaken for
3846
+ it), and in every module the app mounts, where an entry with
3847
+ `exported: false` is a helper of its own module. The exported ones an app
3848
+ calls: `yui_shell_show_info` / `_warning` / `_error` (the message),
3849
+ `yui_shell_show_modal` (a string content, `title`),
3850
+ `yui_shell_confirm_ok` / `_yesno` / `_danger` / `_yesnocancel` (the message,
3851
+ `title`, the `*_label`s), `yui_install_ask_once` (`message`, `yes_label`,
3852
+ `no_label`), `yui_selection_bar` (each action's `label`), `yui_asset_missing`
3853
+ / `yui_asset_element` (`key`), `hook_cell_spec` (the title key) and
3854
+ `period_name` (a granularity id). A function added to the library that hands
3855
+ one of its parameters to `t()`, to `i18n` or to a `data-i18n*` attribute goes
3856
+ in the table.
3857
+
3858
+ **A key the reader cannot follow is PUBLISHED by its module** (since
3859
+ `7.26.27`): an exported `<NAME>_I18N_KEYS` array of string literals, which
3860
+ `validate-locales` reads in every module the app mounts and asks the locales
3861
+ for. `yui_dev.js` publishes the 51 keys of the Developer window
3862
+ (`YUI_DEV_I18N_KEYS`) -- they used to be a comment each consumer copied, and
3863
+ one consumer lacked 35 --, `c_g6_nodes_tree.js` those of its shape tables,
3864
+ popover labels, floating buttons and error messages
3865
+ (`G6_NODES_TREE_I18N_KEYS`). Since `7.26.28` more modules publish one:
3866
+ `SCHEMA_FLAGS_I18N_KEYS` (the group and the description of every flag),
3867
+ `YUI_TIME_I18N_KEYS` (every granularity and rolling window of the catalog:
3868
+ which of them a period control offers is the HOST's data),
3869
+ `C_YUI_FORM_I18N_KEYS` (the message of an empty required field, and the
3870
+ warning a form hands its host when it vetoes a close),
3871
+ `C_YUI_SCHEMA_EDITOR_I18N_KEYS` (the severity of a finding, which the checks of
3872
+ `schema_validate.js` write), `SHELL_ROUTE_MAP_I18N_KEYS` (the labels of the
3873
+ route map's own nodes), and so on: `grep -n _I18N_KEYS src/*.js`.
3874
+
3875
+ A list is for what the reader CANNOT follow. A key it can follow needs no list:
3876
+ write it as a literal or a `const` where it is read, and the reader sees it.
3877
+ `const key = maximized ? "restore" : "maximize"; t(key);` asks for both keys,
3878
+ and so do the pager's ✕, the camera's anchor button, the nav's landmark, the
3879
+ selection bar's clear button and the treedb form's title, which are written
3880
+ that way. The same code with a `let` asks for neither.
3881
+
3882
+ ```js
3883
+ export const MY_VIEW_I18N_KEYS = ['shape', 'radius', 'apply to'];
3884
+ ```
3885
+
3886
+ The list is read as DATA, never run: `export const` (or `let`, `var`),
3887
+ `const X = [...]; export {X}` (or `export {x as X_I18N_KEYS}`), wrapped in
3888
+ `Object.freeze()` or not, each element a string literal or a template with no
3889
+ `${}`. Anything named `*_I18N_KEYS` (in any case) that no consumer can read
3890
+ **fails the build**, naming the file and the list, in the app's own code too:
3891
+ an export that is not an array of literals (a variable, a spread, a call, a
3892
+ template with `${}`), a list that is not exported, one declared inside a
3893
+ function or a block, one declared by destructuring
3894
+ (`export const {X_I18N_KEYS} = tables`), one exported under a name that does
3895
+ not end in `_I18N_KEYS`. Up to `7.26.27` the regex that read the lists dropped
3896
+ such a list in silence, and a key holding `]` dropped the whole array.
3897
+
3898
+ **A MARKER says where the keys of a place come from.** It is a comment that
3899
+ stands right before the node it marks (only whitespace between): an argument,
3900
+ a branch of one, a property value, or the whole call. Two kinds:
3901
+
3902
+ ```js
3903
+ yui_shell_show_error(shell, /* i18n: data */ comment || "print-tranger failed", {t: t});
3904
+ let name = mode_name(/* i18n: C_YUI_PERIOD_I18N_KEYS, YUI_TIME_I18N_KEYS */ mode);
3905
+ confirm_then(gobj, /* i18n: C_YUI_FORM_I18N_KEYS */ kw.warning || "all changes will be lost", ...);
3906
+ ```
3907
+
3908
+ - `i18n: data` -- what stands there is DATA, no key a list can hold: a
3909
+ backend's comment, a topic or column name, a gclass name, a host's own key
3910
+ in its config, a DOM node. The literals inside it (`"print-tranger failed"`
3911
+ above) are still read.
3912
+ - `i18n: <NAME>_I18N_KEYS` (several by commas) -- the keys come from those
3913
+ published lists. The keys of the marked expression that the reader CAN
3914
+ follow must be in them: a marker is a claim, and it is checked.
3915
+
3916
+ **A marked `let` (or `var`) is held to its marker.** A marker that names a list
3917
+ used to let a `let` pass unread, whatever it held: a new literal branch on it
3918
+ was neither asked for nor checked (REV-CLOUD V20). Now, when what a marker
3919
+ marks is a plain `let` / `var`, the reader collects everything the variable is
3920
+ GIVEN, in the scope that declares it -- its initializer and every `=` (`||=`,
3921
+ `??=`, `&&=` too) -- and reads each value like any other. Each literal it finds
3922
+ is a key the locales are asked for and, under a list marker, must be in the
3923
+ lists it names. A write it cannot read (`+=`, `++`, a destructuring, a
3924
+ `for...of` over the variable) keeps the variable unknown; a parameter or
3925
+ another variable of the same name is not a write to it. And a variable that is
3926
+ given nothing but literals is a const that was not written as one, so it
3927
+ fails:
3928
+
3929
+ ```js
3930
+ let key = maximized ? "restore" : "maximize";
3931
+ t(/* i18n: C_YUI_WINDOW_I18N_KEYS */ key);
3932
+ // c_yui_window.js:920: `key` is a let (line 918) that only ever holds literals
3933
+ // ("restore", "maximize"): make it a const, which the reader follows, and drop the marker
3934
+ ```
3935
+
3936
+ The rule is the same under `i18n: data`: the literals of a marked variable are
3937
+ read, and one that holds nothing else fails.
3938
+
3939
+ **A `let` that holds a TABLE is held the same way** (since the same round:
3940
+ up to then a marked member of one passed unread). When what is marked is read
3941
+ FROM a `let` -- a property of it (`LABELS[k]`, `opts.title` at a key
3942
+ position), the element of a `.map()` / `.forEach()` / `for...of` over it, or
3943
+ another `let` given one of those (`let picked = LIST[0]`) -- the reader follows
3944
+ that `let` too: everything it is given, and along the same steps (a property,
3945
+ an element) inside each value. For such a member, what is WRITTEN into the
3946
+ table counts as given: `x.a = v`, `x[k] = v`, `x.push(v)` (also `unshift`,
3947
+ `splice`, `fill`, a spread), `Object.assign(x, v)`. A `+=` or `++` on that
3948
+ member keeps it unknown.
3949
+
3950
+ ```js
3951
+ let LABELS = {ok: "accept", no: "refuse"};
3952
+ t(/* i18n: MY_VIEW_I18N_KEYS */ LABELS[answer]);
3953
+
3954
+ let LIST = ["accept"];
3955
+ let picked = LIST[0];
3956
+ t(/* i18n: MY_VIEW_I18N_KEYS */ picked);
3957
+
3958
+ let EXTRA = [];
3959
+ EXTRA.push("accept");
3960
+ EXTRA.forEach((x) => t(/* i18n: MY_VIEW_I18N_KEYS */ x));
3961
+ ```
3962
+
3963
+ ```
3964
+ main.js:2: `LABELS` is a let (line 1) that only ever holds literals ("accept", "refuse"):
3965
+ make it a const, which the reader follows, and drop the marker
3966
+ main.js:6: `picked` is a let (line 5) that only ever holds literals ("accept"),
3967
+ through the let `LIST` (line 4): make them consts, which the reader follows, and drop the marker
3968
+ ```
3969
+
3970
+ Line 10 is no violation: "accept" is asked for, and must be in
3971
+ `MY_VIEW_I18N_KEYS`. It does not fail with *"make it a const"*, because a const
3972
+ would not carry what is pushed into it to the reader.
3973
+
3974
+ `validate-locales` checks every marker in what it scans, in an app's code too:
3975
+ a comment that starts with `i18n:` and is no marker, a marker that stands
3976
+ before no place a key is read (a key position, `t()`, an `i18n` or
3977
+ `data-i18n*` value), a marker that names a list no module the app mounts
3978
+ publishes, a marked key that is not in the lists it names, a marked variable
3979
+ that only ever holds literals -- each one fails, with the file and the line.
3980
+
3981
+ **The library holds itself to it, per CALL.** `src/i18n_key_functions.test.js`
3982
+ reads every module with the validator's own reader, and every value a key
3983
+ position of a key function can take must be:
3984
+
3985
+ 1. a string literal, a `?:` / `||` / `??` of them, or nothing;
3986
+ 2. a const the reader follows to one of those;
3987
+ 3. a key position of the enclosing table function, handed on (`message`
3988
+ where `message` is a key parameter of the function the call is in); or
3989
+ 4. MARKED, `/* i18n: data */` or `/* i18n: <NAME>_I18N_KEYS */`.
3990
+
3991
+ Anything else fails, naming the file, the line and the call:
3992
+
3993
+ ```
3994
+ c_yui_treedb_topics.js:860 (call at 859) card_action_anchor() [2]: table_key
3995
+ ```
3996
+
3997
+ Publishing a list does not exempt a module: the rule is per call, not per
3998
+ module. The same test fails when a marker is not one
3999
+ or stands nowhere; when a marker names a list that is not published by its
4000
+ module or by one it imports (a consumer would not read it); when a marked key
4001
+ the reader can follow is not in the list; when a marked variable only ever
4002
+ holds literals; when a list is not readable, holds a key that is not
4003
+ lower-case ASCII, holds one twice, or holds one written nowhere else in its
4004
+ module (stale); when an entry of the table names a function its module does
4005
+ not declare.
4006
+
4007
+ **Every key of every list is PINNED**: dropping it from its list fails a test,
4008
+ or loses nothing. For each key, one of these holds, or the test fails with
4009
+ *"reaches a consumer only through this list, and dropping it from the list
4010
+ fails no check"*:
4011
+
4012
+ - **its list is derived from the module's SOURCE** (`COVERAGE`): the test reads
4013
+ that source from the syntax tree and fails on a key the list lacks -- a
4014
+ table (`LAYOUTS` for `C_YUI_GOBJ_TREE_JS_I18N_KEYS`, `VIEWS` /
4015
+ `MACHINE_VIEWS` / `ZONES` for `C_YUI_GCLASS_I18N_KEYS`, the period catalog,
4016
+ the flag catalog, the graph's node views, the default labels of the dialogs
4017
+ and of the install prompt), a default (the wizard's label attrs), a value
4018
+ the module returns or assigns (`field_validation_key()` and `kw.warning` for
4019
+ `C_YUI_FORM_I18N_KEYS`, every `primary_label` of `wizard_helpers.js`), a
4020
+ property it writes (the `error` of `form_json_field.js`, the `label` of the
4021
+ route map's own nodes, the `op` of a schema import), or the severities the
4022
+ checks of `schema_validate.js` write (`C_YUI_SCHEMA_EDITOR_I18N_KEYS`).
4023
+ The source is read with the validator's own reader, in the scope each value
4024
+ is written in: a literal, every branch of `?:` / `||` / `??`, a `const` to
4025
+ its value, a `let` into everything it is given. So a key added there through
4026
+ a const or a let is seen, and fails without the list. A value it cannot
4027
+ follow there (a parameter, a call, a `let` that holds one) FAILS the
4028
+ coverage, naming the file and the line, unless the entry names it as data,
4029
+ with why: the form's `$input.yui_json_error` (the json field's own error,
4030
+ `FORM_JSON_FIELD_I18N_KEYS`), the route map's `map.brand.label` (the app's
4031
+ shell declaration, which `validate-locales` reads), the wizard's
4032
+ `opts.confirm_label` / `opts.next_label` (the host's own keys). An entry
4033
+ named as data that the source no longer reads fails too;
4034
+ - **a marker follows it** -- a `const`, or a marked `let` -- and no other list
4035
+ that marker names holds it, so the marker check fails without it;
4036
+ - **the module reads it anyway** (a literal or a `const` it follows): the list
4037
+ entry is redundant, and dropping it loses no key for a consumer.
4038
+
4039
+ The test proves itself: with the `COVERAGE` entries that pin them taken
4040
+ away, it reports each of the keys REV-CLOUD V20 found pinned by nothing.
4041
+
4042
+ A list is also named by a marker or by `COVERAGE`: one that is neither is an
4043
+ ORPHAN, and fails. Two lists have a test of their own instead, and the pin
4044
+ check leaves them to it: `g6_nodes_tree_i18n_keys.test.js` reads every call of
4045
+ the node graph's key-taking helpers from its syntax tree, and
4046
+ `yui_dev_controls.test.js` builds the Developer window and compares every key
4047
+ it asks for or carries with the list.
4048
+
4049
+ **What is NOT pinned.** `t()` is not a key function, and only the positions of
4050
+ `I18N_KEY_FUNCTIONS` are held to the per-call rule. So a key handed to `t()`,
4051
+ `i18n` or a `data-i18n*` value by an UNMARKED `let`, a parameter, a call or a
4052
+ concatenation is invisible to the validator and fails no test -- in the
4053
+ library and in an app alike. So is what is written INTO a `const` table after
4054
+ its declaration (`LIST.push("k")`, `TABLE.k = "k"`): the reader follows a const
4055
+ to its value, and only a marked `let` is followed into what is written into
4056
+ it. It is found only by reading the page in another language. In an app, so is a key it hands through a wrapper of its own, or to a
4057
+ function the table does not name.
4058
+
4059
+ **What fails the build, besides a key no locale defines:**
4060
+
4061
+ - a locale that cannot be parsed;
4062
+ - a locale that cannot be loaded when it LOOKS like one: it holds a
4063
+ `translation` dict, it exports its own name (`es.js` exporting `es`), or it
4064
+ has a default export -- however the `translation` is built;
4065
+ - a locale that looks like one and exports no `{<code>}` with a
4066
+ `translation` dict;
4067
+ - a source file that cannot be parsed;
4068
+ - a published list no consumer can read (above);
4069
+ - a marker that is wrong (above).
4070
+
4071
+ Up to `7.26.27` the locale cases were "skipped" with an OK: one language less
4072
+ validated, and nobody told. A file of the locales directory with a locale's
4073
+ name that does not look like one (`app.js`, a helper that needs a browser to
4074
+ load) is said and skipped, as before. The script runs the same when it is
4075
+ reached through a symlinked package with `--preserve-symlinks-main` (also from
4076
+ `NODE_OPTIONS`): both sides of its "am I the script?" test are resolved.
3632
4077
 
3633
4078
  Up to `7.26.24` each app carried its own copy, and the copies had drifted into
3634
4079
  three families: only one read the codes of the schema editor, only the other
3635
4080
  two the barrel imports, and two read `t('topic') + ':'` as a key built by
3636
- concatenation and skipped it -- so the gobj tree's *"topic:"* went untranslated
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`.
4081
+ concatenation and skipped it -- so the treedb node graph's (`C_G6_NODES_TREE`)
4082
+ *"topic:"* went untranslated in wattyzer and yunovatios. No app keeps a copy
4083
+ now. `test-app`, this library's own demo, has a catalog of another shape (one
4084
+ `src/locales.js`, English as the source: a key IS the English text, Title Case
4085
+ included), so its `test-app/scripts/validate-locales.mjs` loads that catalog
4086
+ itself and takes everything else from the published reader
4087
+ (`collect_used_keys()`, exported with the rest of the reader): it sees the key
4088
+ functions, the consts, the lists and the markers like every app. Its rules: no
4089
+ duplicate key in a bundle, every key the source asks for in `es`, and every key
4090
+ of the library in `en` as well (they are lower-case keys, not English display
4091
+ text).
3641
4092
 
3642
4093
  ### Dates: never hand-roll them again
3643
4094
 
@@ -3694,7 +4145,10 @@ The library asks the APP's i18next for its keys, so a consumer must define them
3694
4145
  `previous period`, …) — copy the block from `test-app/src/locales.js`, which is
3695
4146
  the complete one: it is the only consumer that declares every mode, `rolling`
3696
4147
  included (`last 24h`, `last 7 days`), and a missing key is **invisible** —
3697
- i18next answers it with the key itself.
4148
+ i18next answers it with the key itself. Since `7.26.28` an app's
4149
+ `validate-locales` asks for the whole catalog, from `YUI_TIME_I18N_KEYS` and
4150
+ `C_YUI_PERIOD_I18N_KEYS`: which of it a control offers is the host's data, out
4151
+ of reach of a scan.
3698
4152
  The picker subscribes ITSELF to the shell's `EV_LANGUAGE_CHANGED` (its labels
3699
4153
  are composed at render time), so a host has nothing to forward — a host that
3700
4154
  forwards the event anyway just repaints it twice, harmlessly. All Intl