PPLR maps: the toolbars, legends, pills, popups and pin card follow the reader's language

The chrome around the maps was typed-in English: the search placeholder, Base map and
Data sources, the legend titles, each view's header pill, the popup row labels, the pin
card and the Rosetta legend. 42 keys in all eleven catalogues, reusing the product's
own words for Legend, Close, Choose a view, Religion and Language.

Registry names stay as they are (ROP3, ROP25, PEID, IMB PGID, JP Code, JoshuaProject,
WCD, PGIC GeoID), as do the Data sources provenance labels. The reference pages
(semantic trees, index, name locales) wait on the English-only ruling.
This commit is contained in:
Doxa Maps 2026-09-27 01:56:58 +00:00
parent b6e04812ec
commit 71826baa3d
25 changed files with 706 additions and 77 deletions

View file

@ -32,5 +32,61 @@
"readyPins": "جاهز · دبابيس الشعوب في الدول: {pins} · انقر أي دبوس",
"readyLanguage": "جاهز · الدبابيس: {pins} · تغطية العائلات {coverage}% · أعلى 25 = {top}%",
"readyReligion": "الدبابيس: {pins} · الأكثر شيوعًا: {religion} ({count})"
},
"ui": {
"chooseView": "اختر عرضًا",
"search": "ابحث بالشعب أو المكان أو اللغة أو الدين…",
"baseMap": "الخريطة الأساسية",
"dataSources": "مصادر البيانات",
"legend": "مفتاح الخريطة",
"openLegend": "افتح مفتاح الخريطة",
"clearSelectionEsc": "إلغاء التحديد (Esc)",
"closeEsc": "إغلاق (Esc)",
"close": "إغلاق",
"retry": "أعد المحاولة",
"loadingMap": "جارٍ تحميل الخريطة",
"flyToPin": "انتقل إلى الدبوس",
"primaryReligion": "الدين الرئيسي",
"ropHierarchy": "تسلسل ROP الهرمي"
},
"pill": {
"religion": "توزيع الأديان · انقر دبوسًا للتفاصيل",
"ropTree": "تسلسل ROP الهرمي · الكتلة ← المجموعة ← الشعب ← PGIC",
"languageFamilies": "العائلات العرقية اللغوية (PPLR) · انقر دبوسًا للتفاصيل",
"sunburst": "تسلسل ROP الهرمي · الكتلة ← مجموعة الشعوب · مرّر للتكبير",
"pins": "كل دبابيس الشعوب في الدول · انقر لمعرّفات الأنظمة",
"coverage": "تغطية الترجمة · التظليل = متوسط اللغات المعبّأة لكل دولة · انقر دبوسًا أو دولة",
"rosetta": "جدول معرّفات الأنظمة · اللون = المعرّفات الموجودة · ★ = صف نموذجي"
},
"legendTitle": {
"avgLocales": "متوسط اللغات / {n}",
"idCoverage": "تغطية المعرّفات"
},
"rosetta": {
"legend": {
"all": "JP + IMB + WCD، الثلاثة",
"imbOnly": "IMB فقط",
"jpOnly": "JP فقط",
"neither": "لا هذا ولا ذاك"
},
"samplePg": "★ شعب نموذجي",
"missingIds": "لم تُربط بعد في PPLR: {ids}. الصف الفارغ نقص في البيانات وليس خطأ."
},
"popup": {
"noName": "(بلا اسم)",
"country": "الدولة",
"religion": "الدين",
"language": "اللغة",
"family": "العائلة",
"affinityBloc": "الكتلة",
"cluster": "المجموعة",
"peopleCluster": "مجموعة الشعوب",
"peopleGroup": "الشعب",
"pgCount": "الشعوب",
"avgLocalesFilled": "متوسط اللغات المعبّأة",
"iso3Country": "رمز الدولة ISO-3"
},
"card": {
"nameInLocales": "الاسم في {n} لغة"
}
}

View file

@ -32,5 +32,61 @@
"readyPins": "Bereit · Pins: {pins} · einen Pin anklicken",
"readyLanguage": "Bereit · Pins: {pins} · Familienabdeckung {coverage} % · Top 25 = {top} %",
"readyReligion": "Pins: {pins} · am häufigsten: {religion} ({count})"
},
"ui": {
"chooseView": "Ansicht wählen",
"search": "Nach Volk, Ort, Sprache, Religion suchen…",
"baseMap": "Grundkarte",
"dataSources": "Datenquellen",
"legend": "Legende",
"openLegend": "Legende öffnen",
"clearSelectionEsc": "Auswahl aufheben (Esc)",
"closeEsc": "Schließen (Esc)",
"close": "Schließen",
"retry": "Erneut versuchen",
"loadingMap": "Karte wird geladen",
"flyToPin": "Zum Pin fliegen",
"primaryReligion": "Hauptreligion",
"ropHierarchy": "ROP-Hierarchie"
},
"pill": {
"religion": "Verteilung der Religionen · Pin anklicken für Details",
"ropTree": "ROP-Hierarchie · Affinitätsblock → Cluster → Volk → PGIC",
"languageFamilies": "Ethnolinguistische Familien (PPLR) · Pin anklicken für Details",
"sunburst": "ROP-Hierarchie · Affinitätsblock → Volkscluster · scrollen zum Zoomen",
"pins": "Alle Volksgruppen-Pins je Land · anklicken für systemübergreifende IDs",
"coverage": "Übersetzungsabdeckung · Färbung = durchschnittlich gefüllte Sprachen je Land · Pin oder Land anklicken",
"rosetta": "ID-Rosetta · Farbe = vorhandene IDs · ★ = Beispielzeile"
},
"legendTitle": {
"avgLocales": "Durchschnitt Sprachen / {n}",
"idCoverage": "ID-Abdeckung"
},
"rosetta": {
"legend": {
"all": "JP + IMB + WCD, alle drei",
"imbOnly": "Nur IMB",
"jpOnly": "Nur JP",
"neither": "keins von beiden"
},
"samplePg": "★ Beispiel-Volk",
"missingIds": "In PPLR noch nicht zugeordnet: {ids}. Eine leere Zeile ist eine Datenlücke, kein Fehler."
},
"popup": {
"noName": "(ohne Namen)",
"country": "Land",
"religion": "Religion",
"language": "Sprache",
"family": "Familie",
"affinityBloc": "Affinitätsblock",
"cluster": "Cluster",
"peopleCluster": "Volkscluster",
"peopleGroup": "Volk",
"pgCount": "Völker",
"avgLocalesFilled": "Durchschnittlich gefüllte Sprachen",
"iso3Country": "ISO-3-Land"
},
"card": {
"nameInLocales": "Name in {n} Sprachen"
}
}

View file

@ -32,5 +32,61 @@
"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})"
},
"ui": {
"chooseView": "Choose a view",
"search": "Search by people, place, language, religion…",
"baseMap": "Base map",
"dataSources": "Data sources",
"legend": "Legend",
"openLegend": "Open legend",
"clearSelectionEsc": "Clear selection (Esc)",
"closeEsc": "Close (Esc)",
"close": "Close",
"retry": "Retry",
"loadingMap": "Loading map",
"flyToPin": "Fly to pin",
"primaryReligion": "Primary religion",
"ropHierarchy": "ROP hierarchy"
},
"pill": {
"religion": "Religion distribution · click a pin for detail",
"ropTree": "ROP hierarchy · Affinity Bloc → Cluster → People Group → PGIC",
"languageFamilies": "Ethnolinguistic families (PPLR) · click a pin for detail",
"sunburst": "ROP hierarchy · Affinity Bloc → People Cluster · scroll to zoom",
"pins": "All people-group-in-country pins · click for cross-system IDs",
"coverage": "Translation coverage · shading = average locales filled per country · click a pin or a country",
"rosetta": "Cross-system ID Rosetta · colour = which IDs exist · ★ = sample row"
},
"legendTitle": {
"avgLocales": "Average locales / {n}",
"idCoverage": "ID coverage"
},
"rosetta": {
"legend": {
"all": "JP + IMB + WCD, all three",
"imbOnly": "IMB only",
"jpOnly": "JP only",
"neither": "neither"
},
"samplePg": "★ sample people group",
"missingIds": "Not yet cross-mapped in PPLR: {ids}. An empty row is a data gap, not an error."
},
"popup": {
"noName": "(no name)",
"country": "Country",
"religion": "Religion",
"language": "Language",
"family": "Family",
"affinityBloc": "Affinity Bloc",
"cluster": "Cluster",
"peopleCluster": "People cluster",
"peopleGroup": "People group",
"pgCount": "People groups",
"avgLocalesFilled": "Average locales filled",
"iso3Country": "ISO-3 country"
},
"card": {
"nameInLocales": "Name in {n} locales"
}
}

