Regions: RTL kept three rules on the wrong side, and the tab strip never localised

Every vs-live diff so far ran at LTR, so RTL was the next coverage gap —
and it is where layout actually breaks, because a physical `left`
silently survives a direction flip while a logical
`inset-inline-start` does not. Their CSS uses the logical form; ours had
physical `left` in three direction-sensitive places.

The new probe compares each element's offset from the INLINE-START edge
— the left edge in LTR, the right edge in RTL — which is the one number
that goes wrong when a physical property survives. Measured against
their live /ar/regions/cameroon:

- .search-icon sat 566px from the inline-start edge against their
  19.5px: it had stayed on the physical left, which is the inline END in
  Arabic. Replaced with their rule verbatim (blocks/_form.scss:119 —
  inset-inline-start:1em, top:0/bottom:0, margin:auto 0, 1em square),
  which also fixed a second difference, since our fixed 18px square
  could not track the input's type where theirs is 1em. Ours now
  measures their exact 19.5px.
- The country bullet kept left:-16px, so in RTL it detached on the
  wrong side — computed right 256.938px instead of a -16px overhang.
- The chooser's region legend was pinned to the physical left. That one
  is a sanctioned addition rather than a clone, but a legend stuck on
  the physical left in an RTL layout reads as broken whoever designed
  it.

Reading the Arabic screenshot then showed every heading, strapline and
map note in Arabic and RTL while the SELECT VIEW strip still read
"Regions List" — the one piece of chrome that never localised, because
regions-mfe hardcoded its labels and imported no i18n at all. Now
routed through t(), with the legend going through ViewSelect's existing
prop so the shared component and its three consumers stay untouched.

No catalogue keys were added, deliberately. Their common.json has
"Regions" and "View" but none of the composites, so there is nothing to
copy; translating our own additions is not this agent's call; and
check:i18n-parity tracks a per-locale debt ceiling — it/ro/zh sit at
exactly 94 missing because DX-life translates 8 of our 11 locales — so
English-only keys would push them to 97 and fail it. The shim falls
back to the key, so today's rendering is byte-identical. Proven rather
than assumed: a sentinel injected into ar/regions.json rendered on tab
1 at locale=ar while tab 2 stayed English, then was reverted.

Two of my own assertions were wrong first. The probe set
profile-config.locale on every custom element, which on the composite
means three regions-dashboard elements — host and both panes — and
writing a fresh {locale:'ar'} onto a pane that had inherited its config
by provide() clobbered it, leaving every root LTR; I nearly "fixed" RTL,
which works on all three leaves. And checking inset-inline-start in an
LTR context is meaningless, since it merely mirrors left.

Worth flagging: check:i18n-parity's DOXA_REPO defaults to wt-packet,
another worktree, so it must be pointed at this repo to mean anything.

Regression: 22 suites green.
This commit is contained in:
doxa-maps-88655c70 2026-09-24 06:24:45 +00:00
parent 66c4d3163b
commit 2ddb09b42c
10 changed files with 110 additions and 84 deletions

View file

