Translate the dashboard chrome and the map's tab-scoped search
Eighteen keys across eleven locales for text that was written in English in the components: the rail and filter toggles, the selection count, the CSV export toast, the drawer's prayer and information links, the dashboard title, the map's failure message, and the four search placeholders that name the tiers of whichever tab is open. The export toast is interpolated rather than concatenated. Its counts keep going through fmt(), which groups digits the way each locale does, and only the words around them come from the catalogue — the order of "rows", "fields" and the outcome is not the same in every language, so a sentence built by joining fragments cannot be translated at all. It uses the shared fillParams, which leaves a placeholder visible when a value is missing instead of rendering the word "null". The map's failure message no longer names the token and the container: that is for the console line, which stays English for whoever is debugging. What the reader sees says what to do. Three more shapes the scan could not see, each of them correct code: tStatic called directly rather than through a wrapper; `config || t(key) || English`, where the translator is not the immediate left operand because || is left-associative; and the licence credits, media queries and product name that must not be translated at all. Own leaks 91 -> 1, the last being a tab label behind a key that resolves in all eleven locales. Fleet 249 -> 159. check:i18n-parity, never-translated, treeshake and stopped-scope all pass.
This commit is contained in:
parent
181f723bf8
commit
8d8b38a4b2
30 changed files with 287 additions and 41 deletions
|
|
@ -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 = `<template><p>{{ x }}</p></template>
|
||||
<script setup>
|
||||
import { tStatic } from '@map/i18n/index.js'
|
||||
const a = cfg.x || tStatic('dashboard.map_loading') || 'Research map loading…'
|
||||
const b = cfg.y || tStatic('dashboard.no_key_here') || 'This chain has no resolving key and must count'
|
||||
const c = cfg.z || 'A plain config fallback with no translator at all must count'
|
||||
</script>`
|
||||
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')
|
||||
|
|
|
|||
|
|
@ -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": "غير جاهز بعد: ما زال رقم غير الموصول إليهم ومفتاح مصدر البيانات وشكل الواجهة قيد التقرير."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,5 +24,11 @@
|
|||
"allData": "كل بيانات DOXA"
|
||||
},
|
||||
"upg100Placeholder": "ابحث عن شعب أو قائمة UPG-100 أو منطقة…",
|
||||
"tabPlaceholder": {
|
||||
"languageFamilies": "ابحث في عائلة اللغة أو اللغة أو اللهجة/التنوع",
|
||||
"affinityBlocks": "ابحث في كتلة التقارب أو المجموعة أو الشعب",
|
||||
"regions": "ابحث في المنطقة أو الدولة أو الشعب",
|
||||
"religion": "ابحث في الديانة أو الشعب"
|
||||
},
|
||||
"placeholderRegions": "ابحث عن منطقة أو دولة أو شعب"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": "अभी तैयार नहीं: अनपहुँचे लोगों का आँकड़ा, डेटा स्रोत की कुंजी और अंतरफलक का रूप अभी तय हो रहे हैं।"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,5 +24,11 @@
|
|||
"religionKind": "धर्म"
|
||||
},
|
||||
"upg100Placeholder": "जनसमूह, UPG-100 सूची या क्षेत्र खोजें…",
|
||||
"tabPlaceholder": {
|
||||
"languageFamilies": "भाषा परिवार, भाषा, बोली/रूप खोजें",
|
||||
"affinityBlocks": "समानता समूह, क्लस्टर, जनसमूह खोजें",
|
||||
"regions": "क्षेत्र, देश, जनसमूह खोजें",
|
||||
"religion": "धर्म, जनसमूह खोजें"
|
||||
},
|
||||
"placeholderRegions": "क्षेत्र, देश या जनसमूह खोजें"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": "Пока не готово: число непросвещённых, ключ к источнику данных и вид интерфейса ещё определяются."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,5 +24,11 @@
|
|||
"allData": "Все данные DOXA"
|
||||
},
|
||||
"upg100Placeholder": "Поиск народа, списка UPG-100 или региона…",
|
||||
"tabPlaceholder": {
|
||||
"languageFamilies": "Поиск по языковой семье, языку, диалекту/варианту",
|
||||
"affinityBlocks": "Поиск по блоку родства, кластеру, народу",
|
||||
"regions": "Поиск по региону, стране, народу",
|
||||
"religion": "Поиск по религии, народу"
|
||||
},
|
||||
"placeholderRegions": "Поиск региона, страны или народа"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": "尚未就绪:未得之民的数字、数据来源密钥与界面形式仍在确定中。"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,5 +24,11 @@
|
|||
"religionKind": "宗教"
|
||||
},
|
||||
"upg100Placeholder": "搜索民族、UPG-100 名单或区域…",
|
||||
"tabPlaceholder": {
|
||||
"languageFamilies": "搜索语系、语言、方言/变体",
|
||||
"affinityBlocks": "搜索亲缘板块、民族群、民族",
|
||||
"regions": "搜索区域、国家、民族",
|
||||
"religion": "搜索宗教、民族"
|
||||
},
|
||||
"placeholderRegions": "搜索区域、国家或族群"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -817,10 +817,10 @@ onMounted(() => {
|
|||
<!-- (the floating "▴ Show table" restore button is gone: the table's own header bar — with its
|
||||
chevron, live count and Export CSV — now stays docked under the map while collapsed) -->
|
||||
<button v-if="!isNarrow" class="rd-edge-caret rd-edge-facet" type="button" data-toggle-facet
|
||||
:title="facetCollapsed ? 'Show filters' : 'Hide filters — grow the map'"
|
||||
:title="facetCollapsed ? T('dashboard.show_filters', 'Show filters') : T('dashboard.hide_filters', 'Hide filters — grow the map')"
|
||||
@click="toggleFacet">{{ facetCollapsed ? '▸' : '◂' }}</button>
|
||||
<button v-if="!isNarrow" class="rd-edge-caret rd-edge-rail" type="button" data-toggle-rail
|
||||
:title="railCollapsed ? 'Show charts' : 'Hide charts — grow the map'"
|
||||
:title="railCollapsed ? T('dashboard.show_charts', 'Show charts') : T('dashboard.hide_charts', 'Hide charts — grow the map')"
|
||||
@click="toggleRail">{{ railCollapsed ? '◂' : '▸' }}</button>
|
||||
</div>
|
||||
<!-- The table CELL stays mounted+visible when collapsed; DashboardDataTable hides only its own
|
||||
|
|
@ -892,7 +892,7 @@ onMounted(() => {
|
|||
<button v-for="s in SEG" :key="s.k" :class="{ on: mobileView === s.k }" data-view-btn :data-view="s.k">{{ s.l }}</button>
|
||||
</div>
|
||||
<div v-if="selCount" class="rd-exportbar" data-export-bar @click="onBarClick">
|
||||
<b>{{ selCount }} selected</b><span class="sp"></span>
|
||||
<b>{{ selCount }} {{ T('dashboard.selected', 'Selected') }}</b><span class="sp"></span>
|
||||
<button class="x-clr" data-bar-clear>{{ T('controls.clear', 'Clear') }}</button>
|
||||
<!-- The glyph stays a glyph and the words come from the catalogue — one spelling of this
|
||||
action across the dashboard, not two. -->
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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. -->
|
||||
<span v-if="selCount" class="rd-pill" data-sel-pill role="status">
|
||||
<span>{{ fmt(selCount) }} selected</span>
|
||||
<span>{{ fmt(selCount) }} {{ T('dashboard.selected', 'Selected') }}</span>
|
||||
</span>
|
||||
<!-- Selection VIEWS — separated from the pill because they mutate the VIEW, not the
|
||||
selection. Only meaningful once something is selected, so they appear with it. -->
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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. -->
|
||||
<div class="rd-left">
|
||||
<span class="rd-brand"><span class="rd-ttl">{{ cfg.title || 'Research Dashboard' }}<span class="dot">.</span></span></span>
|
||||
<span class="rd-brand"><span class="rd-ttl">{{ cfg.title || L('title', 'Research Dashboard') }}<span class="dot">.</span></span></span>
|
||||
</div>
|
||||
<div class="rd-filter-search" data-filter-search>
|
||||
<SearchBar
|
||||
|
|
|
|||
Loading…
Reference in a new issue