View file

@ -32,5 +32,61 @@
"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})"
},
"ui": {
"chooseView": "Elige una vista",
"search": "Buscar por pueblo, lugar, idioma, religión…",
"baseMap": "Mapa base",
"dataSources": "Fuentes de datos",
"legend": "Leyenda",
"openLegend": "Abrir la leyenda",
"clearSelectionEsc": "Borrar la selección (Esc)",
"closeEsc": "Cerrar (Esc)",
"close": "Cerrar",
"retry": "Reintentar",
"loadingMap": "Cargando el mapa",
"flyToPin": "Ir al pin",
"primaryReligion": "Religión principal",
"ropHierarchy": "Jerarquía ROP"
},
"pill": {
"religion": "Distribución de religiones · haz clic en un pin para ver detalles",
"ropTree": "Jerarquía ROP · bloque de afinidad → clúster → pueblo → PGIC",
"languageFamilies": "Familias etnolingüísticas (PPLR) · haz clic en un pin para ver detalles",
"sunburst": "Jerarquía ROP · bloque de afinidad → clúster de pueblos · desplázate para ampliar",
"pins": "Todos los pines de pueblos por país · haz clic para ver los identificadores de cada sistema",
"coverage": "Cobertura de traducción · sombreado = promedio de idiomas completados por país · haz clic en un pin o un país",
"rosetta": "Rosetta de identificadores · color = identificadores existentes · ★ = fila de ejemplo"
},
"legendTitle": {
"avgLocales": "Promedio de idiomas / {n}",
"idCoverage": "Cobertura de identificadores"
},
"rosetta": {
"legend": {
"all": "JP + IMB + WCD, los tres",
"imbOnly": "Solo IMB",
"jpOnly": "Solo JP",
"neither": "ninguno"
},
"samplePg": "★ pueblo de ejemplo",
"missingIds": "Aún sin vincular en PPLR: {ids}. Una fila vacía es un hueco en los datos, no un error."
},
"popup": {
"noName": "(sin nombre)",
"country": "País",
"religion": "Religión",
"language": "Idioma",
"family": "Familia",
"affinityBloc": "Bloque de afinidad",
"cluster": "Clúster",
"peopleCluster": "Clúster de pueblos",
"peopleGroup": "Pueblo",
"pgCount": "Pueblos",
"avgLocalesFilled": "Promedio de idiomas completados",
"iso3Country": "País ISO-3"
},
"card": {
"nameInLocales": "Nombre en {n} idiomas"
}
}

View file

@ -32,5 +32,61 @@
"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})"
},
"ui": {
"chooseView": "Choisir une vue",
"search": "Rechercher par peuple, lieu, langue, religion…",
"baseMap": "Fond de carte",
"dataSources": "Sources des données",
"legend": "Légende",
"openLegend": "Ouvrir la légende",
"clearSelectionEsc": "Effacer la sélection (Échap)",
"closeEsc": "Fermer (Échap)",
"close": "Fermer",
"retry": "Réessayer",
"loadingMap": "Chargement de la carte",
"flyToPin": "Aller à l’épingle",
"primaryReligion": "Religion principale",
"ropHierarchy": "Hiérarchie ROP"
},
"pill": {
"religion": "Répartition des religions · cliquez sur une épingle pour le détail",
"ropTree": "Hiérarchie ROP · bloc d’affinité → grappe → peuple → PGIC",
"languageFamilies": "Familles ethnolinguistiques (PPLR) · cliquez sur une épingle pour le détail",
"sunburst": "Hiérarchie ROP · bloc d’affinité → grappe de peuples · faites défiler pour zoomer",
"pins": "Toutes les épingles peuple-pays · cliquez pour les identifiants des systèmes",
"coverage": "Couverture de traduction · teinte = moyenne des langues renseignées par pays · cliquez sur une épingle ou un pays",
"rosetta": "Rosetta des identifiants · couleur = identifiants présents · ★ = ligne d’exemple"
},
"legendTitle": {
"avgLocales": "Moyenne des langues / {n}",
"idCoverage": "Couverture des identifiants"
},
"rosetta": {
"legend": {
"all": "JP + IMB + WCD, les trois",
"imbOnly": "IMB seulement",
"jpOnly": "JP seulement",
"neither": "aucun des deux"
},
"samplePg": "★ peuple d’exemple",
"missingIds": "Pas encore mis en correspondance dans PPLR : {ids}. Une ligne vide est une lacune des données, pas une erreur."
},
"popup": {
"noName": "(sans nom)",
"country": "Pays",
"religion": "Religion",
"language": "Langue",
"family": "Famille",
"affinityBloc": "Bloc d’affinité",
"cluster": "Grappe",
"peopleCluster": "Grappe de peuples",
"peopleGroup": "Peuple",
"pgCount": "Peuples",
"avgLocalesFilled": "Moyenne des langues renseignées",
"iso3Country": "Pays ISO-3"
},
"card": {
"nameInLocales": "Nom dans {n} langues"
}
}

View file

@ -32,5 +32,61 @@
"readyPins": "तैयार · पिन: {pins} · किसी पिन पर क्लिक करें",
"readyLanguage": "तैयार · पिन: {pins} · परिवार कवरेज {coverage}% · शीर्ष 25 = {top}%",
"readyReligion": "पिन: {pins} · सबसे आम: {religion} ({count})"
},
"ui": {
"chooseView": "दृश्य चुनें",
"search": "जनसमूह, स्थान, भाषा, धर्म से खोजें…",
"baseMap": "आधार मानचित्र",
"dataSources": "डेटा स्रोत",
"legend": "सूचकांक",
"openLegend": "सूचकांक खोलें",
"clearSelectionEsc": "चयन हटाएँ (Esc)",
"closeEsc": "बंद करें (Esc)",
"close": "बंद करें",
"retry": "फिर से कोशिश करें",
"loadingMap": "मानचित्र लोड हो रहा है",
"flyToPin": "पिन पर जाएँ",
"primaryReligion": "प्रमुख धर्म",
"ropHierarchy": "ROP पदानुक्रम"
},
"pill": {
"religion": "धर्मों का वितरण · विवरण के लिए किसी पिन पर क्लिक करें",
"ropTree": "ROP पदानुक्रम · सम्बद्धता खंड → क्लस्टर → जनसमूह → PGIC",
"languageFamilies": "नृजातीय-भाषाई परिवार (PPLR) · विवरण के लिए किसी पिन पर क्लिक करें",
"sunburst": "ROP पदानुक्रम · सम्बद्धता खंड → जनसमूह क्लस्टर · ज़ूम के लिए स्क्रॉल करें",
"pins": "देश-वार सभी जनसमूह पिन · प्रणालियों की पहचान-संख्याओं के लिए क्लिक करें",
"coverage": "अनुवाद कवरेज · रंग = प्रति देश भरी गई भाषाओं का औसत · किसी पिन या देश पर क्लिक करें",
"rosetta": "पहचान-संख्या रोसेटा · रंग = कौन-सी पहचान-संख्याएँ हैं · ★ = नमूना पंक्ति"
},
"legendTitle": {
"avgLocales": "भाषाओं का औसत / {n}",
"idCoverage": "पहचान-संख्या कवरेज"
},
"rosetta": {
"legend": {
"all": "JP + IMB + WCD, तीनों",
"imbOnly": "केवल IMB",
"jpOnly": "केवल JP",
"neither": "दोनों में से कोई नहीं"
},
"samplePg": "★ नमूना जनसमूह",
"missingIds": "PPLR में अभी मिलान नहीं हुआ: {ids}। खाली पंक्ति डेटा की कमी है, त्रुटि नहीं।"
},
"popup": {
"noName": "(कोई नाम नहीं)",
"country": "देश",
"religion": "धर्म",
"language": "भाषा",
"family": "परिवार",
"affinityBloc": "सम्बद्धता खंड",
"cluster": "क्लस्टर",
"peopleCluster": "जनसमूह क्लस्टर",
"peopleGroup": "जनसमूह",
"pgCount": "जनसमूह",
"avgLocalesFilled": "भरी गई भाषाओं का औसत",
"iso3Country": "ISO-3 देश"
},
"card": {
"nameInLocales": "{n} भाषाओं में नाम"
}
}

