The gap sentence is one key, so a translator can move its clauses

charts.vue's hero read as six fragments concatenated around three counts and a
bdi island: "of", "languages have", "— the languages of", "people." Arabic does
not put those clauses in that order, so translating the fragments would have
given Arabic words in English order — grammatically broken, and worse than
leaving it English, because it looks translated and nobody re-checks it.

Now one message with named placeholders through useSentence. The counts and the
quoted phrase go in as VALUES, so the bold and the dir="ltr" survive without a
translator ever reproducing markup, and keep() hands the placeholders back to t()
intact — without it the lookup blanks them and the sentence renders with no
values at all.

Proven through a real createI18n rather than in isolation, which is the lesson
from the keep() trap: en renders as it does today, and ar and ru both LEAD with
the object and put the counts after — the reordering that fragment-by-fragment
translation cannot express. zh reorders too.

The gate moves 144 -> 142 for this one sentence. That is the honest number: six
fragments collapse into one key, but the gate counts findings, not fragments, so
the count falls more slowly than the work proceeds. ~26 fragments remain.
This commit is contained in:
Doxa Maps 2026-09-29 05:49:22 +00:00
parent d9fb0a137b
commit c7aa670426
12 changed files with 58 additions and 14 deletions

View file

@ -333,7 +333,9 @@
"c2Unit": "أشخاص في الشعوب التي هذه لغتها الأساسية — لا المتحدثين بها عمومًا",
"c3Title": "البلدان الأكثر لغاتٍ غير مخدومة",
"c3Sub": "البلدان الـ{n} الأكثر لغاتٍ لم يُعثر لها على شيء. طول العمود هو كل لغة موجودة في ذلك البلد.",
"c3Unit": "لغات موجودة في البلد (اللغة العابرة للحدود تُحتسب في كل بلد)"
"c3Unit": "لغات موجودة في البلد (اللغة العابرة للحدود تُحتسب في كل بلد)",
"noneHero": "{noScripture} في {nNone} لغة من أصل {nLang} — وهي لغات {popNone} نسمة.",
"noneHeroFound": "لا يوجد كتاب مقدس ولا فيلم في المجموعات التي جرى فحصها"
},
"filter": {
"showingOnly": "يُعرض فقط: {what}.",

View file

@ -333,7 +333,9 @@
"c2Unit": "Menschen in den Volksgruppen, deren PRIMÄRE Sprache dies ist — nicht Sprecher der Sprache",
"c3Title": "Länder mit den meisten unversorgten Sprachen",
"c3Sub": "Die {n} Länder mit den meisten Sprachen, für die nichts gefunden wurde. Die Balkenlänge ist jede im Land vorhandene Sprache.",
"c3Unit": "im Land vorhandene Sprachen (eine grenzüberschreitende Sprache zählt in jedem Land)"
"c3Unit": "im Land vorhandene Sprachen (eine grenzüberschreitende Sprache zählt in jedem Land)",
"noneHero": "{nNone} von {nLang} Sprachen haben {noScripture} — die Sprachen von {popNone} Menschen.",
"noneHeroFound": "keine Bibel und keinen Film in den geprüften Beständen"
},
"filter": {
"showingOnly": "Nur {what} werden angezeigt.",

View file

@ -333,7 +333,9 @@
"c2Unit": "people in the people groups whose PRIMARY language this is — not speakers of the language",
"c3Title": "Countries with the most unserved languages",
"c3Sub": "The {n} countries with the most languages that have nothing found. Bar length is every language present in that country.",
"c3Unit": "languages present in the country (a cross-border language counts in each)"
"c3Unit": "languages present in the country (a cross-border language counts in each)",
"noneHero": "{nNone} of {nLang} languages have {noScripture} — the languages of {popNone} people.",
"noneHeroFound": "no scripture and no film found in the pools checked"
},
"filter": {
"showingOnly": "Showing only {what}.",

View file

@ -333,7 +333,9 @@
"c2Unit": "personas en las etnias cuya lengua PRINCIPAL es esta — no hablantes de la lengua",
"c3Title": "Países con más lenguas desatendidas",
"c3Sub": "Los {n} países con más lenguas sin nada encontrado. La longitud de la barra es cada lengua presente en ese país.",
"c3Unit": "lenguas presentes en el país (una lengua transfronteriza cuenta en cada uno)"
"c3Unit": "lenguas presentes en el país (una lengua transfronteriza cuenta en cada uno)",
"noneHero": "{nNone} de {nLang} lenguas tienen {noScripture}: las lenguas de {popNone} personas.",
"noneHeroFound": "ni Escritura ni película en los repositorios consultados"
},
"filter": {
"showingOnly": "Solo se muestran {what}.",

View file

@ -333,7 +333,9 @@
"c2Unit": "personnes dans les peuples dont c'est la langue PRINCIPALE — non les locuteurs de la langue",
"c3Title": "Pays comptant le plus de langues non desservies",
"c3Sub": "Les {n} pays comptant le plus de langues sans rien de trouvé. La longueur de la barre correspond à chaque langue présente dans ce pays.",
"c3Unit": "langues présentes dans le pays (une langue transfrontalière compte dans chacun)"
"c3Unit": "langues présentes dans le pays (une langue transfrontalière compte dans chacun)",
"noneHero": "{nNone} langues sur {nLang} ont {noScripture} — les langues de {popNone} personnes.",
"noneHeroFound": "ni Écriture ni film dans les fonds consultés"
},
"filter": {
"showingOnly": "Affichage limité aux {what}.",

View file

@ -333,7 +333,9 @@
"c2Unit": "उन जनसमूहों के लोग जिनकी मुख्य भाषा यही है — भाषा के सभी वक्ता नहीं",
"c3Title": "सबसे अधिक असेवित भाषाओं वाले देश",
"c3Sub": "{n} देश जिनमें सबसे अधिक भाषाओं के लिए कुछ नहीं मिला। बार की लंबाई उस देश में मौजूद हर भाषा है।",
"c3Unit": "देश में मौजूद भाषाएँ (सीमा-पार भाषा हर देश में गिनी जाती है)"
"c3Unit": "देश में मौजूद भाषाएँ (सीमा-पार भाषा हर देश में गिनी जाती है)",
"noneHero": "{nLang} में से {nNone} भाषाओं में {noScripture} — ये {popNone} लोगों की भाषाएँ हैं।",
"noneHeroFound": "जाँचे गए स्रोतों में न पवित्रशास्त्र मिला, न फ़िल्म"
},
"filter": {
"showingOnly": "केवल {what} दिखाए जा रहे हैं।",

View file

@ -333,7 +333,9 @@
"c2Unit": "persone nei gruppi la cui lingua PRINCIPALE è questa — non i parlanti della lingua",
"c3Title": "Paesi con più lingue non servite",
"c3Sub": "I {n} paesi con più lingue per cui non è stato trovato nulla. La lunghezza della barra è ogni lingua presente in quel paese.",
"c3Unit": "lingue presenti nel paese (una lingua transfrontaliera conta in ciascuno)"
"c3Unit": "lingue presenti nel paese (una lingua transfrontaliera conta in ciascuno)",
"noneHero": "{nNone} lingue su {nLang} hanno {noScripture} — le lingue di {popNone} persone.",
"noneHeroFound": "nessuna Scrittura e nessun film nei repertori consultati"
},
"filter": {
"showingOnly": "Sono mostrati solo {what}.",

View file

@ -333,7 +333,9 @@
"c2Unit": "pessoas nas etnias cuja língua PRINCIPAL é esta — não falantes da língua",
"c3Title": "Países com mais línguas desatendidas",
"c3Sub": "Os {n} países com mais línguas sem nada encontrado. O comprimento da barra é cada língua presente nesse país.",
"c3Unit": "línguas presentes no país (uma língua transfronteiriça conta em cada um)"
"c3Unit": "línguas presentes no país (uma língua transfronteiriça conta em cada um)",
"noneHero": "{nNone} de {nLang} línguas têm {noScripture} — as línguas de {popNone} pessoas.",
"noneHeroFound": "nenhuma Escritura e nenhum filme nos acervos consultados"
},
"filter": {
"showingOnly": "Mostrando apenas {what}.",

View file

@ -333,7 +333,9 @@
"c2Unit": "persoane din grupurile a căror limbă PRINCIPALĂ este aceasta — nu vorbitorii limbii",
"c3Title": "Țări cu cele mai multe limbi neservite",
"c3Sub": "Cele {n} țări cu cele mai multe limbi pentru care nu s-a găsit nimic. Lungimea barei este fiecare limbă prezentă în acea țară.",
"c3Unit": "limbi prezente în țară (o limbă transfrontalieră se numără în fiecare)"
"c3Unit": "limbi prezente în țară (o limbă transfrontalieră se numără în fiecare)",
"noneHero": "{nNone} din {nLang} limbi au {noScripture} — limbile a {popNone} de oameni.",
"noneHeroFound": "nicio Scriptură și niciun film în colecțiile verificate"
},
"filter": {
"showingOnly": "Se afișează doar {what}.",

View file

@ -333,7 +333,9 @@
"c2Unit": "люди в народах, для которых это ОСНОВНОЙ язык, — не все говорящие на нём",
"c3Title": "Страны с наибольшим числом неохваченных языков",
"c3Sub": "{n} стран с наибольшим числом языков, для которых ничего не найдено. Длина столбца — каждый язык, присутствующий в этой стране.",
"c3Unit": "языки, присутствующие в стране (трансграничный язык считается в каждой)"
"c3Unit": "языки, присутствующие в стране (трансграничный язык считается в каждой)",
"noneHero": "{noScripture} — у {nNone} из {nLang} языков, языков {popNone} человек.",
"noneHeroFound": "ни Писания, ни фильма в проверенных источниках"
},
"filter": {
"showingOnly": "Показаны только {what}.",

View file

@ -333,7 +333,9 @@
"c2Unit": "以该语言为主要语言的族群人口 — 不是该语言的全部使用者",
"c3Title": "未得服事语言最多的国家",
"c3Sub": "一无所获语言最多的 {n} 个国家。条形长度为该国存在的全部语言。",
"c3Unit": "该国存在的语言(跨境语言在每个国家各计一次)"
"c3Unit": "该国存在的语言(跨境语言在每个国家各计一次)",
"noneHero": "{nLang} 种语言中有 {nNone} 种{noScripture}——这是 {popNone} 人的语言。",
"noneHeroFound": "在已查询的资源库中既无圣经也无影片"
},
"filter": {
"showingOnly": "仅显示{what}。",

View file

@ -46,6 +46,8 @@
* inlining it into the IIFE would block first paint for every profile in the bundle).
*/
import { useOptionalT } from '@map/composables/useOptionalT.js'
import { sentenceParts, strong, ltr, keep } from '@map/composables/useSentence.js'
import { h } from 'vue'
import { computed, inject, ref, watch } from 'vue'
import BarChart from '@map/components/charts/BarChart.vue'
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
@ -67,6 +69,22 @@ import { useShadowStyles } from '@map/composables/useShadowStyles.js'
// with English as the last resort. tStatic resolves 'gospel.<key>' against the same catalogue.
const tr = useOptionalT('gospel')
// ★ THE HERO SENTENCE, ONE KEY. Each COUNT arrives already formatted and already pluralised from its
// own key: vue-i18n resolves one plural form per key, so a sentence carrying three counts cannot
// pluralise them from its own message. keep() hands the placeholders back to t() intact — without it
// the lookup BLANKS them and the sentence renders with no values at all, in every locale at once.
const heroSentence = computed(() => {
const msg = tr('gospel.charts.noneHero', keep('nNone', 'nLang', 'noScripture', 'popNone'))
return h('span', sentenceParts(msg, {
nNone: strong(fmt(nNone.value)),
nLang: fmt(nLang.value),
// The phrase is quoted from English sources and must not reverse inside an Arabic sentence, so it
// keeps its own dir. Passed in as a VALUE: a translator never has to reproduce the markup.
noScripture: ltr(tr('gospel.charts.noneHeroFound', 'no scripture and no film found in the pools checked'), true),
popNone: strong(fmt(popNone.value)),
}))
})
const profileConfig = inject('profileConfig', ref(null))
const isDark = computed(() => profileConfig.value?.isDark === true)
@ -465,9 +483,13 @@ useShadowStyles(
group that speaks it — so the language is the unit of the gap.
</p>
<div v-if="nLang" class="gc-hero">
<b>{{ fmt(nNone) }}</b> of {{ fmt(nLang) }} languages have <b><bdi dir="ltr">no scripture and no film
found in the pools checked</bdi></b> — the languages of <b>{{ fmt(popNone) }}</b> people.
<!-- ONE KEY, NAMED SLOTS. This was six fragments concatenated around three counts and a bdi
island ("of", "languages have", "the languages of", "people."). Arabic does not put those
clauses in that order, so translating the fragments gives Arabic words in English order:
grammatically broken, and worse than leaving it English because it looks translated. The
sentence is now ONE message whose named placeholders a translator can reorder freely, with
the bold counts and the ltr island passed in as values rather than written into the catalogue. -->
<div v-if="nLang" class="gc-hero"><component :is="heroSentence" />
<template v-if="noCodeGroups">
<br><small><bdi dir="ltr">Counted BY LANGUAGE, so {{ fmt(noCodeGroups) }} people group{{ noCodeGroups === 1 ? '' : 's' }}
with no ISO 639-3 code in the source — {{ fmt(noCodePeople) }} people — cannot appear here.