diff --git a/internal/check-kpi-unit-once.mjs b/internal/check-kpi-unit-once.mjs index eee606189..47e7c2c1b 100644 --- a/internal/check-kpi-unit-once.mjs +++ b/internal/check-kpi-unit-once.mjs @@ -25,7 +25,7 @@ * * NEGATIVE-TESTED: restoring either label ('% unreached' or 'Adopted %') fails by name with exit 1. */ -import { readFileSync, existsSync } from 'node:fs' +import { readFileSync, existsSync, readdirSync } from 'node:fs' import { resolve, dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' @@ -41,10 +41,22 @@ const src = readFileSync(CFG, 'utf8') // Strip comments before scanning. This file now DOCUMENTS the defect in a comment right above the // fixed line, and a naive scan matches that prose and fails on the explanation of the fix — the // same self-reference that has bitten three gates in this repo already. +// The translated labels live in the catalogue, so the rule has to reach them too. +const LOC_DIR = resolve(ROOT, 'library/i18n/locales') +const LOCALES = existsSync(LOC_DIR) ? readdirSync(LOC_DIR) : [] +const catalogue = (loc) => { + try { return JSON.parse(readFileSync(resolve(LOC_DIR, loc, 'dashboard.json'), 'utf8')) } catch { return {} } +} + const code = src.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '') -// Each KPI entry is a single-line-ish object literal: { id: '…', label: '…', kind: '…', … } -const entries = [...code.matchAll(/\{[^{}]*\bid:\s*'([^']+)'[^{}]*\}/g)] +// Each KPI entry is an object literal: { id: '…', label: '…', kind: '…', … }. +// ★ IT MAY NOW CONTAIN ONE LEVEL OF NESTED BRACES. The labels became GETTERS when the dashboard was +// translated (`get label() { return tStatic('dashboard.unreached') || 'unreached' }`), and the old +// single-brace pattern stopped matching every entry — the gate found ZERO pct KPIs and failed +// exactly as it was built to, rather than printing a green PASS about nothing. That refusal is the +// gate working; this is the parse catching up with the config, not a relaxation of the rule. +const entries = [...code.matchAll(/\{(?:[^{}]|\{[^{}]*\})*\bid:\s*'([^']+)'(?:[^{}]|\{[^{}]*\})*\}/g)] const offenders = [] let pctSeen = 0 @@ -53,8 +65,20 @@ for (const m of entries) { const kind = (body.match(/\bkind:\s*'([^']+)'/) || [])[1] if (kind !== 'pct') continue pctSeen++ - const label = (body.match(/\blabel:\s*'([^']*)'/) || [])[1] + // Two label forms: a plain literal, or a getter that falls back to one. + const plain = (body.match(/\blabel:\s*'([^']*)'/) || [])[1] + const viaGetter = body.match(/get label\(\)\s*\{[^}]*?tStatic\('([^']+)'\)\s*\|\|\s*'([^']*)'/) + const label = plain ?? (viaGetter ? viaGetter[2] : undefined) if (label && label.includes('%')) offenders.push({ id: m[1], label }) + // ★ AND CHECK WHAT A NON-ENGLISH READER SEES. The rule is about the rendered label carrying a + // second percent sign, and a translation can reintroduce one where the English fallback has none. + if (viaGetter) { + const key = viaGetter[1].replace(/^dashboard\./, '') + for (const loc of LOCALES) { + const v = catalogue(loc)[key] + if (typeof v === 'string' && v.includes('%')) offenders.push({ id: m[1] + ' [' + loc + ']', label: v }) + } + } } if (!pctSeen) { diff --git a/library/components/ResearchMapFilterPanel.vue b/library/components/ResearchMapFilterPanel.vue index 80f8cea0e..6f8a54a5f 100644 --- a/library/components/ResearchMapFilterPanel.vue +++ b/library/components/ResearchMapFilterPanel.vue @@ -20,6 +20,9 @@ * useMapData when normalizing). If facets aren't ready yet, we render a * placeholder so the panel never blocks initial paint. */ +import { tStatic } from '@map/i18n/index.js' +// See ResearchMapSideMenu for why this reads the catalogue directly rather than through useI18n. +const L = (key, en) => tStatic('controls.' + key) || en import { inject, computed, watch, ref } from 'vue' import { useShadowStyles } from '../composables/useShadowStyles.js' @@ -141,37 +144,37 @@ useShadowStyles(`
- +
- - + +
- +
- +
- - + +
- No languages loaded yet + {{ L('no_languages', 'No languages loaded yet') }} +
diff --git a/library/components/ResearchMapSideMenu.vue b/library/components/ResearchMapSideMenu.vue index 14b67e25c..45ebddc55 100644 --- a/library/components/ResearchMapSideMenu.vue +++ b/library/components/ResearchMapSideMenu.vue @@ -32,15 +32,24 @@ defineProps({ }) const emit = defineEmits(['close', 'open-poster', 'reload-map']) +import { tStatic } from '@map/i18n/index.js' +// ★ THIS PANEL WAS ENGLISH IN ALL ELEVEN LOCALES. Measured on the composite under ?lang=ru: every +// Latin-script string left on the page belonged to this menu and its filter sibling — the map +// around them was fully translated. Read through tStatic rather than useI18n: these components are +// reachable from mounts that install no i18n plugin, and an unguarded useI18n() throws and takes +// the subtree with it (the defect that blanked people-groups-grid, fixed in 6603dc17). English +// stays as the last argument, so a missing key degrades to today's text rather than to a key name. +const L = (key, en) => tStatic('controls.' + key) || en + const mapStore = inject('mapStore') const uiStore = inject('uiStore') // ─── Cluster controls ──────────────────────────────────────────────────────── const CLUSTER_MODES = [ - { id: 'off', label: 'Off', desc: 'Show every pin individually' }, - { id: 'mapbox', label: 'Native', desc: 'Mapbox circle clusters (fastest)' }, - { id: 'mst', label: 'MST', desc: 'Minimum-spanning-tree affinity webs' }, - { id: 'network', label: 'Network', desc: 'Nearest-neighbor connection lines' } + { id: 'off', label: L('off', 'Off'), desc: L('off_desc', 'Show every pin individually') }, + { id: 'mapbox', label: L('native', 'Native'), desc: L('native_desc', 'Mapbox circle clusters (fastest)') }, + { id: 'mst', label: L('mst', 'MST'), desc: L('mst_desc', 'Minimum-spanning-tree affinity webs') }, + { id: 'network', label: L('network', 'Network'), desc: L('network_desc', 'Nearest-neighbor connection lines') } ] const clusterMode = computed({ get: () => mapStore?.clusteringMode ?? 'off', @@ -87,14 +96,14 @@ useShadowStyles(`
diff --git a/library/i18n/index.js b/library/i18n/index.js index 0e6d72305..ec33aec32 100644 --- a/library/i18n/index.js +++ b/library/i18n/index.js @@ -93,34 +93,6 @@ export function detectLocale() { /** * Build a fresh i18n instance. Called once per element. */ -/** - * Read a catalogue string WITHOUT the vue-i18n plugin. - * - * WHY THIS EXISTS. Some components are mounted as their OWN custom element by a bundle entry that - * calls defineCustomElement(Component) and installs nothing — people-groups-grid is one. useI18n() - * inside such a component THROWS, and a thrown setup() removes the subtree silently (this repo has - * already paid for that once: three buttons vanished behind ignorable console noise). So a shared - * component that must work in BOTH mounts needs a plugin-free reader, which is all this is. - * - * Falls back locale -> en -> undefined, so a caller can keep its own literal as the last resort and - * a missing translation degrades to English rather than to a key. - * - * @param {string} path dotted, e.g. 'grid.hide_filters' - * @param {string} [locale] defaults to the detected one (?lang= / host / browser) - */ -export function tStatic(path, locale) { - const tryTree = (loc) => { - let node = messages[loc] - for (const part of String(path).split('.')) { - if (!node || typeof node !== 'object') return undefined - node = node[part] - } - return typeof node === 'string' ? node : undefined - } - const want = locale || detectLocale() - return tryTree(want) ?? tryTree('en') -} - export function createAppI18n() { const locale = detectLocale() // ⚠ REFLECT AN RTL LOCALE ON THE DOCUMENT. Only one component reads RTL_LOCALES @@ -149,3 +121,31 @@ export function createAppI18n() { } export default createAppI18n + +/** + * Read a catalogue string WITHOUT the vue-i18n plugin. + * + * WHY THIS EXISTS. Some components are mounted as their OWN custom element by a bundle entry that + * calls defineCustomElement(Component) and installs nothing — people-groups-grid is one. useI18n() + * inside such a component THROWS, and a thrown setup() removes the subtree silently (this repo has + * already paid for that once: three buttons vanished behind ignorable console noise). So a shared + * component that must work in BOTH mounts needs a plugin-free reader, which is all this is. + * + * Falls back locale -> en -> undefined, so a caller can keep its own literal as the last resort and + * a missing translation degrades to English rather than to a key. + * + * @param {string} path dotted, e.g. 'grid.hide_filters' + * @param {string} [locale] defaults to the detected one (?lang= / host / browser) + */ +export function tStatic(path, locale) { + const tryTree = (loc) => { + let node = messages[loc] + for (const part of String(path).split('.')) { + if (!node || typeof node !== 'object') return undefined + node = node[part] + } + return typeof node === 'string' ? node : undefined + } + const want = locale || detectLocale() + return tryTree(want) ?? tryTree('en') +} diff --git a/library/i18n/locales/ar/controls.json b/library/i18n/locales/ar/controls.json new file mode 100644 index 000000000..46bb80f87 --- /dev/null +++ b/library/i18n/locales/ar/controls.json @@ -0,0 +1,32 @@ +{ + "research_controls": "أدوات البحث", + "clustering": "التجميع", + "filters": "عوامل التصفية", + "pin_dim": "خفوت النقاط", + "export_poster": "تصدير ملصق…", + "reload_data": "إعادة تحميل البيانات", + "off": "إيقاف", + "native": "أصلي", + "mst": "الشجرة الممتدة", + "network": "شبكة", + "off_desc": "إظهار كل نقطة على حدة", + "native_desc": "تجميع دوائر Mapbox (الأسرع)", + "mst_desc": "شبكات تقارب بالشجرة الممتدة الصغرى", + "network_desc": "خطوط ربط بأقرب جار", + "search_languages": "ابحث عن اللغات…", + "search_religions": "ابحث عن الأديان…", + "reset_all": "إعادة تعيين الكل", + "no_languages": "لم تُحمَّل أي لغات بعد", + "no_religions": "لم تُحمَّل أي أديان بعد", + "unadopted": "غير متبنّى", + "min": "الأدنى", + "max": "الأعلى", + "adopted": "متبنَّاة", + "adoption": "التبني", + "engaged": "مُخْدُومَة", + "engagement": "المشاركة", + "unengaged": "يحتاج إلى انخراط", + "language": "اللغة", + "religion": "الدين", + "population": "السكان" +} diff --git a/library/i18n/locales/ar/dashboard.json b/library/i18n/locales/ar/dashboard.json new file mode 100644 index 000000000..c844d6b9a --- /dev/null +++ b/library/i18n/locales/ar/dashboard.json @@ -0,0 +1,35 @@ +{ + "bloc": "كتلة التقارب", + "gospel_access": "الوصول إلى الإنجيل", + "people": "الشعوب", + "people_name": "الشعب (الاسم)", + "praying": "يصلّون", + "religion_family": "عائلة الأديان", + "search_language": "ابحث عن لغة", + "search_people": "ابحث عن شعب", + "search_place": "ابحث عن مكان", + "search_religion": "ابحث عن دين", + "upgs_in_scope": "الشعوب ضمن النطاق", + "countries_touched": "الدول المشمولة", + "unreached": "غير موصولين", + "sum_population": "مجموع السكان", + "sum_praying": "مجموع المصلّين", + "charts": "الرسوم البيانية", + "table": "الجدول", + "cluster": "المجموعة", + "country": "الدولة", + "engagement": "المشاركة", + "language": "اللغة", + "language_family": "عائلة اللغة", + "people_group": "الشعب", + "population": "السكان", + "prayer": "الصلاة", + "region": "المنطقة", + "religion": "الدين", + "status": "الحالة", + "adopted": "متبنَّاة", + "filters": "عوامل التصفية", + "gospel_resources": "موارد الإنجيل", + "map": "الخريطة", + "search_placeholder": "ابحث عن الشعوب والأماكن واللغات والأديان…" +} diff --git a/library/i18n/locales/ar/grid.json b/library/i18n/locales/ar/grid.json new file mode 100644 index 000000000..eacfca689 --- /dev/null +++ b/library/i18n/locales/ar/grid.json @@ -0,0 +1,19 @@ +{ + "search": "ابحث عن الشعوب…", + "hide_filters": "إخفاء عوامل التصفية", + "show_filters": "إظهار عوامل التصفية", + "total": "الإجمالي", + "showing": "المعروض", + "load_more": "عرض المزيد", + "full_profile": "الملف الكامل", + "wagf_region": "المنطقة", + "wagf_block": "كتلة التقارب", + "rop1": "الشعب", + "religion": "الدين", + "exact_filter": "تطابق تام", + "clear_all": "مسح الكل", + "loading": "جارٍ التحميل…", + "no_results": "لا توجد شعوب مطابقة لهذا التصفية.", + "type_to_search": "اكتب للبحث...", + "load_failed": "تعذّر تحميل بيانات الشعوب. هذه مشكلة اتصال، وليست مشكلة في التصفية." +} diff --git a/library/i18n/locales/de/controls.json b/library/i18n/locales/de/controls.json new file mode 100644 index 000000000..c7a602056 --- /dev/null +++ b/library/i18n/locales/de/controls.json @@ -0,0 +1,32 @@ +{ + "research_controls": "Forschungssteuerung", + "clustering": "Clustering", + "filters": "Filter", + "pin_dim": "Pin-Transparenz", + "export_poster": "Poster exportieren…", + "reload_data": "Daten neu laden", + "off": "Aus", + "native": "Nativ", + "mst": "MST", + "network": "Netzwerk", + "off_desc": "Jeden Pin einzeln anzeigen", + "native_desc": "Mapbox-Kreiscluster (am schnellsten)", + "mst_desc": "Affinitätsnetze über minimalen Spannbaum", + "network_desc": "Verbindungslinien zum nächsten Nachbarn", + "search_languages": "Sprachen suchen…", + "search_religions": "Religionen suchen…", + "reset_all": "Alles zurücksetzen", + "no_languages": "Noch keine Sprachen geladen", + "no_religions": "Noch keine Religionen geladen", + "unadopted": "Nicht adoptiert", + "min": "min", + "max": "max", + "adopted": "Adoptiert", + "adoption": "Adoption", + "engaged": "Engagiert", + "engagement": "Engagement", + "unengaged": "Braucht Engagement", + "language": "Sprache", + "religion": "Religion", + "population": "Bevölkerung" +} diff --git a/library/i18n/locales/de/dashboard.json b/library/i18n/locales/de/dashboard.json new file mode 100644 index 000000000..8efa44919 --- /dev/null +++ b/library/i18n/locales/de/dashboard.json @@ -0,0 +1,35 @@ +{ + "bloc": "Affinitätsblock", + "gospel_access": "Zugang zum Evangelium", + "people": "Völker", + "people_name": "Volk (Name)", + "praying": "Betend", + "religion_family": "Religionsfamilie", + "search_language": "Sprache suchen", + "search_people": "Volk suchen", + "search_place": "Ort suchen", + "search_religion": "Religion suchen", + "upgs_in_scope": "UPGs im Bereich", + "countries_touched": "berührte Länder", + "unreached": "unerreicht", + "sum_population": "Σ Bevölkerung", + "sum_praying": "Σ Betende", + "charts": "Diagramme", + "table": "Tabelle", + "cluster": "Cluster", + "country": "Land", + "engagement": "Engagement", + "language": "Sprache", + "language_family": "Sprachfamilie", + "people_group": "Volk", + "population": "Bevölkerung", + "prayer": "Gebet", + "region": "Region", + "religion": "Religion", + "status": "Status", + "adopted": "Adoptiert", + "filters": "Filter", + "gospel_resources": "Evangeliumsressourcen", + "map": "Karte", + "search_placeholder": "Volksgruppen, Orte, Sprachen, Religionen suchen…" +} diff --git a/library/i18n/locales/de/grid.json b/library/i18n/locales/de/grid.json new file mode 100644 index 000000000..12d5b5586 --- /dev/null +++ b/library/i18n/locales/de/grid.json @@ -0,0 +1,19 @@ +{ + "search": "Volksgruppen suchen…", + "hide_filters": "Filter ausblenden", + "show_filters": "Filter anzeigen", + "total": "Gesamt", + "showing": "Angezeigt", + "load_more": "Mehr laden", + "full_profile": "Vollständiges Profil", + "wagf_region": "Region", + "wagf_block": "Affinitätsblock", + "rop1": "Volk", + "religion": "Religion", + "exact_filter": "Exakt", + "clear_all": "Alle löschen", + "loading": "Wird geladen…", + "no_results": "Keine Volksgruppen entsprechen diesem Filter.", + "type_to_search": "Zum Suchen tippen...", + "load_failed": "Die Daten konnten nicht geladen werden. Das ist ein Verbindungsproblem, nicht Ihr Filter." +} diff --git a/library/i18n/locales/en/controls.json b/library/i18n/locales/en/controls.json new file mode 100644 index 000000000..8205e3a0b --- /dev/null +++ b/library/i18n/locales/en/controls.json @@ -0,0 +1,32 @@ +{ + "research_controls": "Research controls", + "clustering": "Clustering", + "filters": "Filters", + "pin_dim": "Pin dim", + "export_poster": "Export poster…", + "reload_data": "Reload data", + "off": "Off", + "native": "Native", + "mst": "MST", + "network": "Network", + "off_desc": "Show every pin individually", + "native_desc": "Mapbox circle clusters (fastest)", + "mst_desc": "Minimum-spanning-tree affinity webs", + "network_desc": "Nearest-neighbor connection lines", + "search_languages": "Search languages…", + "search_religions": "Search religions…", + "reset_all": "Reset all", + "no_languages": "No languages loaded yet", + "no_religions": "No religions loaded yet", + "unadopted": "Unadopted", + "min": "min", + "max": "max", + "adopted": "Adopted", + "adoption": "Adoption", + "engaged": "Engaged", + "engagement": "Engagement", + "unengaged": "Unengaged", + "language": "Language", + "religion": "Religion", + "population": "Population" +} diff --git a/library/i18n/locales/en/dashboard.json b/library/i18n/locales/en/dashboard.json new file mode 100644 index 000000000..97f82875d --- /dev/null +++ b/library/i18n/locales/en/dashboard.json @@ -0,0 +1,35 @@ +{ + "bloc": "Bloc", + "gospel_access": "Gospel Access", + "people": "People", + "people_name": "People (name)", + "praying": "Praying", + "religion_family": "Religion Family", + "search_language": "Search language", + "search_people": "Search people", + "search_place": "Search place", + "search_religion": "Search religion", + "upgs_in_scope": "UPGs in scope", + "countries_touched": "countries touched", + "unreached": "unreached", + "sum_population": "Σ population", + "sum_praying": "Σ praying", + "charts": "Charts", + "table": "Table", + "cluster": "Cluster", + "country": "Country", + "engagement": "Engagement", + "language": "Language", + "language_family": "Language Family", + "people_group": "People Group", + "population": "Population", + "prayer": "Prayer", + "region": "Region", + "religion": "Religion", + "status": "Status", + "adopted": "Adopted", + "filters": "Filters", + "gospel_resources": "Gospel Resources", + "map": "Map", + "search_placeholder": "Search people groups, place, language, religion…" +} diff --git a/library/i18n/locales/en/grid.json b/library/i18n/locales/en/grid.json new file mode 100644 index 000000000..1f36ada1b --- /dev/null +++ b/library/i18n/locales/en/grid.json @@ -0,0 +1,19 @@ +{ + "search": "Search people groups…", + "hide_filters": "Hide Filters", + "show_filters": "Show Filters", + "total": "Total", + "showing": "Showing", + "load_more": "Load more", + "full_profile": "Full profile", + "wagf_region": "Region", + "wagf_block": "Affinity Block", + "rop1": "People Group", + "religion": "Religion", + "exact_filter": "Exact", + "clear_all": "Clear All", + "loading": "Loading…", + "no_results": "No people groups match this filter.", + "type_to_search": "Type to search...", + "load_failed": "People-group data could not be loaded. This is a connection problem, not your filter." +} diff --git a/library/i18n/locales/es/controls.json b/library/i18n/locales/es/controls.json new file mode 100644 index 000000000..e7fec3ff2 --- /dev/null +++ b/library/i18n/locales/es/controls.json @@ -0,0 +1,32 @@ +{ + "research_controls": "Controles de investigación", + "clustering": "Agrupación", + "filters": "Filtros", + "pin_dim": "Atenuar puntos", + "export_poster": "Exportar póster…", + "reload_data": "Recargar datos", + "off": "Desactivado", + "native": "Nativo", + "mst": "MST", + "network": "Red", + "off_desc": "Mostrar cada punto por separado", + "native_desc": "Grupos circulares de Mapbox (lo más rápido)", + "mst_desc": "Redes de afinidad por árbol de expansión mínima", + "network_desc": "Líneas de conexión al vecino más cercano", + "search_languages": "Buscar idiomas…", + "search_religions": "Buscar religiones…", + "reset_all": "Restablecer todo", + "no_languages": "Aún no se han cargado idiomas", + "no_religions": "Aún no se han cargado religiones", + "unadopted": "No adoptado", + "min": "mín", + "max": "máx", + "adopted": "Adoptado", + "adoption": "Adopción", + "engaged": "Comprometido", + "engagement": "Compromiso", + "unengaged": "Necesita implicación", + "language": "Idioma", + "religion": "Religión", + "population": "Población" +} diff --git a/library/i18n/locales/es/dashboard.json b/library/i18n/locales/es/dashboard.json new file mode 100644 index 000000000..d77b3f83f --- /dev/null +++ b/library/i18n/locales/es/dashboard.json @@ -0,0 +1,35 @@ +{ + "bloc": "Bloque de afinidad", + "gospel_access": "Acceso al evangelio", + "people": "Pueblos", + "people_name": "Pueblo (nombre)", + "praying": "Orando", + "religion_family": "Familia religiosa", + "search_language": "Buscar idioma", + "search_people": "Buscar pueblo", + "search_place": "Buscar lugar", + "search_religion": "Buscar religión", + "upgs_in_scope": "UPG en el ámbito", + "countries_touched": "países alcanzados", + "unreached": "no alcanzados", + "sum_population": "Σ población", + "sum_praying": "Σ orando", + "charts": "Gráficos", + "table": "Tabla", + "cluster": "Clúster", + "country": "País", + "engagement": "Compromiso", + "language": "Idioma", + "language_family": "Familia lingüística", + "people_group": "Pueblo", + "population": "Población", + "prayer": "Oración", + "region": "Región", + "religion": "Religión", + "status": "Estado", + "adopted": "Adoptado", + "filters": "Filtros", + "gospel_resources": "Recursos del Evangelio", + "map": "Mapa", + "search_placeholder": "Buscar pueblos, lugares, idiomas, religiones…" +} diff --git a/library/i18n/locales/es/grid.json b/library/i18n/locales/es/grid.json new file mode 100644 index 000000000..efb79fb4d --- /dev/null +++ b/library/i18n/locales/es/grid.json @@ -0,0 +1,19 @@ +{ + "search": "Buscar pueblos…", + "hide_filters": "Ocultar filtros", + "show_filters": "Mostrar filtros", + "total": "Total", + "showing": "Mostrando", + "load_more": "Cargar más", + "full_profile": "Perfil completo", + "wagf_region": "Región", + "wagf_block": "Bloque de afinidad", + "rop1": "Pueblo", + "religion": "Religión", + "exact_filter": "Exacto", + "clear_all": "Borrar todo", + "loading": "Cargando…", + "no_results": "Ningún pueblo coincide con este filtro.", + "type_to_search": "Escribe para buscar...", + "load_failed": "No se pudieron cargar los datos de pueblos. Es un problema de conexión, no de tu filtro." +} diff --git a/library/i18n/locales/fr/controls.json b/library/i18n/locales/fr/controls.json new file mode 100644 index 000000000..c55f3e720 --- /dev/null +++ b/library/i18n/locales/fr/controls.json @@ -0,0 +1,32 @@ +{ + "research_controls": "Commandes de recherche", + "clustering": "Regroupement", + "filters": "Filtres", + "pin_dim": "Atténuation des points", + "export_poster": "Exporter l’affiche…", + "reload_data": "Recharger les données", + "off": "Désactivé", + "native": "Natif", + "mst": "MST", + "network": "Réseau", + "off_desc": "Afficher chaque point séparément", + "native_desc": "Groupes circulaires Mapbox (le plus rapide)", + "mst_desc": "Réseaux d’affinité par arbre couvrant minimal", + "network_desc": "Lignes vers le plus proche voisin", + "search_languages": "Rechercher des langues…", + "search_religions": "Rechercher des religions…", + "reset_all": "Tout réinitialiser", + "no_languages": "Aucune langue chargée pour l’instant", + "no_religions": "Aucune religion chargée pour l’instant", + "unadopted": "Non adopté", + "min": "min", + "max": "max", + "adopted": "Adopté", + "adoption": "Adoption", + "engaged": "Engagé", + "engagement": "Engagement", + "unengaged": "Besoin d'engagement", + "language": "Langue", + "religion": "Religion", + "population": "Population" +} diff --git a/library/i18n/locales/fr/dashboard.json b/library/i18n/locales/fr/dashboard.json new file mode 100644 index 000000000..22cf08bf9 --- /dev/null +++ b/library/i18n/locales/fr/dashboard.json @@ -0,0 +1,35 @@ +{ + "bloc": "Bloc d’affinité", + "gospel_access": "Accès à l’Évangile", + "people": "Peuples", + "people_name": "Peuple (nom)", + "praying": "En prière", + "religion_family": "Famille religieuse", + "search_language": "Rechercher une langue", + "search_people": "Rechercher un peuple", + "search_place": "Rechercher un lieu", + "search_religion": "Rechercher une religion", + "upgs_in_scope": "UPG dans le périmètre", + "countries_touched": "pays touchés", + "unreached": "non atteints", + "sum_population": "Σ population", + "sum_praying": "Σ en prière", + "charts": "Graphiques", + "table": "Tableau", + "cluster": "Grappe", + "country": "Pays", + "engagement": "Engagement", + "language": "Langue", + "language_family": "Famille linguistique", + "people_group": "Peuple", + "population": "Population", + "prayer": "Prière", + "region": "Région", + "religion": "Religion", + "status": "Statut", + "adopted": "Adopté", + "filters": "Filtres", + "gospel_resources": "Ressources de l'Évangile", + "map": "Carte", + "search_placeholder": "Rechercher peuples, lieux, langues, religions…" +} diff --git a/library/i18n/locales/fr/grid.json b/library/i18n/locales/fr/grid.json new file mode 100644 index 000000000..272ae11d5 --- /dev/null +++ b/library/i18n/locales/fr/grid.json @@ -0,0 +1,19 @@ +{ + "search": "Rechercher des peuples…", + "hide_filters": "Masquer les filtres", + "show_filters": "Afficher les filtres", + "total": "Total", + "showing": "Affichage", + "load_more": "Charger plus", + "full_profile": "Profil complet", + "wagf_region": "Région", + "wagf_block": "Bloc d'affinité", + "rop1": "Peuple", + "religion": "Religion", + "exact_filter": "Exact", + "clear_all": "Tout effacer", + "loading": "Chargement…", + "no_results": "Aucun peuple ne correspond à ce filtre.", + "type_to_search": "Tapez pour rechercher...", + "load_failed": "Les données n’ont pas pu être chargées. C’est un problème de connexion, pas votre filtre." +} diff --git a/library/i18n/locales/hi/controls.json b/library/i18n/locales/hi/controls.json new file mode 100644 index 000000000..8cab106f5 --- /dev/null +++ b/library/i18n/locales/hi/controls.json @@ -0,0 +1,32 @@ +{ + "research_controls": "शोध नियंत्रण", + "clustering": "क्लस्टरिंग", + "filters": "फ़िल्टर", + "pin_dim": "पिन धुंधलापन", + "export_poster": "पोस्टर निर्यात करें…", + "reload_data": "डेटा फिर से लोड करें", + "off": "बंद", + "native": "नेटिव", + "mst": "MST", + "network": "नेटवर्क", + "off_desc": "हर पिन अलग-अलग दिखाएँ", + "native_desc": "Mapbox वृत्त क्लस्टर (सबसे तेज़)", + "mst_desc": "न्यूनतम स्पैनिंग ट्री आधारित जाल", + "network_desc": "निकटतम पड़ोसी को जोड़ने वाली रेखाएँ", + "search_languages": "भाषाएँ खोजें…", + "search_religions": "धर्म खोजें…", + "reset_all": "सब रीसेट करें", + "no_languages": "अभी कोई भाषा लोड नहीं हुई", + "no_religions": "अभी कोई धर्म लोड नहीं हुआ", + "unadopted": "अ-गोद लिया", + "min": "न्यून", + "max": "अधि", + "adopted": "गोद लिया गया", + "adoption": "गोद लेना", + "engaged": "संपर्कित", + "engagement": "सहभागिता", + "unengaged": "सहभागिता चाहिए", + "language": "भाषा", + "religion": "धर्म", + "population": "जनसंख्या" +} diff --git a/library/i18n/locales/hi/dashboard.json b/library/i18n/locales/hi/dashboard.json new file mode 100644 index 000000000..52376f0da --- /dev/null +++ b/library/i18n/locales/hi/dashboard.json @@ -0,0 +1,35 @@ +{ + "bloc": "समानता समूह", + "gospel_access": "सुसमाचार पहुँच", + "people": "जनसमूह", + "people_name": "जनसमूह (नाम)", + "praying": "प्रार्थना कर रहे", + "religion_family": "धर्म परिवार", + "search_language": "भाषा खोजें", + "search_people": "जनसमूह खोजें", + "search_place": "स्थान खोजें", + "search_religion": "धर्म खोजें", + "upgs_in_scope": "दायरे में UPG", + "countries_touched": "छुए गए देश", + "unreached": "अनपहुँचे", + "sum_population": "Σ जनसंख्या", + "sum_praying": "Σ प्रार्थना", + "charts": "चार्ट", + "table": "तालिका", + "cluster": "क्लस्टर", + "country": "देश", + "engagement": "सहभागिता", + "language": "भाषा", + "language_family": "भाषा परिवार", + "people_group": "जनसमूह", + "population": "जनसंख्या", + "prayer": "प्रार्थना", + "region": "क्षेत्र", + "religion": "धर्म", + "status": "स्थिति", + "adopted": "गोद लिया गया", + "filters": "फ़िल्टर", + "gospel_resources": "सुसमाचार संसाधन", + "map": "नक्शा", + "search_placeholder": "जनसमूह, स्थान, भाषा, धर्म खोजें…" +} diff --git a/library/i18n/locales/hi/grid.json b/library/i18n/locales/hi/grid.json new file mode 100644 index 000000000..3df36809d --- /dev/null +++ b/library/i18n/locales/hi/grid.json @@ -0,0 +1,19 @@ +{ + "search": "जनसमूह खोजें…", + "hide_filters": "फ़िल्टर छिपाएँ", + "show_filters": "फ़िल्टर दिखाएँ", + "total": "कुल", + "showing": "दिखा रहे हैं", + "load_more": "और देखें", + "full_profile": "पूरी प्रोफ़ाइल", + "wagf_region": "क्षेत्र", + "wagf_block": "समानता समूह", + "rop1": "जनसमूह", + "religion": "धर्म", + "exact_filter": "सटीक", + "clear_all": "सब हटाएँ", + "loading": "लोड हो रहा है…", + "no_results": "इस फ़िल्टर से कोई जनसमूह मेल नहीं खाता।", + "type_to_search": "खोजने के लिए लिखें...", + "load_failed": "जनसमूह डेटा लोड नहीं हो सका। यह कनेक्शन की समस्या है, आपके फ़िल्टर की नहीं।" +} diff --git a/library/i18n/locales/it/controls.json b/library/i18n/locales/it/controls.json new file mode 100644 index 000000000..20806e8c5 --- /dev/null +++ b/library/i18n/locales/it/controls.json @@ -0,0 +1,32 @@ +{ + "research_controls": "Controlli di ricerca", + "clustering": "Raggruppamento", + "filters": "Filtri", + "pin_dim": "Opacità dei punti", + "export_poster": "Esporta poster…", + "reload_data": "Ricarica i dati", + "off": "Disattivato", + "native": "Nativo", + "mst": "MST", + "network": "Rete", + "off_desc": "Mostra ogni punto singolarmente", + "native_desc": "Cluster circolari Mapbox (il più veloce)", + "mst_desc": "Reti di affinità ad albero ricoprente minimo", + "network_desc": "Linee verso il vicino più prossimo", + "search_languages": "Cerca lingue…", + "search_religions": "Cerca religioni…", + "reset_all": "Reimposta tutto", + "no_languages": "Nessuna lingua ancora caricata", + "no_religions": "Nessuna religione ancora caricata", + "unadopted": "Non adottato", + "min": "min", + "max": "max", + "adopted": "Adottato", + "adoption": "Adozione", + "engaged": "Coinvolto", + "engagement": "Impegno", + "unengaged": "Necessita impegno", + "language": "Lingua", + "religion": "Religione", + "population": "Popolazione" +} diff --git a/library/i18n/locales/it/dashboard.json b/library/i18n/locales/it/dashboard.json new file mode 100644 index 000000000..f8b750910 --- /dev/null +++ b/library/i18n/locales/it/dashboard.json @@ -0,0 +1,35 @@ +{ + "bloc": "Blocco di affinità", + "gospel_access": "Accesso al vangelo", + "people": "Popoli", + "people_name": "Popolo (nome)", + "praying": "In preghiera", + "religion_family": "Famiglia religiosa", + "search_language": "Cerca lingua", + "search_people": "Cerca popolo", + "search_place": "Cerca luogo", + "search_religion": "Cerca religione", + "upgs_in_scope": "UPG nell’ambito", + "countries_touched": "paesi toccati", + "unreached": "non raggiunti", + "sum_population": "Σ popolazione", + "sum_praying": "Σ in preghiera", + "charts": "Grafici", + "table": "Tabella", + "cluster": "Cluster", + "country": "Paese", + "engagement": "Impegno", + "language": "Lingua", + "language_family": "Famiglia linguistica", + "people_group": "Popolo", + "population": "Popolazione", + "prayer": "Preghiera", + "region": "Regione", + "religion": "Religione", + "status": "Stato", + "adopted": "Adottato", + "filters": "Filtri", + "gospel_resources": "Risorse del Vangelo", + "map": "Mappa", + "search_placeholder": "Cerca popoli, luoghi, lingue, religioni…" +} diff --git a/library/i18n/locales/it/grid.json b/library/i18n/locales/it/grid.json new file mode 100644 index 000000000..6781f3de0 --- /dev/null +++ b/library/i18n/locales/it/grid.json @@ -0,0 +1,19 @@ +{ + "search": "Cerca popoli…", + "hide_filters": "Nascondi filtri", + "show_filters": "Mostra filtri", + "total": "Totale", + "showing": "Mostrati", + "load_more": "Carica altro", + "full_profile": "Profilo completo", + "wagf_region": "Regione", + "wagf_block": "Blocco di affinità", + "rop1": "Popolo", + "religion": "Religione", + "exact_filter": "Esatto", + "clear_all": "Cancella tutto", + "loading": "Caricamento…", + "no_results": "Nessun popolo corrisponde a questo filtro.", + "type_to_search": "Digita per cercare...", + "load_failed": "Impossibile caricare i dati dei popoli. È un problema di connessione, non del filtro." +} diff --git a/library/i18n/locales/pt/controls.json b/library/i18n/locales/pt/controls.json new file mode 100644 index 000000000..ac9287181 --- /dev/null +++ b/library/i18n/locales/pt/controls.json @@ -0,0 +1,32 @@ +{ + "research_controls": "Controlos de pesquisa", + "clustering": "Agrupamento", + "filters": "Filtros", + "pin_dim": "Esbater pontos", + "export_poster": "Exportar cartaz…", + "reload_data": "Recarregar dados", + "off": "Desligado", + "native": "Nativo", + "mst": "MST", + "network": "Rede", + "off_desc": "Mostrar cada ponto individualmente", + "native_desc": "Grupos circulares Mapbox (mais rápido)", + "mst_desc": "Redes de afinidade por árvore geradora mínima", + "network_desc": "Linhas para o vizinho mais próximo", + "search_languages": "Pesquisar idiomas…", + "search_religions": "Pesquisar religiões…", + "reset_all": "Repor tudo", + "no_languages": "Ainda não há idiomas carregados", + "no_religions": "Ainda não há religiões carregadas", + "unadopted": "Não adotado", + "min": "mín", + "max": "máx", + "adopted": "Adotado", + "adoption": "Adoção", + "engaged": "Engajado", + "engagement": "Engajamento", + "unengaged": "Precisa de envolvimento", + "language": "Idioma", + "religion": "Religião", + "population": "População" +} diff --git a/library/i18n/locales/pt/dashboard.json b/library/i18n/locales/pt/dashboard.json new file mode 100644 index 000000000..fe25e0877 --- /dev/null +++ b/library/i18n/locales/pt/dashboard.json @@ -0,0 +1,35 @@ +{ + "bloc": "Bloco de afinidade", + "gospel_access": "Acesso ao evangelho", + "people": "Povos", + "people_name": "Povo (nome)", + "praying": "A orar", + "religion_family": "Família religiosa", + "search_language": "Pesquisar idioma", + "search_people": "Pesquisar povo", + "search_place": "Pesquisar lugar", + "search_religion": "Pesquisar religião", + "upgs_in_scope": "UPG no âmbito", + "countries_touched": "países alcançados", + "unreached": "não alcançados", + "sum_population": "Σ população", + "sum_praying": "Σ a orar", + "charts": "Gráficos", + "table": "Tabela", + "cluster": "Agrupamento", + "country": "País", + "engagement": "Engajamento", + "language": "Idioma", + "language_family": "Família linguística", + "people_group": "Povo", + "population": "População", + "prayer": "Oração", + "region": "Região", + "religion": "Religião", + "status": "Status", + "adopted": "Adotado", + "filters": "Filtros", + "gospel_resources": "Recursos do Evangelho", + "map": "Mapa", + "search_placeholder": "Pesquisar povos, lugares, idiomas, religiões…" +} diff --git a/library/i18n/locales/pt/grid.json b/library/i18n/locales/pt/grid.json new file mode 100644 index 000000000..e1136ed89 --- /dev/null +++ b/library/i18n/locales/pt/grid.json @@ -0,0 +1,19 @@ +{ + "search": "Pesquisar povos…", + "hide_filters": "Ocultar filtros", + "show_filters": "Mostrar filtros", + "total": "Total", + "showing": "Mostrando", + "load_more": "Carregar mais", + "full_profile": "Perfil completo", + "wagf_region": "Região", + "wagf_block": "Bloco de afinidade", + "rop1": "Povo", + "religion": "Religião", + "exact_filter": "Exato", + "clear_all": "Limpar tudo", + "loading": "A carregar…", + "no_results": "Nenhum povo corresponde a este filtro.", + "type_to_search": "Escreva para pesquisar...", + "load_failed": "Não foi possível carregar os dados dos povos. É um problema de ligação, não do seu filtro." +} diff --git a/library/i18n/locales/ro/controls.json b/library/i18n/locales/ro/controls.json new file mode 100644 index 000000000..0d35378a0 --- /dev/null +++ b/library/i18n/locales/ro/controls.json @@ -0,0 +1,32 @@ +{ + "research_controls": "Comenzi de cercetare", + "clustering": "Grupare", + "filters": "Filtre", + "pin_dim": "Estompare puncte", + "export_poster": "Exportă posterul…", + "reload_data": "Reîncarcă datele", + "off": "Oprit", + "native": "Nativ", + "mst": "MST", + "network": "Rețea", + "off_desc": "Arată fiecare punct separat", + "native_desc": "Grupuri circulare Mapbox (cel mai rapid)", + "mst_desc": "Rețele de afinitate prin arbore minim de acoperire", + "network_desc": "Linii către cel mai apropiat vecin", + "search_languages": "Caută limbi…", + "search_religions": "Caută religii…", + "reset_all": "Resetează tot", + "no_languages": "Nicio limbă încărcată încă", + "no_religions": "Nicio religie încărcată încă", + "unadopted": "Neadoptat", + "min": "min", + "max": "max", + "adopted": "Adoptat", + "adoption": "Adopție", + "engaged": "Implicat", + "engagement": "Implicare", + "unengaged": "Necesită implicare", + "language": "Limbă", + "religion": "Religie", + "population": "Populație" +} diff --git a/library/i18n/locales/ro/dashboard.json b/library/i18n/locales/ro/dashboard.json new file mode 100644 index 000000000..f5dc016da --- /dev/null +++ b/library/i18n/locales/ro/dashboard.json @@ -0,0 +1,35 @@ +{ + "bloc": "Bloc de afinitate", + "gospel_access": "Acces la Evanghelie", + "people": "Popoare", + "people_name": "Popor (nume)", + "praying": "Se roagă", + "religion_family": "Familie religioasă", + "search_language": "Caută limbă", + "search_people": "Caută popor", + "search_place": "Caută loc", + "search_religion": "Caută religie", + "upgs_in_scope": "UPG în domeniu", + "countries_touched": "țări atinse", + "unreached": "neatinși", + "sum_population": "Σ populație", + "sum_praying": "Σ se roagă", + "charts": "Grafice", + "table": "Tabel", + "cluster": "Grup", + "country": "Țară", + "engagement": "Implicare", + "language": "Limbă", + "language_family": "Familie de limbi", + "people_group": "Popor", + "population": "Populație", + "prayer": "Rugăciune", + "region": "Regiune", + "religion": "Religie", + "status": "Stare", + "adopted": "Adoptat", + "filters": "Filtre", + "gospel_resources": "Resurse ale Evangheliei", + "map": "Hartă", + "search_placeholder": "Caută popoare, locuri, limbi, religii…" +} diff --git a/library/i18n/locales/ro/grid.json b/library/i18n/locales/ro/grid.json new file mode 100644 index 000000000..163d25bfc --- /dev/null +++ b/library/i18n/locales/ro/grid.json @@ -0,0 +1,19 @@ +{ + "search": "Caută popoare…", + "hide_filters": "Ascunde filtrele", + "show_filters": "Arată filtrele", + "total": "Total", + "showing": "Se afișează", + "load_more": "Încarcă mai mult", + "full_profile": "Profil complet", + "wagf_region": "Regiune", + "wagf_block": "Bloc de afinitate", + "rop1": "Popor", + "religion": "Religie", + "exact_filter": "Exact", + "clear_all": "Șterge tot", + "loading": "Se încarcă…", + "no_results": "Niciun popor nu corespunde acestui filtru.", + "type_to_search": "Scrie pentru a căuta...", + "load_failed": "Datele despre popoare nu au putut fi încărcate. Este o problemă de conexiune, nu a filtrului." +} diff --git a/library/i18n/locales/ru/controls.json b/library/i18n/locales/ru/controls.json new file mode 100644 index 000000000..e1bd36236 --- /dev/null +++ b/library/i18n/locales/ru/controls.json @@ -0,0 +1,32 @@ +{ + "research_controls": "Инструменты исследования", + "clustering": "Кластеризация", + "filters": "Фильтры", + "pin_dim": "Прозрачность точек", + "export_poster": "Экспорт постера…", + "reload_data": "Обновить данные", + "off": "Выкл.", + "native": "Встроенная", + "mst": "MST", + "network": "Сеть", + "off_desc": "Показывать каждую точку отдельно", + "native_desc": "Круговые кластеры Mapbox (быстрее всего)", + "mst_desc": "Сети родства по минимальному остовному дереву", + "network_desc": "Линии до ближайшего соседа", + "search_languages": "Поиск языков…", + "search_religions": "Поиск религий…", + "reset_all": "Сбросить всё", + "no_languages": "Языки ещё не загружены", + "no_religions": "Религии ещё не загружены", + "unadopted": "Не усыновлён", + "min": "мин", + "max": "макс", + "adopted": "Ответственность взята", + "adoption": "Усыновление", + "engaged": "Вовлечён", + "engagement": "Вовлечённость", + "unengaged": "Нуждается в вовлечённости", + "language": "Язык", + "religion": "Религия", + "population": "Население" +} diff --git a/library/i18n/locales/ru/dashboard.json b/library/i18n/locales/ru/dashboard.json new file mode 100644 index 000000000..7ef3fd8b9 --- /dev/null +++ b/library/i18n/locales/ru/dashboard.json @@ -0,0 +1,35 @@ +{ + "bloc": "Блок родства", + "gospel_access": "Доступ к Евангелию", + "people": "Народы", + "people_name": "Народ (название)", + "praying": "Молятся", + "religion_family": "Семья религий", + "search_language": "Поиск языка", + "search_people": "Поиск народа", + "search_place": "Поиск места", + "search_religion": "Поиск религии", + "upgs_in_scope": "UPG в выборке", + "countries_touched": "затронутых стран", + "unreached": "неохваченные", + "sum_population": "Σ население", + "sum_praying": "Σ молящихся", + "charts": "Графики", + "table": "Таблица", + "cluster": "Кластер", + "country": "Страна", + "engagement": "Вовлечённость", + "language": "Язык", + "language_family": "Языковая семья", + "people_group": "Народ", + "population": "Население", + "prayer": "Молитва", + "region": "Регион", + "religion": "Религия", + "status": "Статус", + "adopted": "Ответственность взята", + "filters": "Фильтры", + "gospel_resources": "Евангельские ресурсы", + "map": "Карта", + "search_placeholder": "Поиск народов, мест, языков, религий…" +} diff --git a/library/i18n/locales/ru/grid.json b/library/i18n/locales/ru/grid.json new file mode 100644 index 000000000..8bf36653b --- /dev/null +++ b/library/i18n/locales/ru/grid.json @@ -0,0 +1,19 @@ +{ + "search": "Поиск народов…", + "hide_filters": "Скрыть фильтры", + "show_filters": "Показать фильтры", + "total": "Всего", + "showing": "Показано", + "load_more": "Показать ещё", + "full_profile": "Полный профиль", + "wagf_region": "Регион", + "wagf_block": "Блок родства", + "rop1": "Народ", + "religion": "Религия", + "exact_filter": "Точное совпадение", + "clear_all": "Очистить всё", + "loading": "Загрузка…", + "no_results": "Нет народов, соответствующих этому фильтру.", + "type_to_search": "Введите для поиска...", + "load_failed": "Не удалось загрузить данные о народах. Это проблема соединения, а не фильтра." +} diff --git a/library/i18n/locales/zh/controls.json b/library/i18n/locales/zh/controls.json new file mode 100644 index 000000000..8414f4dfb --- /dev/null +++ b/library/i18n/locales/zh/controls.json @@ -0,0 +1,32 @@ +{ + "research_controls": "研究控件", + "clustering": "聚类", + "filters": "筛选", + "pin_dim": "圆点淡化", + "export_poster": "导出海报…", + "reload_data": "重新加载数据", + "off": "关闭", + "native": "原生", + "mst": "MST", + "network": "网络", + "off_desc": "逐个显示每个圆点", + "native_desc": "Mapbox 圆形聚类(最快)", + "mst_desc": "最小生成树亲缘网络", + "network_desc": "最近邻连接线", + "search_languages": "搜索语言…", + "search_religions": "搜索宗教…", + "reset_all": "全部重置", + "no_languages": "尚未加载语言", + "no_religions": "尚未加载宗教", + "unadopted": "未认领", + "min": "最小", + "max": "最大", + "adopted": "已认领", + "adoption": "认领", + "engaged": "已参与", + "engagement": "参与", + "unengaged": "需要参与", + "language": "语言", + "religion": "宗教", + "population": "人口" +} diff --git a/library/i18n/locales/zh/dashboard.json b/library/i18n/locales/zh/dashboard.json new file mode 100644 index 000000000..c797b1401 --- /dev/null +++ b/library/i18n/locales/zh/dashboard.json @@ -0,0 +1,35 @@ +{ + "bloc": "亲缘板块", + "gospel_access": "福音可及性", + "people": "民族", + "people_name": "民族(名称)", + "praying": "正在代祷", + "religion_family": "宗教族系", + "search_language": "搜索语言", + "search_people": "搜索民族", + "search_place": "搜索地点", + "search_religion": "搜索宗教", + "upgs_in_scope": "范围内的 UPG", + "countries_touched": "涉及国家", + "unreached": "未得之民", + "sum_population": "Σ 人口", + "sum_praying": "Σ 代祷", + "charts": "图表", + "table": "表格", + "cluster": "民族群", + "country": "國家", + "engagement": "参与", + "language": "语言", + "language_family": "语系", + "people_group": "民族", + "population": "人口", + "prayer": "祷告", + "region": "区域", + "religion": "宗教", + "status": "状态", + "adopted": "已认领", + "filters": "筛选", + "gospel_resources": "福音资源", + "map": "地图", + "search_placeholder": "搜索民族、地点、语言、宗教…" +} diff --git a/library/i18n/locales/zh/grid.json b/library/i18n/locales/zh/grid.json new file mode 100644 index 000000000..fc29aa052 --- /dev/null +++ b/library/i18n/locales/zh/grid.json @@ -0,0 +1,19 @@ +{ + "search": "搜索民族…", + "hide_filters": "隐藏筛选", + "show_filters": "显示筛选", + "total": "总计", + "showing": "显示", + "load_more": "加载更多", + "full_profile": "完整档案", + "wagf_region": "区域", + "wagf_block": "亲缘板块", + "rop1": "民族", + "religion": "宗教", + "exact_filter": "精确", + "clear_all": "全部清除", + "loading": "加载中…", + "no_results": "没有符合此筛选条件的民族。", + "type_to_search": "输入以搜索...", + "load_failed": "无法加载民族数据。这是连接问题,而不是筛选问题。" +} diff --git a/plugin-designs/research-dashboard/dashboard.config.js b/plugin-designs/research-dashboard/dashboard.config.js index 6c9e0845e..b3004fbd2 100644 --- a/plugin-designs/research-dashboard/dashboard.config.js +++ b/plugin-designs/research-dashboard/dashboard.config.js @@ -9,11 +9,22 @@ * migrated bundle behaves identically. */ import { lbl } from './src/composables/useDashboardAggregations.js' +import { tStatic } from '@map/i18n/index.js' import { RELIGION_FAMILIES } from '@map/colors/religion.js' import { canonicalFamilyName } from '@map/colors/language-family.js' import langFamilyByIso from '@map/data/langFamilyByIso.json' import { getPrayerLevel, LABELS as PRAYER_LABELS } from '@map/colors/prayer-progress.js' +// ★ EVERY LABEL IN THIS CONFIG WAS ENGLISH IN ALL ELEVEN LOCALES. Measured under ?lang=ru on Big +// Picture: the facet rail, chart titles, KPI strip and table headers read Region / Bloc / Country / +// Religion while the map beside them was fully translated. +// They are declared with GETTER SYNTAX (`get label() {...}`) rather than resolved values, because +// this module is imported ONCE at load while the locale is only known later (?lang=, the host's +// profile-config, ). A getter resolves on every read, so one config object serves +// whichever locale asks — and note that SPREADING a getter would not work: the spread copies the +// resolved value, not the accessor, which is the trap this comment exists to stop. +// tStatic, not useI18n: plain config, no component instance, and mounts that install no i18n. + // same numeric coercion the DataTable uses for its num columns const num = (v) => { if (typeof v === 'number') return v; const n = parseFloat(String(v ?? '').replace(/[^0-9.\-]/g, '')); return isNaN(n) ? null : n } @@ -67,14 +78,14 @@ export default { // Returns the human LABEL, which is both the display text and the bus value: the same // value-is-label contract every non-People axis follows. prayer_level: { - label: 'Prayer', + get label() { return tStatic('dashboard.prayer') || 'Prayer' }, get: (r) => { const n = r?.people_praying const raw = n && typeof n === 'object' ? n.value : n return PRAYER_LABELS[getPrayerLevel({ peoplePraying: raw ?? null })] }, }, - religion: { label: 'Religion', get: (r) => lbl(r.religion) }, + religion: { get label() { return tStatic('dashboard.religion') || 'Religion' }, get: (r) => lbl(r.religion) }, // ── Religion TIER 1 (card BUILD-STACKED-TREE-LEGEND) ── // The Doxa API's religion code is 1-3 chars whose FIRST LETTER is the family // ('MSN' → 'M' → Islam), which is precisely how the cap-3 map paints religion pins @@ -89,7 +100,7 @@ export default { // Measured live (2,107 rows, 2026-09-20): 10 families, all named in RELIGION_FAMILIES, // family totals sum exactly to the leaf-code totals. religion_family: { - label: 'Religion Family', + get label() { return tStatic('dashboard.religion_family') || 'Religion Family' }, // ★ NO LONGER dashboard-only (2026-09-23). This facet's value is DERIVED ("Hinduism" from // the religion code's first letter), which for months meant no grid row field could match // it -- the honest stopgap was a dashboardOnly flag the rail rendered as "updates this @@ -109,13 +120,13 @@ export default { return RELIGION_FAMILIES[c] ? c : '' }, }, - wagf_region: { label: 'Region', get: (r) => lbl(r.wagf_region) }, + wagf_region: { get label() { return tStatic('dashboard.region') || 'Region' }, get: (r) => lbl(r.wagf_region) }, // "Bloc" (NOT "Block") — a bloc is a SUBSET of a region, per the WAGF place hierarchy // region > bloc > country. Display label only; the facet KEY stays `wagf_block` because the // API field, the fieldMappings, and every consumer prop (wagfBlock) are spelled that way. - wagf_block: { label: 'Bloc', get: (r) => lbl(r.wagf_block) }, - country_code: { label: 'Country', get: (r) => lbl(r.country_code), geoIso: (r) => r.country_code && r.country_code.value }, - engagement_status: { label: 'Status', get: (r) => lbl(r.engagement_status) }, + wagf_block: { get label() { return tStatic('dashboard.bloc') || 'Bloc' }, get: (r) => lbl(r.wagf_block) }, + country_code: { get label() { return tStatic('dashboard.country') || 'Country' }, get: (r) => lbl(r.country_code), geoIso: (r) => r.country_code && r.country_code.value }, + engagement_status: { get label() { return tStatic('dashboard.status') || 'Status' }, get: (r) => lbl(r.engagement_status) }, // ── THE PHASES OF ENGAGEMENT ────────────────────────────────────────────────────────── // `engagement_status` above is a BINARY flag — measured live over 2,108 rows it is 2,081 // Unengaged / 27 Engaged. imb_gsec is the ordinal ladder underneath it (present 2,106/2,108): @@ -130,7 +141,7 @@ export default { // A REAL row field, so unlike language_family/religion_family this one CAN move the grid: 5 // distinct {value,label} pairs with no value-vs-label cross-collision ('0' never reads as a // label), which is the same safety property that made country_code and cluster wireable. - gospel_access: { label: 'Gospel Access', get: (r) => lbl(r.imb_gsec) }, + gospel_access: { get label() { return tStatic('dashboard.gospel_access') || 'Gospel Access' }, get: (r) => lbl(r.imb_gsec) }, // ── Language TIER 1 — API value FIRST, ISO lookup as FALLBACK ── // MEASURED on the live API (2,107 rows, 2026-09-20): `imb_language_family` is null on // 2,073 of them — it is populated for just 34 rows (1.6%). Read alone, the Language tree @@ -148,7 +159,7 @@ export default { // the value this returns is both the display label AND a direct palette key — one projection, // byte-match contract intact. language_family: { - label: 'Language Family', + get label() { return tStatic('dashboard.language_family') || 'Language Family' }, // ★ NO LONGER dashboard-only (2026-09-23) — same arc as religion_family above: the value // is DERIVED (imb_language_family label, else langFamilyByIso over the ISO code), and the // grid now runs the SAME derivation from the SAME @map modules (DERIVED_AXES), so the @@ -161,7 +172,7 @@ export default { return hit ? canonicalFamilyName(hit) : '' }, }, - primary_language: { label: 'Language', get: (r) => lbl(r.primary_language) }, + primary_language: { get label() { return tStatic('dashboard.language') || 'Language' }, get: (r) => lbl(r.primary_language) }, // People tier accessors — key by CODE (DashboardFacetRail resolves code→human name via // useAffinityBlockLegendData). Mirrors research-map's affinity-block → cluster → PG hierarchy. // @@ -188,19 +199,19 @@ export default { // they cannot drift the way a cross-source name lookup could. SearchFilterHeader already // supports value≠label (it resolves a chip's text via `o.value === f.value`). affinity_bloc: { - label: 'People', + get label() { return tStatic('dashboard.people') || 'People' }, get: (r) => code(r.rop1), optionLabel: (r) => lbl(r.rop1), }, - cluster: { label: 'Cluster', get: (r) => code(r.imb_reg_of_people_2) }, - people_group: { label: 'People Group', get: (r) => code(r.imb_reg_of_people_25) }, + cluster: { get label() { return tStatic('dashboard.cluster') || 'Cluster' }, get: (r) => code(r.imb_reg_of_people_2) }, + people_group: { get label() { return tStatic('dashboard.people_group') || 'People Group' }, get: (r) => code(r.imb_reg_of_people_25) }, // SEARCH-ONLY projection of the People name. NOT an axis: no control list names it // (`facetRail.drill`, `charts[].facet` and `researchFilters.dropdowns` all name // `affinity_bloc`), and nothing enumerates `facets` to render a control — every consumer // looks a key UP. It exists solely so free-text search keeps matching "South Asian" rather // than the code 'A012' (buildSearchAccessors resolves searchKeys through facets first). // Never add this key to a control list: that would reintroduce the duplicate axis. - rop1_name: { label: 'People (name)', get: (r) => lbl(r.rop1) }, + rop1_name: { get label() { return tStatic('dashboard.people_name') || 'People (name)' }, get: (r) => lbl(r.rop1) }, }, // ── facet rail sections ── (DashboardFacetRail) — ONE centralized semantic tree mirroring the @@ -241,18 +252,18 @@ export default { // ── KPI strip ── (computed by useDashboardShell; mirrors the original 6 stat computeds) kpis: [ - { id: 'total', label: 'UPGs in scope', kind: 'count', showDelta: true }, - { id: 'pop', label: 'Σ population', kind: 'sum', field: 'population' }, + { id: 'total', get label() { return tStatic('dashboard.upgs_in_scope') || 'UPGs in scope' }, kind: 'count', showDelta: true }, + { id: 'pop', get label() { return tStatic('dashboard.sum_population') || 'Σ population' }, kind: 'sum', field: 'population' }, // ★ NO '%' IN A `kind:'pct'` LABEL. kpiText() returns k.value unchanged for pct, and that // value ALREADY carries its percent sign — so a label that also carries one renders // "99% % UNREACHED". Found by reading the rendered screenshot, not by reading the code: // each half looks correct in isolation and only the composition is wrong, which is why it // survived review. The label says WHAT is measured; the value says how much. - { id: 'unreached', label: 'unreached', kind: 'pct', + { id: 'unreached', get label() { return tStatic('dashboard.unreached') || 'unreached' }, kind: 'pct', test: (r) => !/engaged/i.test(lbl(r.engagement_status)) || /unengaged/i.test(lbl(r.engagement_status)) }, - { id: 'countries', label: 'countries touched', kind: 'distinct', field: 'country_code', via: 'geoIso' }, - { id: 'praying', label: 'Σ praying', kind: 'sum', field: 'people_praying' }, - { id: 'adopted', label: 'adopted', kind: 'pct', test: (r) => Number(r.adopted_by_churches) > 0 }, + { id: 'countries', get label() { return tStatic('dashboard.countries_touched') || 'countries touched' }, kind: 'distinct', field: 'country_code', via: 'geoIso' }, + { id: 'praying', get label() { return tStatic('dashboard.sum_praying') || 'Σ praying' }, kind: 'sum', field: 'people_praying' }, + { id: 'adopted', get label() { return tStatic('dashboard.adopted') || 'adopted' }, kind: 'pct', test: (r) => Number(r.adopted_by_churches) > 0 }, ], // ── table columns ── (DashboardDataTable) @@ -284,13 +295,13 @@ export default { // Deliberately NOT pixel widths: a fixed px column breaks at a viewport you did not test, while a // weight degrades proportionally everywhere. columns: [ - { key: 'name', label: 'People Group', get: (r) => r.name || '', type: 'str', w: 1.34 }, - { key: 'country', label: 'Country', get: (r) => lbl(r.country_code), type: 'str', w: 1.04 }, - { key: 'region', label: 'Region', get: (r) => lbl(r.wagf_region), type: 'str', w: 1.04 }, - { key: 'language', label: 'Language', get: (r) => lbl(r.primary_language), type: 'str', w: 1.04 }, - { key: 'religion', label: 'Religion', get: (r) => lbl(r.religion), type: 'str', w: 1.04 }, - { key: 'population', label: 'Population', get: (r) => num(r.population), type: 'num', w: 0.86 }, - { key: 'people_praying', label: 'Praying', get: (r) => num(r.people_praying), type: 'num', w: 0.66 }, + { key: 'name', get label() { return tStatic('dashboard.people_group') || 'People Group' }, get: (r) => r.name || '', type: 'str', w: 1.34 }, + { key: 'country', get label() { return tStatic('dashboard.country') || 'Country' }, get: (r) => lbl(r.country_code), type: 'str', w: 1.04 }, + { key: 'region', get label() { return tStatic('dashboard.region') || 'Region' }, get: (r) => lbl(r.wagf_region), type: 'str', w: 1.04 }, + { key: 'language', get label() { return tStatic('dashboard.language') || 'Language' }, get: (r) => lbl(r.primary_language), type: 'str', w: 1.04 }, + { key: 'religion', get label() { return tStatic('dashboard.religion') || 'Religion' }, get: (r) => lbl(r.religion), type: 'str', w: 1.04 }, + { key: 'population', get label() { return tStatic('dashboard.population') || 'Population' }, get: (r) => num(r.population), type: 'num', w: 0.86 }, + { key: 'people_praying', get label() { return tStatic('dashboard.praying') || 'Praying' }, get: (r) => num(r.people_praying), type: 'num', w: 0.66 }, { key: 'engagement', label: 'Engagement', get: (r) => lbl(r.engagement_status), type: 'str', w: 0.97 }, ], @@ -477,7 +488,7 @@ export default { // Status tree section, because the grid still filters on it and removing the accessor would break // FACET_TO_PROP. What changed is only whether it is offered as a DROPDOWN in the filter row. dropdowns: ['prayer_level', 'affinity_bloc', 'wagf_region', 'wagf_block', 'country_code', 'primary_language', 'religion'], - searchPlaceholder: 'Search people groups, place, language, religion…', + get searchPlaceholder() { return tStatic('dashboard.search_placeholder') || 'Search people groups, place, language, religion…' }, }, // `rop1_name` (not `affinity_bloc`) so free-text search matches the People NAME — DX-life's // Fuse key set includes 'rop1.label' (UupgsList.vue:299), and searching the axis's canonical @@ -493,13 +504,13 @@ export default { // results go to selectionSet; the rest to filterSet) — the kind-switch itself stays in // the composable. See src/components/dashboards/README.md. search: [ - { id: 'people', label: 'Search people', context: 'people-groups', + { id: 'people', get label() { return tStatic('dashboard.search_people') || 'Search people' }, context: 'people-groups', kinds: ['affinity-bloc', 'cluster', 'people-group'] }, - { id: 'place', label: 'Search place', context: 'regions', + { id: 'place', get label() { return tStatic('dashboard.search_place') || 'Search place' }, context: 'regions', kinds: ['region', 'country'] }, - { id: 'language', label: 'Search language', context: 'language', + { id: 'language', get label() { return tStatic('dashboard.search_language') || 'Search language' }, context: 'language', kinds: ['language-family', 'language', 'dialect'] }, - { id: 'religion', label: 'Search religion', context: 'religion', + { id: 'religion', get label() { return tStatic('dashboard.search_religion') || 'Search religion' }, context: 'religion', kinds: ['religion'] }, ], } diff --git a/plugin-designs/research-dashboard/src/components/dashboards/DashboardChartsRail.vue b/plugin-designs/research-dashboard/src/components/dashboards/DashboardChartsRail.vue index cb09074cb..314ea5e35 100644 --- a/plugin-designs/research-dashboard/src/components/dashboards/DashboardChartsRail.vue +++ b/plugin-designs/research-dashboard/src/components/dashboards/DashboardChartsRail.vue @@ -1,4 +1,5 @@