View file

@ -32,5 +32,61 @@
"readyPins": "Pronto · segnaposto: {pins} · clicca un segnaposto",
"readyLanguage": "Pronto · segnaposto: {pins} · copertura famiglie {coverage}% · top 25 = {top}%",
"readyReligion": "Segnaposto: {pins} · più comune: {religion} ({count})"
},
"ui": {
"chooseView": "Scegli una vista",
"search": "Cerca per popolo, luogo, lingua, religione…",
"baseMap": "Mappa di base",
"dataSources": "Fonti dei dati",
"legend": "Legenda",
"openLegend": "Apri la legenda",
"clearSelectionEsc": "Cancella la selezione (Esc)",
"closeEsc": "Chiudi (Esc)",
"close": "Chiudi",
"retry": "Riprova",
"loadingMap": "Caricamento della mappa",
"flyToPin": "Vai al segnaposto",
"primaryReligion": "Religione principale",
"ropHierarchy": "Gerarchia ROP"
},
"pill": {
"religion": "Distribuzione delle religioni · clicca un segnaposto per i dettagli",
"ropTree": "Gerarchia ROP · blocco di affinità → cluster → popolo → PGIC",
"languageFamilies": "Famiglie etnolinguistiche (PPLR) · clicca un segnaposto per i dettagli",
"sunburst": "Gerarchia ROP · blocco di affinità → cluster di popoli · scorri per ingrandire",
"pins": "Tutti i segnaposto popolo-paese · clicca per gli identificativi dei sistemi",
"coverage": "Copertura delle traduzioni · colore = media delle lingue compilate per paese · clicca un segnaposto o un paese",
"rosetta": "Rosetta degli identificativi · colore = identificativi presenti · ★ = riga di esempio"
},
"legendTitle": {
"avgLocales": "Media delle lingue / {n}",
"idCoverage": "Copertura degli identificativi"
},
"rosetta": {
"legend": {
"all": "JP + IMB + WCD, tutti e tre",
"imbOnly": "Solo IMB",
"jpOnly": "Solo JP",
"neither": "nessuno dei due"
},
"samplePg": "★ popolo di esempio",
"missingIds": "Non ancora collegati in PPLR: {ids}. Una riga vuota è una lacuna nei dati, non un errore."
},
"popup": {
"noName": "(senza nome)",
"country": "Paese",
"religion": "Religione",
"language": "Lingua",
"family": "Famiglia",
"affinityBloc": "Blocco di affinità",
"cluster": "Cluster",
"peopleCluster": "Cluster di popoli",
"peopleGroup": "Popolo",
"pgCount": "Popoli",
"avgLocalesFilled": "Media delle lingue compilate",
"iso3Country": "Paese ISO-3"
},
"card": {
"nameInLocales": "Nome in {n} lingue"
}
}

View file

@ -32,5 +32,61 @@
"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})"
},
"ui": {
"chooseView": "Escolha uma vista",
"search": "Pesquisar por povo, lugar, idioma, religião…",
"baseMap": "Mapa base",
"dataSources": "Fontes de dados",
"legend": "Legenda",
"openLegend": "Abrir a legenda",
"clearSelectionEsc": "Limpar a seleção (Esc)",
"closeEsc": "Fechar (Esc)",
"close": "Fechar",
"retry": "Tentar novamente",
"loadingMap": "A carregar o mapa",
"flyToPin": "Ir para o marcador",
"primaryReligion": "Religião principal",
"ropHierarchy": "Hierarquia ROP"
},
"pill": {
"religion": "Distribuição das religiões · clique num marcador para ver detalhes",
"ropTree": "Hierarquia ROP · bloco de afinidade → agrupamento → povo → PGIC",
"languageFamilies": "Famílias etnolinguísticas (PPLR) · clique num marcador para ver detalhes",
"sunburst": "Hierarquia ROP · bloco de afinidade → agrupamento de povos · role para ampliar",
"pins": "Todos os marcadores povo-país · clique para ver os identificadores dos sistemas",
"coverage": "Cobertura de tradução · cor = média de idiomas preenchidos por país · clique num marcador ou num país",
"rosetta": "Rosetta de identificadores · cor = identificadores existentes · ★ = linha de exemplo"
},
"legendTitle": {
"avgLocales": "Média de idiomas / {n}",
"idCoverage": "Cobertura de identificadores"
},
"rosetta": {
"legend": {
"all": "JP + IMB + WCD, os três",
"imbOnly": "Apenas IMB",
"jpOnly": "Apenas JP",
"neither": "nenhum"
},
"samplePg": "★ povo de exemplo",
"missingIds": "Ainda não correspondidos no PPLR: {ids}. Uma linha vazia é uma lacuna nos dados, não um erro."
},
"popup": {
"noName": "(sem nome)",
"country": "País",
"religion": "Religião",
"language": "Idioma",
"family": "Família",
"affinityBloc": "Bloco de afinidade",
"cluster": "Agrupamento",
"peopleCluster": "Agrupamento de povos",
"peopleGroup": "Povo",
"pgCount": "Povos",
"avgLocalesFilled": "Média de idiomas preenchidos",
"iso3Country": "País ISO-3"
},
"card": {
"nameInLocales": "Nome em {n} idiomas"
}
}

View file

@ -32,5 +32,61 @@
"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})"
},
"ui": {
"chooseView": "Alegeți o vedere",
"search": "Căutați după popor, loc, limbă, religie…",
"baseMap": "Hartă de bază",
"dataSources": "Surse de date",
"legend": "Legendă",
"openLegend": "Deschide legenda",
"clearSelectionEsc": "Șterge selecția (Esc)",
"closeEsc": "Închide (Esc)",
"close": "Închide",
"retry": "Reîncercați",
"loadingMap": "Se încarcă harta",
"flyToPin": "Mergi la reper",
"primaryReligion": "Religia principală",
"ropHierarchy": "Ierarhia ROP"
},
"pill": {
"religion": "Distribuția religiilor · dați clic pe un reper pentru detalii",
"ropTree": "Ierarhia ROP · bloc de afinitate → grup → popor → PGIC",
"languageFamilies": "Familii etnolingvistice (PPLR) · dați clic pe un reper pentru detalii",
"sunburst": "Ierarhia ROP · bloc de afinitate → grup de popoare · derulați pentru zoom",
"pins": "Toate reperele popor-țară · dați clic pentru identificatorii sistemelor",
"coverage": "Acoperirea traducerilor · nuanța = media limbilor completate pe țară · dați clic pe un reper sau pe o țară",
"rosetta": "Rosetta identificatorilor · culoare = identificatorii existenți · ★ = rând de exemplu"
},
"legendTitle": {
"avgLocales": "Media limbilor / {n}",
"idCoverage": "Acoperirea identificatorilor"
},
"rosetta": {
"legend": {
"all": "JP + IMB + WCD, toate trei",
"imbOnly": "Doar IMB",
"jpOnly": "Doar JP",
"neither": "niciunul"
},
"samplePg": "★ popor de exemplu",
"missingIds": "Încă neasociate în PPLR: {ids}. Un rând gol este o lipsă de date, nu o eroare."
},
"popup": {
"noName": "(fără nume)",
"country": "Țară",
"religion": "Religie",
"language": "Limbă",
"family": "Familie",
"affinityBloc": "Bloc de afinitate",
"cluster": "Grup",
"peopleCluster": "Grup de popoare",
"peopleGroup": "Popor",
"pgCount": "Popoare",
"avgLocalesFilled": "Media limbilor completate",
"iso3Country": "Țară ISO-3"
},
"card": {
"nameInLocales": "Numele în {n} limbi"
}
}

