That header described a bug that had already been fixed

check-embedded-rtl opens with "TWO DEFECTS", in the present tense, describing
library/i18n/index.js reflecting RTL at construction time and writing
document.documentElement.dir unconditionally. Both were fixed. The header was
not.

It cost a real wrong answer today: I read it as a description of current code
and reported a live library defect on that basis, when main already guards the
documentElement write behind window.MAP_OWNS_PAGE === true, and the plugin's own
direction has moved to ProfileLoader, which computes dirAttr from effectiveLocale
and binds it on .profile-loader. effectiveLocale falls back through lang.value,
which is the profile-config channel -- the very path the header says never
arrives.

So the section is now marked as history, dated, and written in the past tense,
with a pointer to the two places to look before believing either defect is live
again. What the check asserts is unchanged and stated plainly at the end: the
plugin's own root carries direction, the partner's page must not, which is why
it reads .profile-loader inside the shadow root and records partnerHtmlDir
separately.

The trap is worth naming in the file rather than only in a commit message,
because the next reader is likely to be someone interpreting a FAILURE of this
gate, which is the worst moment to be handed a stale account of the code. A
gate's header is evidence of what was true when it was written.

Comment-only: 33 insertions, 9 deletions, no executable line touched.
This commit is contained in:
Doxa Maps 2026-09-28 22:32:42 +00:00
parent b7cd91d3e3
commit ee3a121ae9

View file

@ -3,17 +3,41 @@
* check-embedded-rtl — an RTL locale must turn the PLUGIN right-to-left, and must NEVER
* turn the PARTNER'S PAGE right-to-left.
*
* TWO DEFECTS, one block (library/i18n/index.js, createAppI18n):
* 1. TIMING. It reflects RTL at CONSTRUCTION time from detectLocale(), which reads only the URL
* and <html lang>. A locale arriving later through profile-config — the channel a HOST uses —
* never reaches it, so Arabic text renders inside a left-to-right layout. Measured: htmlDir
* null on every host-channel run, including leaves whose chrome was fully Arabic.
* 2. TARGET. It writes document.documentElement.dir. An EMBEDDED widget doing that flips the
* partner's WHOLE PAGE. The existing comment already worries about the host ("a WordPress RTL
* theme owns that decision") but only guards against CLEARING it, not against setting it.
* ⚠ THE TWO DEFECTS BELOW WERE FIXED. This section is HISTORY, kept because it explains what the
* check asserts and why — not a description of the code today. Read it in the past tense; verified
* against main 2026-09-28.
*
* So direction belongs on the plugin's OWN element, driven by the locale ProfileLoader actually
* applies; <html dir> stays only where the plugin owns the page (a standalone leaf).
* WHAT USED TO BE WRONG, both in one block (library/i18n/index.js, createAppI18n):
* 1. TIMING. It reflected RTL at CONSTRUCTION time from detectLocale(), which reads only the URL
* and <html lang>. A locale arriving later through profile-config — the channel a HOST uses —
* never reached it, so Arabic text rendered inside a left-to-right layout. Measured at the time:
* htmlDir null on every host-channel run, including leaves whose chrome was fully Arabic.
* 2. TARGET. It wrote document.documentElement.dir unconditionally. An EMBEDDED widget doing that
* flips the partner's WHOLE PAGE. The comment of the day worried about the host ("a WordPress
* RTL theme owns that decision") but guarded only against CLEARING dir, not against setting it.
*
* HOW THEY WERE FIXED, and where to look before believing either is still live:
* - (2) library/i18n/index.js guards the documentElement write with
* `const ownsPage = window.MAP_OWNS_PAGE === true`, so only a page the plugin OWNS gets <html
* dir>/<html lang>. An embed cannot touch the partner's page. (lang travels with dir there: an
* owned page that flipped to rtl while keeping lang="en" had a screen reader speak Arabic in an
* English voice.)
* - (1) direction on the plugin's own root is ProfileLoader's job, not this block's:
* ProfileLoader.vue computes dirAttr from effectiveLocale and binds :dir="dirAttr" on
* .profile-loader (see its :208 and :285). effectiveLocale is `lang.value || appI18n locale ||
* 'en'`, and lang.value IS the profile-config channel — so a host-supplied locale does flip the
* embedded root.
*
* ★ A STALE HEADER IS A TRAP, WHICH IS WHY THIS ONE IS DATED. Written in the present tense, this
* block read as a live defect long after both were fixed, and cost a reader a wrong bug report:
* the header was cited as current instead of the code being read. A gate's header is evidence of
* what WAS true when it was written. If you are about to act on one, check the source it names.
*
* SO WHAT THIS CHECK ASSERTS, which has not changed: direction belongs on the plugin's OWN element,
* driven by the locale ProfileLoader actually applies; <html dir> stays only where the plugin owns
* the page (a standalone leaf). It reads .profile-loader inside the shadow root for the plugin's
* own direction, and records partnerHtmlDir separately, because for an embed that one must stay
* unset.
*
* ⚠ THE HOST PAGE IS ON A DIFFERENT ORIGIN, deliberately: same-origin would let a bug pass that
* only bites a real partner. The tree is served on one port, the partner page on another.