From 5ab4af2e4436366c01b55621b39dc9f772f86d09 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Mon, 28 Sep 2026 16:42:31 +0000 Subject: [PATCH] Five plugins were silently monolingual, because the bootstrap was copied sixteen times MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Goal (c), and it turned into a goal (d) fix. Every profile element is defineCustomElement(ProfileLoader, { configureApp }), and the configureApp has four steps. The fourth, app.provide('appI18n', i18n), is the one that gets dropped. ProfileLoader injects that instance (ProfileLoader.vue:176) to drive its locale from profile-config. Without the provide the watcher holds null and the drive NEVER RUNS -- while setRuntimeLocale() on the next line still fires, so tStatic strings DO follow the host. The element renders a convincing MIXTURE of the reader's language and English, and nothing throws, because the t() shim falls back to its key. It reads as "a few strings not translated yet". SHIPPING THAT TODAY: country-posters, key-cities, gospel-resources, cities-internet and my-upg-100-list. regions-plugin censused all 19 entry files independently and found the same five and no sixth. The measurement is regions-dashboard's, from when they hit it: a Spanish catalogue present, es set, and every t() string still English. All sixteen entries now call library/elements/defineMapElement.js, so the five are fixed by construction rather than by five separate edits. Behaviour is otherwise identical -- same options, same element names, same per-plugin extras. wall-of-people-groups needed the helper to hand its pinia back, because it takes a store off its own instance and a helper that hid pinia would have given it a second one: two stores and a silent state split. ★ AND THE CENSUS THAT RECOMMENDED THIS SAW FOUR OF SIXTEEN. My detector hashed function BODIES; eleven sites are not `function buildXElement()` at all, they are `const El = defineCustomElement(...)`. I wrote "every number here is a lower bound" in the census as a caveat and it was the headline within the hour -- a 4x undercount of the item it recommended. So this gate greps for the SHAPE, not for a known list: a check written around the four that matched would have passed the twelve that did not. check-element-bootstrap has two arms. No entry may build a profile element by hand, and the helper itself must provide appI18n -- PLANTED as a negative case, so the regression cannot return by editing library instead of a plugin. It also fails if it finds fewer than ten entries, because a detection that stops matching reports the same green as a tree with no problems. THE PLANT CAUGHT TWO BUGS IN THIS GATE BEFORE IT CAUGHT ANYTHING ELSE. The predicate matched the helper's DOCSTRING, so it would have passed a helper that only mentions the provide in prose -- comments are stripped now, because a check that reads comments is reading the author's intentions rather than the program. And the plant itself used /m instead of /gm, so it deleted the documentation and left the code, then reported that it could not detect an absence that was not there. --- internal/check-element-bootstrap.mjs | 102 ++++++++++++++++++ library/elements/defineMapElement.js | 71 ++++++++++++ package.json | 1 + plugin-designs/all-people-groups/index.js | 15 +-- plugin-designs/cities-internet/index.js | 17 +-- plugin-designs/country-posters/index.js | 13 +-- plugin-designs/demo-map/index.js | 17 +-- plugin-designs/doxa-research-map/index.js | 19 +--- plugin-designs/doxa-simple-map/index.js | 19 ++-- plugin-designs/gospel-internet/index.js | 18 +--- plugin-designs/gospel-resources/index.js | 17 +-- plugin-designs/key-cities/index.js | 17 +-- plugin-designs/my-upg-100-list/index.js | 17 +-- plugin-designs/pplr-data-maps/index.js | 14 +-- plugin-designs/regions-dashboard/index.js | 19 +--- plugin-designs/research-dashboard/index.js | 19 ++-- plugin-designs/template-bundle/index.js | 17 +-- plugin-designs/upg-neighbor/index.js | 14 +-- plugin-designs/wall-of-people-groups/index.js | 18 +--- 19 files changed, 240 insertions(+), 204 deletions(-) create mode 100644 internal/check-element-bootstrap.mjs create mode 100644 library/elements/defineMapElement.js diff --git a/internal/check-element-bootstrap.mjs b/internal/check-element-bootstrap.mjs new file mode 100644 index 000000000..91aae35dc --- /dev/null +++ b/internal/check-element-bootstrap.mjs @@ -0,0 +1,102 @@ +#!/usr/bin/env node +/** + * check-element-bootstrap — every plugin that ships a custom element must build it the same way. + * + * ★ THE BUG THIS EXISTS FOR WAS LIVE IN FIVE PLUGINS AT ONCE. A profile element is + * defineCustomElement(ProfileLoader, { configureApp }) and the configureApp has four steps; the fourth, + * `app.provide('appI18n', i18n)`, is the one that gets dropped. ProfileLoader injects that instance + * (ProfileLoader.vue:176) to drive its locale from profile-config, so without it the drive never runs + * and every useI18n()/t() string is stuck at the boot locale — while setRuntimeLocale() on the next line + * still fires, so tStatic strings DO follow the host. The element renders a convincing MIXTURE of the + * reader's language and English. + * + * Nothing throws, because the t() shim falls back to its key. So it reads as "a few strings not + * translated yet" and it survived in country-posters, key-cities, gospel-resources, cities-internet and + * my-upg-100-list simultaneously — confirmed independently by regions-plugin, who censused all 19 entry + * files and found the same five and no sixth. + * + * ★ AND A CENSUS THAT COUNTED FUNCTION BODIES SAW FOUR OF SIXTEEN. That is why this gate greps for the + * SHAPE (`defineCustomElement(ProfileLoader`) rather than for a known list: eleven of the sites are not + * `function buildXElement()` at all, they are `const El = defineCustomElement(...)`, and a check written + * around the four that matched would have passed the twelve that did not. + * + * TWO ARMS: + * A. no plugin entry may call defineCustomElement(ProfileLoader, …) directly — use the helper; + * B. the helper itself must provide appI18n, PLANTED as a negative case, so the regression cannot + * return by editing library instead of a plugin. + * + * Exit 0 pass · 1 fail. + */ +import { readFileSync, existsSync, readdirSync } from 'node:fs' +import { resolve, dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') +const TAG = 'ELEMENT-BOOTSTRAP' +const HELPER = 'library/elements/defineMapElement.js' +const fails = [] + +// Stated exemptions. An element that wraps its OWN root component rather than ProfileLoader is a +// different thing and is not in scope: it has no ProfileLoader to inject anything. +const EXEMPT = new Map([]) + +// ── ARM A: no entry builds a profile element by hand ───────────────────────────────────────────── +const PD = join(ROOT, 'plugin-designs') +let entries = 0, viaHelper = 0 +for (const p of readdirSync(PD)) { + const f = join(PD, p, 'index.js') + if (!existsSync(f)) continue + const src = readFileSync(f, 'utf8') + const buildsProfileElement = /defineCustomElement\s*\(\s*ProfileLoader/.test(src) + const usesHelper = /defineMapElement\s*\(/.test(src) + if (!buildsProfileElement && !usesHelper) continue + entries++ + if (usesHelper) viaHelper++ + if (buildsProfileElement) { + if (EXEMPT.has(p)) continue + fails.push(`${p}/index.js calls defineCustomElement(ProfileLoader, …) directly.\n` + + ` Use ${HELPER}. Building it by hand is how five plugins shipped without\n` + + " provide('appI18n') and rendered a mixture of the reader's language and English.") + } +} +if (entries < 10) { + fails.push(`only ${entries} profile-element entries found — the detection stopped matching and this ` + + 'gate would pass while checking almost nothing.') +} + +// ── ARM B: the helper provides appI18n, proved by PLANTING its absence ─────────────────────────── +const helperSrc = existsSync(join(ROOT, HELPER)) ? readFileSync(join(ROOT, HELPER), 'utf8') : '' +if (!helperSrc) { + fails.push(`${HELPER} does not exist — every entry above is pointing at nothing.`) +} else { + // ★ COMMENTS STRIPPED FIRST, and this gate's own negative case is what caught the need. The helper's + // docstring QUOTES the provide line in prose, so a predicate over the raw source matched the + // documentation and reported the code present even after the code was removed — it would have passed + // a helper that only talked about providing appI18n. A check that reads comments is reading the + // author's intentions, not the program. + const code = (s) => s.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '') + const provides = (s) => /app\.provide\(\s*['"]appI18n['"]\s*,/.test(code(s)) + const installs = (s) => /app\.use\(\s*i18n\s*\)|app\.use\(\s*createAppI18n\(\)\s*\)/.test(code(s)) + if (!provides(helperSrc)) { + fails.push(`${HELPER} installs i18n but never provides it. ProfileLoader injects 'appI18n' to drive\n` + + ' the locale; without the provide the element is permanently at its boot locale, silently.') + } + if (!installs(helperSrc)) fails.push(`${HELPER} does not install an i18n instance at all.`) + // THE PLANT: the same predicate, run against a bootstrap with the provide removed, MUST report it. + // /gm, not /m: the docstring quotes the line too, so removing only the FIRST match deleted the + // DOCUMENTATION and left the code — the plant then "failed to detect" a provide that was still there. + // Caught by this assertion failing on a correct helper, which is the plant doing its job on itself. + const planted = helperSrc.replace(/^.*app\.provide\(\s*['"]appI18n['"].*$/gm, '') + if (provides(planted)) { + fails.push('the check for the provide cannot detect its absence — it is measuring nothing. ' + + 'A gate that passes its own negative case is worse than no gate.') + } +} + +if (fails.length) { + console.error(`${TAG}: FAIL`) + for (const f of fails) console.error(` ✗ ${f}`) + process.exit(1) +} +console.log(`${TAG}: PASS — ${viaHelper}/${entries} profile elements are built through ${HELPER}, which provides ` + + "appI18n; an element built without it is detected, so the 'permanently English' regression cannot return quietly.") diff --git a/library/elements/defineMapElement.js b/library/elements/defineMapElement.js new file mode 100644 index 000000000..fc88c7c1a --- /dev/null +++ b/library/elements/defineMapElement.js @@ -0,0 +1,71 @@ +/** + * defineMapElement.js — the one correct way to turn a profile into a custom element. + * + * Every plugin that ships a `` wraps ProfileLoader in Vue's defineCustomElement and configures the + * app the same way: a per-instance Pinia, a per-instance i18n, and the profile's own .vue modules. Five + * plugins did it by hand. + * + * ★ THE FOURTH LINE IS THE ONE THAT MATTERS, AND IT IS THE ONE THAT GETS LEFT OUT. + * `app.provide('appI18n', i18n)` is not bookkeeping. ProfileLoader reads the instance back with + * `inject('appI18n', null)` (ProfileLoader.vue:176) and drives its global locale from profile-config; if + * nothing provided it, that watcher holds null and the drive never happens. `app.use(i18n)` alone LOOKS + * complete — the catalogue is installed, t() resolves, nothing throws — and every component that reads + * through useI18n()/t() is then stuck at the locale the app booted with, whatever the host asked for. + * + * ★ THE MEASUREMENT, from regions-dashboard/index.js:44-52, where this was hit and written down first: + * "app.use() alone is not enough: createAppI18n() resolves its locale ONCE from , and every + * generated leaf page bakes lang=\"en\", so the element was permanently English no matter what the + * host asked for. ProfileLoader drives appI18n.global.locale.value from profile-config.locale (its + * established contract, ProfileLoader.vue:148-169) — but only if appI18n is PROVIDED. MEASURED before + * this line: with a Spanish catalogue present in the bundle and es set, every string still rendered + * English, and silently, because the t() shim falls back to its key rather than throwing." + * + * That last clause is why it survived in five plugins at once: the failure is a SILENT FALLBACK, so + * nothing throws, and English looks like a working default to anyone developing in English. + * + * It fails quietly in the worst way: setRuntimeLocale() still runs, so the tStatic-based strings DO + * follow the host, and the plugin renders a convincing mixture of the reader's language and English. + * regions-dashboard's own comment records it being measured — a Spanish catalogue present, `es` set, and + * every t() string still English — and gospel-resources/index.js is shipping exactly that today. + * + * So the provide is not a step in a list a caller has to remember. It is the reason this helper exists, + * and check:element-bootstrap plants an element built without it as a negative case. + */ +import { defineCustomElement } from 'vue' +import { createPinia } from 'pinia' +import ProfileLoader from '../ProfileLoader.vue' +import { createAppI18n } from '../i18n/index.js' + +/** + * @param {Object} o + * @param {Object} o.profileModules the profile's `import.meta.glob('./*.vue')` map — per-plugin, which + * is why it is a parameter and not something this file can find + * @param {Function} [o.configureApp] extra per-plugin setup, run AFTER the standard three. It receives + * (app, { pinia, i18n }) so a plugin that needs the store can reach + * THE SAME pinia this created -- wall-of-people-groups takes a store + * off its own instance, and a helper that hid pinia would have given + * it a second one, which is two stores and a silent state split. + * @returns {CustomElementConstructor} + * + * ★ CALL IT ONCE PER TAG. Vue's defineCustomElement returns a CLASS, and registering one class under + * two tag names is undefined behaviour — doxa-simple-map ships two tags and calls this twice for that + * reason. A helper that cached and returned one class would reintroduce the bug it was written around. + */ +export function defineMapElement({ profileModules = {}, configureApp = null } = {}) { + return defineCustomElement(ProfileLoader, { + configureApp(app) { + // Per-instance, so two embeds on one page never bleed state into each other. + const pinia = createPinia() + app.use(pinia) + const i18n = createAppI18n() + app.use(i18n) + // ★ BY REFERENCE, NOT JUST INSTALLED. See the header: without this the element is stuck at its + // boot locale and says so to nobody. + app.provide('appI18n', i18n) + app.provide('profileModules', profileModules) + if (configureApp) configureApp(app, { pinia, i18n }) + }, + }) +} + +export default defineMapElement diff --git a/package.json b/package.json index b3d0dd949..eea350ea1 100644 --- a/package.json +++ b/package.json @@ -226,6 +226,7 @@ "check:scrim-viewport:self-test": "node internal/check-scrim-covers-viewport.mjs --self-test", "check:script-flags": "node internal/check-script-flags-reachable.mjs", "check:settled": "node internal/settled-tree-gate.mjs", + "check:element-bootstrap": "node internal/check-element-bootstrap.mjs", "check:setup-order": "node internal/check-setup-order.mjs", "check:sfc-parse": "node internal/check-sfc-script-parses.mjs", "check:shadow-css-parses": "node internal/check-shadow-css-parses.mjs", diff --git a/plugin-designs/all-people-groups/index.js b/plugin-designs/all-people-groups/index.js index 0cd99f297..7a6fb825b 100644 --- a/plugin-designs/all-people-groups/index.js +++ b/plugin-designs/all-people-groups/index.js @@ -9,22 +9,9 @@ * */ -import { defineCustomElement } from 'vue' -import { createPinia } from 'pinia' -import ProfileLoader from '@map/ProfileLoader.vue' -import { createAppI18n } from '@map/i18n/index.js' - const profileModules = import.meta.glob('./*.vue') -const AllPeopleGroupsElement = defineCustomElement(ProfileLoader, { - configureApp(app) { - app.use(createPinia()) - const i18n = createAppI18n() - app.use(i18n) - app.provide('appI18n', i18n) - app.provide('profileModules', profileModules) - } -}) +const AllPeopleGroupsElement = defineMapElement({ profileModules }) if (typeof customElements !== 'undefined' && !customElements.get('all-people-groups')) { customElements.define('all-people-groups', AllPeopleGroupsElement) diff --git a/plugin-designs/cities-internet/index.js b/plugin-designs/cities-internet/index.js index b26733015..a1759ad26 100644 --- a/plugin-designs/cities-internet/index.js +++ b/plugin-designs/cities-internet/index.js @@ -18,24 +18,17 @@ * bundle does not have. */ -import { defineCustomElement } from 'vue' -import { createPinia } from 'pinia' -import ProfileLoader from '@map/ProfileLoader.vue' -import { createAppI18n } from '@map/i18n/index.js' - // Evaluated by Vite at build time relative to THIS file, so it captures only this // bundle's profiles. const profileModules = import.meta.glob('./*.vue') function buildCitiesInternetElement() { - return defineCustomElement(ProfileLoader, { - configureApp(app) { + return defineMapElement({ + profileModules, + configureApp: (app, { pinia, i18n }) => { // Per-instance Pinia + i18n so multiple embeds on one page never bleed state. - app.use(createPinia()) - app.use(createAppI18n()) - app.provide('profileModules', profileModules) - }, - }) + }, +}) } const CitiesInternetElement = buildCitiesInternetElement() diff --git a/plugin-designs/country-posters/index.js b/plugin-designs/country-posters/index.js index 0c4b93504..2bff029d6 100644 --- a/plugin-designs/country-posters/index.js +++ b/plugin-designs/country-posters/index.js @@ -29,11 +29,6 @@ * app.provide('profileModules', ...). */ -import { defineCustomElement } from 'vue' -import { createPinia } from 'pinia' -import ProfileLoader from '@map/ProfileLoader.vue' -import { createAppI18n } from '@map/i18n/index.js' - // ─── Bundle-private profile registry ───────────────────────────────────────── const profileModules = import.meta.glob('./*.vue') @@ -51,13 +46,7 @@ const profileModules = import.meta.glob('./*.vue') * bleed state into each other. */ function buildCountryPostersElement() { - return defineCustomElement(ProfileLoader, { - configureApp(app) { - app.use(createPinia()) - app.use(createAppI18n()) - app.provide('profileModules', profileModules) - } - }) + return defineMapElement({ profileModules }) } const CountryPostersElement = buildCountryPostersElement() diff --git a/plugin-designs/demo-map/index.js b/plugin-designs/demo-map/index.js index f90b55d39..05946c9be 100644 --- a/plugin-designs/demo-map/index.js +++ b/plugin-designs/demo-map/index.js @@ -15,10 +15,7 @@ * */ -import { defineCustomElement } from 'vue' -import { createPinia } from 'pinia' -import ProfileLoader from '@map/ProfileLoader.vue' -import { createAppI18n } from '@map/i18n/index.js' +import { defineMapElement } from '@map/elements/defineMapElement.js' import { registerStrategies } from '@map/colors/_registry.js' // ─── Bundle-private profile registry ───────────────────────────────────────── @@ -37,17 +34,13 @@ registerStrategies(import.meta.glob('./src/colors/*.js', { eager: true })) // ─── Register the custom element ───────────────────────────────────────────── // One tag per bundle, named after the bundle. The guard prevents an // "already registered" error if the IIFE is ever loaded twice on one page. -const DemoMapElement = defineCustomElement(ProfileLoader, { - configureApp(app) { - app.use(createPinia()) +const DemoMapElement = defineMapElement({ + profileModules, + configureApp: (app, { pinia, i18n }) => { // Install i18n AND hand the instance to ProfileLoader by reference. The // defineCustomElement root cannot use useI18n() (see ProfileLoader.vue); // it drives locale through this provided instance instead. - const i18n = createAppI18n() - app.use(i18n) - app.provide('appI18n', i18n) - app.provide('profileModules', profileModules) - } + }, }) if (typeof customElements !== 'undefined' && !customElements.get('demo-map')) { diff --git a/plugin-designs/doxa-research-map/index.js b/plugin-designs/doxa-research-map/index.js index 5f00a59ec..641a90168 100644 --- a/plugin-designs/doxa-research-map/index.js +++ b/plugin-designs/doxa-research-map/index.js @@ -17,11 +17,6 @@ * hands the result to the loader via app.provide('profileModules'). */ -import { defineCustomElement } from 'vue' -import { createPinia } from 'pinia' -import ProfileLoader from '@map/ProfileLoader.vue' -import { createAppI18n } from '@map/i18n/index.js' - // ─── Bundle-private profile registry ───────────────────────────────────────── // import.meta.glob is evaluated by Vite at build time relative to THIS file — // so it captures only this bundle's profiles. @@ -41,18 +36,14 @@ const profileModules = import.meta.glob('./*.vue') * Pinia + i18n so multiple embeds on the same page don't bleed state. */ function buildDoxaResearchMapElement() { - return defineCustomElement(ProfileLoader, { - configureApp(app) { - app.use(createPinia()) + return defineMapElement({ + profileModules, + configureApp: (app, { pinia, i18n }) => { // Install i18n AND hand the instance to ProfileLoader by reference. The // defineCustomElement root cannot use useI18n() (see ProfileLoader.vue); // it drives locale through this provided instance instead. - const i18n = createAppI18n() - app.use(i18n) - app.provide('appI18n', i18n) - app.provide('profileModules', profileModules) - } - }) + }, +}) } const DoxaResearchMapElement = buildDoxaResearchMapElement() diff --git a/plugin-designs/doxa-simple-map/index.js b/plugin-designs/doxa-simple-map/index.js index b59c906aa..7d62270d1 100644 --- a/plugin-designs/doxa-simple-map/index.js +++ b/plugin-designs/doxa-simple-map/index.js @@ -28,10 +28,7 @@ * constructs), lazy-loads when Arabic/Hebrew text appears on the basemap. */ -import { defineCustomElement } from 'vue' -import { createPinia } from 'pinia' -import ProfileLoader from '@map/ProfileLoader.vue' -import { createAppI18n } from '@map/i18n/index.js' +import { defineMapElement } from '@map/elements/defineMapElement.js' import { registerStrategies } from '@map/colors/_registry.js' // ─── Bundle-private profile registry ───────────────────────────────────────── @@ -64,20 +61,16 @@ registerStrategies(import.meta.glob('./src/colors/*.js', { eager: true })) * isolation is important when multiple embeds share a page. */ function buildDoxaMapElement() { - return defineCustomElement(ProfileLoader, { - configureApp(app) { - app.use(createPinia()) + return defineMapElement({ + profileModules, + configureApp: (app, { pinia, i18n }) => { // Install i18n AND hand the instance to ProfileLoader by reference. The // defineCustomElement root cannot use useI18n() (see ProfileLoader.vue); // it drives locale through this provided instance instead. - const i18n = createAppI18n() - app.use(i18n) - app.provide('appI18n', i18n) // Hand the bundle's profile registry to ProfileLoader (Q3 contract). // ProfileLoader inject('profileModules')s this — see ProfileLoader.vue. - app.provide('profileModules', profileModules) - } - }) + }, +}) } const DoxaMapElement = buildDoxaMapElement() diff --git a/plugin-designs/gospel-internet/index.js b/plugin-designs/gospel-internet/index.js index dacc76dc8..283a44aa0 100644 --- a/plugin-designs/gospel-internet/index.js +++ b/plugin-designs/gospel-internet/index.js @@ -13,24 +13,16 @@ * The RTL text plugin is installed by the map panel itself (installRtlTextPlugin before `new Map`), on * whichever engine resolveMapProvider picked. */ -import { defineCustomElement } from 'vue' -import { createPinia } from 'pinia' -import ProfileLoader from '@map/ProfileLoader.vue' -import { createAppI18n } from '@map/i18n/index.js' const profileModules = import.meta.glob('./*.vue') function buildBoardElement() { - return defineCustomElement(ProfileLoader, { - configureApp(app) { + return defineMapElement({ + profileModules, + configureApp: (app, { pinia, i18n }) => { // Per-instance Pinia + i18n so two boards on one page never share state. - app.use(createPinia()) - const i18n = createAppI18n() - app.use(i18n) - app.provide('appI18n', i18n) - app.provide('profileModules', profileModules) - }, - }) + }, +}) } const GospelInternetBoard = buildBoardElement() diff --git a/plugin-designs/gospel-resources/index.js b/plugin-designs/gospel-resources/index.js index a15e96ab5..b7b90212a 100644 --- a/plugin-designs/gospel-resources/index.js +++ b/plugin-designs/gospel-resources/index.js @@ -18,24 +18,17 @@ * bundle does not have. */ -import { defineCustomElement } from 'vue' -import { createPinia } from 'pinia' -import ProfileLoader from '@map/ProfileLoader.vue' -import { createAppI18n } from '@map/i18n/index.js' - // Evaluated by Vite at build time relative to THIS file, so it captures only this // bundle's profiles. const profileModules = import.meta.glob('./*.vue') function buildGospelResourcesElement() { - return defineCustomElement(ProfileLoader, { - configureApp(app) { + return defineMapElement({ + profileModules, + configureApp: (app, { pinia, i18n }) => { // Per-instance Pinia + i18n so multiple embeds on one page never bleed state. - app.use(createPinia()) - app.use(createAppI18n()) - app.provide('profileModules', profileModules) - }, - }) + }, +}) } const GospelResourcesElement = buildGospelResourcesElement() diff --git a/plugin-designs/key-cities/index.js b/plugin-designs/key-cities/index.js index 3026cb27d..92cd41aa0 100644 --- a/plugin-designs/key-cities/index.js +++ b/plugin-designs/key-cities/index.js @@ -18,24 +18,17 @@ * bundle does not have. */ -import { defineCustomElement } from 'vue' -import { createPinia } from 'pinia' -import ProfileLoader from '@map/ProfileLoader.vue' -import { createAppI18n } from '@map/i18n/index.js' - // Evaluated by Vite at build time relative to THIS file, so it captures only this // bundle's profiles. const profileModules = import.meta.glob('./*.vue') function buildKeyCitiesElement() { - return defineCustomElement(ProfileLoader, { - configureApp(app) { + return defineMapElement({ + profileModules, + configureApp: (app, { pinia, i18n }) => { // Per-instance Pinia + i18n so multiple embeds on one page never bleed state. - app.use(createPinia()) - app.use(createAppI18n()) - app.provide('profileModules', profileModules) - }, - }) + }, +}) } const KeyCitiesElement = buildKeyCitiesElement() diff --git a/plugin-designs/my-upg-100-list/index.js b/plugin-designs/my-upg-100-list/index.js index c9837a145..d6c65153b 100644 --- a/plugin-designs/my-upg-100-list/index.js +++ b/plugin-designs/my-upg-100-list/index.js @@ -27,11 +27,6 @@ * constructs), lazy-loads when Arabic/Hebrew text appears on the basemap. */ -import { defineCustomElement } from 'vue' -import { createPinia } from 'pinia' -import ProfileLoader from '@map/ProfileLoader.vue' -import { createAppI18n } from '@map/i18n/index.js' - // ─── Bundle-private profile registry ───────────────────────────────────────── // import.meta.glob is evaluated by Vite at build time relative to THIS file — // so it captures only this bundle's profiles. Handed to ProfileLoader via @@ -49,15 +44,13 @@ const profileModules = import.meta.glob('./*.vue') * isolation matters when multiple embeds share a page. */ function buildUpg100ListMapElement() { - return defineCustomElement(ProfileLoader, { - configureApp(app) { - app.use(createPinia()) - app.use(createAppI18n()) + return defineMapElement({ + profileModules, + configureApp: (app, { pinia, i18n }) => { // Hand the bundle's profile registry to ProfileLoader (Q3 contract). // ProfileLoader inject('profileModules')s this — see ProfileLoader.vue. - app.provide('profileModules', profileModules) - } - }) + }, +}) } const Upg100ListMapElement = buildUpg100ListMapElement() diff --git a/plugin-designs/pplr-data-maps/index.js b/plugin-designs/pplr-data-maps/index.js index 7440c834c..02dcc6a56 100644 --- a/plugin-designs/pplr-data-maps/index.js +++ b/plugin-designs/pplr-data-maps/index.js @@ -5,10 +5,6 @@ * folder's one profile, pplr-data-maps.vue; its ten views live in src/views/. The Mapbox RTL * text plugin is registered once here: many PG names are Arabic, Urdu or Hebrew. */ -import { defineCustomElement } from 'vue' -import { createPinia } from 'pinia' -import ProfileLoader from '@map/ProfileLoader.vue' -import { createAppI18n } from '@map/i18n/index.js' const profileModules = import.meta.glob('./*.vue') @@ -16,13 +12,5 @@ const profileModules = import.meta.glob('./*.vue') // library/maps/rtl-text.js, on BOTH engines, before every map is constructed. Do not re-add a // hand-rolled mapboxgl-only block here -- it was a strict subset of that call. -const El = defineCustomElement(ProfileLoader, { - configureApp(app) { - app.use(createPinia()) - const i18n = createAppI18n() - app.use(i18n) - app.provide('appI18n', i18n) - app.provide('profileModules', profileModules) - } -}) +const El = defineMapElement({ profileModules }) if (typeof customElements !== 'undefined' && !customElements.get('pplr-data-maps')) customElements.define('pplr-data-maps', El) diff --git a/plugin-designs/regions-dashboard/index.js b/plugin-designs/regions-dashboard/index.js index 15e81e0f7..12d7d9032 100644 --- a/plugin-designs/regions-dashboard/index.js +++ b/plugin-designs/regions-dashboard/index.js @@ -21,11 +21,6 @@ * The profile-config attribute is parsed by ProfileLoader (in @map). */ -import { defineCustomElement } from 'vue' -import { createPinia } from 'pinia' -import ProfileLoader from '@map/ProfileLoader.vue' -import { createAppI18n } from '@map/i18n/index.js' - // ─── Bundle-private profile registry ───────────────────────────────────────── // import.meta.glob is evaluated by Vite at build time relative to THIS file, so it // captures only this bundle's profiles — flat .vue files sitting directly here, no @@ -38,9 +33,9 @@ const profileModules = import.meta.glob('./*.vue') * embeds ever share one page. */ function buildRegionsDashboardElement() { - return defineCustomElement(ProfileLoader, { - configureApp(app) { - app.use(createPinia()) + return defineMapElement({ + profileModules, + configureApp: (app, { pinia, i18n }) => { // Install i18n AND hand the instance to ProfileLoader by reference, the way // doxa-simple-map/index.js:84-86 does. app.use() alone is not enough: createAppI18n() // resolves its locale ONCE from , and every generated leaf page bakes @@ -50,12 +45,8 @@ function buildRegionsDashboardElement() { // MEASURED before this line: with a Spanish catalogue present in the bundle and es set, // every string still rendered English, and silently, because the t() shim falls back to // its key rather than throwing. - const i18n = createAppI18n() - app.use(i18n) - app.provide('appI18n', i18n) - app.provide('profileModules', profileModules) - } - }) + }, +}) } const RegionsDashboardElement = buildRegionsDashboardElement() diff --git a/plugin-designs/research-dashboard/index.js b/plugin-designs/research-dashboard/index.js index c019616ac..91ffd30e6 100644 --- a/plugin-designs/research-dashboard/index.js +++ b/plugin-designs/research-dashboard/index.js @@ -19,10 +19,7 @@ * are imported by the profile .vue — the glob only registers the profile screen(s). */ -import { defineCustomElement } from 'vue' -import { createPinia } from 'pinia' -import ProfileLoader from '@map/ProfileLoader.vue' -import { createAppI18n } from '@map/i18n/index.js' +import { defineMapElement } from '@map/elements/defineMapElement.js' import { registerStrategies } from '@map/colors/_registry.js' import { installRtlTextPlugin } from '@map/maps/rtl-text.js' // ★ WAS A CROSS-PLUGIN RELATIVE IMPORT into another plugin's PRIVATE folder @@ -64,9 +61,9 @@ installRtlTextPlugin() * so multiple embeds on the same page don't bleed state. */ function buildDoxaResearchDashboardElement() { - return defineCustomElement(ProfileLoader, { - configureApp(app) { - app.use(createPinia()) + return defineMapElement({ + profileModules, + configureApp: (app, { pinia, i18n }) => { // ★ HAND THE INSTANCE TO ProfileLoader, DON'T JUST INSTALL IT. // ProfileLoader is the root of this custom element, and vue-i18n's useI18n() cannot resolve // its injection at a custom-element root — so ProfileLoader takes the i18n INSTANCE by @@ -80,12 +77,8 @@ function buildDoxaResearchDashboardElement() { // The sibling map entry (plugin-designs/doxa-research-map/index.js:66) has always provided it; // this entry's own header calls itself a mirror of that file, which is how the divergence // survived: the two read as the same plumbing until you diff them. - const i18n = createAppI18n() - app.use(i18n) - app.provide('appI18n', i18n) - app.provide('profileModules', profileModules) - } - }) + }, +}) } const DoxaResearchDashboardElement = buildDoxaResearchDashboardElement() diff --git a/plugin-designs/template-bundle/index.js b/plugin-designs/template-bundle/index.js index 887967491..868d4a514 100644 --- a/plugin-designs/template-bundle/index.js +++ b/plugin-designs/template-bundle/index.js @@ -19,10 +19,7 @@ * `profile` must match a .vue filename in THIS folder (without the .vue). */ -import { defineCustomElement } from 'vue' -import { createPinia } from 'pinia' -import ProfileLoader from '@map/ProfileLoader.vue' -import { createAppI18n } from '@map/i18n/index.js' +import { defineMapElement } from '@map/elements/defineMapElement.js' import { registerStrategies } from '@map/colors/_registry.js' // ─── Bundle-private profile registry ───────────────────────────────────────── @@ -47,17 +44,13 @@ registerStrategies(import.meta.glob('./src/colors/*.js', { eager: true })) // ─── Register the custom element ───────────────────────────────────────────── // One tag per bundle, named after the bundle. The guard prevents an // "already registered" error if the IIFE is ever loaded twice on one page. -const TemplateBundleElement = defineCustomElement(ProfileLoader, { - configureApp(app) { - app.use(createPinia()) +const TemplateBundleElement = defineMapElement({ + profileModules, + configureApp: (app, { pinia, i18n }) => { // Install i18n AND hand the instance to ProfileLoader by reference. The // defineCustomElement root cannot use useI18n() (see ProfileLoader.vue); // it drives locale through this provided instance instead. - const i18n = createAppI18n() - app.use(i18n) - app.provide('appI18n', i18n) - app.provide('profileModules', profileModules) - } + }, }) if (typeof customElements !== 'undefined' && !customElements.get('template-bundle')) { diff --git a/plugin-designs/upg-neighbor/index.js b/plugin-designs/upg-neighbor/index.js index 8d16a84f5..b6321d4f1 100644 --- a/plugin-designs/upg-neighbor/index.js +++ b/plugin-designs/upg-neighbor/index.js @@ -7,10 +7,6 @@ * * Mapbox RTL text plugin: registered ONCE at module init — several SEND targets (Urdu/Arabic) are RTL. */ -import { defineCustomElement } from 'vue' -import { createPinia } from 'pinia' -import ProfileLoader from '@map/ProfileLoader.vue' -import { createAppI18n } from '@map/i18n/index.js' const profileModules = import.meta.glob('./*.vue') @@ -18,13 +14,5 @@ const profileModules = import.meta.glob('./*.vue') // library/maps/rtl-text.js, on BOTH engines, before every map is constructed. Do not re-add a // hand-rolled mapboxgl-only block here -- it was a strict subset of that call. -const El = defineCustomElement(ProfileLoader, { - configureApp(app) { - app.use(createPinia()) - const i18n = createAppI18n() - app.use(i18n) - app.provide('appI18n', i18n) - app.provide('profileModules', profileModules) - } -}) +const El = defineMapElement({ profileModules }) if (typeof customElements !== 'undefined' && !customElements.get('upg-neighbor')) customElements.define('upg-neighbor', El) diff --git a/plugin-designs/wall-of-people-groups/index.js b/plugin-designs/wall-of-people-groups/index.js index b7d53f924..25b9b0119 100644 --- a/plugin-designs/wall-of-people-groups/index.js +++ b/plugin-designs/wall-of-people-groups/index.js @@ -4,25 +4,17 @@ * profile; its components live in src/. The wall's components read the store as this.$store (the * original app's global property), so it is created here, per element, on the element's Pinia. */ -import { defineCustomElement } from 'vue' -import { createPinia } from 'pinia' -import ProfileLoader from '@map/ProfileLoader.vue' -import { createAppI18n } from '@map/i18n/index.js' +import { defineMapElement } from '@map/elements/defineMapElement.js' import { usePeopleGroupsStore } from './src/stores/peopleGroupsStore.js' const profileModules = import.meta.glob('./*.vue') -const El = defineCustomElement(ProfileLoader, { - configureApp(app) { - const pinia = createPinia() - app.use(pinia) - const i18n = createAppI18n() - app.use(i18n) - app.provide('appI18n', i18n) - app.provide('profileModules', profileModules) +const El = defineMapElement({ + profileModules, + configureApp: (app, { pinia, i18n }) => { const store = usePeopleGroupsStore(pinia) store.loadFromLocalStorage() app.config.globalProperties.$store = store - } + }, }) if (typeof customElements !== 'undefined' && !customElements.get('wall-of-people-groups')) customElements.define('wall-of-people-groups', El)