View file

@ -32,5 +32,61 @@
"readyPins": "Готово · меток: {pins} · нажмите на метку",
"readyLanguage": "Готово · меток: {pins} · покрытие семей {coverage}% · топ-25 = {top}%",
"readyReligion": "Меток: {pins} · чаще всего: {religion} ({count})"
},
"ui": {
"chooseView": "Выберите вид",
"search": "Поиск по народу, месту, языку, религии…",
"baseMap": "Базовая карта",
"dataSources": "Источники данных",
"legend": "Легенда",
"openLegend": "Открыть легенду",
"clearSelectionEsc": "Снять выделение (Esc)",
"closeEsc": "Закрыть (Esc)",
"close": "Закрыть",
"retry": "Повторить",
"loadingMap": "Загрузка карты",
"flyToPin": "Перейти к метке",
"primaryReligion": "Основная религия",
"ropHierarchy": "Иерархия ROP"
},
"pill": {
"religion": "Распределение религий · нажмите на метку, чтобы узнать подробности",
"ropTree": "Иерархия ROP · блок родства → кластер → народ → PGIC",
"languageFamilies": "Этнолингвистические семьи (PPLR) · нажмите на метку, чтобы узнать подробности",
"sunburst": "Иерархия ROP · блок родства → кластер народов · прокрутите для масштаба",
"pins": "Все метки «народ в стране» · нажмите, чтобы увидеть идентификаторы систем",
"coverage": "Покрытие переводов · заливка = среднее число заполненных языков по стране · нажмите на метку или страну",
"rosetta": "Розетта идентификаторов · цвет = какие идентификаторы есть · ★ = пример"
},
"legendTitle": {
"avgLocales": "Среднее число языков / {n}",
"idCoverage": "Покрытие идентификаторов"
},
"rosetta": {
"legend": {
"all": "JP + IMB + WCD, все три",
"imbOnly": "Только IMB",
"jpOnly": "Только JP",
"neither": "ни того, ни другого"
},
"samplePg": "★ пример народа",
"missingIds": "Ещё не сопоставлено в PPLR: {ids}. Пустая строка — пробел в данных, а не ошибка."
},
"popup": {
"noName": "(без названия)",
"country": "Страна",
"religion": "Религия",
"language": "Язык",
"family": "Семья",
"affinityBloc": "Блок родства",
"cluster": "Кластер",
"peopleCluster": "Кластер народов",
"peopleGroup": "Народ",
"pgCount": "Народы",
"avgLocalesFilled": "Среднее число заполненных языков",
"iso3Country": "Страна ISO-3"
},
"card": {
"nameInLocales": "Название на {n} языках"
}
}

View file

@ -32,5 +32,61 @@
"readyPins": "就绪 · 族群标记:{pins} · 点击任一标记",
"readyLanguage": "就绪 · 标记:{pins} · 语系覆盖率 {coverage}% · 前 25 = {top}%",
"readyReligion": "标记:{pins} · 最常见:{religion}({count})"
},
"ui": {
"chooseView": "选择视图",
"search": "按族群、地点、语言、宗教搜索…",
"baseMap": "底图",
"dataSources": "数据来源",
"legend": "图例",
"openLegend": "打开图例",
"clearSelectionEsc": "清除选择 (Esc)",
"closeEsc": "关闭 (Esc)",
"close": "关闭",
"retry": "重试",
"loadingMap": "正在加载地图",
"flyToPin": "飞到标记",
"primaryReligion": "主要宗教",
"ropHierarchy": "ROP 层级"
},
"pill": {
"religion": "宗教分布 · 点击标记查看详情",
"ropTree": "ROP 层级 · 亲缘区块 → 民族群 → 族群 → PGIC",
"languageFamilies": "民族语言语系 (PPLR) · 点击标记查看详情",
"sunburst": "ROP 层级 · 亲缘区块 → 民族群 · 滚动缩放",
"pins": "全部国家内族群标记 · 点击查看跨系统 ID",
"coverage": "翻译覆盖率 · 着色 = 各国平均已填语种数 · 点击标记或国家",
"rosetta": "跨系统 ID 对照 · 颜色 = 已有哪些 ID · ★ = 示例行"
},
"legendTitle": {
"avgLocales": "平均语种数 / {n}",
"idCoverage": "ID 覆盖"
},
"rosetta": {
"legend": {
"all": "JP + IMB + WCD 三者俱全",
"imbOnly": "仅 IMB",
"jpOnly": "仅 JP",
"neither": "两者皆无"
},
"samplePg": "★ 示例族群",
"missingIds": "PPLR 尚未对应:{ids}。空行是数据缺口,不是错误。"
},
"popup": {
"noName": "(无名称)",
"country": "国家",
"religion": "宗教",
"language": "语言",
"family": "语系",
"affinityBloc": "亲缘区块",
"cluster": "民族群",
"peopleCluster": "民族群",
"peopleGroup": "族群",
"pgCount": "族群数",
"avgLocalesFilled": "平均已填语种数",
"iso3Country": "ISO-3 国家"
},
"card": {
"nameInLocales": "{n} 种语言中的名称"
}
}

View file

@ -74,7 +74,7 @@ useShadowStyles(
<div class="pdm-root">
<div class="pdm-bar">
<span class="pdm-brand">PPLR data maps</span>
<div class="pdm-seg" role="tablist" aria-label="Choose a view">
<div class="pdm-seg" role="tablist" :aria-label="t('pplr.ui.chooseView')">
<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)">{{ t(v.tKey) }}</button>
</div>

View file

@ -9,6 +9,7 @@
* component into its toolbar.
*/
import { ref } from 'vue'
import { usePplrT } from '../composables/usePplrT.js'
defineProps({
/** Array of { label, url, fields[] } */
@ -16,19 +17,20 @@ defineProps({
})
const open = ref(false)
const t = usePplrT()
function toggle() { open.value = !open.value }
</script>
<template>
<div class="dsd-wrap">
<button class="toolbar-btn round" type="button" title="Data sources" @click="toggle">
<button class="toolbar-btn round" type="button" :title="t('pplr.ui.dataSources')" @click="toggle">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="1.8"/>
<path d="M19 12a7 7 0 0 0-.1-1.2l2-1.5-2-3.5-2.4 1a7 7 0 0 0-2.1-1.2L14 3h-4l-.4 2.6A7 7 0 0 0 7.5 6.8l-2.4-1-2 3.5 2 1.5A7 7 0 0 0 5 12c0 .4 0 .8.1 1.2l-2 1.5 2 3.5 2.4-1a7 7 0 0 0 2.1 1.2L10 21h4l.4-2.6a7 7 0 0 0 2.1-1.2l2.4 1 2-3.5-2-1.5c.06-.4.1-.8.1-1.2z" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/>
</svg>
</button>
<div v-if="open" class="dsd-panel">
<div class="dsd-header">Data sources</div>
<div class="dsd-header">{{ t('pplr.ui.dataSources') }}</div>
<div v-for="src in sources" :key="src.url" class="dsd-src">
<div class="dsd-src-label">{{ src.label }}</div>
<div class="dsd-src-url">{{ src.url }}</div>

View file

@ -20,6 +20,7 @@
*/
import { ref, computed } from 'vue'
import { useInstance } from '../composables/usePplrInstance.js'
import { usePplrT } from '../composables/usePplrT.js'
const props = defineProps({
items: { type: Array, default: () => [] },
@ -30,6 +31,7 @@ const props = defineProps({
const emit = defineEmits(['select'])
const instance = useInstance()
const t = usePplrT()
const collapsed = ref(false)
const theme = computed(() => instance?.theme.value || 'dark')
@ -77,7 +79,7 @@ function clearSel() {
</svg>
</button>
<div class="fl-title">{{ title }}</div>
<button v-if="selectedKey" class="fl-clear" title="Clear selection (Esc)" @click="clearSel">×</button>
<button v-if="selectedKey" class="fl-clear" :title="t('pplr.ui.clearSelectionEsc')" @click="clearSel">×</button>
</div>
<div class="fl-rows" :class="{ 'has-sel': !!selectedKey }">
<button
@ -99,12 +101,12 @@ function clearSel() {
<button v-if="collapsed"
class="fl-reopen"
:data-theme="theme"
title="Open legend"
:title="t('pplr.ui.openLegend')"
@click="collapsed = false">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none">
<polyline points="9,18 15,12 9,6" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span>Legend</span>
<span>{{ t('pplr.ui.legend') }}</span>
</button>
</template>

View file

@ -12,12 +12,14 @@
* <LoadingGlobe v-if="!isReady" :label="loadingStage" @retry="retry" />
*/
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
import { usePplrT } from '../composables/usePplrT.js'
const props = defineProps({
label: { type: String, default: 'Loading map' },
label: { type: String, default: '' }, // empty = the translated "Loading map"
stuckAfterMs:{ type: Number, default: 30000 },
})
const emit = defineEmits(['retry'])
const t = usePplrT()
const stuck = ref(false)
let stuckTimer = null
@ -56,13 +58,13 @@ function onRetry() {
<line x1="6" y1="32" x2="58" y2="32" stroke="#73A17F" stroke-width="1.2" opacity="0.45"/>
<line x1="32" y1="6" x2="32" y2="58" stroke="#73A17F" stroke-width="1.2" opacity="0.45"/>
</svg>
<span class="lg-text">{{ label }}</span>
<span class="lg-text">{{ label || t('pplr.ui.loadingMap') }}</span>
<button v-if="stuck" class="lg-retry" type="button" @click="onRetry">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M21 12a9 9 0 1 1-3-6.7" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<polyline points="21 4 21 11 14 11" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Retry
{{ t('pplr.ui.retry') }}
</button>
</div>
</template>

View file

@ -19,6 +19,7 @@ import { computed, ref, watch, onMounted, onBeforeUnmount } from 'vue'
import { useInstance } from '../composables/usePplrInstance.js'
import { LOCALES, LOCALE_NAMES } from '../utils/pplrMap.js'
import { loadNames } from '../utils/pplrData.js'
import { usePplrT } from '../composables/usePplrT.js'
const props = defineProps({
/** Optional override — usually read from instance.pinDetail */
@ -30,6 +31,7 @@ const props = defineProps({
const emit = defineEmits(['close', 'locate'])
const instance = useInstance()
const tr = usePplrT() // not `t`: the names table below is already called t
const pinData = computed(() => props.pin || instance?.pinDetail.value || null)
// All 17 locale names for this PG (n_en, n_hi, n_bn, …). Per user 2026-05-02:
@ -59,12 +61,12 @@ const fields = computed(() => {
const dash = v => (v == null || v === 'None' || v === '' || v === '*') ? '—' : v
// Curated field order — most identifying first, registry IDs last
return [
{ label: 'Country', value: dash(p.country), mono: false },
{ label: 'Religion', value: dash(p.religion), mono: false },
{ label: 'Language', value: dash(p.language), mono: false },
{ label: 'ISO-3 Country', value: dash(p.iso3), mono: true, hidden: !p.iso3 },
{ label: 'Affinity Bloc', value: dash(p.rop1), mono: true, hidden: !p.rop1 },
{ label: 'People Cluster', value: dash(p.rop2), mono: true, hidden: !p.rop2 },
{ label: tr('pplr.popup.country'), value: dash(p.country), mono: false },
{ label: tr('pplr.popup.religion'), value: dash(p.religion), mono: false },
{ label: tr('pplr.popup.language'), value: dash(p.language), mono: false },
{ label: tr('pplr.popup.iso3Country'), value: dash(p.iso3), mono: true, hidden: !p.iso3 },
{ label: tr('pplr.popup.affinityBloc'), value: dash(p.rop1), mono: true, hidden: !p.rop1 },
{ label: tr('pplr.popup.peopleCluster'), value: dash(p.rop2), mono: true, hidden: !p.rop2 },
{ label: 'ROP25', value: dash(p.rop3), mono: true, hidden: !p.rop3 },
{ label: 'PGIC GeoID', value: dash(p.rop25), mono: true, hidden: !p.rop25 },
{ label: 'PEID', value: dash(p.peid), mono: true, hidden: !p.peid },
@ -98,8 +100,8 @@ onBeforeUnmount(() => document.removeEventListener('keydown', onKey))
<!-- Title + close -->
<div class="pdc-header">
<span class="pdc-eyebrow">People Group</span>
<button class="pdc-close" title="Close (Esc)" @click="close">
<span class="pdc-eyebrow">{{ tr('pplr.popup.peopleGroup') }}</span>
<button class="pdc-close" :title="tr('pplr.ui.closeEsc')" @click="close">
<svg width="11" height="11" viewBox="0 0 12 12" fill="none">
<path d="M2 2L10 10M10 2L2 10" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>
</svg>
@ -117,7 +119,7 @@ onBeforeUnmount(() => document.removeEventListener('keydown', onKey))
</div>
<div class="pdc-section-divider">
<span>Name in 17 locales</span>
<span>{{ tr('pplr.card.nameInLocales', { n: locales.length }) }}</span>
</div>
<div v-for="loc in locales" :key="loc.code" class="pdc-field pdc-locale">
<span class="pdc-field-label">{{ loc.name }} <code>{{ loc.code }}</code></span>
@ -134,9 +136,9 @@ onBeforeUnmount(() => document.removeEventListener('keydown', onKey))
<path d="M12 21s7-7 7-12a7 7 0 1 0-14 0c0 5 7 12 7 12z" stroke="currentColor" stroke-width="2"/>
<circle cx="12" cy="9" r="2.5" stroke="currentColor" stroke-width="2"/>
</svg>
Fly to pin
{{ tr('pplr.ui.flyToPin') }}
</button>
<button class="pdc-btn" @click="close">Close</button>
<button class="pdc-btn" @click="close">{{ tr('pplr.ui.close') }}</button>
</div>
</div>
</template>

View file

@ -34,6 +34,7 @@ const A11Y_EN = {
'legend.loadingRows': 'Loading {label}…', 'legend.loadingData': 'Loading data…',
'legend.noDataAtThisLevel': 'No data at this level', 'legend.showingTop': 'Showing top {n} of {m}',
'legend.useGeocoderHint': 'use the geocoder above to find the rest',
'legend.sortByPop': 'Sort by Population',
}
const fillA11y = (str, p) => String(str).replace(/\{(\w+)\}/g, (m, k) => (p && k in p ? p[k] : m))
let ta = (key, p) => fillA11y(A11Y_EN[key] ?? key, p)
@ -441,7 +442,7 @@ function rowClasses(row) {
</button>
<button v-if="hasPop" class="stl-hdr-num"
:class="{ active: sortBy === 'pop' }"
title="Sort by Population"
:title="ta('legend.sortByPop')"
@click="clickSortHeader('pop')">
Pop
<span v-if="sortBy === 'pop'" class="stl-sort-arrow">{{ sortDir === 'desc' ? '▼' : '▲' }}</span>

View file

@ -30,6 +30,7 @@ export const LOCALE_NAMES = {
// ── PPLR FeatureServer — country polygons (live, not bundled) ────────────────
import { dataUrl, glFor } from './pplrData.js'
import { tStatic } from '@map/i18n/index.js'
export const COUNTRY_FS_URL = 'https://services2.arcgis.com/S4ydGgujXcif36k3/arcgis/rest/services/aCountry/FeatureServer/0/query'
export const COUNTRY_FS_FIELDS = ['GENCalpha3', 'ISOalpha2', 'CtryNm', 'ctryNmDisp']
@ -332,17 +333,20 @@ export const escHtml = (v) => String(v ?? '').replace(/[&<>"']/g, (c) => ({ '&':
export function popupForPG(p, extraRows = '') {
const dash = v => (v == null || v === 'None' || v === '' ? '<span class="m">—</span>' : escHtml(v))
// Labels in the page's language (tStatic falls back to English). Registry names (ROP3, ROP25,
// JoshuaProject, IMB People, WCD) are identifiers and stay as they are.
const L = (k) => escHtml(tStatic('pplr.popup.' + k))
return `<div class="pplr-popup">
<h3>${p.name ? escHtml(p.name) : '(no name)'}</h3>
<h3>${p.name ? escHtml(p.name) : L('noName')}</h3>
<table>
<tr><td>Country</td><td>${dash(p.country)} <code>${dash(p.iso3)}</code></td></tr>
<tr><td>${L('country')}</td><td>${dash(p.country)} <code>${dash(p.iso3)}</code></td></tr>
<tr><td>ROP3</td><td><code>${dash(p.rop3)}</code></td></tr>
<tr><td>ROP25 (Ethne)</td><td><code>${dash(p.rop25)}</code></td></tr>
<tr><td>JoshuaProject</td><td><code>${dash(p.jp)}</code></td></tr>
<tr><td>IMB People</td><td><code>${dash(p.imb)}</code></td></tr>
<tr><td>WCD</td><td><code>${dash(p.wcd)}</code></td></tr>
<tr><td>Religion</td><td>${dash(p.religion)}</td></tr>
<tr><td>Language</td><td>${dash(p.language)}</td></tr>
<tr><td>${L('religion')}</td><td>${dash(p.religion)}</td></tr>
<tr><td>${L('language')}</td><td>${dash(p.language)}</td></tr>
${extraRows}
</table>
</div>`

View file

@ -103,8 +103,8 @@ onMounted(async () => {
const html = `<div class="pplr-popup">
<h3>${escHtml(p.CtryNm || p.ctryNmDisp || '—')}<small>${escHtml(p.GENCalpha3 || p.ISOalpha2 || '?')}</small></h3>
<table>
<tr><td>PG count</td><td><b>${escHtml(p.pgCount || 0)}</b></td></tr>
<tr><td>Avg locales filled</td><td>
<tr><td>${escHtml(t('pplr.popup.pgCount'))}</td><td><b>${escHtml(p.pgCount || 0)}</b></td></tr>
<tr><td>${escHtml(t('pplr.popup.avgLocalesFilled'))}</td><td>
<b>${p.avgFilled != null ? p.avgFilled.toFixed(1) : '—'}</b> / 17</td></tr>
</table>
</div>`
@ -148,9 +148,9 @@ const COV_POPUP_CSS = POPUP_CSS + `
<LoadingGlobe v-if="!isMapReady || !dataReady" :label="status" @retry="reload" />
<div ref="mapContainer" class="map" />
<div class="status">{{ status }}</div>
<div class="header-pill">Translation coverage · choropleth = avg locales filled per country · click PG pin or country</div>
<div class="header-pill">{{ t('pplr.pill.coverage') }}</div>
<div class="legend">
<div class="title">Avg locales / 17</div>
<div class="title">{{ t('pplr.legendTitle.avgLocales', { n: 17 }) }}</div>
<div class="bar">
<div v-for="s in SCALE" :key="s.stop" class="seg" :style="{ background: s.color }">
<span v-if="s.label">{{ s.label }}</span>

View file

@ -65,7 +65,7 @@ onMounted(async () => {
<template>
<div class="profile">
<LoadingGlobe v-if="!tree" label="Loading ROP hierarchy" @retry="reload" />
<LoadingGlobe v-if="!tree" :label="t('pplr.status.loadingRop')" @retry="reload" />
<template v-else>
<SemanticSunburst
:tree="tree"
@ -74,18 +74,18 @@ onMounted(async () => {
:depth-labels="['','Affinity Bloc','People Cluster','PG (not rendered)']"
:max-depth="2"
/>
<div class="header-pill">ROP Hierarchy · Affinity Bloc → People Cluster · scroll to zoom</div>
<div class="header-pill">{{ t('pplr.pill.sunburst') }}</div>
<div class="status">{{ status }}</div>
<div class="map-toolbar">
<div class="settings-wrap">
<button class="toolbar-btn round" title="Data sources" @click="showSettings = !showSettings">
<button class="toolbar-btn round" :title="t('pplr.ui.dataSources')" @click="showSettings = !showSettings">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="1.8"/>
<path d="M19 12a7 7 0 0 0-.1-1.2l2-1.5-2-3.5-2.4 1a7 7 0 0 0-2.1-1.2L14 3h-4l-.4 2.6A7 7 0 0 0 7.5 6.8l-2.4-1-2 3.5 2 1.5A7 7 0 0 0 5 12c0 .4 0 .8.1 1.2l-2 1.5 2 3.5 2.4-1a7 7 0 0 0 2.1 1.2L10 21h4l.4-2.6a7 7 0 0 0 2.1-1.2l2.4 1 2-3.5-2-1.5c.06-.4.1-.8.1-1.2z" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/>
</svg>
</button>
<div v-if="showSettings" class="settings-panel">
<div class="settings-header">Data sources</div>
<div class="settings-header">{{ t('pplr.ui.dataSources') }}</div>
<div v-for="src in DATA_SOURCES" :key="src.url" class="settings-src">
<div class="settings-src-label">{{ src.label }}</div>
<div class="settings-src-url">{{ src.url }}</div>

View file

@ -415,10 +415,10 @@ function popupHtml(p) {
return `<div class="pplr-popup">
<h3>${d(p.name)}<small>${d(p.country)}</small></h3>
<table>
<tr><td>Language</td><td>${d(p.language)}</td></tr>
<tr><td>Family</td><td><b style="color:${color}">${d(p.fam)}</b></td></tr>
<tr><td>Affinity Bloc</td><td>${d(p.rop1)}</td></tr>
<tr><td>Religion</td><td>${d(p.religion)}</td></tr>
<tr><td>${escHtml(t('pplr.popup.language'))}</td><td>${d(p.language)}</td></tr>
<tr><td>${escHtml(t('pplr.popup.family'))}</td><td><b style="color:${color}">${d(p.fam)}</b></td></tr>
<tr><td>${escHtml(t('pplr.popup.affinityBloc'))}</td><td>${d(p.rop1)}</td></tr>
<tr><td>${escHtml(t('pplr.popup.religion'))}</td><td>${d(p.religion)}</td></tr>
<tr><td>IMB PGID</td><td><code>${d(p.imb)}</code></td></tr>
<tr><td>JP Code</td><td><code>${d(p.jp)}</code></td></tr>
</table>
@ -522,7 +522,7 @@ const LF_CSS = POPUP_CSS + `
:local-index="langLocalIndex"
:kind-labels="LANG_KIND_LABELS"
:mapbox-token="mapboxToken"
placeholder="Search by people, place, language, religion…"
:placeholder="t('pplr.ui.search')"
@place="onGeocoderPlace"
/>
</div>
@ -539,13 +539,13 @@ const LF_CSS = POPUP_CSS + `
v-else
:nodes="langTree"
:tabs="LANG_TABS"
title="Language Families"
:title="t('pplr.tabs.languageFamilies')"
@select="onLegendSelect"
/>
<div ref="mapContainer" class="map" />
<div class="status">{{ status }}</div>
<div class="header-pill">Ethnolinguistic families (PPLR) · click pin for detail</div>
<div class="header-pill">{{ t('pplr.pill.languageFamilies') }}</div>
<!-- Toolbar — rounded pill controls -->
<div class="map-toolbar">
@ -564,28 +564,28 @@ const LF_CSS = POPUP_CSS + `
<span class="toolbar-icon">{{ projection === 'globe' ? '🌐' : '🗺' }}</span>
</button>
<div class="basemap-wrap">
<button class="toolbar-btn round" title="Base map" @click="showBasemap = !showBasemap">
<button class="toolbar-btn round" :title="t('pplr.ui.baseMap')" @click="showBasemap = !showBasemap">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none">
<path d="M3 7l9-4 9 4-9 4-9-4z" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/>
<path d="M3 12l9 4 9-4M3 17l9 4 9-4" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/>
</svg>
</button>
<div v-if="showBasemap" class="basemap-panel">
<div class="basemap-title">Base map</div>
<div class="basemap-title">{{ t('pplr.ui.baseMap') }}</div>
<button v-for="b in BASEMAPS" :key="b.key" class="basemap-opt"
:class="{ active: currentBasemap === b.key }"
@click="setBasemap(b.key)">{{ b.label }}</button>
</div>
</div>
<div class="settings-wrap">
<button class="toolbar-btn round" title="Data sources" @click="showSettings = !showSettings">
<button class="toolbar-btn round" :title="t('pplr.ui.dataSources')" @click="showSettings = !showSettings">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="1.8"/>
<path d="M19 12a7 7 0 0 0-.1-1.2l2-1.5-2-3.5-2.4 1a7 7 0 0 0-2.1-1.2L14 3h-4l-.4 2.6A7 7 0 0 0 7.5 6.8l-2.4-1-2 3.5 2 1.5A7 7 0 0 0 5 12c0 .4 0 .8.1 1.2l-2 1.5 2 3.5 2.4-1a7 7 0 0 0 2.1 1.2L10 21h4l.4-2.6a7 7 0 0 0 2.1-1.2l2.4 1 2-3.5-2-1.5c.06-.4.1-.8.1-1.2z" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/>
</svg>
</button>
<div v-if="showSettings" class="settings-panel">
<div class="settings-header">Data sources</div>
<div class="settings-header">{{ t('pplr.ui.dataSources') }}</div>
<div v-for="src in DATA_SOURCES" :key="src.url" class="settings-src">
<div class="settings-src-label">{{ src.label }}</div>
<div class="settings-src-url">{{ src.url }}</div>

View file

@ -68,18 +68,18 @@ onBeforeUnmount(() => destroy())
<LoadingGlobe v-if="!isMapReady || !dataReady" :label="status" @retry="reload" />
<div ref="mapContainer" class="map" />
<div class="status">{{ status }}</div>
<div class="header-pill">All PG-in-country pins · click for cross-system IDs</div>
<div class="header-pill">{{ t('pplr.pill.pins') }}</div>
<div class="map-toolbar">
<div class="settings-wrap">
<button class="toolbar-btn round" title="Data sources" @click="showSettings = !showSettings">
<button class="toolbar-btn round" :title="t('pplr.ui.dataSources')" @click="showSettings = !showSettings">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="1.8"/>
<path d="M19 12a7 7 0 0 0-.1-1.2l2-1.5-2-3.5-2.4 1a7 7 0 0 0-2.1-1.2L14 3h-4l-.4 2.6A7 7 0 0 0 7.5 6.8l-2.4-1-2 3.5 2 1.5A7 7 0 0 0 5 12c0 .4 0 .8.1 1.2l-2 1.5 2 3.5 2.4-1a7 7 0 0 0 2.1 1.2L10 21h4l.4-2.6a7 7 0 0 0 2.1-1.2l2.4 1 2-3.5-2-1.5c.06-.4.1-.8.1-1.2z" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/>
</svg>
</button>
<div v-if="showSettings" class="settings-panel">
<div class="settings-header">Data sources</div>
<div class="settings-header">{{ t('pplr.ui.dataSources') }}</div>
<div v-for="src in DATA_SOURCES" :key="src.url" class="settings-src">
<div class="settings-src-label">{{ src.label }}</div>
<div class="settings-src-url">{{ src.url }}</div>

View file

@ -236,8 +236,8 @@ function popupHtml(p) {
return `<div class="pplr-popup">
<h3>${d(p.name)}<small>${d(p.country)}</small></h3>
<table>
<tr><td>Religion</td><td><b style="color:${grp?.color || '#ccc'}">${d(p.religion)}</b></td></tr>
<tr><td>Language</td><td>${d(p.language)}</td></tr>
<tr><td>${escHtml(t('pplr.popup.religion'))}</td><td><b style="color:${grp?.color || '#ccc'}">${d(p.religion)}</b></td></tr>
<tr><td>${escHtml(t('pplr.popup.language'))}</td><td>${d(p.language)}</td></tr>
<tr><td>IMB PGID</td><td><code>${d(p.imb)}</code></td></tr>
<tr><td>JP Code</td><td><code>${d(p.jp)}</code></td></tr>
</table>
@ -294,7 +294,7 @@ const REL_CSS = POPUP_CSS + `
<LoadingGlobe v-if="!isMapReady || !dataReady" :label="status" @retry="reload" />
<div ref="mapContainer" class="map" />
<div class="status">{{ status }}</div>
<div class="header-pill">Religion distribution · click pin for detail</div>
<div class="header-pill">{{ t('pplr.pill.religion') }}</div>
<!-- Decoupled geocoder — searches religions + countries + places -->
<div class="geocoder-slot">
@ -302,7 +302,7 @@ const REL_CSS = POPUP_CSS + `
:local-index="religionLocalIndex"
:kind-labels="REL_KIND_LABELS"
:mapbox-token="mapboxToken"
placeholder="Search by people, place, language, religion…"
:placeholder="t('pplr.ui.search')"
@place="onGeocoderPlace"
/>
</div>
@ -318,7 +318,7 @@ const REL_CSS = POPUP_CSS + `
<FlatLegend
v-else
:items="religionLegendItems"
title="Primary Religion"
:title="t('pplr.ui.primaryReligion')"
@select="onLegendSelect"
/>
@ -339,27 +339,27 @@ const REL_CSS = POPUP_CSS + `
<span class="toolbar-icon">{{ projection === 'globe' ? '🌐' : '🗺' }}</span>
</button>
<div class="basemap-wrap">
<button class="toolbar-btn round" title="Base map" @click="showBasemap = !showBasemap">
<button class="toolbar-btn round" :title="t('pplr.ui.baseMap')" @click="showBasemap = !showBasemap">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none">
<path d="M3 7l9-4 9 4-9 4-9-4z" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/>
<path d="M3 12l9 4 9-4M3 17l9 4 9-4" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/>
</svg>
</button>
<div v-if="showBasemap" class="basemap-panel">
<div class="basemap-title">Base map</div>
<div class="basemap-title">{{ t('pplr.ui.baseMap') }}</div>
<button v-for="b in BASEMAPS" :key="b.key" class="basemap-opt" :class="{ active: currentBasemap === b.key }" @click="setBasemap(b.key)">{{ b.label }}</button>
</div>
</div>
<!-- Data sources — moved into the toolbar to match ROP-tree (per user 2026-05-02) -->
<div class="settings-wrap">
<button class="toolbar-btn round" title="Data sources" @click="showSettings = !showSettings">
<button class="toolbar-btn round" :title="t('pplr.ui.dataSources')" @click="showSettings = !showSettings">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="1.8"/>
<path d="M19 12a7 7 0 0 0-.1-1.2l2-1.5-2-3.5-2.4 1a7 7 0 0 0-2.1-1.2L14 3h-4l-.4 2.6A7 7 0 0 0 7.5 6.8l-2.4-1-2 3.5 2 1.5A7 7 0 0 0 5 12c0 .4 0 .8.1 1.2l-2 1.5 2 3.5 2.4-1a7 7 0 0 0 2.1 1.2L10 21h4l.4-2.6a7 7 0 0 0 2.1-1.2l2.4 1 2-3.5-2-1.5c.06-.4.1-.8.1-1.2z" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/>
</svg>
</button>
<div v-if="showSettings" class="settings-panel toolbar-attached">
<div class="settings-header">Data sources</div>
<div class="settings-header">{{ t('pplr.ui.dataSources') }}</div>
<div v-for="src in DATA_SOURCES" :key="src.url" class="settings-src">
<div class="settings-src-label">{{ src.label }}</div>
<div class="settings-src-url">{{ src.url }}</div>

View file

@ -380,14 +380,14 @@ function popupHtml(p) {
return `<div class="pplr-popup">
<h3>${d(p.name)}<small>${d(p.country)}</small></h3>
<table>
<tr><td>Affinity Bloc</td><td><b>${d(p.rop1)}</b></td></tr>
<tr><td>Cluster</td><td><b>${d(p.rop2)}</b></td></tr>
<tr><td>People Group</td><td><b>${d(p.rop3)}</b></td></tr>
<tr><td>${escHtml(t('pplr.popup.affinityBloc'))}</td><td><b>${d(p.rop1)}</b></td></tr>
<tr><td>${escHtml(t('pplr.popup.cluster'))}</td><td><b>${d(p.rop2)}</b></td></tr>
<tr><td>${escHtml(t('pplr.popup.peopleGroup'))}</td><td><b>${d(p.rop3)}</b></td></tr>
<tr><td>PEID</td><td><code>${d(p.peid)}</code></td></tr>
<tr><td>IMB PGID</td><td><code>${d(p.imb)}</code></td></tr>
<tr><td>JP Code</td><td><code>${d(p.jp)}</code></td></tr>
<tr><td>Religion</td><td>${d(p.religion)}</td></tr>
<tr><td>Language</td><td>${d(p.language)}</td></tr>
<tr><td>${escHtml(t('pplr.popup.religion'))}</td><td>${d(p.religion)}</td></tr>
<tr><td>${escHtml(t('pplr.popup.language'))}</td><td>${d(p.language)}</td></tr>
</table>
</div>`
}
@ -491,7 +491,7 @@ const TREE_POPUP_CSS = POPUP_CSS + `
:local-index="localIndex"
:kind-labels="KIND_LABELS"
:mapbox-token="mapboxToken"
placeholder="Search by people, place, language, religion…"
:placeholder="t('pplr.ui.search')"
@place="onGeocoderPlace"
/>
</div>
@ -508,13 +508,13 @@ const TREE_POPUP_CSS = POPUP_CSS + `
v-else
:nodes="ropTree"
:tabs="ROP_TABS"
title="ROP Hierarchy"
:title="t('pplr.ui.ropHierarchy')"
@select="onRopLegendSelect"
/>
<div ref="mapContainer" class="map" />
<div class="status">{{ status }}</div>
<div class="header-pill">ROP Hierarchy · Affinity Bloc → Cluster → People Group → PGIC</div>
<div class="header-pill">{{ t('pplr.pill.ropTree') }}</div>
<!-- Toolbar — rounded pill controls -->
<div class="map-toolbar">
@ -533,7 +533,7 @@ const TREE_POPUP_CSS = POPUP_CSS + `
<span class="toolbar-icon">{{ projection === 'globe' ? '🌐' : '🗺' }}</span>
</button>
<div class="basemap-wrap">
<button class="toolbar-btn round" title="Base map"
<button class="toolbar-btn round" :title="t('pplr.ui.baseMap')"
@click="showBasemap = !showBasemap">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none">
<path d="M3 7l9-4 9 4-9 4-9-4z" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/>
@ -541,21 +541,21 @@ const TREE_POPUP_CSS = POPUP_CSS + `
</svg>
</button>
<div v-if="showBasemap" class="basemap-panel">
<div class="basemap-title">Base map</div>
<div class="basemap-title">{{ t('pplr.ui.baseMap') }}</div>
<button v-for="b in BASEMAPS" :key="b.key" class="basemap-opt"
:class="{ active: currentBasemap === b.key }"
@click="setBasemap(b.key)">{{ b.label }}</button>
</div>
</div>
<div class="settings-wrap">
<button class="toolbar-btn round" title="Data sources" @click="showSettings = !showSettings">
<button class="toolbar-btn round" :title="t('pplr.ui.dataSources')" @click="showSettings = !showSettings">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="1.8"/>
<path d="M19 12a7 7 0 0 0-.1-1.2l2-1.5-2-3.5-2.4 1a7 7 0 0 0-2.1-1.2L14 3h-4l-.4 2.6A7 7 0 0 0 7.5 6.8l-2.4-1-2 3.5 2 1.5A7 7 0 0 0 5 12c0 .4 0 .8.1 1.2l-2 1.5 2 3.5 2.4-1a7 7 0 0 0 2.1 1.2L10 21h4l.4-2.6a7 7 0 0 0 2.1-1.2l2.4 1 2-3.5-2-1.5c.06-.4.1-.8.1-1.2z" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/>
</svg>
</button>
<div v-if="showSettings" class="settings-panel">
<div class="settings-header">Data sources</div>
<div class="settings-header">{{ t('pplr.ui.dataSources') }}</div>
<div v-for="src in DATA_SOURCES" :key="src.url" class="settings-src">
<div class="settings-src-label">{{ src.label }}</div>
<div class="settings-src-url">{{ src.url }}</div>

View file

@ -16,7 +16,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 { addCountryPolygons, loadOnce, popupForPG, POPUP_CSS, escHtml } from '../utils/pplrMap.js'
import LoadingGlobe from '../components/LoadingGlobe.vue'
import DataSourcesDropdown from '../components/DataSourcesDropdown.vue'
@ -123,11 +123,11 @@ onMounted(async () => {
].filter(Boolean)
const rosettaRow = sample
? `<tr><td>Rosetta ★</td><td><b style="color:#d97706">★ sample PG</b></td></tr>`
? `<tr><td>Rosetta ★</td><td><b style="color:#d97706">${escHtml(t('pplr.rosetta.samplePg'))}</b></td></tr>`
: ''
const missingNote = missingIds.length
? `<tr><td colspan="2" style="padding-top:6px;font-size:10px;color:#6b7280;font-style:italic">
${missingIds.join(', ')} ID${missingIds.length > 1 ? 's' : ''} not yet cross-mapped in PPLR — empty rows are a data gap, not an error.
${escHtml(t('pplr.rosetta.missingIds', { ids: missingIds.join(', ') }))}
</td></tr>`
: ''
new (glFor(map.value)).Popup({ maxWidth: '360px' })
@ -154,11 +154,11 @@ onMounted(async () => {
onBeforeUnmount(() => destroy())
const LEGEND = [
{ color: '#fbbf24', label: 'JP + IMB + WCD all' },
{ color: '#fbbf24', label: t('pplr.rosetta.legend.all') },
{ color: '#3b82f6', label: 'JP + IMB' },
{ color: '#a78bfa', label: 'IMB only' },
{ color: '#ec4899', label: 'JP only' },
{ color: '#6b7280', label: 'neither' },
{ color: '#a78bfa', label: t('pplr.rosetta.legend.imbOnly') },
{ color: '#ec4899', label: t('pplr.rosetta.legend.jpOnly') },
{ color: '#6b7280', label: t('pplr.rosetta.legend.neither') },
]
</script>
@ -167,9 +167,9 @@ const LEGEND = [
<LoadingGlobe v-if="!isMapReady || !dataReady" :label="status" @retry="reload" />
<div ref="mapContainer" class="map" />
<div class="status">{{ status }}</div>
<div class="header-pill">Cross-system ID Rosetta · color = which IDs exist · ★ = sample row</div>
<div class="header-pill">{{ t('pplr.pill.rosetta') }}</div>
<div class="legend">
<div class="title">ID coverage</div>
<div class="title">{{ t('pplr.legendTitle.idCoverage') }}</div>
<div v-for="l in LEGEND" :key="l.label" class="row">
<span class="dot" :style="{ background: l.color }" />{{ l.label }}
</div>