PPLR data maps: tab labels and status lines speak the reader's language

The universal-props matrix flagged pplr-data-maps as a locale BYPASS: every tab label and every
loading or ready line was an English literal. A new pplr area (library/i18n/locales/<lc>/pplr.json,
all 11 locales) carries the 10 tab labels and 20 status lines; usePplrT() reads them through a
guarded useI18n with the English catalog as fallback. Counts go in as parameters, and their number
formatting is unchanged pending the Driver's numerals decision. The views' own legend and panel text
is the next pass.
check:i18n-parity PASS (all 11 complete); the bundle builds. A library catalog change (the i18n index
globs every area into every i18n bundle), so studio lands it (rule 5B).
needs eyes: PPLR Data Maps with ?lang=ar: the tab bar and the loading and ready lines.
This commit is contained in:
Doxa Maps 2026-09-26 00:41:11 +00:00
parent c35bc84794
commit 4a16f0538b
21 changed files with 480 additions and 48 deletions

View file

@ -0,0 +1,36 @@
{
"tabs": {
"religion": "الدين",
"ropTree": "شجرة ROP",
"languageFamilies": "عائلات اللغات",
"semanticTrees": "الأشجار الدلالية",
"nameLocales": "لغات أسماء الشعوب",
"ropSunburst": "شمس ROP",
"allPins": "كل دبابيس الشعوب في الدول",
"rosetta": "جدول معرّفات الأنظمة",
"coverage": "تغطية الترجمة",
"index": "الفهرس"
},
"status": {
"loading": "جارٍ التحميل…",
"loadingPgData": "جارٍ تحميل بيانات الشعوب…",
"loadingPins": "جارٍ تحميل دبابيس الشعوب…",
"loadingPolygons": "جارٍ تحميل حدود الدول…",
"addingPolygons": "جارٍ إضافة حدود الدول…",
"addingPins": "جارٍ إضافة دبابيس الشعوب…",
"loadingCoverage": "جارٍ تحميل التغطية لكل دولة…",
"addingPinsOnTop": "جارٍ إضافة دبابيس الشعوب فوقها…",
"loadingRop": "جارٍ تحميل تسلسل ROP…",
"loadingLanguage": "جارٍ تحميل بيانات اللغات…",
"loadingPinsRosetta": "جارٍ تحميل الدبابيس وعيّنة Rosetta…",
"error": "خطأ: {message}",
"readyTranslations": "الدبابيس: {pins} · انقر أي دبوس لرؤية الاسم بـ17 لغة",
"readyCoverage": "جاهز · الدول: {countries} ملوّنة بمتوسط لغات الترجمة / 17 (كل الصفوف: {rows})",
"readyD3": "المدخلات: {entries} · مرّر للتكبير · اسحب للتحريك",
"readyRop": "جاهز · الدبابيس: {pins} · بمعرّف PEID: {peid}",
"readyRosetta": "جاهز · الدبابيس: {pins} · عيّنات Rosetta: {samples} (★ ذهبية)",
"readyPins": "جاهز · دبابيس الشعوب في الدول: {pins} · انقر أي دبوس",
"readyLanguage": "جاهز · الدبابيس: {pins} · تغطية العائلات {coverage}% · أعلى 25 = {top}%",
"readyReligion": "الدبابيس: {pins} · الأكثر شيوعًا: {religion} ({count})"
}
}

View file

@ -0,0 +1,36 @@
{
"tabs": {
"religion": "Religion",
"ropTree": "ROP-Baum",
"languageFamilies": "Sprachfamilien",
"semanticTrees": "Semantische Bäume",
"nameLocales": "Namensvarianten",
"ropSunburst": "ROP-Sunburst",
"allPins": "Alle Volksgruppen-Pins",
"rosetta": "ID-Rosetta",
"coverage": "Übersetzungsabdeckung",
"index": "Index"
},
"status": {
"loading": "Wird geladen…",
"loadingPgData": "Volksgruppendaten werden geladen…",
"loadingPins": "Pins werden geladen…",
"loadingPolygons": "Ländergrenzen werden geladen…",
"addingPolygons": "Ländergrenzen werden hinzugefügt…",
"addingPins": "Pins werden hinzugefügt…",
"loadingCoverage": "Abdeckung je Land wird geladen…",
"addingPinsOnTop": "Pins werden darübergelegt…",
"loadingRop": "ROP-Hierarchie wird geladen…",
"loadingLanguage": "Sprachdaten werden geladen…",
"loadingPinsRosetta": "Pins und Rosetta-Stichprobe werden geladen…",
"error": "Fehler: {message}",
"readyTranslations": "Pins: {pins} · einen Pin anklicken für alle 17 Namensvarianten",
"readyCoverage": "Bereit · Länder: {countries}, gefärbt nach durchschnittlichen Übersetzungen / 17 (alle Zeilen: {rows})",
"readyD3": "Einträge: {entries} · scrollen zum Zoomen · ziehen zum Verschieben",
"readyRop": "Bereit · Pins: {pins} · mit PEID: {peid}",
"readyRosetta": "Bereit · Pins: {pins} · Rosetta-Stichproben: {samples} (gold ★)",
"readyPins": "Bereit · Pins: {pins} · einen Pin anklicken",
"readyLanguage": "Bereit · Pins: {pins} · Familienabdeckung {coverage} % · Top 25 = {top} %",
"readyReligion": "Pins: {pins} · am häufigsten: {religion} ({count})"
}
}

View file

