doxa-maps/library/elements/defineMapElement.js
Doxa Maps 5ab4af2e44 Five plugins were silently monolingual, because the bootstrap was copied sixteen times
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.
2026-09-28 17:40:34 +00:00

71 lines
4.3 KiB
JavaScript

/**
* defineMapElement.js — the one correct way to turn a profile into a custom element.
*
* Every plugin that ships a `<tag>` 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 <html lang>, 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