diff --git a/internal/check-hardcoded-english.mjs b/internal/check-hardcoded-english.mjs
index de39895df..6ba093f61 100644
--- a/internal/check-hardcoded-english.mjs
+++ b/internal/check-hardcoded-english.mjs
@@ -269,7 +269,12 @@ function resolvedFallbackOf(node, parent, stack, names, root = '.') {
// form 2: the literal is the RIGHT side of `t(…) || 'English'` or `t(…) ?? 'English'`
const lg = stack[stack.length - 1]
if (lg && lg.type === 'LogicalExpression' && (lg.operator === '||' || lg.operator === '??') && lg.right === node) {
+ // `||` is left-associative, so `config || t('key') || 'English'` parses with the whole
+ // `config || t('key')` on the left. The translator is the LAST alternative before the English,
+ // which is the rightmost call down that spine — a real shape here: an instance may override the
+ // wording, otherwise the catalogue, and the English only if neither exists.
let call = lg.left
+ while (call?.type === 'LogicalExpression' && (call.operator === '||' || call.operator === '??')) call = call.right
if (call?.type === 'CallExpression' && names.has(calleeName(call))) {
const k = call.arguments[0]
if (k?.type === 'StringLiteral') return k.value
@@ -771,6 +776,20 @@ const b = tStatic('dashboard.no_such_key_at_all') || 'This one has no key and mu
'QUIET on a tStatic(key) || English fallback whose key resolves')
ok(ts.some((f) => /must still count/.test(f.text)),
'COUNTS a tStatic fallback whose key does not resolve')
+ // `config || t('key') || 'English'` — an instance may override the wording, else the catalogue,
+ // else the English. The translator is not the immediate left operand, so the spine is walked.
+ const CH = `
{{ x }}
+`
+ const ch = scanSfc(CH, 'chain.vue')
+ ok(!ch.some((f) => /Research map loading/.test(f.text)),
+ 'QUIET on config || t(key) || English when the key resolves')
+ ok(ch.some((f) => /no resolving key/.test(f.text)) && ch.some((f) => /no translator at all/.test(f.text)),
+ 'COUNTS the same chain when the key is missing, and when there is no translator in it')
// the catalogue-side rule, tested directly: the two facts that make a fallback safe
ok(fallbackFullyResolved('dashboard.map_loading'),
'a key present and really translated in all 11 locales counts as resolved')
diff --git a/library/i18n/locales/ar/dashboard.json b/library/i18n/locales/ar/dashboard.json
index dbed586a2..9cd50d3a6 100644
--- a/library/i18n/locales/ar/dashboard.json
+++ b/library/i18n/locales/ar/dashboard.json
@@ -94,5 +94,19 @@
"fs_exit": "الخروج من ملء الشاشة",
"fs_open": "فتح الجدول بملء الشاشة",
"export_selected_hint": "تصدير جميع الحقول — الشعوب المحددة: {n}",
- "export_all_hint": "تصدير جميع الحقول — الشعوب: {n}"
+ "export_all_hint": "تصدير جميع الحقول — الشعوب: {n}",
+ "map_init_failed": "تعذّر تشغيل الخريطة. تحقّق من رمز الوصول ومن حاوية الخريطة.",
+ "show_filters": "إظهار المرشّحات",
+ "hide_filters": "إخفاء المرشّحات — لتوسيع الخريطة",
+ "show_charts": "إظهار الرسوم",
+ "hide_charts": "إخفاء الرسوم — لتوسيع الخريطة",
+ "export_copied": "نُسخ إلى الحافظة",
+ "export_done": "صُدّر",
+ "export_ready": "جاهز",
+ "export_toast": "✓ {rows} صفًّا × {fields} حقلًا {how}",
+ "export_failed": "✕ فشل التصدير",
+ "pray_for_group": "صلِّ لأجل هذا الشعب",
+ "more_information": "مزيد من المعلومات",
+ "title": "لوحة البحث",
+ "gospel_pending": "غير جاهز بعد: ما زال رقم غير الموصول إليهم ومفتاح مصدر البيانات وشكل الواجهة قيد التقرير."
}
diff --git a/library/i18n/locales/ar/search.json b/library/i18n/locales/ar/search.json
index 44526b8ac..6df068929 100644
--- a/library/i18n/locales/ar/search.json
+++ b/library/i18n/locales/ar/search.json
@@ -24,5 +24,11 @@
"allData": "كل بيانات DOXA"
},
"upg100Placeholder": "ابحث عن شعب أو قائمة UPG-100 أو منطقة…",
+ "tabPlaceholder": {
+ "languageFamilies": "ابحث في عائلة اللغة أو اللغة أو اللهجة/التنوع",
+ "affinityBlocks": "ابحث في كتلة التقارب أو المجموعة أو الشعب",
+ "regions": "ابحث في المنطقة أو الدولة أو الشعب",
+ "religion": "ابحث في الديانة أو الشعب"
+ },
"placeholderRegions": "ابحث عن منطقة أو دولة أو شعب"
}
diff --git a/library/i18n/locales/de/dashboard.json b/library/i18n/locales/de/dashboard.json
index 217316692..ea804a50e 100644
--- a/library/i18n/locales/de/dashboard.json
+++ b/library/i18n/locales/de/dashboard.json
@@ -94,5 +94,19 @@
"fs_exit": "Vollbild beenden",
"fs_open": "Tabelle im Vollbild öffnen",
"export_selected_hint": "Alle Felder exportieren — ausgewählte Volksgruppen: {n}",
- "export_all_hint": "Alle Felder exportieren — Volksgruppen: {n}"
+ "export_all_hint": "Alle Felder exportieren — Volksgruppen: {n}",
+ "map_init_failed": "Die Karte konnte nicht starten. Prüfen Sie das Zugriffstoken und den Kartencontainer.",
+ "show_filters": "Filter einblenden",
+ "hide_filters": "Filter ausblenden — Karte vergrößern",
+ "show_charts": "Diagramme einblenden",
+ "hide_charts": "Diagramme ausblenden — Karte vergrößern",
+ "export_copied": "in die Zwischenablage kopiert",
+ "export_done": "exportiert",
+ "export_ready": "bereit",
+ "export_toast": "✓ {rows} Zeilen × {fields} Felder {how}",
+ "export_failed": "✕ Export fehlgeschlagen",
+ "pray_for_group": "Für diese Volksgruppe beten",
+ "more_information": "Weitere Informationen",
+ "title": "Forschungs-Dashboard",
+ "gospel_pending": "Noch nicht so weit: die Zahl der Unerreichten, der Schlüssel zur Datenquelle und die Gestaltung werden noch entschieden."
}
diff --git a/library/i18n/locales/de/search.json b/library/i18n/locales/de/search.json
index d609786dc..a6af3edf9 100644
--- a/library/i18n/locales/de/search.json
+++ b/library/i18n/locales/de/search.json
@@ -24,5 +24,11 @@
"religionKind": "Religion"
},
"upg100Placeholder": "Volksgruppe, UPG-100-Liste oder Region suchen…",
+ "tabPlaceholder": {
+ "languageFamilies": "Sprachfamilie, Sprache, Dialekt/Varietät suchen",
+ "affinityBlocks": "Affinitätsblock, Cluster, Volksgruppe suchen",
+ "regions": "Region, Land, Volksgruppe suchen",
+ "religion": "Religion, Volksgruppe suchen"
+ },
"placeholderRegions": "Region, Land oder Volksgruppe suchen"
}
diff --git a/library/i18n/locales/en/dashboard.json b/library/i18n/locales/en/dashboard.json
index 03231a952..bdb577afe 100644
--- a/library/i18n/locales/en/dashboard.json
+++ b/library/i18n/locales/en/dashboard.json
@@ -94,5 +94,19 @@
"fs_exit": "Exit full screen",
"fs_open": "Open table full screen",
"export_selected_hint": "Export all fields — selected people groups: {n}",
- "export_all_hint": "Export all fields — people groups: {n}"
+ "export_all_hint": "Export all fields — people groups: {n}",
+ "map_init_failed": "The map could not start. Check the access token and the map container.",
+ "show_filters": "Show filters",
+ "hide_filters": "Hide filters — grow the map",
+ "show_charts": "Show charts",
+ "hide_charts": "Hide charts — grow the map",
+ "export_copied": "copied to clipboard",
+ "export_done": "exported",
+ "export_ready": "ready",
+ "export_toast": "✓ {rows} rows × {fields} fields {how}",
+ "export_failed": "✕ Export failed",
+ "pray_for_group": "Pray for this people group",
+ "more_information": "More information",
+ "title": "Research Dashboard",
+ "gospel_pending": "Not ready yet: the lostness figure, the data-source key and the layout are still being decided."
}
diff --git a/library/i18n/locales/en/search.json b/library/i18n/locales/en/search.json
index 23ca1427c..1b79016bb 100644
--- a/library/i18n/locales/en/search.json
+++ b/library/i18n/locales/en/search.json
@@ -24,5 +24,11 @@
"allData": "All DOXA Data"
},
"upg100Placeholder": "Search people group, UPG-100 list, or region…",
+ "tabPlaceholder": {
+ "languageFamilies": "Search language family, language, dialect/variety",
+ "affinityBlocks": "Search affinity bloc, cluster, people group",
+ "regions": "Search region, country, people group",
+ "religion": "Search religion, people group"
+ },
"placeholderRegions": "Search region, country, people group"
}
diff --git a/library/i18n/locales/es/dashboard.json b/library/i18n/locales/es/dashboard.json
index 847f01560..d5a970f29 100644
--- a/library/i18n/locales/es/dashboard.json
+++ b/library/i18n/locales/es/dashboard.json
@@ -94,5 +94,19 @@
"fs_exit": "Salir de pantalla completa",
"fs_open": "Abrir la tabla a pantalla completa",
"export_selected_hint": "Exportar todos los campos — pueblos seleccionados: {n}",
- "export_all_hint": "Exportar todos los campos — pueblos: {n}"
+ "export_all_hint": "Exportar todos los campos — pueblos: {n}",
+ "map_init_failed": "El mapa no pudo iniciarse. Compruebe el token de acceso y el contenedor del mapa.",
+ "show_filters": "Mostrar filtros",
+ "hide_filters": "Ocultar filtros — ampliar el mapa",
+ "show_charts": "Mostrar gráficos",
+ "hide_charts": "Ocultar gráficos — ampliar el mapa",
+ "export_copied": "copiado al portapapeles",
+ "export_done": "exportado",
+ "export_ready": "listo",
+ "export_toast": "✓ {rows} filas × {fields} campos {how}",
+ "export_failed": "✕ Error al exportar",
+ "pray_for_group": "Ore por este pueblo",
+ "more_information": "Más información",
+ "title": "Panel de investigación",
+ "gospel_pending": "Aún no está listo: la cifra de no alcanzados, la clave de la fuente de datos y la forma de la interfaz siguen por decidir."
}
diff --git a/library/i18n/locales/es/search.json b/library/i18n/locales/es/search.json
index 6ae264dd8..ac8da59ee 100644
--- a/library/i18n/locales/es/search.json
+++ b/library/i18n/locales/es/search.json
@@ -24,5 +24,11 @@
"allData": "Todos los datos DOXA"
},
"upg100Placeholder": "Buscar pueblo, lista UPG-100 o región…",
+ "tabPlaceholder": {
+ "languageFamilies": "Buscar familia lingüística, idioma, dialecto/variedad",
+ "affinityBlocks": "Buscar bloque de afinidad, clúster, pueblo",
+ "regions": "Buscar región, país, pueblo",
+ "religion": "Buscar religión, pueblo"
+ },
"placeholderRegions": "Buscar región, país o pueblo"
}
diff --git a/library/i18n/locales/fr/dashboard.json b/library/i18n/locales/fr/dashboard.json
index 3bbf5b026..f452bce64 100644
--- a/library/i18n/locales/fr/dashboard.json
+++ b/library/i18n/locales/fr/dashboard.json
@@ -94,5 +94,19 @@
"fs_exit": "Quitter le plein écran",
"fs_open": "Ouvrir le tableau en plein écran",
"export_selected_hint": "Exporter tous les champs — peuples sélectionnés : {n}",
- "export_all_hint": "Exporter tous les champs — peuples : {n}"
+ "export_all_hint": "Exporter tous les champs — peuples : {n}",
+ "map_init_failed": "La carte n’a pas pu démarrer. Vérifiez le jeton d’accès et le conteneur de la carte.",
+ "show_filters": "Afficher les filtres",
+ "hide_filters": "Masquer les filtres — agrandir la carte",
+ "show_charts": "Afficher les graphiques",
+ "hide_charts": "Masquer les graphiques — agrandir la carte",
+ "export_copied": "copié dans le presse-papiers",
+ "export_done": "exporté",
+ "export_ready": "prêt",
+ "export_toast": "✓ {rows} lignes × {fields} champs {how}",
+ "export_failed": "✕ Échec de l’export",
+ "pray_for_group": "Prier pour ce peuple",
+ "more_information": "Plus d’informations",
+ "title": "Tableau de bord de recherche",
+ "gospel_pending": "Pas encore prêt : le chiffre des non atteints, la clé de la source de données et la forme de l’interface restent à décider."
}
diff --git a/library/i18n/locales/fr/search.json b/library/i18n/locales/fr/search.json
index 6fb5283eb..af26ea1d5 100644
--- a/library/i18n/locales/fr/search.json
+++ b/library/i18n/locales/fr/search.json
@@ -24,5 +24,11 @@
"allData": "Toutes les données DOXA"
},
"upg100Placeholder": "Rechercher un peuple, une liste UPG-100 ou une région…",
+ "tabPlaceholder": {
+ "languageFamilies": "Rechercher une famille linguistique, une langue, un dialecte/une variété",
+ "affinityBlocks": "Rechercher un bloc d’affinité, une grappe, un peuple",
+ "regions": "Rechercher une région, un pays, un peuple",
+ "religion": "Rechercher une religion, un peuple"
+ },
"placeholderRegions": "Rechercher une région, un pays, un peuple"
}
diff --git a/library/i18n/locales/hi/dashboard.json b/library/i18n/locales/hi/dashboard.json
index 95ddeb0b2..c52d4e144 100644
--- a/library/i18n/locales/hi/dashboard.json
+++ b/library/i18n/locales/hi/dashboard.json
@@ -94,5 +94,19 @@
"fs_exit": "पूर्ण स्क्रीन से बाहर निकलें",
"fs_open": "तालिका को पूर्ण स्क्रीन में खोलें",
"export_selected_hint": "सभी फ़ील्ड निर्यात करें — चयनित जनसमूह: {n}",
- "export_all_hint": "सभी फ़ील्ड निर्यात करें — जनसमूह: {n}"
+ "export_all_hint": "सभी फ़ील्ड निर्यात करें — जनसमूह: {n}",
+ "map_init_failed": "मानचित्र आरंभ नहीं हो सका। एक्सेस टोकन और मानचित्र कंटेनर जाँचें।",
+ "show_filters": "फ़िल्टर दिखाएँ",
+ "hide_filters": "फ़िल्टर छिपाएँ — मानचित्र बड़ा करें",
+ "show_charts": "चार्ट दिखाएँ",
+ "hide_charts": "चार्ट छिपाएँ — मानचित्र बड़ा करें",
+ "export_copied": "क्लिपबोर्ड पर कॉपी हुआ",
+ "export_done": "निर्यात हुआ",
+ "export_ready": "तैयार",
+ "export_toast": "✓ {rows} पंक्तियाँ × {fields} क्षेत्र {how}",
+ "export_failed": "✕ निर्यात विफल",
+ "pray_for_group": "इस जनसमूह के लिए प्रार्थना करें",
+ "more_information": "अधिक जानकारी",
+ "title": "शोध डैशबोर्ड",
+ "gospel_pending": "अभी तैयार नहीं: अनपहुँचे लोगों का आँकड़ा, डेटा स्रोत की कुंजी और अंतरफलक का रूप अभी तय हो रहे हैं।"
}
diff --git a/library/i18n/locales/hi/search.json b/library/i18n/locales/hi/search.json
index 8161c951f..5af971919 100644
--- a/library/i18n/locales/hi/search.json
+++ b/library/i18n/locales/hi/search.json
@@ -24,5 +24,11 @@
"religionKind": "धर्म"
},
"upg100Placeholder": "जनसमूह, UPG-100 सूची या क्षेत्र खोजें…",
+ "tabPlaceholder": {
+ "languageFamilies": "भाषा परिवार, भाषा, बोली/रूप खोजें",
+ "affinityBlocks": "समानता समूह, क्लस्टर, जनसमूह खोजें",
+ "regions": "क्षेत्र, देश, जनसमूह खोजें",
+ "religion": "धर्म, जनसमूह खोजें"
+ },
"placeholderRegions": "क्षेत्र, देश या जनसमूह खोजें"
}
diff --git a/library/i18n/locales/it/dashboard.json b/library/i18n/locales/it/dashboard.json
index 04ded77f2..1abb8929d 100644
--- a/library/i18n/locales/it/dashboard.json
+++ b/library/i18n/locales/it/dashboard.json
@@ -94,5 +94,19 @@
"fs_exit": "Esci da schermo intero",
"fs_open": "Apri la tabella a schermo intero",
"export_selected_hint": "Esporta tutti i campi — popoli selezionati: {n}",
- "export_all_hint": "Esporta tutti i campi — popoli: {n}"
+ "export_all_hint": "Esporta tutti i campi — popoli: {n}",
+ "map_init_failed": "La mappa non è riuscita ad avviarsi. Controlla il token di accesso e il contenitore della mappa.",
+ "show_filters": "Mostra i filtri",
+ "hide_filters": "Nascondi i filtri — ingrandisci la mappa",
+ "show_charts": "Mostra i grafici",
+ "hide_charts": "Nascondi i grafici — ingrandisci la mappa",
+ "export_copied": "copiato negli appunti",
+ "export_done": "esportato",
+ "export_ready": "pronto",
+ "export_toast": "✓ {rows} righe × {fields} campi {how}",
+ "export_failed": "✕ Esportazione non riuscita",
+ "pray_for_group": "Prega per questo popolo",
+ "more_information": "Maggiori informazioni",
+ "title": "Cruscotto di ricerca",
+ "gospel_pending": "Non ancora pronto: il numero dei non raggiunti, la chiave della fonte dati e la forma dell’interfaccia sono ancora da decidere."
}
diff --git a/library/i18n/locales/it/search.json b/library/i18n/locales/it/search.json
index e45e4c159..165fdfee0 100644
--- a/library/i18n/locales/it/search.json
+++ b/library/i18n/locales/it/search.json
@@ -24,5 +24,11 @@
"religionKind": "Religione"
},
"upg100Placeholder": "Cerca popolo, elenco UPG-100 o regione…",
+ "tabPlaceholder": {
+ "languageFamilies": "Cerca famiglia linguistica, lingua, dialetto/variante",
+ "affinityBlocks": "Cerca blocco di affinità, cluster, popolo",
+ "regions": "Cerca regione, paese, popolo",
+ "religion": "Cerca religione, popolo"
+ },
"placeholderRegions": "Cerca regione, paese o popolo"
}
diff --git a/library/i18n/locales/pt/dashboard.json b/library/i18n/locales/pt/dashboard.json
index 7d3e099a9..e09f6169c 100644
--- a/library/i18n/locales/pt/dashboard.json
+++ b/library/i18n/locales/pt/dashboard.json
@@ -94,5 +94,19 @@
"fs_exit": "Sair da tela cheia",
"fs_open": "Abrir a tabela em tela cheia",
"export_selected_hint": "Exportar todos os campos — povos selecionados: {n}",
- "export_all_hint": "Exportar todos os campos — povos: {n}"
+ "export_all_hint": "Exportar todos os campos — povos: {n}",
+ "map_init_failed": "O mapa não conseguiu iniciar. Verifique o token de acesso e o contentor do mapa.",
+ "show_filters": "Mostrar filtros",
+ "hide_filters": "Ocultar filtros — ampliar o mapa",
+ "show_charts": "Mostrar gráficos",
+ "hide_charts": "Ocultar gráficos — ampliar o mapa",
+ "export_copied": "copiado para a área de transferência",
+ "export_done": "exportado",
+ "export_ready": "pronto",
+ "export_toast": "✓ {rows} linhas × {fields} campos {how}",
+ "export_failed": "✕ Falha ao exportar",
+ "pray_for_group": "Ore por este povo",
+ "more_information": "Mais informações",
+ "title": "Painel de pesquisa",
+ "gospel_pending": "Ainda não está pronto: o número de não alcançados, a chave da fonte de dados e a forma da interface estão por decidir."
}
diff --git a/library/i18n/locales/pt/search.json b/library/i18n/locales/pt/search.json
index 8accd6dbb..b8dcbecb8 100644
--- a/library/i18n/locales/pt/search.json
+++ b/library/i18n/locales/pt/search.json
@@ -24,5 +24,11 @@
"allData": "Todos os dados DOXA"
},
"upg100Placeholder": "Procurar povo, lista UPG-100 ou região…",
+ "tabPlaceholder": {
+ "languageFamilies": "Pesquisar família linguística, idioma, dialeto/variedade",
+ "affinityBlocks": "Pesquisar bloco de afinidade, agrupamento, povo",
+ "regions": "Pesquisar região, país, povo",
+ "religion": "Pesquisar religião, povo"
+ },
"placeholderRegions": "Procurar região, país ou povo"
}
diff --git a/library/i18n/locales/ro/dashboard.json b/library/i18n/locales/ro/dashboard.json
index 4e965da62..b768298bd 100644
--- a/library/i18n/locales/ro/dashboard.json
+++ b/library/i18n/locales/ro/dashboard.json
@@ -94,5 +94,19 @@
"fs_exit": "Ieși din ecran complet",
"fs_open": "Deschide tabelul pe ecran complet",
"export_selected_hint": "Exportă toate câmpurile — popoare selectate: {n}",
- "export_all_hint": "Exportă toate câmpurile — popoare: {n}"
+ "export_all_hint": "Exportă toate câmpurile — popoare: {n}",
+ "map_init_failed": "Harta nu a putut porni. Verificați jetonul de acces și containerul hărții.",
+ "show_filters": "Arată filtrele",
+ "hide_filters": "Ascunde filtrele — mărește harta",
+ "show_charts": "Arată graficele",
+ "hide_charts": "Ascunde graficele — mărește harta",
+ "export_copied": "copiat în clipboard",
+ "export_done": "exportat",
+ "export_ready": "gata",
+ "export_toast": "✓ {rows} rânduri × {fields} câmpuri {how}",
+ "export_failed": "✕ Exportul a eșuat",
+ "pray_for_group": "Roagă-te pentru acest popor",
+ "more_information": "Mai multe informații",
+ "title": "Tablou de bord de cercetare",
+ "gospel_pending": "Încă nu este gata: cifra celor neatinși, cheia sursei de date și forma interfeței sunt încă de hotărât."
}
diff --git a/library/i18n/locales/ro/search.json b/library/i18n/locales/ro/search.json
index 380c4f024..33b41cc89 100644
--- a/library/i18n/locales/ro/search.json
+++ b/library/i18n/locales/ro/search.json
@@ -24,5 +24,11 @@
"religionKind": "Religie"
},
"upg100Placeholder": "Caută popor, listă UPG-100 sau regiune…",
+ "tabPlaceholder": {
+ "languageFamilies": "Caută familie de limbi, limbă, dialect/variantă",
+ "affinityBlocks": "Caută bloc de afinitate, grup, popor",
+ "regions": "Caută regiune, țară, popor",
+ "religion": "Caută religie, popor"
+ },
"placeholderRegions": "Caută regiune, țară sau popor"
}
diff --git a/library/i18n/locales/ru/dashboard.json b/library/i18n/locales/ru/dashboard.json
index be0d8e332..475c541ee 100644
--- a/library/i18n/locales/ru/dashboard.json
+++ b/library/i18n/locales/ru/dashboard.json
@@ -94,5 +94,19 @@
"fs_exit": "Выйти из полноэкранного режима",
"fs_open": "Открыть таблицу на весь экран",
"export_selected_hint": "Экспорт всех полей — выбрано народов: {n}",
- "export_all_hint": "Экспорт всех полей — народов: {n}"
+ "export_all_hint": "Экспорт всех полей — народов: {n}",
+ "map_init_failed": "Карта не запустилась. Проверьте токен доступа и контейнер карты.",
+ "show_filters": "Показать фильтры",
+ "hide_filters": "Скрыть фильтры — расширить карту",
+ "show_charts": "Показать диаграммы",
+ "hide_charts": "Скрыть диаграммы — расширить карту",
+ "export_copied": "скопировано в буфер обмена",
+ "export_done": "экспортировано",
+ "export_ready": "готово",
+ "export_toast": "✓ {rows} строк × {fields} полей {how}",
+ "export_failed": "✕ Не удалось экспортировать",
+ "pray_for_group": "Молиться об этом народе",
+ "more_information": "Подробнее",
+ "title": "Исследовательская панель",
+ "gospel_pending": "Пока не готово: число непросвещённых, ключ к источнику данных и вид интерфейса ещё определяются."
}
diff --git a/library/i18n/locales/ru/search.json b/library/i18n/locales/ru/search.json
index aaf424e70..c1475c794 100644
--- a/library/i18n/locales/ru/search.json
+++ b/library/i18n/locales/ru/search.json
@@ -24,5 +24,11 @@
"allData": "Все данные DOXA"
},
"upg100Placeholder": "Поиск народа, списка UPG-100 или региона…",
+ "tabPlaceholder": {
+ "languageFamilies": "Поиск по языковой семье, языку, диалекту/варианту",
+ "affinityBlocks": "Поиск по блоку родства, кластеру, народу",
+ "regions": "Поиск по региону, стране, народу",
+ "religion": "Поиск по религии, народу"
+ },
"placeholderRegions": "Поиск региона, страны или народа"
}
diff --git a/library/i18n/locales/zh/dashboard.json b/library/i18n/locales/zh/dashboard.json
index 6b80317a4..6b62debde 100644
--- a/library/i18n/locales/zh/dashboard.json
+++ b/library/i18n/locales/zh/dashboard.json
@@ -94,5 +94,19 @@
"fs_exit": "退出全屏",
"fs_open": "全屏打开表格",
"export_selected_hint": "导出全部字段 — 已选民族:{n}",
- "export_all_hint": "导出全部字段 — 民族:{n}"
+ "export_all_hint": "导出全部字段 — 民族:{n}",
+ "map_init_failed": "地图无法启动。请检查访问令牌与地图容器。",
+ "show_filters": "显示筛选",
+ "hide_filters": "隐藏筛选 —— 放大地图",
+ "show_charts": "显示图表",
+ "hide_charts": "隐藏图表 —— 放大地图",
+ "export_copied": "已复制到剪贴板",
+ "export_done": "已导出",
+ "export_ready": "已就绪",
+ "export_toast": "✓ {rows} 行 × {fields} 字段 {how}",
+ "export_failed": "✕ 导出失败",
+ "pray_for_group": "为这个民族祷告",
+ "more_information": "更多信息",
+ "title": "研究面板",
+ "gospel_pending": "尚未就绪:未得之民的数字、数据来源密钥与界面形式仍在确定中。"
}
diff --git a/library/i18n/locales/zh/search.json b/library/i18n/locales/zh/search.json
index 870923afa..83a48efa9 100644
--- a/library/i18n/locales/zh/search.json
+++ b/library/i18n/locales/zh/search.json
@@ -24,5 +24,11 @@
"religionKind": "宗教"
},
"upg100Placeholder": "搜索民族、UPG-100 名单或区域…",
+ "tabPlaceholder": {
+ "languageFamilies": "搜索语系、语言、方言/变体",
+ "affinityBlocks": "搜索亲缘板块、民族群、民族",
+ "regions": "搜索区域、国家、民族",
+ "religion": "搜索宗教、民族"
+ },
"placeholderRegions": "搜索区域、国家或族群"
}
diff --git a/plugin-designs/doxa-research-map/research-map.vue b/plugin-designs/doxa-research-map/research-map.vue
index dc4d9cbdf..931566b56 100644
--- a/plugin-designs/doxa-research-map/research-map.vue
+++ b/plugin-designs/doxa-research-map/research-map.vue
@@ -606,23 +606,19 @@ watch(() => activeTab.value?.colorStrategy, (strategy) => mapTheme.setLook(mapTh
const activeLegendType = computed(() => activeTab.value?.legend ?? 'default')
const activePopupAction = computed(() => activeTab.value?.popup ?? 'pray')
-// Per-tab geocoder placeholder. The Language Families tab makes the search
-// scope explicit ("Search language family, language, dialect/variety") so users
-// know the search is wired to the legend's hierarchy. Other tabs use the
-// default i18n placeholder ("Search people, place, language, religion").
+// Per-tab geocoder placeholder. The four tiered tabs name their own tiers, so the reader can see
+// the search is wired to the legend's hierarchy rather than to the map at large; the flat tabs fall
+// through to the default placeholder. Empty string means "no opinion" — GeocoderComponent then uses
+// its own i18n default, which is why an unresolved key returns '' rather than English.
+const GEOCODER_PLACEHOLDER_KEY = {
+ 'language-families': 'search.tabPlaceholder.languageFamilies',
+ 'affinity-blocks': 'search.tabPlaceholder.affinityBlocks',
+ 'doxa-regions': 'search.tabPlaceholder.regions',
+ 'religion': 'search.tabPlaceholder.religion',
+}
const geocoderPlaceholder = computed(() => {
- if (activeTab.value?.id === 'language-families') {
- return 'Search language family, language, dialect/variety'
- }
- if (activeTab.value?.id === 'affinity-blocks') {
- return 'Search affinity bloc, cluster, people group'
- }
- if (activeTab.value?.id === 'doxa-regions') {
- return 'Search region, country, people group'
- }
- if (activeTab.value?.id === 'religion') {
- return 'Search religion, people group'
- }
+ const key = GEOCODER_PLACEHOLDER_KEY[activeTab.value?.id]
+ if (key && te(key)) return t(key)
return '' // empty → GeocoderComponent falls back to i18n default
})
@@ -3103,8 +3099,10 @@ onMounted(async () => {
if (!map.value) {
// The code has already concluded the map is dead — say so instead of leaving
// the loading line up, which is indistinguishable from "still loading".
+ // The console line stays English: it is for whoever is debugging. What the READER sees comes
+ // from the catalogue, and says what to do rather than naming the internals that failed.
console.error('[research-map] Map failed to initialize — check token and container')
- mapError.value = 'Map failed to initialize — check token and container'
+ mapError.value = t('dashboard.map_init_failed')
return
}
map.value.once('style.load', async () => {
diff --git a/plugin-designs/research-dashboard/research-dashboard.vue b/plugin-designs/research-dashboard/research-dashboard.vue
index 073370bcb..c4c18b3bb 100644
--- a/plugin-designs/research-dashboard/research-dashboard.vue
+++ b/plugin-designs/research-dashboard/research-dashboard.vue
@@ -817,10 +817,10 @@ onMounted(() => {
diff --git a/plugin-designs/research-dashboard/src/components/dashboards/DashboardChartsRail.vue b/plugin-designs/research-dashboard/src/components/dashboards/DashboardChartsRail.vue
index 831e16ad1..49c8f414d 100644
--- a/plugin-designs/research-dashboard/src/components/dashboards/DashboardChartsRail.vue
+++ b/plugin-designs/research-dashboard/src/components/dashboards/DashboardChartsRail.vue
@@ -40,7 +40,7 @@ const tabs = [
id: 'gospel',
get label() { return tStatic('dashboard.gospel_resources') || 'Gospel Resources' },
disabled: true,
- why: 'Waiting on three decisions: the official lostness number, key-less vs a registered Joshua Project key, and the UI shape. See IDEA-GOSPEL-RESOURCES-INTEGRATION-STRATEGY.',
+ get why() { return tStatic('dashboard.gospel_pending') || 'Not ready yet: the lostness figure, the data-source key and the layout are still being decided.' },
},
]
const activeTab = ref('charts')
diff --git a/plugin-designs/research-dashboard/src/components/dashboards/DashboardDataTable.vue b/plugin-designs/research-dashboard/src/components/dashboards/DashboardDataTable.vue
index 74fdb83f8..db65406e6 100644
--- a/plugin-designs/research-dashboard/src/components/dashboards/DashboardDataTable.vue
+++ b/plugin-designs/research-dashboard/src/components/dashboards/DashboardDataTable.vue
@@ -18,6 +18,7 @@ import { useCsvExport } from '@map/composables/useCsvExport.js'
import { formatCount } from '@map/utils/formatCount.js'
import { useTreeLegendLabels } from '../../composables/useTreeLegendLabels.js'
import { tStatic } from '@map/i18n/index.js'
+import { fillParams } from '@map/composables/useOptionalT.js'
// This table's chrome, read out of the catalogue. tStatic and not useI18n, for the reason the rest
// of this tree gives: these components mount inside custom elements that install no i18n plugin,
@@ -561,10 +562,16 @@ async function onExport() {
exporting.value = true
try {
const r = await csv.exportCsvAsync()
- const how = r.downloaded ? 'exported' : r.clipboard ? 'copied to clipboard' : 'ready'
- exportToast.value = `✓ ${fmt(r.rows)} rows × ${r.fields} fields ${how}`
+ // The counts keep going through fmt(), which groups digits the way the locale does; only the
+ // words around them come from the catalogue. Interpolated rather than concatenated, because the
+ // order of "rows", "fields" and the outcome is not the same in every language.
+ const how = r.downloaded ? T('dashboard.export_done', 'exported')
+ : r.clipboard ? T('dashboard.export_copied', 'copied to clipboard')
+ : T('dashboard.export_ready', 'ready')
+ exportToast.value = fillParams(T('dashboard.export_toast', '✓ {rows} rows × {fields} fields {how}'),
+ { rows: fmt(r.rows), fields: r.fields, how })
} catch (e) {
- exportToast.value = '✕ Export failed'
+ exportToast.value = T('dashboard.export_failed', '✕ Export failed')
} finally {
exporting.value = false
clearTimeout(_toastT); _toastT = setTimeout(() => { exportToast.value = '' }, 3200)
@@ -805,7 +812,7 @@ useShadowStyles(`
bus and any future caller. This also deletes the one control in the cluster that read
as "a button that doesn't make sense" -- a bare x with no label. -->
- {{ fmt(selCount) }} selected
+ {{ fmt(selCount) }} {{ T('dashboard.selected', 'Selected') }}
diff --git a/plugin-designs/research-dashboard/src/components/dashboards/DashboardDetailDrawer.vue b/plugin-designs/research-dashboard/src/components/dashboards/DashboardDetailDrawer.vue
index 69eb98d3f..4703f5f58 100644
--- a/plugin-designs/research-dashboard/src/components/dashboards/DashboardDetailDrawer.vue
+++ b/plugin-designs/research-dashboard/src/components/dashboards/DashboardDetailDrawer.vue
@@ -96,8 +96,8 @@ const moreInfoHref = computed(() => {
const def = `${origin}/research/${encodeURIComponent(r.slug || '')}/?utm_source=${utm}`
return resolveUrl(detailCfg.value.moreInfoUrl, r, def)
})
-const prayLabel = computed(() => detailCfg.value.prayLabel || 'Pray for this people group')
-const moreInfoLabel = computed(() => detailCfg.value.moreInfoLabel || 'More information')
+const prayLabel = computed(() => detailCfg.value.prayLabel || T('dashboard.pray_for_group', 'Pray for this people group'))
+const moreInfoLabel = computed(() => detailCfg.value.moreInfoLabel || T('dashboard.more_information', 'More information'))
function exportThis() {
if (!exporter || !bus) return
diff --git a/plugin-designs/research-dashboard/src/components/dashboards/DashboardResearchFilters.vue b/plugin-designs/research-dashboard/src/components/dashboards/DashboardResearchFilters.vue
index f22c83e3e..18f8f854f 100644
--- a/plugin-designs/research-dashboard/src/components/dashboards/DashboardResearchFilters.vue
+++ b/plugin-designs/research-dashboard/src/components/dashboards/DashboardResearchFilters.vue
@@ -111,7 +111,7 @@ const dropdownKeys = computed(() => {
const configured = rf.value.dropdowns || ['wagf_region', 'wagf_block', 'country_code', 'people_group', 'religion']
return [PRAYER_KEY, ...configured.filter((k) => k !== PRAYER_KEY)]
})
-const searchPlaceholder = computed(() => rf.value.searchPlaceholder || 'Search people groups, place, language, religion…')
+const searchPlaceholder = computed(() => rf.value.searchPlaceholder || tStatic('dashboard.search_placeholder') || 'Search people groups, place, language, religion…')
// Apply a filter-set (facetKey -> label) to a row list — reused for cross-filtered counts.
function applyWith(fset, list) {
diff --git a/plugin-designs/research-dashboard/src/components/dashboards/DashboardTopBar.vue b/plugin-designs/research-dashboard/src/components/dashboards/DashboardTopBar.vue
index d3a69c205..ad2d8401c 100644
--- a/plugin-designs/research-dashboard/src/components/dashboards/DashboardTopBar.vue
+++ b/plugin-designs/research-dashboard/src/components/dashboards/DashboardTopBar.vue
@@ -119,7 +119,9 @@ function onAlignLayout() { if (bus) bus.emit('resetLayout') }
// The value the panels honor lives on the bus; this bar reads/writes it via bus.searchTerm /
// bus.setSearch, so it is the SAME free-text search the whole surface re-derives against.
const filterSearch = computed(() => bus?.searchTerm?.value || '')
-const filterSearchPlaceholder = computed(() => cfg.researchFilters?.searchPlaceholder || 'Search people groups, place, language, religion…')
+// The config's own wording wins when an instance sets one; otherwise the catalogue, not an English
+// constant — dashboard.search_placeholder is the same sentence, already translated everywhere.
+const filterSearchPlaceholder = computed(() => cfg.researchFilters?.searchPlaceholder || L('search_placeholder', 'Search people groups, place, language, religion…'))
function onFilterSearch(payload) { bus?.setSearch(payload?.value ?? '') }
// ── live typeahead search over the in-memory row store ──
@@ -304,7 +306,7 @@ useShadowStyles(`
cfg.title alone would run under it. It costs nothing and it is the guard that fails safe if
anyone re-adds a variable-width member here. -->