Fourteen English strings in a .js module reached readers in every locale

gospel-resource-rows.js builds each resource row's display text, and its
fallbacks were English literals: a record with no name of its own showed
"Scripture in this language", "Audio Scripture", "Listen in a browser" to an
Arabic or Chinese reader — precisely the rows whose source data was least
complete. check:hardcoded-english does not read .js files, which is why the
bundle's sweep never saw them.

Thirteen keys in eleven locales, and the translator is PASSED IN rather than
imported: the module is a pure function with no component context, and it stays
optional, so a caller without one degrades to the English written at the call
site instead of throwing.

Three publisher names stay verbatim — JESUS Film, Scripture Earth, Zume — but
where a name sits inside an ordinary phrase the phrase is keyed and the name is
INTERPOLATED, so "Zume discipleship training" translates around a brand a
translator never retypes. SE_KIND's values are displayed and take keys; its keys
are the upstream feed's codes and must not be touched.

Verified by running the module both ways: without a translator every row reads as
it does today; with an Arabic one the labels and scopes are Arabic while
"Scripture Earth" and "Zume" are unchanged.

ROUTED TO STUDIO, not self-landed: the eleven locale files make this a library
change. The route test says 11, and the last time I judged a landing by which
plugin the work was for rather than which directories it touched, the scan ran
against most of the fleet and timed out twice.
This commit is contained in:
Doxa Maps 2026-09-29 09:39:47 +00:00
parent 47937dea3b
commit 404b2c866e
13 changed files with 209 additions and 12 deletions

View file

@ -420,5 +420,21 @@
"nobodyPraying": "لا شيء موجود، ولا أحد يصلّي",
"concentrates": "أين تتركّز الفجوة",
"linkUnchecked": "لم يتم فحص هذا الرابط."
},
"rows": {
"scriptureHere": "الكتاب المقدس بهذه اللغة",
"audio": "الكتاب المقدس",
"bibleVideo": "فيديو كتابي",
"gospelPresentation": "عرض الإنجيل",
"studyNotes": "ملاحظات دراسية",
"readOnline": "القراءة عبر الإنترنت",
"audioScripture": "كتاب مقدس مسموع",
"listenBrowser": "استمع في المتصفح",
"inBrowser": "في المتصفح — {media}",
"readHere": "اقرأ بهذه اللغة",
"listenHere": "استمع بهذه اللغة",
"wordsOfLife": "كلمات الحياة — {name}",
"gospelAudio": "تسجيلات الإنجيل",
"discipleship": "تدريب {brand} على صناعة التلاميذ"
}
}

View file

@ -420,5 +420,21 @@
"nobodyPraying": "Nichts gefunden, und niemand betet",
"concentrates": "Wo sich die Lücke ballt",
"linkUnchecked": "Dieser Link wurde nicht geprüft."
},
"rows": {
"scriptureHere": "Bibel in dieser Sprache",
"audio": "Bibel",
"bibleVideo": "Bibelvideo",
"gospelPresentation": "Evangeliumsdarstellung",
"studyNotes": "Studiennotizen",
"readOnline": "online lesen",
"audioScripture": "Bibel als Audio",
"listenBrowser": "Im Browser anhören",
"inBrowser": "Im Browser — {media}",
"readHere": "In dieser Sprache lesen",
"listenHere": "In dieser Sprache hören",
"wordsOfLife": "Worte des Lebens — {name}",
"gospelAudio": "Evangeliums-Audio",
"discipleship": "{brand} Jüngerschafts-Schulung"
}
}

View file

@ -420,5 +420,21 @@
"nobodyPraying": "Nothing found, and nobody praying",
"concentrates": "Where the gap concentrates",
"linkUnchecked": "This link has not been checked."
},
"rows": {
"scriptureHere": "Scripture in this language",
"audio": "Scripture",
"bibleVideo": "Bible video",
"gospelPresentation": "Gospel presentation",
"studyNotes": "study notes",
"readOnline": "read online",
"audioScripture": "Audio Scripture",
"listenBrowser": "Listen in a browser",
"inBrowser": "In a browser — {media}",
"readHere": "Read in this language",
"listenHere": "Listen in this language",
"wordsOfLife": "Words of Life — {name}",
"gospelAudio": "Gospel audio",
"discipleship": "{brand} discipleship training"
}
}

View file