@ -0,0 +1,36 @@
{
"tabs": {
"religion": "Religion",
"ropTree": "ROP Tree",
"languageFamilies": "Language Families",
"semanticTrees": "Semantic Trees",
"nameLocales": "PG Name Locales",
"ropSunburst": "ROP Sunburst",
"allPins": "All PG-in-country pins",
"rosetta": "Cross-system ID Rosetta",
"coverage": "Translation coverage",
"index": "Index"
},
"status": {
"loading": "Loading…",
"loadingPgData": "Loading PG data…",
"loadingPins": "Loading PG pins…",
"loadingPolygons": "Loading country polygons…",
"addingPolygons": "Adding country polygons…",
"addingPins": "Adding PG pins…",
"loadingCoverage": "Loading per-country coverage…",
"addingPinsOnTop": "Adding PG pins on top…",
"loadingRop": "Loading ROP hierarchy…",
"loadingLanguage": "Loading language data…",
"loadingPinsRosetta": "Loading PG pins + Rosetta sample…",
"error": "Error: {message}",
"readyTranslations": "{pins} pins · click any pin to see all 17 locale names",
"readyCoverage": "Ready · {countries} countries coloured by average translation locales / 17 (all {rows} PG-in-country rows)",
"readyD3": "{entries} entries · scroll to zoom · drag to pan",
"readyRop": "Ready · {pins} pins · {peid} with PEID",
"readyRosetta": "Ready · {pins} pins · {samples} Rosetta samples (gold ★)",
"readyPins": "Ready · {pins} PG-in-country pins · click any pin",
"readyLanguage": "Ready · {pins} pins · {coverage}% family coverage · top-25 = {top}%",
"readyReligion": "{pins} pins · {religion} most common ({count})"
}
}

View file

@ -0,0 +1,36 @@
{
"tabs": {
"religion": "Religión",
"ropTree": "Árbol ROP",
"languageFamilies": "Familias lingüísticas",
"semanticTrees": "Árboles semánticos",
"nameLocales": "Nombres por idioma",
"ropSunburst": "Sunburst ROP",
"allPins": "Todos los pines",
"rosetta": "Rosetta de identificadores",
"coverage": "Cobertura de traducción",
"index": "Índice"
},
"status": {
"loading": "Cargando…",
"loadingPgData": "Cargando datos de pueblos…",
"loadingPins": "Cargando pines…",
"loadingPolygons": "Cargando límites de países…",
"addingPolygons": "Añadiendo límites de países…",
"addingPins": "Añadiendo pines…",
"loadingCoverage": "Cargando cobertura por país…",
"addingPinsOnTop": "Añadiendo pines encima…",
"loadingRop": "Cargando jerarquía ROP…",
"loadingLanguage": "Cargando datos lingüísticos…",
"loadingPinsRosetta": "Cargando pines y muestra Rosetta…",
"error": "Error: {message}",
"readyTranslations": "Pines: {pins} · haz clic en un pin para ver sus 17 nombres",
"readyCoverage": "Listo · países: {countries}, coloreados por idiomas de traducción promedio / 17 (todas las filas: {rows})",
"readyD3": "Entradas: {entries} · desplaza para acercar · arrastra para mover",
"readyRop": "Listo · pines: {pins} · con PEID: {peid}",
"readyRosetta": "Listo · pines: {pins} · muestras Rosetta: {samples} (★ dorada)",
"readyPins": "Listo · pines: {pins} · haz clic en un pin",
"readyLanguage": "Listo · pines: {pins} · cobertura de familias {coverage} % · top 25 = {top} %",
"readyReligion": "Pines: {pins} · más común: {religion} ({count})"
}
}

View file

@ -0,0 +1,36 @@
{
"tabs": {
"religion": "Religion",
"ropTree": "Arbre ROP",
"languageFamilies": "Familles de langues",
"semanticTrees": "Arbres sémantiques",
"nameLocales": "Noms par langue",
"ropSunburst": "Sunburst ROP",
"allPins": "Toutes les épingles",
"rosetta": "Rosetta des identifiants",
"coverage": "Couverture de traduction",
"index": "Index"
},
"status": {
"loading": "Chargement…",
"loadingPgData": "Chargement des données des peuples…",
"loadingPins": "Chargement des épingles…",
"loadingPolygons": "Chargement des frontières…",
"addingPolygons": "Ajout des frontières…",
"addingPins": "Ajout des épingles…",
"loadingCoverage": "Chargement de la couverture par pays…",
"addingPinsOnTop": "Ajout des épingles par-dessus…",
"loadingRop": "Chargement de la hiérarchie ROP…",
"loadingLanguage": "Chargement des données linguistiques…",
"loadingPinsRosetta": "Chargement des épingles et de l’échantillon Rosetta…",
"error": "Erreur : {message}",
"readyTranslations": "Épingles : {pins} · cliquez sur une épingle pour voir ses 17 noms",
"readyCoverage": "Prêt · pays : {countries}, colorés selon la moyenne des traductions / 17 (toutes les lignes : {rows})",
"readyD3": "Entrées : {entries} · faites défiler pour zoomer · glissez pour déplacer",
"readyRop": "Prêt · épingles : {pins} · avec PEID : {peid}",
"readyRosetta": "Prêt · épingles : {pins} · échantillons Rosetta : {samples} (★ or)",
"readyPins": "Prêt · épingles : {pins} · cliquez sur une épingle",
"readyLanguage": "Prêt · épingles : {pins} · couverture des familles {coverage} % · top 25 = {top} %",
"readyReligion": "Épingles : {pins} · la plus fréquente : {religion} ({count})"
}
}

View file

@ -0,0 +1,36 @@
{
"tabs": {
"religion": "धर्म",
"ropTree": "ROP वृक्ष",
"languageFamilies": "भाषा परिवार",
"semanticTrees": "अर्थ-वृक्ष",
"nameLocales": "नाम की भाषाएँ",
"ropSunburst": "ROP सनबर्स्ट",
"allPins": "सभी जनसमूह पिन",
"rosetta": "पहचान-संख्या रोसेटा",
"coverage": "अनुवाद कवरेज",
"index": "सूची"
},
"status": {
"loading": "लोड हो रहा है…",
"loadingPgData": "जनसमूह डेटा लोड हो रहा है…",
"loadingPins": "पिन लोड हो रहे हैं…",
"loadingPolygons": "देशों की सीमाएँ लोड हो रही हैं…",
"addingPolygons": "देशों की सीमाएँ जोड़ी जा रही हैं…",
"addingPins": "पिन जोड़े जा रहे हैं…",
"loadingCoverage": "देश-वार कवरेज लोड हो रहा है…",
"addingPinsOnTop": "ऊपर पिन जोड़े जा रहे हैं…",
"loadingRop": "ROP पदानुक्रम लोड हो रहा है…",
"loadingLanguage": "भाषा डेटा लोड हो रहा है…",
"loadingPinsRosetta": "पिन और रोसेटा नमूना लोड हो रहे हैं…",
"error": "त्रुटि: {message}",
"readyTranslations": "पिन: {pins} · सभी 17 भाषाओं के नाम देखने के लिए किसी पिन पर क्लिक करें",
"readyCoverage": "तैयार · देश: {countries}, औसत अनुवाद भाषाओं / 17 के अनुसार रंगे गए (सभी पंक्तियाँ: {rows})",
"readyD3": "प्रविष्टियाँ: {entries} · ज़ूम के लिए स्क्रॉल करें · खिसकाने के लिए खींचें",
"readyRop": "तैयार · पिन: {pins} · PEID सहित: {peid}",
"readyRosetta": "तैयार · पिन: {pins} · रोसेटा नमूने: {samples} (सुनहरा ★)",
"readyPins": "तैयार · पिन: {pins} · किसी पिन पर क्लिक करें",
"readyLanguage": "तैयार · पिन: {pins} · परिवार कवरेज {coverage}% · शीर्ष 25 = {top}%",
"readyReligion": "पिन: {pins} · सबसे आम: {religion} ({count})"
}
}

View file

@ -0,0 +1,36 @@
{
"tabs": {
"religion": "Religione",
"ropTree": "Albero ROP",
"languageFamilies": "Famiglie linguistiche",
"semanticTrees": "Alberi semantici",
"nameLocales": "Nomi per lingua",
"ropSunburst": "Sunburst ROP",
"allPins": "Tutti i segnaposto",
"rosetta": "Rosetta degli identificativi",
"coverage": "Copertura delle traduzioni",
"index": "Indice"
},
"status": {
"loading": "Caricamento…",
"loadingPgData": "Caricamento dati dei popoli…",
"loadingPins": "Caricamento segnaposto…",
"loadingPolygons": "Caricamento confini…",
"addingPolygons": "Aggiunta dei confini…",
"addingPins": "Aggiunta dei segnaposto…",
"loadingCoverage": "Caricamento copertura per paese…",
"addingPinsOnTop": "Aggiunta dei segnaposto sopra…",
"loadingRop": "Caricamento gerarchia ROP…",
"loadingLanguage": "Caricamento dati linguistici…",
"loadingPinsRosetta": "Caricamento segnaposto e campione Rosetta…",
"error": "Errore: {message}",
"readyTranslations": "Segnaposto: {pins} · clicca un segnaposto per i 17 nomi",
"readyCoverage": "Pronto · paesi: {countries}, colorati per media delle traduzioni / 17 (tutte le righe: {rows})",
"readyD3": "Voci: {entries} · scorri per zoomare · trascina per spostare",
"readyRop": "Pronto · segnaposto: {pins} · con PEID: {peid}",
"readyRosetta": "Pronto · segnaposto: {pins} · campioni Rosetta: {samples} (★ oro)",
"readyPins": "Pronto · segnaposto: {pins} · clicca un segnaposto",
"readyLanguage": "Pronto · segnaposto: {pins} · copertura famiglie {coverage}% · top 25 = {top}%",
"readyReligion": "Segnaposto: {pins} · più comune: {religion} ({count})"
}
}

View file

@ -0,0 +1,36 @@
{
"tabs": {
"religion": "Religião",
"ropTree": "Árvore ROP",
"languageFamilies": "Famílias linguísticas",
"semanticTrees": "Árvores semânticas",
"nameLocales": "Nomes por idioma",
"ropSunburst": "Sunburst ROP",
"allPins": "Todos os marcadores",
"rosetta": "Rosetta de identificadores",
"coverage": "Cobertura de tradução",
"index": "Índice"
},
"status": {
"loading": "A carregar…",
"loadingPgData": "A carregar dados dos povos…",
"loadingPins": "A carregar marcadores…",
"loadingPolygons": "A carregar fronteiras…",
"addingPolygons": "A adicionar fronteiras…",
"addingPins": "A adicionar marcadores…",
"loadingCoverage": "A carregar cobertura por país…",
"addingPinsOnTop": "A adicionar marcadores por cima…",
"loadingRop": "A carregar hierarquia ROP…",
"loadingLanguage": "A carregar dados linguísticos…",
"loadingPinsRosetta": "A carregar marcadores e amostra Rosetta…",
"error": "Erro: {message}",
"readyTranslations": "Marcadores: {pins} · clique num marcador para ver os 17 nomes",
"readyCoverage": "Pronto · países: {countries}, coloridos pela média de idiomas de tradução / 17 (todas as linhas: {rows})",
"readyD3": "Entradas: {entries} · role para ampliar · arraste para mover",
"readyRop": "Pronto · marcadores: {pins} · com PEID: {peid}",
"readyRosetta": "Pronto · marcadores: {pins} · amostras Rosetta: {samples} (★ dourada)",
"readyPins": "Pronto · marcadores: {pins} · clique num marcador",
"readyLanguage": "Pronto · marcadores: {pins} · cobertura de famílias {coverage}% · top 25 = {top}%",
"readyReligion": "Marcadores: {pins} · mais comum: {religion} ({count})"
}
}

View file

@ -0,0 +1,36 @@
{
"tabs": {
"religion": "Religie",
"ropTree": "Arbore ROP",
"languageFamilies": "Familii de limbi",
"semanticTrees": "Arbori semantici",
"nameLocales": "Nume pe limbi",
"ropSunburst": "Sunburst ROP",
"allPins": "Toate reperele",
"rosetta": "Rosetta identificatorilor",
"coverage": "Acoperirea traducerilor",
"index": "Index"
},
"status": {
"loading": "Se încarcă…",
"loadingPgData": "Se încarcă datele popoarelor…",
"loadingPins": "Se încarcă reperele…",
"loadingPolygons": "Se încarcă granițele…",
"addingPolygons": "Se adaugă granițele…",
"addingPins": "Se adaugă reperele…",
"loadingCoverage": "Se încarcă acoperirea pe țări…",
"addingPinsOnTop": "Se adaugă reperele deasupra…",
"loadingRop": "Se încarcă ierarhia ROP…",
"loadingLanguage": "Se încarcă datele lingvistice…",
"loadingPinsRosetta": "Se încarcă reperele și eșantionul Rosetta…",
"error": "Eroare: {message}",
"readyTranslations": "Repere: {pins} · dați clic pe un reper pentru cele 17 nume",
"readyCoverage": "Gata · țări: {countries}, colorate după media traducerilor / 17 (toate rândurile: {rows})",
"readyD3": "Intrări: {entries} · derulați pentru zoom · trageți pentru deplasare",
"readyRop": "Gata · repere: {pins} · cu PEID: {peid}",
"readyRosetta": "Gata · repere: {pins} · eșantioane Rosetta: {samples} (★ aurie)",
"readyPins": "Gata · repere: {pins} · dați clic pe un reper",
"readyLanguage": "Gata · repere: {pins} · acoperirea familiilor {coverage}% · top 25 = {top}%",
"readyReligion": "Repere: {pins} · cea mai frecventă: {religion} ({count})"
}
}

View file

@ -0,0 +1,36 @@
{
"tabs": {
"religion": "Религия",
"ropTree": "Дерево ROP",
"languageFamilies": "Языковые семьи",
"semanticTrees": "Семантические деревья",
"nameLocales": "Названия по языкам",
"ropSunburst": "ROP-санберст",
"allPins": "Все метки",
"rosetta": "Розетта идентификаторов",
"coverage": "Покрытие переводов",
"index": "Оглавление"
},
"status": {
"loading": "Загрузка…",
"loadingPgData": "Загрузка данных о народах…",
"loadingPins": "Загрузка меток…",
"loadingPolygons": "Загрузка границ стран…",
"addingPolygons": "Добавление границ стран…",
"addingPins": "Добавление меток…",
"loadingCoverage": "Загрузка покрытия по странам…",
"addingPinsOnTop": "Добавление меток поверх…",
"loadingRop": "Загрузка иерархии ROP…",
"loadingLanguage": "Загрузка языковых данных…",
"loadingPinsRosetta": "Загрузка меток и выборки Rosetta…",
"error": "Ошибка: {message}",
"readyTranslations": "Меток: {pins} · нажмите на метку, чтобы увидеть все 17 названий",
"readyCoverage": "Готово · стран: {countries}, окрашены по среднему числу переводов / 17 (всего строк: {rows})",
"readyD3": "Записей: {entries} · прокрутка — масштаб · перетаскивание — сдвиг",
"readyRop": "Готово · меток: {pins} · с PEID: {peid}",
"readyRosetta": "Готово · меток: {pins} · образцов Rosetta: {samples} (золотая ★)",
"readyPins": "Готово · меток: {pins} · нажмите на метку",
"readyLanguage": "Готово · меток: {pins} · покрытие семей {coverage}% · топ-25 = {top}%",
"readyReligion": "Меток: {pins} · чаще всего: {religion} ({count})"
}
}

View file

@ -0,0 +1,36 @@
{
"tabs": {
"religion": "宗教",
"ropTree": "ROP 树",
"languageFamilies": "语系",
"semanticTrees": "语义树",
"nameLocales": "族群名称语种",
"ropSunburst": "ROP 旭日图",
"allPins": "全部族群标记",
"rosetta": "跨系统 ID 对照",
"coverage": "翻译覆盖率",
"index": "索引"
},
"status": {
"loading": "正在加载…",
"loadingPgData": "正在加载族群数据…",
"loadingPins": "正在加载标记…",
"loadingPolygons": "正在加载国家边界…",
"addingPolygons": "正在添加国家边界…",
"addingPins": "正在添加标记…",
"loadingCoverage": "正在加载各国覆盖率…",
"addingPinsOnTop": "正在叠加标记…",
"loadingRop": "正在加载 ROP 层级…",
"loadingLanguage": "正在加载语言数据…",
"loadingPinsRosetta": "正在加载标记和 Rosetta 样本…",
"error": "错误:{message}",
"readyTranslations": "标记:{pins} · 点击任一标记查看全部 17 种语言名称",
"readyCoverage": "就绪 · 国家:{countries},按平均翻译语种数 / 17 着色(全部行:{rows})",
"readyD3": "条目:{entries} · 滚动缩放 · 拖动平移",
"readyRop": "就绪 · 标记:{pins} · 含 PEID:{peid}",
"readyRosetta": "就绪 · 标记:{pins} · Rosetta 样本:{samples}(金色 ★)",
"readyPins": "就绪 · 族群标记:{pins} · 点击任一标记",
"readyLanguage": "就绪 · 标记:{pins} · 语系覆盖率 {coverage}% · 前 25 = {top}%",
"readyReligion": "标记:{pins} · 最常见:{religion}({count})"
}
}

View file

@ -22,23 +22,26 @@ import { inject, ref, computed, defineAsyncComponent, onMounted, onBeforeUnmount
import { useShadowStyles } from '@map/composables/useShadowStyles.js'
import { resolveAssetsBase } from '@map/utils/embedBase.js'
import { setDataBase, ensureD3 } from './src/utils/pplrData.js'
import { usePplrT } from './src/composables/usePplrT.js'
const profileConfig = inject('profileConfig')
const cfg = computed(() => profileConfig?.value || {})
setDataBase(resolveAssetsBase(cfg.value))
// Order and labels follow the original showcase page's tab bar.
// Tab labels are the chrome a reader sees first; they come from pplr.json (all 11 locales).
const t = usePplrT()
const VIEWS = [
{ key: 'pplr-religion', label: 'Religion', load: () => import('./src/views/pplr-religion.vue') },
{ key: 'pplr-rop-tree', label: 'ROP Tree', load: () => import('./src/views/pplr-rop-tree.vue') },
{ key: 'pplr-language-families', label: 'Language Families', load: () => import('./src/views/pplr-language-families.vue') },
{ key: 'pplr-semantic-trees', label: 'Semantic Trees', load: () => import('./src/views/pplr-semantic-trees.vue') },
{ key: 'pplr-translations', label: 'PG Name Locales', load: () => import('./src/views/pplr-translations.vue') },
{ key: 'pplr-d3-tree', label: 'ROP Sunburst', load: () => ensureD3().then(() => import('./src/views/pplr-d3-tree.vue')) },
{ key: 'pplr-pins', label: 'All PG-in-country pins', load: () => import('./src/views/pplr-pins.vue') },
{ key: 'pplr-rosetta-cards', label: 'Cross-system ID Rosetta', load: () => import('./src/views/pplr-rosetta-cards.vue') },
{ key: 'pplr-coverage', label: 'Translation coverage', load: () => import('./src/views/pplr-coverage.vue') },
{ key: 'pplr-index', label: 'Index', load: () => import('./src/views/pplr-index.vue') },
{ key: 'pplr-religion', label: 'Religion', tKey: 'pplr.tabs.religion', load: () => import('./src/views/pplr-religion.vue') },
{ key: 'pplr-rop-tree', label: 'ROP Tree', tKey: 'pplr.tabs.ropTree', load: () => import('./src/views/pplr-rop-tree.vue') },
{ key: 'pplr-language-families', label: 'Language Families', tKey: 'pplr.tabs.languageFamilies', load: () => import('./src/views/pplr-language-families.vue') },
{ key: 'pplr-semantic-trees', label: 'Semantic Trees', tKey: 'pplr.tabs.semanticTrees', load: () => import('./src/views/pplr-semantic-trees.vue') },
{ key: 'pplr-translations', label: 'PG Name Locales', tKey: 'pplr.tabs.nameLocales', load: () => import('./src/views/pplr-translations.vue') },
{ key: 'pplr-d3-tree', label: 'ROP Sunburst', tKey: 'pplr.tabs.ropSunburst', load: () => ensureD3().then(() => import('./src/views/pplr-d3-tree.vue')) },
{ key: 'pplr-pins', label: 'All PG-in-country pins', tKey: 'pplr.tabs.allPins', load: () => import('./src/views/pplr-pins.vue') },
{ key: 'pplr-rosetta-cards', label: 'Cross-system ID Rosetta', tKey: 'pplr.tabs.rosetta', load: () => import('./src/views/pplr-rosetta-cards.vue') },
{ key: 'pplr-coverage', label: 'Translation coverage', tKey: 'pplr.tabs.coverage', load: () => import('./src/views/pplr-coverage.vue') },
{ key: 'pplr-index', label: 'Index', tKey: 'pplr.tabs.index', load: () => import('./src/views/pplr-index.vue') },
]
const COMPONENTS = Object.fromEntries(VIEWS.map((v) => [v.key, defineAsyncComponent(v.load)]))
const VALID = new Set(VIEWS.map((v) => v.key))
@ -73,7 +76,7 @@ useShadowStyles(
<span class="pdm-brand">PPLR data maps</span>
<div class="pdm-seg" role="tablist" aria-label="Choose a view">
<button v-for="v in VIEWS" :key="v.key" role="tab" :aria-selected="view === v.key"
:class="{ on: view === v.key }" @click="setView(v.key)">{{ v.label }}</button>
:class="{ on: view === v.key }" @click="setView(v.key)">{{ t(v.tKey) }}</button>
</div>
</div>
<div class="pdm-stage">

View file

@ -0,0 +1,17 @@
// usePplrT — PPLR's UI text: library/i18n/locales/<lc>/pplr.json, in all eleven locales.
// i18n is OPTIONAL (the connectivity-map pattern): useI18n() throws on a host that never installed
// it, so the English catalog is the fallback, never a blank or a raw key. Call it inside setup().
import { useI18n } from 'vue-i18n'
import enPplr from '@map/i18n/locales/en/pplr.json'
const enLookup = (key) => String(key).replace(/^pplr\./, '').split('.').reduce((o, k) => (o == null ? o : o[k]), enPplr)
const fill = (str, params) => (params ? String(str).replace(/\{(\w+)\}/g, (m, k) => (k in params ? params[k] : m)) : str)
export function usePplrT() {
try {
const i18n = useI18n()
return (key, params) => { const v = i18n.t(key, params || {}); return v === key ? fill(enLookup(key) ?? key, params) : v }
} catch (_) {
return (key, params) => fill(enLookup(key) ?? String(key), params)
}
}

View file

@ -1,5 +1,6 @@
<script setup>
import { dataUrl, glFor, exposeMap, whenGeojsonDrawn } from '../utils/pplrData.js'
import { usePplrT } from '../composables/usePplrT.js'
/**
* pplr-coverage.vue — Country choropleth: avg # of locales filled per PG in that country.
*
@ -24,7 +25,8 @@ const DATA_SOURCES = [
const mapboxToken = inject('mapboxToken')
const mapContainer = ref(null)
const status = ref('Loading…')
const t = usePplrT()
const status = ref(t('pplr.status.loading'))
const dataReady = ref(false)
function reload() { window.location.reload() }
@ -53,9 +55,9 @@ onMounted(async () => {
exposeMap(map.value)
map.value.on('load', async () => {
try {
status.value = 'Loading per-country coverage…'
status.value = t('pplr.status.loadingCoverage')
await streamCoverage()
status.value = 'Loading country polygons…'
status.value = t('pplr.status.loadingPolygons')
const { fc, sourceId } = await addCountryPolygons(map.value, { fillOpacity: 0, blocking: true })
// Add a choropleth fill layer above the base
@ -110,19 +112,19 @@ onMounted(async () => {
.setLngLat(e.lngLat).setHTML(html).addTo(map.value)
})
status.value = 'Adding PG pins on top…'
status.value = t('pplr.status.addingPinsOnTop')
const { pinLayerId } = await addPGPoints(map.value, { circleColor: '#fef3c7' })
map.value.on('click', pinLayerId, e => {
const f = e.features[0]
new (glFor(map.value)).Popup({ maxWidth: '320px' })
.setLngLat(f.geometry.coordinates).setHTML(popupForPG(f.properties)).addTo(map.value)
})
status.value = `Ready · ${countryStats.size} countries colored by avg translation locales / 17 (all ${[...countryStats.values()].reduce((n, c) => n + c.pgCount, 0).toLocaleString('en-US')} PG-in-country rows)`
status.value = t('pplr.status.readyCoverage', { countries: countryStats.size, rows: [...countryStats.values()].reduce((n, c) => n + c.pgCount, 0).toLocaleString('en-US') })
// keep the LoadingGlobe up until the pins are actually drawn, not merely added
await whenGeojsonDrawn(map.value)
dataReady.value = true
} catch (e) {
status.value = `Error: ${e.message}`
status.value = t('pplr.status.error', { message: e.message })
}
})
})

View file

@ -1,5 +1,6 @@
<script setup>
import { dataUrl } from '../utils/pplrData.js'
import { usePplrT } from '../composables/usePplrT.js'
/**
* pplr-d3-tree.vue — ROP People-Affinity-Cluster sunburst.
*
@ -19,7 +20,8 @@ import SemanticSunburst from '../components/SemanticSunburst.vue'
import LoadingGlobe from '../components/LoadingGlobe.vue'
const tree = ref(null)
const status = ref('Loading ROP hierarchy…')
const t = usePplrT()
const status = ref(t('pplr.status.loadingRop'))
const showSettings = ref(false)
function reload() { window.location.reload() }
@ -54,9 +56,9 @@ onMounted(async () => {
})),
}
const total = flat.reduce((a, b) => a + b.clusters.reduce((c, d) => c + d.groups.reduce((e, f) => e + f.entries.length, 0), 0), 0)
status.value = `${total.toLocaleString()} entries · scroll to zoom · drag to pan`
status.value = t('pplr.status.readyD3', { entries: total.toLocaleString() })
} catch (e) {
status.value = `Error: ${e.message}`
status.value = t('pplr.status.error', { message: e.message })
}
})
</script>

View file

@ -1,5 +1,6 @@
<script setup>
import { dataUrl, glFor, exposeMap, whenGeojsonDrawn } from '../utils/pplrData.js'
import { usePplrT } from '../composables/usePplrT.js'
/**
* pplr-language-families.vue — PG pins colored by PPLR ethnolinguistic family.
*
@ -30,7 +31,8 @@ const mapProvider = resolveMapProvider(inject('profileConfig', null))
const mapboxToken = inject('mapboxToken')
const mapContainer = ref(null)
const profileRoot = ref(null) // FullscreenControl target — fullscreens .profile so chrome stays visible
const status = ref('Loading…')
const t = usePplrT()
const status = ref(t('pplr.status.loading'))
const PIN_LAYER = 'lf-pins'
const { map, isMapReady, initializeMap, destroy } = useMapInstance({
@ -438,7 +440,7 @@ onMounted(async () => {
map.value.setPadding({ left: 380, top: 0, right: 0, bottom: 0 })
addCountryPolygons(map.value, { theme: pplrInstance.theme.value })
status.value = 'Loading language data…'
status.value = t('pplr.status.loadingLanguage')
const [pgResp, lfResp] = await Promise.all([
fetch(dataUrl('pg-points.json')),
fetch(dataUrl('pplr-geoid-family.json'))
@ -489,12 +491,12 @@ onMounted(async () => {
famLangGroups.value = result
const coverage = Math.round(100 * matched / fc.features.length)
status.value = `Ready · ${fc.features.length.toLocaleString()} pins · ${coverage}% family coverage · top-25 = ${Math.round(100 * inTop25 / fc.features.length)}%`
status.value = t('pplr.status.readyLanguage', { pins: fc.features.length.toLocaleString(), coverage, top: Math.round(100 * inTop25 / fc.features.length) })
// keep the LoadingGlobe up until the pins are actually drawn, not merely added
await whenGeojsonDrawn(map.value)
dataReady.value = true
} catch (err) {
status.value = `Error: ${err.message}`
status.value = t('pplr.status.error', { message: err.message })
}
})
})

View file

@ -6,13 +6,15 @@
*/
import { inject, onMounted, onBeforeUnmount, ref } from 'vue'
import { glFor, exposeMap, whenGeojsonDrawn } from '../utils/pplrData.js'
import { usePplrT } from '../composables/usePplrT.js'
import { useMapInstance } from '@map/composables/useMapInstance.js'
import { addCountryPolygons, addPGPoints, popupForPG, POPUP_CSS } from '../utils/pplrMap.js'
import LoadingGlobe from '../components/LoadingGlobe.vue'
const mapboxToken = inject('mapboxToken')
const mapContainer = ref(null)
const status = ref('Loading…')
const t = usePplrT()
const status = ref(t('pplr.status.loading'))
const dataReady = ref(false)
function reload() { window.location.reload() }
@ -37,9 +39,9 @@ onMounted(async () => {
exposeMap(map.value)
map.value.on('load', async () => {
try {
status.value = 'Adding country polygons…'
status.value = t('pplr.status.addingPolygons')
await addCountryPolygons(map.value)
status.value = 'Adding PG pins…'
status.value = t('pplr.status.addingPins')
const { fc, pinLayerId } = await addPGPoints(map.value)
map.value.on('click', pinLayerId, e => {
@ -49,12 +51,12 @@ onMounted(async () => {
.setHTML(popupForPG(f.properties))
.addTo(map.value)
})
status.value = `Ready · ${fc.features.length.toLocaleString('en-US')} PG-in-country pins · click any pin`
status.value = t('pplr.status.readyPins', { pins: fc.features.length.toLocaleString('en-US') })
// keep the LoadingGlobe up until the pins are actually drawn, not merely added
await whenGeojsonDrawn(map.value)
dataReady.value = true
} catch (e) {
status.value = `Error: ${e.message}`
status.value = t('pplr.status.error', { message: e.message })
}
})
})

View file

@ -1,5 +1,6 @@
<script setup>
import { dataUrl, glFor, exposeMap, whenGeojsonDrawn } from '../utils/pplrData.js'
import { usePplrT } from '../composables/usePplrT.js'
/**
* pplr-religion.vue — PG pins colored by primary religion.
*
@ -26,7 +27,8 @@ const mapProvider = resolveMapProvider(inject('profileConfig', null))
const mapboxToken = inject('mapboxToken')
const mapContainer = ref(null)
const profileRoot = ref(null) // FullscreenControl target — fullscreens .profile so chrome stays visible
const status = ref('Loading…')
const t = usePplrT()
const status = ref(t('pplr.status.loading'))
const PIN_LAYER = 'rel-pins'
const showSettings = ref(false)
@ -255,9 +257,9 @@ onMounted(async () => {
// logical center right of that column so the world / globe isn't drawn
// under the legend column.
map.value.setPadding({ left: 320, top: 0, right: 0, bottom: 0 })
status.value = 'Loading country polygons…'
status.value = t('pplr.status.loadingPolygons')
addCountryPolygons(map.value, { theme: pplrInstance.theme.value })
status.value = 'Loading PG pins…'
status.value = t('pplr.status.loadingPins')
const resp = await fetch(dataUrl('pg-points.json'))
pgData.value = await resp.json()
setupLayers()
@ -266,12 +268,12 @@ onMounted(async () => {
religionCounts.value = counts
const dominant = RELIGION_GROUPS.reduce((a, b) => (counts[a.key] || 0) > (counts[b.key] || 0) ? a : b)
// Per user 2026-05-02: drop the "Ready" prefix — it was unclear and chewed up width.
status.value = `${pgData.value.features.length.toLocaleString()} pins · ${dominant.label} most common (${(counts[dominant.key]||0).toLocaleString()})`
status.value = t('pplr.status.readyReligion', { pins: pgData.value.features.length.toLocaleString(), religion: dominant.label, count: (counts[dominant.key]||0).toLocaleString() })
// keep the LoadingGlobe up until the pins are actually drawn, not merely added
await whenGeojsonDrawn(map.value)
dataReady.value = true
} catch (err) {
status.value = `Error: ${err.message}`
status.value = t('pplr.status.error', { message: err.message })
}
})
})

View file

@ -1,5 +1,6 @@
<script setup>
import { dataUrl, glFor, exposeMap } from '../utils/pplrData.js'
import { usePplrT } from '../composables/usePplrT.js'
/**
* pplr-rop-tree.vue — ROP hierarchy: Affinity Bloc → Cluster → People Group → PGIC.
*
@ -27,7 +28,8 @@ const mapProvider = resolveMapProvider(inject('profileConfig', null))
const mapboxToken = inject('mapboxToken')
const mapContainer = ref(null)
const profileRoot = ref(null) // FullscreenControl target — fullscreens the .profile so chrome stays visible
const status = ref('Loading…')
const t = usePplrT()
const status = ref(t('pplr.status.loading'))
const PIN_LAYER = 'rop-pins'
const { map, isMapReady, initializeMap, destroy } = useMapInstance({
@ -415,7 +417,7 @@ onMounted(async () => {
map.value.setPadding({ left: 380, top: 0, right: 0, bottom: 0 })
addCountryPolygons(map.value, { theme: pplrInstance.theme.value })
status.value = 'Loading PG pins…'
status.value = t('pplr.status.loadingPins')
const resp = await fetch(dataUrl('pg-points.json'))
const fc = await resp.json()
pgData.value = fc
@ -459,9 +461,9 @@ onMounted(async () => {
const peidCount = fc.features.filter(f => f.properties.peid != null).length
mapLoaded.value = true
status.value = `Ready · ${fc.features.length.toLocaleString()} pins · ${peidCount.toLocaleString()} with PEID`
status.value = t('pplr.status.readyRop', { pins: fc.features.length.toLocaleString(), peid: peidCount.toLocaleString() })
} catch (err) {
status.value = `Error: ${err.message}`
status.value = t('pplr.status.error', { message: err.message })
}
})
})

View file

@ -14,6 +14,7 @@
*/
import { inject, onMounted, onBeforeUnmount, ref } from 'vue'
import { glFor, exposeMap, whenGeojsonDrawn } from '../utils/pplrData.js'
import { usePplrT } from '../composables/usePplrT.js'
import { useMapInstance } from '@map/composables/useMapInstance.js'
import { addCountryPolygons, loadOnce, popupForPG, POPUP_CSS } from '../utils/pplrMap.js'
import LoadingGlobe from '../components/LoadingGlobe.vue'
@ -28,7 +29,8 @@ const DATA_SOURCES = [
const mapboxToken = inject('mapboxToken')
const mapContainer = ref(null)
const status = ref('Loading…')
const t = usePplrT()
const status = ref(t('pplr.status.loading'))
const dataReady = ref(false)
function reload() { window.location.reload() }
@ -45,10 +47,10 @@ onMounted(async () => {
exposeMap(map.value)
map.value.on('load', async () => {
try {
status.value = 'Loading PG data…'
status.value = t('pplr.status.loadingPgData')
await addCountryPolygons(map.value)
status.value = 'Loading PG pins + Rosetta sample…'
status.value = t('pplr.status.loadingPinsRosetta')
const [pgFc, rosetta] = await Promise.all([
loadOnce('pg-points.json'),
loadOnce('rosetta-sample.json')
@ -140,12 +142,12 @@ onMounted(async () => {
map.value.on('mouseenter', 'rosetta-pin', () => map.value.getCanvas().style.cursor = 'pointer')
map.value.on('mouseleave', 'rosetta-pin', () => map.value.getCanvas().style.cursor = '')
status.value = `Ready · ${pgFc.features.length} pins · ${rosKeys.size} Rosetta samples (gold ★)`
status.value = t('pplr.status.readyRosetta', { pins: pgFc.features.length, samples: rosKeys.size })
// keep the LoadingGlobe up until the pins are actually drawn, not merely added
await whenGeojsonDrawn(map.value)
dataReady.value = true
} catch (e) {
status.value = `Error: ${e.message}`
status.value = t('pplr.status.error', { message: e.message })
}
})
})

View file

@ -1,5 +1,6 @@
<script setup>
import { dataUrl, glFor, exposeMap, whenGeojsonDrawn } from '../utils/pplrData.js'
import { usePplrT } from '../composables/usePplrT.js'
/**
* pplr-translations.vue — PG name in all 17 locales.
*
@ -20,7 +21,8 @@ provideInstance(pplrInstance)
const mapboxToken = inject('mapboxToken')
const mapContainer = ref(null)
const profileRoot = ref(null)
const status = ref('Loading…')
const t = usePplrT()
const status = ref(t('pplr.status.loading'))
const PIN_LAYER = 'trans-pins'
const { map, isMapReady, initializeMap, destroy } = useMapInstance({
@ -64,10 +66,10 @@ onMounted(async () => {
map.value.on('load', async () => {
try {
map.value.setPadding({ left: 380, top: 0, right: 0, bottom: 0 })
status.value = 'Loading PG data…'
status.value = t('pplr.status.loadingPgData')
addCountryPolygons(map.value, { theme: pplrInstance.theme.value })
status.value = 'Loading PG pins…'
status.value = t('pplr.status.loadingPins')
const resp = await fetch(dataUrl('pg-points.json'))
const fc = await resp.json()
@ -91,12 +93,12 @@ onMounted(async () => {
pinIndicator.show(map.value, [lng, lat], { color: '#a78bfa' })
})
status.value = `${fc.features.length.toLocaleString()} pins · click any pin to see all 17 locale names`
status.value = t('pplr.status.readyTranslations', { pins: fc.features.length.toLocaleString() })
// keep the LoadingGlobe up until the pins are actually drawn, not merely added
await whenGeojsonDrawn(map.value)
dataReady.value = true
} catch (err) {
status.value = `Error: ${err.message}`
status.value = t('pplr.status.error', { message: err.message })
}
})
})