#!/usr/bin/env node /** * check-embedded-rtl — an RTL locale must turn the PLUGIN right-to-left, and must NEVER * turn the PARTNER'S PAGE right-to-left. * * ⚠ 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. * * 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 . 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 /. 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; 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. * * Usage: node internal/check-embedded-rtl.mjs [baseUrl] */ import { chromium } from './lib/playwright-guarded.mjs' import { createServer } from 'node:http' import { resolve, dirname } from 'node:path' import { fileURLToPath } from 'node:url' import { servesThisTree } from './_serves-this-tree.mjs' import { TREE_NAME } from './_tree-name.mjs' const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') // ⚠ FILTER FLAGS OUT OF THE POSITIONAL. `--emit` was consumed as the baseUrl and the gate // reported "SKIP — nothing served at --emit". Exactly the bug I hit on check-leaf-english-leaks // last week; the same shape, in a different file, three days apart. const BASE = process.argv.slice(2).find((a) => !a.startsWith('--')) || process.env.BASE_URL || 'http://127.0.0.1:4173' const HOST_PORT = Number(process.env.RTL_HOST_PORT || 47393) // ★ --emit WRITES THE PARTNER PAGE TO DISK INSTEAD OF MEASURING IT. // Under the Playwright policy (Driver, 2026-09-25) only rega runs a browser, so this check's // result now has to be produced by someone else's eyes. But the partner page only ever existed // INSIDE this process — two createServer calls on ephemeral ports — so "open a partner page at a // foreign origin" was not a thing anyone could actually do. NEEDS-EYES rows 3, 4 and 6 name a // runnable probe or an exact URL; row 7 (mine) named a construct that lived in my own memory. // --emit closes that: it writes a self-contained page plus a two-line serve recipe, so the item // is reproducible by whoever has the browser. const EMIT = process.argv.includes('--emit') let reachable = false try { const r = await fetch(`${BASE}/${TREE_NAME}/manifest.json`, { signal: AbortSignal.timeout(4000) }); reachable = r.ok } catch { /* precondition */ } if (!reachable) { console.log(`EMBEDDED-RTL: SKIP — nothing served at ${BASE}.`); process.exit(0) } const treeCheck = await servesThisTree(BASE, ROOT) if (!treeCheck.ok) { console.error(`EMBEDDED-RTL: NOT MEASURED — ${treeCheck.why}`); process.exit(2) } // A PARTNER PAGE on a foreign origin: its own is LTR and must stay that way. // ⚠ BOTH CHANNELS. profile-config is how a HOST sets the language; ?lang= on the partner's OWN // url is how a READER does, and detectLocale() reads the URL FIRST. They reach the // document-direction code by different routes, so a check that drives only one proves nothing // about the other — which is why the URL case exists as a row here rather than as a variant of // the config one. The emit writes a fixture per row, so this array IS the needs-eyes inventory. const CASES = [ { id: 'my-upg-100-list', tag: 'my-upg-100-list-map', profile: 'upg-100-mfe' }, { id: 'my-upg-100-list', tag: 'my-upg-100-list-map', profile: 'poster-wall' }, { id: 'my-upg-100-list', tag: 'my-upg-100-list-map', profile: 'poster-wall', urlLang: 'ar' }, ] const TREE_PORT = Number(process.env.RTL_TREE_PORT || 47395) const TREE = `http://127.0.0.1:${TREE_PORT}` // ⚠ THE URL CASE MUST NOT ALSO SET locale IN profile-config. The two channels reach the // document-dir code by different routes and the whole point of the ?lang= case is to exercise the // URL one alone. Emitting all three files byte-identical (measured: 1114 bytes each) would hand a // reader a page that cannot distinguish them — and would let the config channel supply the locale // while the reader believed the URL had. const hostHtml = (c) => ` partner

Partner page