@ -420,5 +420,21 @@
"nobodyPraying": "Nada encontrado, y nadie orando",
"concentrates": "Dónde se concentra la brecha",
"linkUnchecked": "Este enlace no ha sido verificado."
},
"rows": {
"scriptureHere": "Escritura en esta lengua",
"audio": "Escritura",
"bibleVideo": "Video bíblico",
"gospelPresentation": "Presentación del evangelio",
"studyNotes": "notas de estudio",
"readOnline": "leer en línea",
"audioScripture": "Escritura en audio",
"listenBrowser": "Escuchar en el navegador",
"inBrowser": "En el navegador — {media}",
"readHere": "Leer en esta lengua",
"listenHere": "Escuchar en esta lengua",
"wordsOfLife": "Palabras de Vida — {name}",
"gospelAudio": "Audio del evangelio",
"discipleship": "formación de discipulado {brand}"
}
}

View file

@ -420,5 +420,21 @@
"nobodyPraying": "Rien trouvé, et personne ne prie",
"concentrates": "Où l’écart se concentre",
"linkUnchecked": "Ce lien n’a pas été vérifié."
},
"rows": {
"scriptureHere": "Écriture dans cette langue",
"audio": "Écriture",
"bibleVideo": "Vidéo biblique",
"gospelPresentation": "Présentation de l’Évangile",
"studyNotes": "notes d’étude",
"readOnline": "lire en ligne",
"audioScripture": "Écriture audio",
"listenBrowser": "Écouter dans le navigateur",
"inBrowser": "Dans le navigateur — {media}",
"readHere": "Lire dans cette langue",
"listenHere": "Écouter dans cette langue",
"wordsOfLife": "Paroles de Vie — {name}",
"gospelAudio": "Audio de l’Évangile",
"discipleship": "formation au discipulat {brand}"
}
}

View file

@ -420,5 +420,21 @@
"nobodyPraying": "कुछ नहीं मिला, और कोई प्रार्थना नहीं कर रहा",
"concentrates": "अंतर कहाँ केंद्रित है",
"linkUnchecked": "यह लिंक जाँचा नहीं गया है।"
},
"rows": {
"scriptureHere": "इस भाषा में पवित्रशास्त्र",
"audio": "पवित्रशास्त्र",
"bibleVideo": "बाइबल वीडियो",
"gospelPresentation": "सुसमाचार प्रस्तुति",
"studyNotes": "अध्ययन टिप्पणियाँ",
"readOnline": "ऑनलाइन पढ़ें",
"audioScripture": "ऑडियो पवित्रशास्त्र",
"listenBrowser": "ब्राउज़र में सुनें",
"inBrowser": "ब्राउज़र में — {media}",
"readHere": "इस भाषा में पढ़ें",
"listenHere": "इस भाषा में सुनें",
"wordsOfLife": "जीवन के वचन — {name}",
"gospelAudio": "सुसमाचार ऑडियो",
"discipleship": "{brand} शिष्यत्व प्रशिक्षण"
}
}

View file

@ -420,5 +420,21 @@
"nobodyPraying": "Nulla trovato, e nessuno in preghiera",
"concentrates": "Dove si concentra il divario",
"linkUnchecked": "Questo collegamento non è stato verificato."
},
"rows": {
"scriptureHere": "Scrittura in questa lingua",
"audio": "Scrittura",
"bibleVideo": "Video biblico",
"gospelPresentation": "Presentazione del vangelo",
"studyNotes": "note di studio",
"readOnline": "leggi online",
"audioScripture": "Scrittura audio",
"listenBrowser": "Ascolta nel browser",
"inBrowser": "Nel browser — {media}",
"readHere": "Leggi in questa lingua",
"listenHere": "Ascolta in questa lingua",
"wordsOfLife": "Parole di Vita — {name}",
"gospelAudio": "Audio del vangelo",
"discipleship": "formazione al discepolato {brand}"
}
}

View file

@ -420,5 +420,21 @@
"nobodyPraying": "Nada encontrado, e ninguém orando",
"concentrates": "Onde a lacuna se concentra",
"linkUnchecked": "Este link não foi verificado."
},
"rows": {
"scriptureHere": "Escritura nesta língua",
"audio": "Escritura",
"bibleVideo": "Vídeo bíblico",
"gospelPresentation": "Apresentação do evangelho",
"studyNotes": "notas de estudo",
"readOnline": "ler online",
"audioScripture": "Escritura em áudio",
"listenBrowser": "Ouvir no navegador",
"inBrowser": "No navegador — {media}",
"readHere": "Ler nesta língua",
"listenHere": "Ouvir nesta língua",
"wordsOfLife": "Palavras de Vida — {name}",
"gospelAudio": "Áudio do evangelho",
"discipleship": "treinamento de discipulado {brand}"
}
}

View file

@ -420,5 +420,21 @@
"nobodyPraying": "Nimic găsit și nimeni nu se roagă",
"concentrates": "Unde se concentrează golul",
"linkUnchecked": "Acest link nu a fost verificat."
},
"rows": {
"scriptureHere": "Scriptura în această limbă",
"audio": "Scriptură",
"bibleVideo": "Video biblic",
"gospelPresentation": "Prezentarea Evangheliei",
"studyNotes": "note de studiu",
"readOnline": "citește online",
"audioScripture": "Scriptură audio",
"listenBrowser": "Ascultă în browser",
"inBrowser": "În browser — {media}",
"readHere": "Citește în această limbă",
"listenHere": "Ascultă în această limbă",
"wordsOfLife": "Cuvintele Vieții — {name}",
"gospelAudio": "Audio al Evangheliei",
"discipleship": "instruire de ucenicizare {brand}"
}
}

View file

@ -420,5 +420,21 @@
"nobodyPraying": "Ничего не найдено, и никто не молится",
"concentrates": "Где сосредоточен разрыв",
"linkUnchecked": "Эта ссылка не проверялась."
},
"rows": {
"scriptureHere": "Писание на этом языке",
"audio": "Писание",
"bibleVideo": "Библейское видео",
"gospelPresentation": "Изложение Евангелия",
"studyNotes": "учебные заметки",
"readOnline": "читать онлайн",
"audioScripture": "Аудио-Писание",
"listenBrowser": "Слушать в браузере",
"inBrowser": "В браузере — {media}",
"readHere": "Читать на этом языке",
"listenHere": "Слушать на этом языке",
"wordsOfLife": "Слова жизни — {name}",
"gospelAudio": "Аудио Евангелия",
"discipleship": "обучение ученичеству {brand}"
}
}

View file

@ -420,5 +420,21 @@
"nobodyPraying": "一无所获,也无人祷告",
"concentrates": "差距集中在哪里",
"linkUnchecked": "此链接尚未核查。"
},
"rows": {
"scriptureHere": "这种语言的圣经",
"audio": "圣经",
"bibleVideo": "圣经视频",
"gospelPresentation": "福音陈述",
"studyNotes": "研读注释",
"readOnline": "在线阅读",
"audioScripture": "有声圣经",
"listenBrowser": "在浏览器中收听",
"inBrowser": "在浏览器中 — {media}",
"readHere": "用这种语言阅读",
"listenHere": "用这种语言收听",
"wordsOfLife": "生命之道 — {name}",
"gospelAudio": "福音录音",
"discipleship": "{brand} 门徒训练"
}
}

View file