@ -430,7 +430,10 @@ useShadowStyles(
'.country-link{ appearance:none; background:none; border:0; padding:2px 0; margin:0;' +
' text-align:left; cursor:pointer; color:#4e594f; font:inherit; font-size:19.5312px;' +
' font-weight:500; width:100%; position:relative; }' +
'.country-link::before{ content:""; position:absolute; left:-16px; top:0.6em; width:6px; height:6px;' +
// inset-inline-start, NOT left: the bullet hangs off the text's starting edge, which is the
// RIGHT edge in Arabic. With `left:-16px` it stayed on the physical left in RTL — measured
// at locale=ar, computed right was 256.938px instead of the -16px near-edge overhang.
'.country-link::before{ content:""; position:absolute; inset-inline-start:-16px; top:0.6em; width:6px; height:6px;' +
' border-radius:50%; background:#92b195; }' +
'.country-link:hover{ text-decoration:underline; text-decoration-color:#92b195; }' +
'.country-link:focus-visible{ outline:2px solid #92b195; outline-offset:2px; border-radius:4px; }' +

View file

@ -29,7 +29,21 @@ import { buildPaneColumn, PANE_GRID_STYLES } from '@map/layouts/pane-wrapper/pan
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
import ViewSelect from '@map/components/ViewSelect.vue'
import ShareButton from '@map/components/map-controls/ShareButton.vue'
// The SELECT VIEW strip is one of the sanctioned additions, so DX-life has no equivalent
// markup to clone — and until now its labels were hardcoded English, making it the one piece
// of chrome that never localised (visible at locale=ar: the panes and every heading go RTL
// and Arabic while the tabs still read "Regions List").
//
// t() here uses the same literal-English-as-key convention as the rest of the port, and the
// shim FALLS BACK TO THE KEY, so today's rendering is byte-identical. Deliberately NO keys
// were added to the catalogues: (a) translating our own additions is not this agent's call,
// and (b) check:i18n-parity tracks a per-locale debt CEILING — it/ro/zh sit at exactly 94
// missing because DX-life translates 8 of our 11 locales — so English-only keys would push
// them to 97 and fail the gate. The moment someone adds these three strings to all locales,
// the strip localises with no code change here.
import { useRegionsI18n } from './src/useRegionsI18n.js'
const { t } = useRegionsI18n()
const cfg = inject('profileConfig', ref(null))
const uiStore = inject('uiStore', null)
const isDark = computed(() => !!uiStore?.isDark)
@ -49,8 +63,8 @@ const spec = computed(() => ({
// pages were correctly clean — the same cap-4 inheritance gap assetsBase hit.
config: { profile: 'regions-list', instanceId: instanceId.value, pinHint: false },
panes: [
{ id: 'list', label: 'Regions List', at: [0, 0], config: { profile: 'regions-list' } },
{ id: 'dashboard', label: 'Region Dashboards', at: [1, 0], config: { profile: 'region-dashboards' } },
{ id: 'list', label: t('Regions List'), at: [0, 0], config: { profile: 'regions-list' } },
{ id: 'dashboard', label: t('Region Dashboards'), at: [1, 0], config: { profile: 'region-dashboards' } },
],
// The View switcher lives in this element's own header, so the layout's "… below ↓" pill
// would be a second, redundant control (same call upg-100-mfe makes).
@ -176,8 +190,8 @@ useShadowStyles(PANE_GRID_STYLES + '\n' +
<ViewSelect
:views="spec.panes"
:current="view"
legend="Select View"
label="Views"
:legend="t('Select View')"
:label="t('Views')"
@select="go"
/>
<ShareButton :is-dark="isDark" :embed-path="SHARE_EMBED_PATH" />

View file

@ -404,7 +404,10 @@ useShadowStyles(
'.rcm__note{ position:absolute; inset:0; display:flex; align-items:center; justify-content:center;' +
' text-align:center; padding:24px; color:#6d766e; font-size:15.625px; background:#f3f3f1; }' +
// Their key, overlaid bottom-left above the canvas (CountryMap.vue's .country-map__legend).
'.rcm__legend{ position:absolute; left:10px; bottom:28px; z-index:2; display:flex;' +
// Logical, so the chooser legend moves to the reading-start corner in Arabic. This is one
// of the sanctioned additions rather than a clone of theirs, but a legend pinned to the
// physical left in an RTL layout reads as broken regardless of whose design it is.
'.rcm__legend{ position:absolute; inset-inline-start:10px; bottom:28px; z-index:2; display:flex;' +
' flex-direction:column; gap:4px; padding:8px 10px; border-radius:8px;' +
' background:rgba(255,255,255,0.92); font-size:12.5px; font-weight:600; color:#2a332a; }' +
'.rcm__legend span{ display:inline-flex; align-items:center; gap:6px; }' +

View file

@ -276,8 +276,14 @@ useShadowStyles(
' background:var(--dx-white); padding:10px 23.5264px; text-indent:1.5em; font:inherit;' +
' font-size:19.5312px; color:var(--dx-text); }' +
'.search-box input::placeholder{ color:gray; }' +
'.search-icon{ position:absolute; left:16px; top:50%; transform:translateY(-50%); width:18px;' +
' height:18px; fill:none; stroke:var(--dx-muted); stroke-width:2; pointer-events:none; }' +
// THEIRS verbatim, blocks/_form.scss:119-128 — inset-inline-start:1em, top:0/bottom:0 with
// margin:auto 0 for the centring, and 1em square so the icon scales with the input's type.
// Ours had `left:16px` with a translate, which in RTL left the icon on the physical LEFT:
// MEASURED against their live /ar/regions/cameroon, ours sat 566px from the inline-start edge
// against their 19.5px. A physical property survives a direction flip; a logical one does not.
// Their fixed 18px square also could not track the font, where theirs is 1em.
'.search-icon{ position:absolute; inset-inline-start:1em; top:0; bottom:0; margin:auto 0;' +
' width:1em; height:1em; fill:none; stroke:var(--dx-muted); stroke-width:2; pointer-events:none; }' +
// .filters__toggle — centred, chevron rotates when expanded
'.filters__toggle{ background:none; border:0; cursor:pointer; font:inherit; font-size:15.625px;' +
' display:inline-flex; align-items:center; gap:7.5188px; margin-inline:auto; color:var(--dx-text); }' +

File diff suppressed because one or more lines are too long

View file

@ -856,7 +856,7 @@
"bundle": "regions-dashboard",
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"output": "doxa-maps/regions-dashboard/regions-dashboard.js",
"integrity": "sha384-eL6693hPLbKK0icPn0mXmueTCwYZuRdOUuctnLjcX1hb5u5sKdXYKtPnI0vC17MQ",
"integrity": "sha384-oNDJVhNNO4Jit8slUl90yyI2LJB/zSC6Bepz9rg2Qc5wskOyotayodyjGyfDs5We",
"elements": [
"regions-dashboard"
],
@ -876,7 +876,7 @@
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"embed": {
"iframe": "<iframe src=\"./regions-dashboard/region-dashboards/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-eL6693hPLbKK0icPn0mXmueTCwYZuRdOUuctnLjcX1hb5u5sKdXYKtPnI0vC17MQ\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-oNDJVhNNO4Jit8slUl90yyI2LJB/zSC6Bepz9rg2Qc5wskOyotayodyjGyfDs5We\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"region-dashboards\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
}
},
{
@ -890,7 +890,7 @@
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"embed": {
"iframe": "<iframe src=\"./regions-dashboard/regions-list/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-eL6693hPLbKK0icPn0mXmueTCwYZuRdOUuctnLjcX1hb5u5sKdXYKtPnI0vC17MQ\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-oNDJVhNNO4Jit8slUl90yyI2LJB/zSC6Bepz9rg2Qc5wskOyotayodyjGyfDs5We\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-list\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
}
},
{
@ -904,7 +904,7 @@
"scriptUrl": "./regions-dashboard/regions-dashboard.js",
"embed": {
"iframe": "<iframe src=\"./regions-dashboard/regions-mfe/index.html?utm_source=YOUR_SOURCE\" width=\"100%\" height=\"600\" style=\"border:none;border-radius:12px;\" allowfullscreen></iframe>",
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-eL6693hPLbKK0icPn0mXmueTCwYZuRdOUuctnLjcX1hb5u5sKdXYKtPnI0vC17MQ\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
"script": "<script>window.MAP_APP_API_URL=\"https://pray.doxa.life\";</script>\n<script src=\"./regions-dashboard/regions-dashboard.js\" integrity=\"sha384-oNDJVhNNO4Jit8slUl90yyI2LJB/zSC6Bepz9rg2Qc5wskOyotayodyjGyfDs5We\" crossorigin=\"anonymous\"></script>\n<regions-dashboard profile-config='{\"profile\":\"regions-mfe\",\"tk\":\"YOUR_MAPBOX_TOKEN\",\"utmSource\":\"YOUR_SOURCE\",\"assetsBase\":\"YOUR_TREE_URL/assets\"}'></regions-dashboard>"
}
}
]

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
<script src="../regions-dashboard.js" integrity="sha384-eL6693hPLbKK0icPn0mXmueTCwYZuRdOUuctnLjcX1hb5u5sKdXYKtPnI0vC17MQ" crossorigin="anonymous"></script>
<script src="../regions-dashboard.js" integrity="sha384-oNDJVhNNO4Jit8slUl90yyI2LJB/zSC6Bepz9rg2Qc5wskOyotayodyjGyfDs5We" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

File diff suppressed because one or more lines are too long

View file

@ -29,7 +29,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
<script src="../regions-dashboard.js" integrity="sha384-eL6693hPLbKK0icPn0mXmueTCwYZuRdOUuctnLjcX1hb5u5sKdXYKtPnI0vC17MQ" crossorigin="anonymous"></script>
<script src="../regions-dashboard.js" integrity="sha384-oNDJVhNNO4Jit8slUl90yyI2LJB/zSC6Bepz9rg2Qc5wskOyotayodyjGyfDs5We" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order:

View file

@ -40,7 +40,7 @@
<script>window.MAP_APP_API_URL = "https://pray.doxa.life";</script>
<!-- Bundle IIFE — registers the <regions-dashboard> custom element. -->
<script src="../regions-dashboard.js" integrity="sha384-eL6693hPLbKK0icPn0mXmueTCwYZuRdOUuctnLjcX1hb5u5sKdXYKtPnI0vC17MQ" crossorigin="anonymous"></script>
<script src="../regions-dashboard.js" integrity="sha384-oNDJVhNNO4Jit8slUl90yyI2LJB/zSC6Bepz9rg2Qc5wskOyotayodyjGyfDs5We" crossorigin="anonymous"></script>
<script>
// Mapbox token resolution — the A3 ladder, in precedence order: