diff --git a/documentation/reference-universal-props.md b/documentation/reference-universal-props.md index 4d0aa4350..b027adc0d 100644 --- a/documentation/reference-universal-props.md +++ b/documentation/reference-universal-props.md @@ -148,6 +148,9 @@ the IMB / peoplegroups.org dataset (never a hand-kept roster). Aliases name them from the IMB CSV and the DOXA snapshot that `build-vision-funnel.mjs` saves). - **An unknown list** falls back to `'doxa'` with a console warning. **A list whose membership cannot load** shows no groups (fails closed), never DOXA's whole list under another list's name. +- **Showing a list's name:** `listLabel(id, t)` and `listDefinition(id, t)` read the reader's catalogue + (`i18n/locales//lists.json`, area `lists`) through the caller's translator, and fall back to the + registry's English. Never print `lists.json`'s `label` directly: it is the English fallback only. - **Guarded by** `check:universal-props` (LIST-FETCH, LIST-DSM, LIST-KEYS) and `check:list-prop-unit`. diff --git a/internal/check-list-prop-unit.mjs b/internal/check-list-prop-unit.mjs index 936671d23..d094f8716 100644 --- a/internal/check-list-prop-unit.mjs +++ b/internal/check-list-prop-unit.mjs @@ -8,11 +8,14 @@ * - the default list returns the SAME array, untouched, with no fetch at all; * - a list keeps exactly the rows whose slug is in its membership file, fetched from the asset pool; * - a membership that cannot load FAILS CLOSED: no rows, never the whole DOXA list; - * - every registry list with an imb filter has a membership file, and its counts match the slugs. + * - every registry list with an imb filter has a membership file, and its counts match the slugs; + * - listLabel / listDefinition read the reader's catalogue (library/i18n/locales//lists.json), + * fall back to the registry's English, the English catalogue equals the registry, and every + * locale carries a label and a definition for every list. * USAGE node internal/check-list-prop-unit.mjs EXIT 0 pass · 1 fail */ import { register } from 'node:module' -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' @@ -65,6 +68,27 @@ for (const l of L.peopleGroupLists().filter((x) => x.source === 'imb')) { check(`${l.id}: file matches its registry filter, counts match its slugs`, [JSON.stringify(d.filter) === JSON.stringify(l.filter), d.counts.doxa === d.slugs.length, d.counts.imb >= d.counts.doxa], [true, true, true]) } +console.log('labels in the reader\'s language') +const cat = (lc) => JSON.parse(readFileSync(join(ROOT, 'library/i18n/locales', lc, 'lists.json'), 'utf8')) +const tFrom = (lc) => (key) => key.split('.').slice(1).reduce((o, k) => (o == null ? o : o[k]), cat(lc)) ?? key +check('no translator -> the registry English', L.listLabel('700'), 'Project 700: frontier people groups of 500,000 or more') +check('a translator that returns the key -> English', L.listLabel('frontier', (k) => k), 'Project 4700: frontier people groups') +check('Arabic catalogue, by alias', L.listLabel('4700', tFrom('ar')), cat('ar').frontier.label) +check('definition, German catalogue', L.listDefinition('unreached', tFrom('de')), cat('de').unreached.definition) +check('an unknown list -> empty, not a crash', L.listLabel('project-9999', tFrom('en')), '') +{ const en = cat('en'), bad = [] + for (const l of L.peopleGroupLists()) { + if (en[l.id]?.label !== l.label) bad.push(`${l.id}.label`) + if ((en[l.id]?.definition || '') !== (l.definition || '')) bad.push(`${l.id}.definition`) + } + check('the English catalogue matches the registry, word for word', bad, []) } +{ const missing = [] + for (const lc of readdirSync(join(ROOT, 'library/i18n/locales'))) { + const c = cat(lc) + for (const l of L.peopleGroupLists()) for (const f of ['label', 'definition']) if (!c[l.id]?.[f]) missing.push(`${lc}:${l.id}.${f}`) + } + check('every locale has a label and a definition for every list', missing, []) } + const failed = results.filter((x) => !x).length -console.log(failed ? `LIST-PROP: FAIL — ${failed} case(s)` : `LIST-PROP: PASS — ${results.length} cases: the ladder, aliases, the untouched default, membership filtering, fail-closed, and the membership files`) +console.log(failed ? `LIST-PROP: FAIL — ${failed} case(s)` : `LIST-PROP: PASS — ${results.length} cases: the ladder, aliases, the untouched default, membership filtering, fail-closed, the membership files, and labels in every locale`) process.exit(failed ? 1 : 0) diff --git a/library/i18n/locales/ar/lists.json b/library/i18n/locales/ar/lists.json new file mode 100644 index 000000000..da613c379 --- /dev/null +++ b/library/i18n/locales/ar/lists.json @@ -0,0 +1,22 @@ +{ + "doxa": { + "label": "المشروع 2000: الشعوب التي تخدمها DOXA", + "definition": "الشعوب التي تخدمها DOXA كما تقدّمها قائمتها الحيّة" + }, + "unreached": { + "label": "المشروع 7000: الشعوب غير المُبشَّرة", + "definition": "أولوية الضياع لدى IMB من 0 إلى 2 (حدودية أو رائدة أو متوسّعة غير مُبشَّرة: أقل من 2% إنجيليون)" + }, + "frontier": { + "label": "المشروع 4700: الشعوب الحدودية", + "definition": "أولوية الضياع لدى IMB بقيمة 0 (حدودية غير مُبشَّرة: 0.1% إنجيليون أو أقل)" + }, + "frontier-500k": { + "label": "المشروع 700: الشعوب الحدودية التي يبلغ عددها 500,000 أو أكثر", + "definition": "أولوية الضياع لدى IMB بقيمة 0 وعدد سكان لا يقل عن 500,000" + }, + "unengaged": { + "label": "الشعوب غير المُبشَّرة التي لم يُعمَل بينها", + "definition": "أولوية الضياع لدى IMB من 0 إلى 2 وحالة العمل: لم يُعمَل بينها" + } +} diff --git a/library/i18n/locales/de/lists.json b/library/i18n/locales/de/lists.json new file mode 100644 index 000000000..b2eeebdfa --- /dev/null +++ b/library/i18n/locales/de/lists.json @@ -0,0 +1,22 @@ +{ + "doxa": { + "label": "Projekt 2000: die Volksgruppen von DOXA", + "definition": "Die Volksgruppen, denen DOXA dient, wie seine Live-Liste sie angibt" + }, + "unreached": { + "label": "Projekt 7000: unerreichte Volksgruppen", + "definition": "IMB Lostness Priority 0–2 (Frontier, Pioneer oder Expanding Unreached: unter 2 % Evangelikale)" + }, + "frontier": { + "label": "Projekt 4700: Frontier-Volksgruppen", + "definition": "IMB Lostness Priority 0 (Frontier Unreached: 0,1 % Evangelikale oder weniger)" + }, + "frontier-500k": { + "label": "Projekt 700: Frontier-Volksgruppen mit 500.000 oder mehr Menschen", + "definition": "IMB Lostness Priority 0 und eine Bevölkerung von mindestens 500.000" + }, + "unengaged": { + "label": "Unerreichte Volksgruppen ohne Engagement", + "definition": "IMB Lostness Priority 0–2 und Engagement-Status: ohne Engagement" + } +} diff --git a/library/i18n/locales/en/lists.json b/library/i18n/locales/en/lists.json new file mode 100644 index 000000000..06c446a67 --- /dev/null +++ b/library/i18n/locales/en/lists.json @@ -0,0 +1,22 @@ +{ + "doxa": { + "label": "Project 2000: DOXA's people groups", + "definition": "The people groups DOXA serves, as its live list gives them" + }, + "unreached": { + "label": "Project 7000: unreached people groups", + "definition": "IMB Lostness Priority 0-2 (Frontier, Pioneer or Expanding Unreached: under 2% evangelical)" + }, + "frontier": { + "label": "Project 4700: frontier people groups", + "definition": "IMB Lostness Priority 0 (Frontier Unreached: 0.1% evangelical or less)" + }, + "frontier-500k": { + "label": "Project 700: frontier people groups of 500,000 or more", + "definition": "IMB Lostness Priority 0 and Population of at least 500,000" + }, + "unengaged": { + "label": "Unengaged unreached people groups", + "definition": "IMB Lostness Priority 0-2 and Engagement Status Unengaged" + } +} diff --git a/library/i18n/locales/es/lists.json b/library/i18n/locales/es/lists.json new file mode 100644 index 000000000..f72d88035 --- /dev/null +++ b/library/i18n/locales/es/lists.json @@ -0,0 +1,22 @@ +{ + "doxa": { + "label": "Proyecto 2000: los pueblos de DOXA", + "definition": "Los pueblos a los que sirve DOXA, tal como los da su lista en vivo" + }, + "unreached": { + "label": "Proyecto 7000: pueblos no alcanzados", + "definition": "Prioridad de perdición de IMB 0–2 (no alcanzados de frontera, pioneros o en expansión: menos del 2 % evangélicos)" + }, + "frontier": { + "label": "Proyecto 4700: pueblos de frontera", + "definition": "Prioridad de perdición de IMB 0 (no alcanzados de frontera: 0,1 % evangélicos o menos)" + }, + "frontier-500k": { + "label": "Proyecto 700: pueblos de frontera de 500.000 o más", + "definition": "Prioridad de perdición de IMB 0 y una población de al menos 500.000" + }, + "unengaged": { + "label": "Pueblos no alcanzados y no atendidos", + "definition": "Prioridad de perdición de IMB 0–2 y estado de compromiso: no atendido" + } +} diff --git a/library/i18n/locales/fr/lists.json b/library/i18n/locales/fr/lists.json new file mode 100644 index 000000000..c05ff373f --- /dev/null +++ b/library/i18n/locales/fr/lists.json @@ -0,0 +1,22 @@ +{ + "doxa": { + "label": "Projet 2000 : les peuples de DOXA", + "definition": "Les peuples que DOXA sert, tels que sa liste en direct les donne" + }, + "unreached": { + "label": "Projet 7000 : peuples non atteints", + "definition": "Priorité de perdition IMB 0–2 (non atteints frontières, pionniers ou en expansion : moins de 2 % d’évangéliques)" + }, + "frontier": { + "label": "Projet 4700 : peuples frontières", + "definition": "Priorité de perdition IMB 0 (non atteints frontières : 0,1 % d’évangéliques ou moins)" + }, + "frontier-500k": { + "label": "Projet 700 : peuples frontières de 500 000 personnes ou plus", + "definition": "Priorité de perdition IMB 0 et une population d’au moins 500 000" + }, + "unengaged": { + "label": "Peuples non atteints et non engagés", + "definition": "Priorité de perdition IMB 0–2 et statut d’engagement : non engagé" + } +} diff --git a/library/i18n/locales/hi/lists.json b/library/i18n/locales/hi/lists.json new file mode 100644 index 000000000..80b40b85e --- /dev/null +++ b/library/i18n/locales/hi/lists.json @@ -0,0 +1,22 @@ +{ + "doxa": { + "label": "प्रोजेक्ट 2000: DOXA के जनसमूह", + "definition": "वे जनसमूह जिनकी DOXA सेवा करता है, जैसे उसकी लाइव सूची उन्हें देती है" + }, + "unreached": { + "label": "प्रोजेक्ट 7000: अपहुँचे जनसमूह", + "definition": "IMB लॉस्टनेस प्राथमिकता 0–2 (फ़्रंटियर, पायनियर या एक्सपैंडिंग अपहुँचे: 2% से कम इवेंजेलिकल)" + }, + "frontier": { + "label": "प्रोजेक्ट 4700: फ़्रंटियर जनसमूह", + "definition": "IMB लॉस्टनेस प्राथमिकता 0 (फ़्रंटियर अपहुँचे: 0.1% या उससे कम इवेंजेलिकल)" + }, + "frontier-500k": { + "label": "प्रोजेक्ट 700: 5,00,000 या उससे अधिक के फ़्रंटियर जनसमूह", + "definition": "IMB लॉस्टनेस प्राथमिकता 0 और कम से कम 5,00,000 की जनसंख्या" + }, + "unengaged": { + "label": "अपहुँचे और असंलग्न जनसमूह", + "definition": "IMB लॉस्टनेस प्राथमिकता 0–2 और संलग्नता स्थिति: असंलग्न" + } +} diff --git a/library/i18n/locales/it/lists.json b/library/i18n/locales/it/lists.json new file mode 100644 index 000000000..1863390c8 --- /dev/null +++ b/library/i18n/locales/it/lists.json @@ -0,0 +1,22 @@ +{ + "doxa": { + "label": "Progetto 2000: i popoli di DOXA", + "definition": "I popoli che DOXA serve, come li fornisce la sua lista in tempo reale" + }, + "unreached": { + "label": "Progetto 7000: popoli non raggiunti", + "definition": "Priorità di perdizione IMB 0–2 (non raggiunti di frontiera, pionieri o in espansione: meno del 2% evangelici)" + }, + "frontier": { + "label": "Progetto 4700: popoli di frontiera", + "definition": "Priorità di perdizione IMB 0 (non raggiunti di frontiera: 0,1% evangelici o meno)" + }, + "frontier-500k": { + "label": "Progetto 700: popoli di frontiera di 500.000 o più", + "definition": "Priorità di perdizione IMB 0 e una popolazione di almeno 500.000" + }, + "unengaged": { + "label": "Popoli non raggiunti e non coinvolti", + "definition": "Priorità di perdizione IMB 0–2 e stato di coinvolgimento: non coinvolto" + } +} diff --git a/library/i18n/locales/pt/lists.json b/library/i18n/locales/pt/lists.json new file mode 100644 index 000000000..025be7ed5 --- /dev/null +++ b/library/i18n/locales/pt/lists.json @@ -0,0 +1,22 @@ +{ + "doxa": { + "label": "Projeto 2000: os povos da DOXA", + "definition": "Os povos que a DOXA serve, como a sua lista ao vivo os apresenta" + }, + "unreached": { + "label": "Projeto 7000: povos não alcançados", + "definition": "Prioridade de perdição do IMB 0–2 (não alcançados de fronteira, pioneiros ou em expansão: menos de 2% evangélicos)" + }, + "frontier": { + "label": "Projeto 4700: povos de fronteira", + "definition": "Prioridade de perdição do IMB 0 (não alcançados de fronteira: 0,1% evangélicos ou menos)" + }, + "frontier-500k": { + "label": "Projeto 700: povos de fronteira de 500.000 ou mais", + "definition": "Prioridade de perdição do IMB 0 e uma população de pelo menos 500.000" + }, + "unengaged": { + "label": "Povos não alcançados e não engajados", + "definition": "Prioridade de perdição do IMB 0–2 e estado de engajamento: não engajado" + } +} diff --git a/library/i18n/locales/ro/lists.json b/library/i18n/locales/ro/lists.json new file mode 100644 index 000000000..8cd853025 --- /dev/null +++ b/library/i18n/locales/ro/lists.json @@ -0,0 +1,22 @@ +{ + "doxa": { + "label": "Proiectul 2000: popoarele DOXA", + "definition": "Popoarele pe care DOXA le slujește, așa cum le dă lista sa live" + }, + "unreached": { + "label": "Proiectul 7000: popoare neatinse", + "definition": "Prioritatea de pierzare IMB 0–2 (neatinse de frontieră, pionier sau în expansiune: sub 2% evanghelici)" + }, + "frontier": { + "label": "Proiectul 4700: popoare de frontieră", + "definition": "Prioritatea de pierzare IMB 0 (neatinse de frontieră: 0,1% evanghelici sau mai puțin)" + }, + "frontier-500k": { + "label": "Proiectul 700: popoare de frontieră de 500.000 sau mai mult", + "definition": "Prioritatea de pierzare IMB 0 și o populație de cel puțin 500.000" + }, + "unengaged": { + "label": "Popoare neatinse și neangajate", + "definition": "Prioritatea de pierzare IMB 0–2 și starea de angajare: neangajat" + } +} diff --git a/library/i18n/locales/ru/lists.json b/library/i18n/locales/ru/lists.json new file mode 100644 index 000000000..46d35dc88 --- /dev/null +++ b/library/i18n/locales/ru/lists.json @@ -0,0 +1,22 @@ +{ + "doxa": { + "label": "Проект 2000: народы DOXA", + "definition": "Народы, которым служит DOXA, как их даёт её живой список" + }, + "unreached": { + "label": "Проект 7000: недостигнутые народы", + "definition": "Приоритет потерянности IMB 0–2 (пограничные, первопроходческие или расширяющиеся недостигнутые: менее 2% евангельских христиан)" + }, + "frontier": { + "label": "Проект 4700: пограничные народы", + "definition": "Приоритет потерянности IMB 0 (пограничные недостигнутые: 0,1% евангельских христиан или меньше)" + }, + "frontier-500k": { + "label": "Проект 700: пограничные народы численностью от 500 000", + "definition": "Приоритет потерянности IMB 0 и численность не менее 500 000" + }, + "unengaged": { + "label": "Недостигнутые народы без служения", + "definition": "Приоритет потерянности IMB 0–2 и статус служения: без служения" + } +} diff --git a/library/i18n/locales/zh/lists.json b/library/i18n/locales/zh/lists.json new file mode 100644 index 000000000..2f5226bf1 --- /dev/null +++ b/library/i18n/locales/zh/lists.json @@ -0,0 +1,22 @@ +{ + "doxa": { + "label": "2000 计划:DOXA 服侍的族群", + "definition": "DOXA 服侍的族群,以其实时名单为准" + }, + "unreached": { + "label": "7000 计划:未得之民族群", + "definition": "IMB 失丧优先级 0–2(前沿、先锋或扩展中的未得之民:福音派少于 2%)" + }, + "frontier": { + "label": "4700 计划:前沿族群", + "definition": "IMB 失丧优先级 0(前沿未得之民:福音派 0.1% 或更少)" + }, + "frontier-500k": { + "label": "700 计划:人口 50 万或以上的前沿族群", + "definition": "IMB 失丧优先级 0,且人口至少 50 万" + }, + "unengaged": { + "label": "尚无人参与的未得之民族群", + "definition": "IMB 失丧优先级 0–2,且参与状态为:无人参与" + } +} diff --git a/library/lists/lists.json b/library/lists/lists.json index e641fe56a..34b4e2c55 100644 --- a/library/lists/lists.json +++ b/library/lists/lists.json @@ -1,11 +1,12 @@ { - "_about": "The people-group LISTS a map can be asked to show (profile-config `list`, ?list=, window.MAP_LIST; see peopleGroupList.js). Each list is a FILTER, never a hand-kept roster: `source: doxa` is DOXA's live list as the API serves it; `source: imb` is a filter over the IMB / peoplegroups.org dataset in data/people-groups/people_groups.csv, using its own columns (LostnessPriority, Population, EngagementStatus). library/scripts/people-groups/build-lists.mjs turns each imb filter into data/people-groups/lists/.json. TIER A (today): a map shows the DOXA groups that belong to the list, because the DOXA API is where every map's rows and prayer fields come from. TIER B (later, per map): the list's groups that DOXA does not carry. Definitions and counts: _qa-cycle/03-deployment-cycle/SPEC-PEOPLE-GROUP-LIST-PROP.md.", + "_about": "The people-group LISTS a map can be asked to show (profile-config `list`, ?list=, window.MAP_LIST; see peopleGroupList.js). Each list is a FILTER, never a hand-kept roster: `source: doxa` is DOXA's live list as the API serves it; `source: imb` is a filter over the IMB / peoplegroups.org dataset in data/people-groups/people_groups.csv, using its own columns (LostnessPriority, Population, EngagementStatus). library/scripts/people-groups/build-lists.mjs turns each imb filter into data/people-groups/lists/.json. TIER A (today): a map shows the DOXA groups that belong to the list, because the DOXA API is where every map's rows and prayer fields come from. TIER B (later, per map): the list's groups that DOXA does not carry. Definitions and counts: _qa-cycle/03-deployment-cycle/SPEC-PEOPLE-GROUP-LIST-PROP.md. LABELS HERE ARE THE ENGLISH FALLBACK ONLY: every locale's label and definition live in library/i18n/locales//lists.json (read them with listLabel / listDefinition), and check:list-prop-unit fails if the English catalogue and this file disagree.", "default": "doxa", "lists": [ { "id": "doxa", "aliases": ["2000", "project-2000"], "label": "Project 2000: DOXA's people groups", + "definition": "The people groups DOXA serves, as its live list gives them", "source": "doxa", "filter": null }, diff --git a/library/lists/peopleGroupList.js b/library/lists/peopleGroupList.js index 80c114451..2f60b093f 100644 --- a/library/lists/peopleGroupList.js +++ b/library/lists/peopleGroupList.js @@ -42,6 +42,24 @@ export function listInfo(idOrAlias) { return byKey.get(String(idOrAlias ?? '').toLowerCase().trim()) || null } +// A list's name and definition in the READER'S language. They live in the i18n catalogues +// (library/i18n/locales//lists.json, area `lists`), not here, so every surface that shows a list +// speaks the page's locale. `t` is the caller's translator (useI18n's t, tStatic, or any (key) => +// string); this module takes it as an argument instead of importing the i18n runtime, so it stays +// pure. With no translator, or a key the catalogue lacks, the registry's English answers. +function listText(idOrAlias, field, t) { + const l = listInfo(idOrAlias) + if (!l) return '' + const key = `lists.${l.id}.${field}` + let v = '' + if (typeof t === 'function') { try { v = t(key) } catch { v = '' } } + return v && v !== key ? v : (l[field] || '') +} +/** The list's label ("Project 700: …") in the reader's language, English as the fallback. */ +export function listLabel(idOrAlias, t) { return listText(idOrAlias, 'label', t) } +/** The list's one-line definition in the reader's language, English as the fallback. */ +export function listDefinition(idOrAlias, t) { return listText(idOrAlias, 'definition', t) } + /** Unwrap a ref/computed to its config object; parse a JSON string; pass plain objects through. */ function unwrapConfig(profileConfig) { let cfg = profileConfig