diff --git a/plugin-designs/gospel-resources/charts.vue b/plugin-designs/gospel-resources/charts.vue
index 52b066b28..f07758e36 100644
--- a/plugin-designs/gospel-resources/charts.vue
+++ b/plugin-designs/gospel-resources/charts.vue
@@ -397,7 +397,32 @@ const active = ref('coverage')
const picked = ref(null)
const onPick = (row) => { picked.value = row?.meta ? row : null }
-const fmt = (n) => (Number(n) || 0).toLocaleString()
+/**
+ * ═══ NUMBERS FOLLOW THE READER'S LOCALE, DIGITS DO NOT CHANGE ════════════════════════════
+ *
+ * toLocaleString() with no argument formats for the JS RUNTIME's locale — the browser's — which
+ * has nothing to do with the locale this plugin was asked to render in. A reader who opens the
+ * Arabic build on an English browser got English grouping, and the same page on a French browser
+ * got French grouping: the same plugin, the same locale, two different numbers.
+ *
+ * ⚠ numberingSystem IS PINNED TO latn, and it is worth being exact about what that does, because
+ * the obvious claim is wrong. MEASURED against full ICU: the eleven tags this bundle ships all
+ * resolve to latn ALREADY — plain "ar" included — so for them the pin changes nothing. It earns
+ * its place only if a host passes a REGION tag: ar-EG resolves to arab and fa to arabext, and
+ * those would switch the digits. The numeral format is held for the Driver, so the pin keeps that
+ * decision his rather than a host's.
+ *
+ * What DOES change today is which locale decides the separators: the reader's, not the browser's.
+ * Hindi groups in lakhs, so 1234567 reads 12,34,567 in hi, and fr and ru use a space. Those are
+ * correct for the locale and are a visible change from what shipped before.
+ */
+const localeTag = () => {
+ try { return String(useI18n().locale.value || 'en') } catch (_) { return 'en' }
+}
+const fmt = (n) => {
+ const v = Number(n) || 0
+ try { return v.toLocaleString(localeTag(), { numberingSystem: 'latn' }) } catch (_) { return v.toLocaleString('en') }
+}
useShadowStyles(
'.gc{position:absolute;inset:0;overflow-y:auto;padding:18px;background:#f6f7f9;color:#0f1419;' +
diff --git a/plugin-designs/gospel-resources/data-tables.vue b/plugin-designs/gospel-resources/data-tables.vue
index 0efb4e8fb..c128ea1b5 100644
--- a/plugin-designs/gospel-resources/data-tables.vue
+++ b/plugin-designs/gospel-resources/data-tables.vue
@@ -265,7 +265,32 @@ const gap = computed(() => {
deathsDay: Math.round(g.reduce((n, x) => n + (x.est_deaths_per_day || 0), 0)),
}
})
-const fmt = (n) => (Number(n) || 0).toLocaleString()
+/**
+ * ═══ NUMBERS FOLLOW THE READER'S LOCALE, DIGITS DO NOT CHANGE ════════════════════════════
+ *
+ * toLocaleString() with no argument formats for the JS RUNTIME's locale — the browser's — which
+ * has nothing to do with the locale this plugin was asked to render in. A reader who opens the
+ * Arabic build on an English browser got English grouping, and the same page on a French browser
+ * got French grouping: the same plugin, the same locale, two different numbers.
+ *
+ * ⚠ numberingSystem IS PINNED TO latn, and it is worth being exact about what that does, because
+ * the obvious claim is wrong. MEASURED against full ICU: the eleven tags this bundle ships all
+ * resolve to latn ALREADY — plain "ar" included — so for them the pin changes nothing. It earns
+ * its place only if a host passes a REGION tag: ar-EG resolves to arab and fa to arabext, and
+ * those would switch the digits. The numeral format is held for the Driver, so the pin keeps that
+ * decision his rather than a host's.
+ *
+ * What DOES change today is which locale decides the separators: the reader's, not the browser's.
+ * Hindi groups in lakhs, so 1234567 reads 12,34,567 in hi, and fr and ru use a space. Those are
+ * correct for the locale and are a visible change from what shipped before.
+ */
+const localeTag = () => {
+ try { return String(useI18n().locale.value || 'en') } catch (_) { return 'en' }
+}
+const fmt = (n) => {
+ const v = Number(n) || 0
+ try { return v.toLocaleString(localeTag(), { numberingSystem: 'latn' }) } catch (_) { return v.toLocaleString('en') }
+}
useShadowStyles(
'.gt{position:absolute;inset:0;overflow-y:auto;padding:18px;background:#f6f7f9;color:#0f1419;' +
@@ -334,7 +359,7 @@ useShadowStyles(
diff --git a/plugin-designs/gospel-resources/resources-list.vue b/plugin-designs/gospel-resources/resources-list.vue
index bfd632b52..a11cfb957 100644
--- a/plugin-designs/gospel-resources/resources-list.vue
+++ b/plugin-designs/gospel-resources/resources-list.vue
@@ -202,10 +202,35 @@ const biggestUnfound = computed(() =>
.map((l) => `${l.language} (${fmtShort(l.population)})`).join(', '))
const fmtShort = (n) => {
const v = Number(n) || 0
- return v >= 1e6 ? `${Math.round(v / 1e5) / 10}M` : v.toLocaleString()
+ return v >= 1e6 ? `${Math.round(v / 1e5) / 10}M` : fmt(v)
}
-const fmt = (n) => (Number(n) || 0).toLocaleString()
+/**
+ * ═══ NUMBERS FOLLOW THE READER'S LOCALE, DIGITS DO NOT CHANGE ════════════════════════════
+ *
+ * toLocaleString() with no argument formats for the JS RUNTIME's locale — the browser's — which
+ * has nothing to do with the locale this plugin was asked to render in. A reader who opens the
+ * Arabic build on an English browser got English grouping, and the same page on a French browser
+ * got French grouping: the same plugin, the same locale, two different numbers.
+ *
+ * ⚠ numberingSystem IS PINNED TO latn, and it is worth being exact about what that does, because
+ * the obvious claim is wrong. MEASURED against full ICU: the eleven tags this bundle ships all
+ * resolve to latn ALREADY — plain "ar" included — so for them the pin changes nothing. It earns
+ * its place only if a host passes a REGION tag: ar-EG resolves to arab and fa to arabext, and
+ * those would switch the digits. The numeral format is held for the Driver, so the pin keeps that
+ * decision his rather than a host's.
+ *
+ * What DOES change today is which locale decides the separators: the reader's, not the browser's.
+ * Hindi groups in lakhs, so 1234567 reads 12,34,567 in hi, and fr and ru use a space. Those are
+ * correct for the locale and are a visible change from what shipped before.
+ */
+const localeTag = () => {
+ try { return String(useI18n().locale.value || 'en') } catch (_) { return 'en' }
+}
+const fmt = (n) => {
+ const v = Number(n) || 0
+ try { return v.toLocaleString(localeTag(), { numberingSystem: 'latn' }) } catch (_) { return v.toLocaleString('en') }
+}
// ── EVERY LINK THE ROW ACTUALLY HAS, not just the scripture ones ─────────────────────────────
// THE DEFECT THIS FIXES (measured 2026-09-23 over all 862 languages in resources.json):
diff --git a/plugin-designs/gospel-resources/resources-map.vue b/plugin-designs/gospel-resources/resources-map.vue
index c130fe14f..2ef85852a 100644
--- a/plugin-designs/gospel-resources/resources-map.vue
+++ b/plugin-designs/gospel-resources/resources-map.vue
@@ -93,7 +93,15 @@ import { resolveNamedStyle } from '@map/styles/registry.js'
const profileConfig = inject('profileConfig', ref(null))
const isDark = computed(() => profileConfig.value?.isDark === true)
-const mapboxToken = computed(() => profileConfig.value?.tk || window.DOXA_TOKEN || '')
+// The token through the library's channel (documentation/reference-universal-props.md): the
+// profile-config tk, then ProfileLoader's injected mapboxToken (which carries the page-wide shared
+// token, fed by window.MAP_TOKEN), then window.MAP_TOKEN. This read window.DOXA_TOKEN, which only a
+// few dev pages set, so in the studio and on every built page the map ignored the page's token.
+// Same fix as cities-internet and key-cities (1cf57f7a) — the pattern is copied, not re-invented,
+// because three raw maps drifting apart on how they find a token is how this happened.
+const injectedTk = inject('mapboxToken', null)
+const mapboxToken = computed(() => profileConfig.value?.tk || injectedTk?.value
+ || (typeof window !== 'undefined' && window.MAP_TOKEN) || '')
// ── HONOUR THE PROVIDER SWITCH: this profile builds its map RAW ─────────────
// Bundles that go through useMapInstance get the switch for free; this one calls `new gl.Map`
@@ -1194,7 +1202,32 @@ function setMode(m) {
if (mapRef.value && adm0Ref.value) paint(mapRef.value, adm0Ref.value)
}
-const fmt = (n) => (Number(n) || 0).toLocaleString()
+/**
+ * ═══ NUMBERS FOLLOW THE READER'S LOCALE, DIGITS DO NOT CHANGE ════════════════════════════
+ *
+ * toLocaleString() with no argument formats for the JS RUNTIME's locale — the browser's — which
+ * has nothing to do with the locale this plugin was asked to render in. A reader who opens the
+ * Arabic build on an English browser got English grouping, and the same page on a French browser
+ * got French grouping: the same plugin, the same locale, two different numbers.
+ *
+ * ⚠ numberingSystem IS PINNED TO latn, and it is worth being exact about what that does, because
+ * the obvious claim is wrong. MEASURED against full ICU: the eleven tags this bundle ships all
+ * resolve to latn ALREADY — plain "ar" included — so for them the pin changes nothing. It earns
+ * its place only if a host passes a REGION tag: ar-EG resolves to arab and fa to arabext, and
+ * those would switch the digits. The numeral format is held for the Driver, so the pin keeps that
+ * decision his rather than a host's.
+ *
+ * What DOES change today is which locale decides the separators: the reader's, not the browser's.
+ * Hindi groups in lakhs, so 1234567 reads 12,34,567 in hi, and fr and ru use a space. Those are
+ * correct for the locale and are a visible change from what shipped before.
+ */
+const localeTag = () => {
+ try { return String(useI18n().locale.value || 'en') } catch (_) { return 'en' }
+}
+const fmt = (n) => {
+ const v = Number(n) || 0
+ try { return v.toLocaleString(localeTag(), { numberingSystem: 'latn' }) } catch (_) { return v.toLocaleString('en') }
+}
// The table lives on a SIBLING page, so "see all of these" is a URL. Built here rather
// than inline in the template so the group link and the country link cannot drift apart.
diff --git a/public/dx-maps/doxa-maps-spa/manifest.json b/public/dx-maps/doxa-maps-spa/manifest.json
index 6de1ce182..e3f078a5c 100644
--- a/public/dx-maps/doxa-maps-spa/manifest.json
+++ b/public/dx-maps/doxa-maps-spa/manifest.json
@@ -579,7 +579,7 @@
"bundle": "gospel-resources",
"scriptUrl": "./gospel-resources/gospel-resources.js",
"output": "dx-maps/gospel-resources/gospel-resources.js",
- "integrity": "sha384-eXLtFilz5hpBxBl0C9BjZHXXFUl6KWwvqcVbe+XkQwZYD13DZdUGmJZx4Gi7Y78z",
+ "integrity": "sha384-w99J54Kg5/CBs37TxGIqgtErXVD9o1qIdlOZ6psyxFW+GPkTSclQMuoe8XusNIMD",
"elements": [
"gospel-resources-map"
],
@@ -599,7 +599,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "",
- "script": "\n\n"
+ "script": "\n\n"
}
},
{
@@ -613,7 +613,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "",
- "script": "\n\n"
+ "script": "\n\n"
}
},
{
@@ -627,7 +627,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "",
- "script": "\n\n\n\n"
+ "script": "\n\n\n\n"
}
},
{
@@ -641,7 +641,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "",
- "script": "\n\n"
+ "script": "\n\n"
}
},
{
@@ -655,7 +655,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "",
- "script": "\n\n\n\n"
+ "script": "\n\n\n\n"
}
}
]
diff --git a/public/dx-maps/gospel-resources/charts/index.html b/public/dx-maps/gospel-resources/charts/index.html
index ee0a54a31..4e9c760d1 100644
--- a/public/dx-maps/gospel-resources/charts/index.html
+++ b/public/dx-maps/gospel-resources/charts/index.html
@@ -26,7 +26,7 @@
-
+
-
+
-
+
-
+
-
+
\n\n"
+ "script": "\n\n"
}
},
{
@@ -613,7 +613,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "",
- "script": "\n\n"
+ "script": "\n\n"
}
},
{
@@ -627,7 +627,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "",
- "script": "\n\n\n\n"
+ "script": "\n\n\n\n"
}
},
{
@@ -641,7 +641,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "",
- "script": "\n\n"
+ "script": "\n\n"
}
},
{
@@ -655,7 +655,7 @@
"scriptUrl": "./gospel-resources/gospel-resources.js",
"embed": {
"iframe": "",
- "script": "\n\n\n\n"
+ "script": "\n\n\n\n"
}
}
]