@ -41,21 +41,36 @@ export const hasReadableScripture = (c) =>
*/
/** Scripture Earth directory names, as a reader would say them. */
const SE_KIND = { PDF: 'PDF', audio: 'audio', sab: 'app', study: 'study notes', viewer: 'read online' }
// The VALUES here are displayed (joined into a row's scope), so they are labels and take keys; the
// KEYS are the upstream feed's own codes and must never be translated. PDF/audio/app stay as they
// are: an abbreviation and two words that are the same token in the locales we ship.
const SE_KIND = (tx) => ({ PDF: 'PDF', audio: 'audio', sab: 'app',
study: tx('gospel.rows.studyNotes', 'study notes'), viewer: tx('gospel.rows.readOnline', 'read online') })
/**
* Every resource a language offers, flattened for a list or a table.
* @param {object} c one entry of the pin index's codes[]
* @returns {{url:string,title:string,type:string,scope:string}[]}
*/
export function resourceRows(c) {
// ★ THE FALLBACKS BELOW ARE UI TEXT, NOT DATA. Each `c.x || 'English'` is shown when a record
// carries no name of its own, so an Arabic or Chinese reader saw English for exactly the rows whose
// source was least complete. check:hardcoded-english does not read .js files yet, which is why these
// survived the bundle's sweep.
//
// `t` is PASSED IN rather than imported: this module is a pure function with no component context,
// and the plugin already threads its translator the same way. It is optional and falls back to the
// English written here, so a caller without a translator degrades to today's behaviour.
//
// 'JESUS Film' below is deliberately NOT keyed: it is the project's name, not a label.
export function resourceRows(c, t = null) {
const tx = (key, en) => { try { const v = t && t(key); return v && v !== key ? v : en } catch (_) { return en } }
if (!c) return []
const out = []
const add = (title, type, url, scope = '') => {
if (url) out.push({ url, title, type, scope })
}
add(c.sv || 'Scripture in this language', 'Scripture', c.s, c.ss || '')
add(c.sv || tx('gospel.rows.scriptureHere', 'Scripture in this language'), 'Scripture', c.s, c.ss || '')
/* c.fT is set only where the JESUS Film component does NOT carry this language and a
different JESUS Film Project feature does — The Savior, Magdalena. Calling that "JESUS
Film" would be the same class of lie as calling Open Bible Stories Scripture. */
@ -66,26 +81,32 @@ export function resourceRows(c) {
if (c.au) {
const media = String(c.au.m || 'audio')
add(
c.au.n || 'Scripture',
c.au.n || tx('gospel.rows.audio', 'Scripture'),
/* ⚠ ONE RECORD, TWO CAPABILITIES, AND THE TYPE ONLY SAID ONE. Faith Comes By Hearing's media
list is often "audio, text, video": this typed every such row 'Audio Scripture', so the
access table's byType.Scripture never saw it and 2,001 people groups were published as
"watch or hear, cannot read" while their own readable text sat behind that link. TEXT
decides the type; the media list stays in the scope so nothing is hidden, and the access
builder counts a Scripture row whose scope names audio as hearable too. */
/text/.test(media) ? 'Scripture' : 'Audio Scripture',
/text/.test(media) ? tx('gospel.rows.audio', 'Scripture') : tx('gospel.rows.audioScripture', 'Audio Scripture'),
c.au.u,
media === 'audio' ? 'Listen in a browser' : `In a browser — ${media}`,
media === 'audio' ? tx('gospel.rows.listenBrowser', 'Listen in a browser')
: tx('gospel.rows.inBrowser', 'In a browser — {media}').replace('{media}', media),
)
}
if (c.bv) add(c.bv.n || 'Bible video', 'Bible video', c.bv.u, c.bv.sc || '')
if (c.gp) add(c.gp.n || 'Gospel presentation', 'Gospel presentation', c.gp.u, 'Read in this language')
if (c.bv) add(c.bv.n || tx('gospel.rows.bibleVideo', 'Bible video'), 'Bible video', c.bv.u, c.bv.sc || '')
if (c.gp) add(c.gp.n || tx('gospel.rows.gospelPresentation', 'Gospel presentation'), 'Gospel presentation', c.gp.u, tx('gospel.rows.readHere', 'Read in this language'))
if (c.se) {
add('Scripture Earth', 'Scripture', c.se.u, (c.se.k || []).map((k) => SE_KIND[k] || k).join(', '))
add('Scripture Earth', 'Scripture', c.se.u, (c.se.k || []).map((k) => SE_KIND(tx)[k] || k).join(', '))
}
if (c.ga) {
add(c.ga.n ? `Words of Life — ${c.ga.n}` : 'Gospel audio', 'Gospel audio', c.ga.u, 'Listen in this language')
add(c.ga.n ? tx('gospel.rows.wordsOfLife', 'Words of Life — {name}').replace('{name}', c.ga.n)
: tx('gospel.rows.gospelAudio', 'Gospel audio'), 'Gospel audio', c.ga.u,
tx('gospel.rows.listenHere', 'Listen in this language'))
}
add('Zume discipleship training', 'Discipleship training', c.z)
// The brand is INTERPOLATED, not retyped by a translator: "Zume" is the publisher's name and must
// survive every locale unchanged, while "discipleship training" is an ordinary noun phrase.
add(tx('gospel.rows.discipleship', '{brand} discipleship training').replace('{brand}', 'Zume'),
'Discipleship training', c.z)
return out
}

View file

@ -105,7 +105,7 @@ const languages = computed(() => {
// listed only c.s and c.d, so film, audio, Bible video, tracts, Scripture Earth and
// GRN never appeared here — a language whose only resource was a Words of Life
// recording rendered with no links while the map offered a working one.
const res = resourceRows(c)
const res = resourceRows(c, tr)
e = {
iso: code,
language: c.l || 'unknown',