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(`
- Engagement
+ {{ L('engagement', 'Engagement') }}
Any
- Engaged
- Unengaged
+ {{ L('engaged', 'Engaged') }}
+ {{ L('unengaged', 'Unengaged') }}
- Adoption
+ {{ L('adoption', 'Adoption') }}
Any
- Adopted
- Unadopted
+ {{ L('adopted', 'Adopted') }}
+ {{ L('unadopted', 'Unadopted') }}
-
Language
-
+
{{ L('language', 'Language') }}
+
-
No languages loaded yet
+
{{ L('no_languages', 'No languages loaded yet') }}
-
Religion
-
+
{{ L('religion', 'Religion') }}
+
- No religions loaded yet
+ {{ L('no_religions', 'No religions 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(`
-
Clustering
+
{{ L('clustering', 'Clustering') }}
-
Pin dim
+
{{ L('pin_dim', 'Pin dim') }}
-
Filters
+ {{ L('filters', 'Filters') }}
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 @@