Plugin Directory

Changeset 3749166


Ignore:
Timestamp:
10/15/2026 07:04:36 PM (3 weeks ago)
Author:
urryes
Message:

Release 4.2.4: readable and Julian dates, persona templates with colour sets, privacy fixes (menus, titles, sitemaps, living status from family, private page redirect), page creation for big trees, residence duplicates, divorce/other names/cited pages, settings tabs, Russian translation.

Location:
rootspersona/trunk
Files:
7 added
27 edited

Legend:

Unmodified
Added
Removed
  • rootspersona/trunk/admin/class-rootspersona-svg-admin.php

    r3746101 r3749166  
    6262                    'nonce'   => wp_create_nonce( 'rootspersona_clear_svg_cache' ),
    6363                    'loading' => __( 'Clearing...', 'rootspersona' ),
    64                     'success' => __( '? Cache cleared.', 'rootspersona' ),
    65                     'error'   => __( '? Error clearing cache.', 'rootspersona' ),
    66                     'network' => __( '? Network error.', 'rootspersona' ),
     64                    'success' => __( '✓ Cache cleared.', 'rootspersona' ),
     65                    'error'   => __( '✗ Error clearing cache.', 'rootspersona' ),
     66                    'network' => __( '✗ Network error.', 'rootspersona' ),
    6767                )
    6868            ) . ';
  • rootspersona/trunk/js/rootsUtilities.js

    r3716808 r3749166  
    458458        jQuery.get(refreshUrl, data, function(response) {
    459459        document.body.style.cursor = "default";
    460         var res = jQuery.parseJSON(response);
    461         jQuery.each(res, function(index, p) {
     460        // wp_send_json_success() answers {success, data}; jQuery may have parsed it already.
     461        var res = (typeof response === 'string') ? jQuery.parseJSON(response) : response;
     462        if (res && typeof res.success !== 'undefined') {
     463            res = res.data;
     464        }
     465        jQuery.each(res || [], function(index, p) {
    462466                  if(index == 'error') {
    463467                      alert(p);
    464468                  } else {
    465                     var label = (p.surname || '') + ", " + (p.given || '');
     469                    var label = p.label || ((p.surname || '') + ", " + (p.given || ''));
    466470                    jQuery("#persons").append(
    467471                        jQuery("<option/>", {
    468472                            id: String(p.id || ''),
     473                            value: p.value || (String(p.id || '') + '|' + (p.surname || '') + ', ' + (p.given || '')),
    469474                            text: label
    470475                        })
    … …  
    661666        ];
    662667
     668    // jQuery UI autocomplete is loaded only on admin screens (edit form).
     669    var rpHasAutocomplete = typeof jQuery.fn.autocomplete === 'function';
     670
    663671    jQuery('.claimType').each(function(index) {
     672            if (!rpHasAutocomplete) { return; }
    664673            jQuery(this).autocomplete({
    665674                source: claimTypes
    … …  
    717726                jQuery(this).append(newRow);
    718727            });
    719             jQuery('#newclaim').autocomplete({
    720                 source: claimTypes
    721             });
     728            if (rpHasAutocomplete) {
     729                jQuery('#newclaim').autocomplete({
     730                    source: claimTypes
     731                });
     732            }
    722733            jQuery('#newclaim').focus();
    723734            jQuery('#newclassification').blur(addFactsRow);
    … …  
    959970    }
    960971
    961     jQuery('#parental_text').autocomplete({
    962         source: rp_autoCompleteParental,
    963         select: rp_autoSelectPerson,
    964         minLength: 1
    965     });
    966 
    967     jQuery('#spousal_text').autocomplete({
    968         source: rp_autoCompleteSpousal,
    969         select: rp_autoSelectPerson,
    970         minLength: 1
    971     });
     972    if (rpHasAutocomplete) {
     973        jQuery('#parental_text').autocomplete({
     974            source: rp_autoCompleteParental,
     975            select: rp_autoSelectPerson,
     976            minLength: 1
     977        });
     978
     979        jQuery('#spousal_text').autocomplete({
     980            source: rp_autoCompleteSpousal,
     981            select: rp_autoSelectPerson,
     982            minLength: 1
     983        });
     984    }
    972985
    973986    document.body.style.cursor = "default";
  • rootspersona/trunk/js/rp-persons-add-multiselect.js

    r3577169 r3749166  
    8080        });
    8181})();
     82
     83/**
     84 * Add Uploaded Persons: send the selection to the server in chunks.
     85 * A plain form post carries one field per person; above ~1000 people PHP's
     86 * max_input_vars silently truncates it and no page is created at all.
     87 */
     88(function () {
     89        'use strict';
     90        var CHUNK = 100;
     91
     92        document.addEventListener('DOMContentLoaded', function () {
     93                var form = document.getElementById('rp-add-pages-form');
     94                var sel = document.getElementById('persons');
     95                var cfg = window.rootspersonaAddPages;
     96                if (!form || !sel || !cfg || !window.fetch) {
     97                        return;
     98                }
     99                var msg = document.getElementById('rp-add-pages-msg');
     100                var submit = form.querySelector('input[name="submitAddPageForm"]');
     101
     102                function say(text) {
     103                        if (msg) {
     104                                msg.textContent = text;
     105                        }
     106                }
     107
     108                function format(tpl, a, b) {
     109                        return tpl.replace('%1$s', a).replace('%2$s', b);
     110                }
     111
     112                function postChunk(values) {
     113                        var body = new FormData();
     114                        body.append('action', 'rootspersona_add_pages');
     115                        body.append('_wpnonce', form.querySelector('input[name="_wpnonce"]').value);
     116                        body.append('batch_id', form.querySelector('input[name="batch_id"]').value);
     117                        values.forEach(function (v) {
     118                                body.append('persons[]', v);
     119                        });
     120                        return fetch(cfg.ajaxurl, { method: 'POST', body: body, credentials: 'same-origin' })
     121                                .then(function (r) {
     122                                        return r.json();
     123                                })
     124                                .then(function (json) {
     125                                        if (!json || !json.success) {
     126                                                throw new Error(json && json.data && json.data.message ? json.data.message : cfg.i18n.failed);
     127                                        }
     128                                        return json.data;
     129                                });
     130                }
     131
     132                form.addEventListener('submit', function (e) {
     133                        var selected = Array.prototype.filter.call(sel.options, function (o) {
     134                                return o.selected;
     135                        });
     136                        if (!selected.length) {
     137                                return; // let the server answer "No people selected."
     138                        }
     139                        e.preventDefault();
     140                        if (submit) {
     141                                submit.disabled = true;
     142                        }
     143                        document.body.style.cursor = 'wait';
     144
     145                        var total = selected.length;
     146                        var done = 0;
     147                        var created = 0;
     148                        var errors = 0;
     149                        var lines = [];
     150
     151                        function next() {
     152                                if (done >= total) {
     153                                        document.body.style.cursor = 'default';
     154                                        if (submit) {
     155                                                submit.disabled = false;
     156                                        }
     157                                        say(format(cfg.i18n.finished, String(created), String(errors)));
     158                                        if (msg && lines.length) {
     159                                                lines.forEach(function (line) {
     160                                                        msg.appendChild(document.createElement('br'));
     161                                                        msg.appendChild(document.createTextNode(line));
     162                                                });
     163                                        }
     164                                        return;
     165                                }
     166                                var slice = selected.slice(done, done + CHUNK);
     167                                postChunk(slice.map(function (o) {
     168                                        return o.value;
     169                                }))
     170                                        .then(function (data) {
     171                                                created += data.created;
     172                                                errors += data.errors;
     173                                                data.lines.forEach(function (line) {
     174                                                        // Lines arrive as escaped HTML; show them as plain text.
     175                                                        var tmp = document.createElement('div');
     176                                                        tmp.innerHTML = line;
     177                                                        lines.push(tmp.textContent);
     178                                                });
     179                                                slice.forEach(function (o) {
     180                                                        o.parentNode.removeChild(o);
     181                                                });
     182                                                done += slice.length;
     183                                                say(format(cfg.i18n.progress, String(done), String(total)));
     184                                                next();
     185                                        })
     186                                        .catch(function (err) {
     187                                                document.body.style.cursor = 'default';
     188                                                if (submit) {
     189                                                        submit.disabled = false;
     190                                                }
     191                                                say(format(cfg.i18n.stopped, String(created), String(total)) + ' ' + err.message);
     192                                        });
     193                        }
     194
     195                        say(format(cfg.i18n.progress, '0', String(total)));
     196                        next();
     197                });
     198        });
     199})();
  • rootspersona/trunk/localization/rootspersona-ru_RU.po

    r3572543 r3749166  
    1 # Translation of Plugins - Rootspersona - Stable (latest release) in Russian
    2 # This file is distributed under the same license as the Plugins - Rootspersona - Stable (latest release) package.
    31msgid ""
    42msgstr ""
    5 "PO-Revision-Date: 2026-01-24 18:38+0300\n"
     3"Project-Id-Version: Plugins - Rootspersona - Stable (latest release)\n"
     4"Report-Msgid-Bugs-To: \n"
     5"Last-Translator: \n"
     6"Language-Team: \n"
    67"MIME-Version: 1.0\n"
    78"Content-Type: text/plain; charset=UTF-8\n"
    89"Content-Transfer-Encoding: 8bit\n"
     10"POT-Creation-Date: \n"
     11"PO-Revision-Date: 2026-10-15 10:00+0300\n"
     12"Language: ru\n"
    913"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
    1014"X-Generator: Poedit 2.0.7\n"
    11 "Language: ru\n"
    12 "Project-Id-Version: Plugins - Rootspersona - Stable (latest release)\n"
    13 "POT-Creation-Date: \n"
    14 "Last-Translator: \n"
    15 "Language-Team: \n"
    16 
    17 #. Plugin URI of the plugin/theme
    18 msgid "https://rootspersona.com"
    19 msgstr "https://rootspersona.com"
    20 
    21 #. Plugin Name of the plugin/theme
     15
     16#. Plugin Name of the plugin
     17#: rootspersona.php
    2218msgid "RootsPersona"
    2319msgstr "RootsPersona"
    2420
    25 #. Author URI of the plugin/theme
    26 msgid "http://rootspersona.com/"
    27 msgstr "http://rootspersona.com/"
    28 
    29 #. Author of the plugin/theme
    30 msgid "Ed Thompson"
    31 msgstr "Эд Томпсон"
    32 
    33 #: rootspersona.php:485
     21#: rootspersona.php:1031
    3422msgid "You do not have permission to upload files."
    3523msgstr "У вас нет прав на загрузку файлов."
    3624
    37 #: php/pages/panels/class-RP-Bio-Panel-Creator.php:15
    38 #: php/pages/panels/class-RP-Bio-Panel-Creator.php:28
     25#: php/pages/panels/class-RP-Bio-Panel-Creator.php:47
     26#: php/pages/panels/class-RP-Bio-Panel-Creator.php:65
     27#: php/templates/class-RP-Persona-Templates.php:330
     28#: php/templates/class-RP-Persona-Templates.php:574
    3929msgid "Biography"
    4030msgstr "Биография"
    4131
    42 #: php/class-RP-Persona-Installer.php:219
     32#: php/class-RP-Persona-Installer.php:285
    4333msgid "(Picture a front page with 100 or more entries in the menu bar)"
    4434msgstr "(Представьте себе первую страницу со 100 или более записями в строке меню)"
    4535
    46 #: php/pages/class-RP-Add-Page-Builder.php:63
     36#: php/pages/class-RP-Add-Page-Builder.php:68
    4737msgid "Add"
    4838msgstr "Добавить"
    4939
    50 #: php/pages/class-RP-Tools-Page-Builder.php:104
     40#: php/pages/class-RP-Tools-Page-Builder.php:125
    5141msgid "Add Evidence Pages"
    5242msgstr "Добавить Страницы Данных"
    5343
    54 #: php/pages/class-RP-Tools-Page-Builder.php:89
     44#: php/pages/class-RP-Tools-Page-Builder.php:107
    5545msgid "Add Individual"
    5646msgstr "Добавить человека"
    … …  
    6050msgstr "Добавить Загруженные Персоны"
    6151
    62 #: php/pages/class-RP-Tools-Page-Builder.php:107
     52#: php/pages/class-RP-Tools-Page-Builder.php:128
    6353msgid "Add missing evidence pages."
    6454msgstr "Добавить недостающие Страницы Данных"
    6555
    66 #: php/pages/class-RP-Include-Page-Builder.php:19
     56#. translators: %s: plural label (e.g. personas).
     57#: php/pages/class-RP-Include-Page-Builder.php:23
     58#, php-format
    6759msgid "All %s have been included"
    6860msgstr "Все %s включены"
    6961
    70 #: php/pages/class-RP-Add-Page-Builder.php:23
    71 msgid "All available persons have been added."
    72 msgstr "Все персоны добавлены"
    73 
    74 #: php/pages/class-RP-Option-Page-Builder.php:457
     62#: php/pages/class-RP-Option-Page-Builder.php:485
    7563msgid "Allow Editor"
    7664msgstr "Разрешать редактирование"
    7765
    78 #: php/pages/class-RP-Option-Page-Builder.php:470
     66#: php/pages/class-RP-Option-Page-Builder.php:498
    7967msgid "Allow Editor to edit persona pages"
    8068msgstr "Разрешать редактирование страниц персон"
    8169
    82 #: php/pages/panels/class-RP-Ancestors-Panel-Creator.php:23
     70#: php/pages/panels/class-RP-Ancestors-Panel-Creator.php:27
    8371msgid "Ancestors"
    8472msgstr "Предки"
    8573
    86 #: php/pages/class-RP-Option-Page-Builder.php:324
     74#: php/pages/class-RP-Option-Page-Builder.php:369
    8775msgid "Banner Background Color"
    8876msgstr "Цвет фона баннера"
    8977
    90 #: php/pages/class-RP-Option-Page-Builder.php:318
     78#: php/pages/class-RP-Option-Page-Builder.php:363
    9179msgid "Banner Foreground Color"
    9280msgstr "Цвет переднего плана баннера"
    9381
    94 #: php/pages/class-RP-Option-Page-Builder.php:330
     82#: php/pages/class-RP-Option-Page-Builder.php:375
    9583msgid "Banner Image"
    9684msgstr "Изображение Баннера"
    9785
    98 #: php/pages/class-RP-Option-Page-Builder.php:121
     86#: php/pages/class-RP-Option-Page-Builder.php:163
    9987msgid "Bio"
    10088msgstr "Био"
    10189
    102 #: php/pages/panels/class-RP-Group-Sheet-Panel-Creator.php:93
    103 #: php/pages/panels/class-RP-Group-Sheet-Panel-Creator.php:236
     90#: php/pages/panels/class-RP-Group-Sheet-Panel-Creator.php:107
     91#: php/pages/panels/class-RP-Group-Sheet-Panel-Creator.php:260
    10492msgid "Birth"
    10593msgstr "Дата рождения"
    10694
    107 #: php/pages/panels/class-RP-Group-Sheet-Panel-Creator.php:211
     95#: php/pages/panels/class-RP-Group-Sheet-Panel-Creator.php:235
    10896msgid "CHILDREN"
    10997msgstr "ДЕТИ"
    11098
    111 #: php/pages/class-RP-Option-Page-Builder.php:122
     99#: php/pages/class-RP-Option-Page-Builder.php:164
    112100msgid "Choose a style for the header panel"
    113101msgstr "Выберите стиль для панели заголовка"
    114102
    115 #: php/pages/class-RP-Evidence-Page-Builder.php:20
     103#: php/pages/class-RP-Evidence-Page-Builder.php:23
    116104msgid "Citations"
    117105msgstr "Цитирование"
    118106
    119 #: php/pages/class-RP-Option-Page-Builder.php:379
    120 msgid "Custom Style"
    121 msgstr "Свой Стиль"
    122 
    123 #: php/pages/panels/class-RP-Group-Sheet-Panel-Creator.php:102
    124 #: php/pages/panels/class-RP-Group-Sheet-Panel-Creator.php:245
     107#: php/pages/panels/class-RP-Group-Sheet-Panel-Creator.php:116
     108#: php/pages/panels/class-RP-Group-Sheet-Panel-Creator.php:269
    125109msgid "Death"
    126110msgstr "Дата ухода из жизни"
    127111
    128 #: php/pages/class-RP-Option-Page-Builder.php:57
     112#: php/pages/class-RP-Option-Page-Builder.php:67
    129113msgid "Default Privacy Setting"
    130114msgstr "Настройки приватности по умолчанию"
    131115
    132 #: php/class-RP-Persona-Site-Mender.php:329
     116#: php/class-RP-Persona-Site-Mender.php:351
    133117msgid "Deleta Data From Database?"
    134118msgstr "Удалить данные из базы данных?"
    135119
    136 #: php/pages/class-RP-Persona-Page-Builder.php:100
     120#: php/pages/class-RP-Persona-Page-Builder.php:154
    137121msgid "Delete Person"
    138122msgstr "Удалить персону"
    139123
    140 #: php/pages/class-RP-Tools-Page-Builder.php:149
     124#: php/pages/class-RP-Tools-Page-Builder.php:181
    141125msgid "Delete persona Pages"
    142126msgstr "Удалить страницы персоны"
    143127
    144 #: php/class-RP-Persona-Site-Mender.php:70
    145 #: php/class-RP-Persona-Site-Mender.php:78
     128#. translators: %s: page or form name.
    146129#: php/class-RP-Persona-Site-Mender.php:86
    147130#: php/class-RP-Persona-Site-Mender.php:94
    148131#: php/class-RP-Persona-Site-Mender.php:102
     132#: php/class-RP-Persona-Site-Mender.php:110
     133#: php/class-RP-Persona-Site-Mender.php:118
     134#, php-format
    149135msgid "Deleted obsolete %s page."
    150136msgstr "Удалена устаревшая страница %s."
    151137
    152 #: php/pages/class-RP-Option-Page-Builder.php:105
     138#: php/pages/class-RP-Option-Page-Builder.php:138
    153139msgid "Display Options"
    154140msgstr "Настройки Отображения"
    155141
    156 #: php/pages/class-RP-Persona-Page-Builder.php:92
     142#: php/pages/class-RP-Persona-Page-Builder.php:137
    157143msgid "Edit Person"
    158144msgstr "Редактировать данные персоны"
    159145
    160 #: php/pages/class-RP-Option-Page-Builder.php:417
     146#: php/pages/class-RP-Option-Page-Builder.php:461
    161147msgid "Entries Per Page"
    162148msgstr "Записей на Страницу"
    163149
    164 #: rootspersona.php:369
     150#: rootspersona.php:787
    165151msgid "Error creating page for"
    166152msgstr "Ошибка создания страницы для"
    167153
    168 #: php/pages/panels/class-RP-Facts-Panel-Creator.php:15
     154#: php/pages/panels/class-RP-Facts-Panel-Creator.php:19
    169155msgid "Facts"
    170156msgstr "Факты"
    171157
    172 #: php/pages/panels/class-RP-Group-Sheet-Panel-Creator.php:115
     158#: php/pages/panels/class-RP-Group-Sheet-Panel-Creator.php:129
     159#: php/templates/class-RP-Persona-Templates.php:343
     160#: php/templates/class-RP-Persona-Templates.php:436
     161#: php/templates/class-RP-Persona-Templates.php:535
    173162msgid "Father"
    174163msgstr "Отец"
    175164
    176 #: rootspersona.php:471
     165#: rootspersona.php:1017
    177166msgid "For internal use only"
    178167msgstr "Только для внутреннего пользования"
    179168
    180 #: php/pages/class-RP-Option-Page-Builder.php:337
     169#: php/pages/class-RP-Option-Page-Builder.php:382
    181170msgid "Group Foreground Color"
    182171msgstr "Цвет переднего плана для группы"
    183172
    184 #: php/pages/class-RP-Option-Page-Builder.php:349
     173#: php/pages/class-RP-Option-Page-Builder.php:394
    185174msgid "Group Image"
    186175msgstr "Изображение для группы"
    187176
    188 #: php/pages/class-RP-Option-Page-Builder.php:109
     177#: php/pages/class-RP-Option-Page-Builder.php:153
    189178msgid "Header Style"
    190179msgstr "Стиль Заголовка"
    191180
    192 #: php/pages/class-RP-Option-Page-Builder.php:177
     181#: php/pages/class-RP-Option-Page-Builder.php:219
    193182msgid "Hide Ancestors"
    194183msgstr "Скрыть предков"
    195184
    196 #: php/pages/class-RP-Option-Page-Builder.php:143
     185#: php/pages/class-RP-Option-Page-Builder.php:185
    197186msgid "Hide Bio"
    198187msgstr "Скрыть Био"
    199188
    200 #: php/pages/class-RP-Option-Page-Builder.php:211
     189#: php/pages/class-RP-Option-Page-Builder.php:253
    201190msgid "Hide Child Family Group"
    202191msgstr "Скрыть Группу Семья Ребенка"
    203192
    204 #: php/pages/class-RP-Option-Page-Builder.php:76
     193#: php/pages/class-RP-Option-Page-Builder.php:111
    205194msgid "Hide Dates"
    206195msgstr "Скрыть Даты"
    207196
    208 #: php/pages/class-RP-Option-Page-Builder.php:386
     197#: php/pages/class-RP-Option-Page-Builder.php:430
    209198msgid "Hide Donation Button"
    210199msgstr "Скрыть кнопку пожертвования"
    211200
    212 #: php/pages/class-RP-Option-Page-Builder.php:274
     201#: php/pages/class-RP-Option-Page-Builder.php:317
    213202msgid "Hide Edit Links"
    214203msgstr "Скрыть ссылку редактирования"
    215204
    216 #: php/pages/class-RP-Option-Page-Builder.php:160
     205#: php/pages/class-RP-Option-Page-Builder.php:202
    217206msgid "Hide Facts"
    218207msgstr "Скрыть Факты"
    219208
    220 #: php/pages/class-RP-Option-Page-Builder.php:126
     209#: php/pages/class-RP-Option-Page-Builder.php:168
    221210msgid "Hide Header"
    222211msgstr "Скрыть Заголовок"
    223212
    224 #: php/pages/class-RP-Option-Page-Builder.php:91
     213#: php/pages/class-RP-Option-Page-Builder.php:125
    225214msgid "Hide Locations"
    226215msgstr "Скрыть местоположение"
    227216
    228 #: php/pages/class-RP-Option-Page-Builder.php:243
     217#: php/pages/class-RP-Option-Page-Builder.php:285
    229218msgid "Hide Picture"
    230219msgstr "Скрыть картинку"
    231220
    232 #: php/pages/class-RP-Option-Page-Builder.php:290
     221#: php/pages/class-RP-Option-Page-Builder.php:334
    233222msgid "Hide Silhouettes"
    234223msgstr "Скрыть силуэты"
    235224
    236 #: php/pages/class-RP-Option-Page-Builder.php:228
     225#: php/pages/class-RP-Option-Page-Builder.php:270
    237226msgid "Hide Spousal Family Groups"
    238227msgstr "Скрыть Семейные группы супругов"
    239228
    240 #: php/pages/class-RP-Option-Page-Builder.php:399
     229#: php/pages/class-RP-Option-Page-Builder.php:443
    241230msgid "Hide the PayPal Donation button"
    242231msgstr "Скрыть кнопку Пожертвования с помощью PayPal"
    243232
    244 #: php/pages/class-RP-Include-Page-Builder.php:38
     233#: php/pages/class-RP-Include-Page-Builder.php:44
    245234msgid "Include"
    246235msgstr "Включить"
    247236
    248 #: php/pages/class-RP-Option-Page-Builder.php:361
     237#: php/pages/class-RP-Option-Page-Builder.php:406
    249238msgid "Index Even Row Color"
    250239msgstr "Цвет четных строк индекса"
    251240
    252 #: php/pages/class-RP-Option-Page-Builder.php:373
     241#: php/pages/class-RP-Option-Page-Builder.php:418
    253242msgid "Index Header Row Color"
    254243msgstr "Цвет строки заголовка индекса"
    255244
    256 #: php/pages/class-RP-Option-Page-Builder.php:367
     245#: php/pages/class-RP-Option-Page-Builder.php:412
    257246msgid "Index Odd Row Color"
    258247msgstr "Цвет нечетных строк индекса"
    259248
    260 #: rootspersona.php:117
     249#: rootspersona.php:409
    261250msgid "Invalid person id."
    262251msgstr "Неверный ID (идентификатор) персоны"
    263252
    264 #: php/class-RP-Persona-Site-Mender.php:173
     253#: php/class-RP-Persona-Site-Mender.php:189
    265254msgid "Invalid person page assignment in database (no personid)."
    266255msgstr "Неверное сопоставление страницы персоны в базе данных (отсутствует идентификатор персоны (personid) )"
    267256
    268 #: php/class-RP-Persona-Site-Mender.php:232
     257#: php/class-RP-Persona-Site-Mender.php:251
    269258msgid "Invalid source page assignment in database (no sourceid)."
    270259msgstr "Неверное сопоставление страницы источника  в базе данных (отсутствует идентификатор источника (sourceid))"
    271260
    272 #: php/class-RP-Persona-Site-Mender.php:222
     261#: php/class-RP-Persona-Site-Mender.php:241
    273262msgid "Invalid source page assignment in database (page missing)."
    274263msgstr "Неверное сопоставление страницы источника  в базе данных (страница утеряна)"
    275264
    276 #: php/class-RP-Persona-Site-Mender.php:240
     265#: php/class-RP-Persona-Site-Mender.php:259
    277266msgid "Invalid source page assignment in database (wrong sourceId)."
    278267msgstr "Неверное сопоставление страницы источника  в базе данных (неверный идентификатор источника (sourceid))"
    279268
    280 #: php/pages/panels/class-RP-Group-Sheet-Panel-Creator.php:171
     269#: php/pages/panels/class-RP-Group-Sheet-Panel-Creator.php:187
    281270msgid "Marriage"
    282271msgstr "В браке"
    283272
    284 #: php/pages/class-RP-Option-Page-Builder.php:53
    285 #: php/pages/class-RP-Option-Page-Builder.php:72
     273#: php/pages/class-RP-Option-Page-Builder.php:63
     274#: php/pages/class-RP-Option-Page-Builder.php:82
    286275msgid "Members Only"
    287276msgstr "Только для Участников"
    288277
    289 #: php/pages/class-RP-Option-Page-Builder.php:383
     278#: php/pages/class-RP-Option-Page-Builder.php:427
    290279msgid "Misc Options"
    291280msgstr "Разное"
    292281
    293 #: rootspersona.php:277
    294 msgid "Missing"
    295 msgstr "Не найдено"
    296 
    297 #: php/pages/panels/class-RP-Group-Sheet-Panel-Creator.php:123
     282#: php/pages/panels/class-RP-Group-Sheet-Panel-Creator.php:137
     283#: php/templates/class-RP-Persona-Templates.php:343
     284#: php/templates/class-RP-Persona-Templates.php:437
     285#: php/templates/class-RP-Persona-Templates.php:535
    298286msgid "Mother"
    299287msgstr "Мать"
    300288
    301 #: php/pages/class-RP-Option-Page-Builder.php:88
    302 #: php/pages/class-RP-Option-Page-Builder.php:103
    303 #: php/pages/class-RP-Option-Page-Builder.php:138
    304 #: php/pages/class-RP-Option-Page-Builder.php:155
    305 #: php/pages/class-RP-Option-Page-Builder.php:172
    306 #: php/pages/class-RP-Option-Page-Builder.php:189
    307 #: php/pages/class-RP-Option-Page-Builder.php:206
    308 #: php/pages/class-RP-Option-Page-Builder.php:223
    309 #: php/pages/class-RP-Option-Page-Builder.php:240
    310 #: php/pages/class-RP-Option-Page-Builder.php:255
    311 #: php/pages/class-RP-Option-Page-Builder.php:270
    312 #: php/pages/class-RP-Option-Page-Builder.php:286
    313 #: php/pages/class-RP-Option-Page-Builder.php:302
     289#: php/pages/class-RP-Option-Page-Builder.php:121
     290#: php/pages/class-RP-Option-Page-Builder.php:135
     291#: php/pages/class-RP-Option-Page-Builder.php:180
     292#: php/pages/class-RP-Option-Page-Builder.php:197
     293#: php/pages/class-RP-Option-Page-Builder.php:214
     294#: php/pages/class-RP-Option-Page-Builder.php:231
     295#: php/pages/class-RP-Option-Page-Builder.php:248
     296#: php/pages/class-RP-Option-Page-Builder.php:265
     297#: php/pages/class-RP-Option-Page-Builder.php:282
     298#: php/pages/class-RP-Option-Page-Builder.php:297
     299#: php/pages/class-RP-Option-Page-Builder.php:313
     300#: php/pages/class-RP-Option-Page-Builder.php:329
     301#: php/pages/class-RP-Option-Page-Builder.php:346
    314302msgid "No"
    315303msgstr "Нет"
    316304
    317 #: rootspersona.php:352 rootspersona.php:414
     305#: rootspersona.php:816
     306#: rootspersona.php:855
     307#: rootspersona.php:942
    318308msgid "No people selected."
    319309msgstr "Не выбрано Людей"
    320310
    321 #: php/class-RP-Persona-Site-Mender.php:273
     311#: php/class-RP-Persona-Site-Mender.php:295
    322312msgid "No persona pages found."
    323313msgstr "Страницы персоны не найдены"
    324314
    325 #: php/class-RP-Persona-Site-Mender.php:74
    326 #: php/class-RP-Persona-Site-Mender.php:82
    327 #: php/class-RP-Persona-Site-Mender.php:90
    328 #: php/class-RP-Persona-Site-Mender.php:98
    329 #: php/class-RP-Persona-Site-Mender.php:106
     315#: php/class-RP-Persona-Site-Mender.php:89
     316#: php/class-RP-Persona-Site-Mender.php:97
     317#: php/class-RP-Persona-Site-Mender.php:105
     318#: php/class-RP-Persona-Site-Mender.php:113
     319#: php/class-RP-Persona-Site-Mender.php:121
    330320msgid "Obsolete"
    331321msgstr "Устарело"
    332322
    333 #: php/pages/class-RP-Option-Page-Builder.php:120
     323#: php/pages/class-RP-Option-Page-Builder.php:162
    334324msgid "Original"
    335325msgstr "Оригинал"
    336326
    337 #: php/pages/panels/class-RP-Group-Sheet-Panel-Creator.php:80
     327#: php/pages/panels/class-RP-Group-Sheet-Panel-Creator.php:94
    338328msgid "PARENT"
    339329msgstr "РОДИТЕЛИ"
    340330
    341 #: php/class-RP-Persona-Site-Mender.php:143
    342 #: php/class-RP-Persona-Site-Mender.php:204
    343 #: php/class-RP-Persona-Site-Mender.php:263
    344 #: php/pages/class-RP-Option-Page-Builder.php:312
     331#: php/pages/class-RP-Option-Page-Builder.php:356
    345332msgid "Page"
    346333msgstr "Страница"
    347334
    348 #: rootspersona.php:363
     335#. translators: %s: page ID created by RootsPersona.
     336#: rootspersona.php:783
     337#, php-format
    349338msgid "Page %s created for"
    350339msgstr "Страница %s  создана для"
    351340
    352 #: php/class-RP-Persona-Site-Mender.php:191
     341#: php/class-RP-Persona-Site-Mender.php:207
    353342msgid "Page title out of synch with person name"
    354343msgstr "Заголовок страницы не синхронизирован с именем персоны"
    355344
    356 #: php/class-RP-Persona-Site-Mender.php:250
     345#: php/class-RP-Persona-Site-Mender.php:269
    357346msgid "Page title out of synch with source name"
    358347msgstr "Заголовок страницы не синхронизирован с названием источника"
    359348
    360 #: php/class-RP-Persona-Site-Mender.php:185
    361 #: php/class-RP-Persona-Site-Mender.php:244
     349#: php/class-RP-Persona-Site-Mender.php:201
     350#: php/class-RP-Persona-Site-Mender.php:263
    362351msgid "Page title updated."
    363352msgstr "Заголовок страницы обновлен."
    364353
    365 #: php/pages/class-RP-Persona-Page-Builder.php:82
     354#: php/pages/class-RP-Persona-Page-Builder.php:126
    366355msgid "Page will be removed but supporting data will not be deleted.  Proceed?"
    367356msgstr "Страница будет удалена,  но данные - оставлены. Продолжить?"
    368357
    369 #: php/pages/class-RP-Evidence-Page-Builder.php:29
     358#: php/pages/class-RP-Evidence-Page-Builder.php:32
    370359msgid "Person(s) of Interest"
    371360msgstr "Заинтересованное Лицо (-а)"
    372361
    373 #: php/pages/class-RP-Option-Page-Builder.php:355
     362#: php/pages/class-RP-Option-Page-Builder.php:400
    374363msgid "Persona Frames"
    375364msgstr "Кадры Персоны"
    376365
    377 #: php/pages/class-RP-Include-Page-Builder.php:21
     366#: php/pages/class-RP-Include-Page-Builder.php:25
    378367msgid "Previously excluded persons must be re-imported"
    379368msgstr "Ранее исключенные персоны нужно повторно импортировать"
    380369
    381 #: php/pages/class-RP-Option-Page-Builder.php:31
     370#: php/pages/class-RP-Option-Page-Builder.php:41
    382371msgid "Privacy Options"
    383372msgstr "Настройки Конфиденциальности"
    384373
    385 #: php/pages/class-RP-Option-Page-Builder.php:74
     374#: php/pages/class-RP-Option-Page-Builder.php:84
    386375msgid "Privacy Setting to use when nothing else applies"
    387376msgstr "Настройки Конфиденциальности если не применяется никаких дополнительных настроек"
    388377
    389 #: php/pages/class-RP-Option-Page-Builder.php:52
    390 #: php/pages/class-RP-Option-Page-Builder.php:71
     378#: php/pages/class-RP-Option-Page-Builder.php:62
     379#: php/pages/class-RP-Option-Page-Builder.php:81
    391380msgid "Private, Admins Only"
    392381msgstr "Приватно, только для Администраторов"
    393382
    394 #: php/pages/class-RP-Option-Page-Builder.php:54
    395 #: php/pages/class-RP-Option-Page-Builder.php:73
     383#: php/pages/class-RP-Option-Page-Builder.php:64
     384#: php/pages/class-RP-Option-Page-Builder.php:83
    396385msgid "Public"
    397386msgstr "Публично"
    398387
    399 #: php/class-RP-Persona-Site-Mender.php:284
     388#: php/class-RP-Persona-Site-Mender.php:306
    400389msgid "Repair Inconsistencies?"
    401390msgstr "Исправить Ошибки?"
    402391
    403 #: php/pages/class-RP-Add-Page-Builder.php:65
    404 #: php/pages/class-RP-Include-Page-Builder.php:40
    405 #: php/pages/class-RP-Upload-Page-Builder.php:66
     392#: php/pages/class-RP-Add-Page-Builder.php:70
     393#: php/pages/class-RP-Include-Page-Builder.php:46
     394#: php/pages/class-RP-Upload-Page-Builder.php:81
    406395msgid "Reset"
    407396msgstr "Перезагрузить"
    408397
    409 #: php/class-RP-Persona-Site-Mender.php:293
    410 #: php/class-RP-Persona-Site-Mender.php:336
    411 #: php/class-RP-Persona-Site-Mender.php:359
     398#: php/class-RP-Persona-Site-Mender.php:313
     399#: php/class-RP-Persona-Site-Mender.php:356
    412400#: php/class-RP-Persona-Site-Mender.php:378
     401#: php/class-RP-Persona-Site-Mender.php:395
    413402msgid "Return"
    414403msgstr "Вернуться"
    415404
    416 #: php/pages/class-RP-Tools-Page-Builder.php:118
     405#: php/pages/class-RP-Tools-Page-Builder.php:140
    417406msgid "Review Excluded Persons"
    418407msgstr "Обзор Исключенных Персон"
    419408
    420 #: php/pages/class-RP-Tools-Page-Builder.php:74
    421 msgid "Review the list of people you have uploaded but not created pages for."
    422 msgstr "Смотреть список людей которые были загружены, но для которых не были созданы страницы"
    423 
    424 #: php/pages/class-RP-Option-Page-Builder.php:29
    425 #: php/pages/class-RP-Option-Page-Builder.php:479
     409#: php/pages/class-RP-Option-Page-Builder.php:39
     410#: php/pages/class-RP-Option-Page-Builder.php:504
    426411msgid "Save Changes"
    427412msgstr "Сохранить изменения"
    428413
    429 #: php/pages/class-RP-Option-Page-Builder.php:89
     414#. translators: %s: context (e.g. persona).
     415#: php/pages/class-RP-Option-Page-Builder.php:123
     416#, php-format
    430417msgid "Some people may want to hide dates for privacy purposes.  This is a global flag (impacts all %s pages)."
    431418msgstr "Некоторые люди, возможно захотят скрыть даты по соображениям конфиденциальности. Это глобальная настройка (влияет на все %s страницы)"
    432419
    433 #: php/pages/class-RP-Option-Page-Builder.php:287
     420#. translators: %s: context (e.g. persona).
     421#: php/pages/class-RP-Option-Page-Builder.php:331
     422#, php-format
    434423msgid "Some people may want to hide the edit links at the bottom of the %s page"
    435424msgstr "Некоторые люди, возможно захотят скрыть ссылку для редактирования внизу %s страницы"
    436425
    437 #: php/pages/class-RP-Option-Page-Builder.php:303
     426#: php/pages/class-RP-Option-Page-Builder.php:347
    438427msgid "Some people may want to hide the silhouette placeholders in the picture panel"
    439428msgstr "Если вы хотите скрыть замещающие силуэты на панели фото - выберите Да иначе - Нет"
    440429
    441 #: php/pages/class-RP-Evidence-Page-Builder.php:15
     430#: php/pages/class-RP-Evidence-Page-Builder.php:18
    442431msgid "Source"
    443432msgstr "Источник"
    444433
    445 #: php/pages/class-RP-Option-Page-Builder.php:315
     434#: php/pages/class-RP-Option-Page-Builder.php:360
    446435msgid "Style Options"
    447436msgstr "Настройки Стилей"
    448437
    449 #: php/class-RP-Persona-Site-Mender.php:371
     438#: php/class-RP-Persona-Site-Mender.php:390
    450439msgid "Tables emptied."
    451440msgstr "Таблицы Очищены"
    452441
    453 #: php/pages/class-RP-Upload-Page-Builder.php:70
     442#: php/pages/class-RP-Upload-Page-Builder.php:85
    454443msgid "The following PHP settings have an impact on GEDCOM file processing"
    455444msgstr "Следующие настройки PHP влияют на обработку GEDCOM-файла"
    456445
    457 #: php/pages/class-RP-Upload-Page-Builder.php:21
     446#: php/pages/class-RP-Upload-Page-Builder.php:24
    458447msgid "The system will be uploading the file AND parsing each person into the database"
    459448msgstr "Система загрузит файл и занесет каждую персону в базу данных"
    460449
    461 #: php/pages/class-RP-Edit-Page-Builder.php:138
     450#: php/pages/class-RP-Edit-Page-Builder.php:152
    462451msgid "This person will be visible based on the privacy setting for living persons or the global default setting."
    463452msgstr "Видимость данной персоны будет зависеть от настроек конфиденциальности для ныне живущих персон или глобальных настроек по умолчанию"
    464453
    465 #: php/pages/class-RP-Edit-Page-Builder.php:132
     454#: php/pages/class-RP-Edit-Page-Builder.php:146
    466455msgid "This person will be visible to ANYONE, even if they are living."
    467456msgstr "Эта персона будет видна кому угодно, даже если это ныне живущая персона"
    468457
    469 #: php/pages/class-RP-Edit-Page-Builder.php:126
     458#: php/pages/class-RP-Edit-Page-Builder.php:140
    470459msgid "This person will only be visible to users who are logged in."
    471460msgstr "Эта персона будет видна только зарегистрированному пользователю  который выполнил вход на сайте"
    472461
    473 #: php/pages/class-RP-Upload-Page-Builder.php:64
     462#: php/pages/class-RP-Upload-Page-Builder.php:79
    474463msgid "Upload"
    475464msgstr "Загрузить"
    476465
    477 #: php/pages/class-RP-Tools-Page-Builder.php:58
    478 msgid "Upload (or re-upload) a GEDCOM file."
    479 msgstr "Загрузить (или повторно загрузить) GEDCOM-файл"
    480 
    481 #: php/pages/class-RP-Option-Page-Builder.php:306
     466#: php/pages/class-RP-Option-Page-Builder.php:350
    482467msgid "Utility Pages"
    483468msgstr "Страницы Настроек"
    484469
    485 #: php/pages/class-RP-Tools-Page-Builder.php:133
     470#: php/pages/class-RP-Tools-Page-Builder.php:159
    486471msgid "Validate persona Pages"
    487472msgstr "Проверить страницы Персоны"
    488473
    489 #: php/pages/class-RP-Option-Page-Builder.php:87
    490 #: php/pages/class-RP-Option-Page-Builder.php:102
    491 #: php/pages/class-RP-Option-Page-Builder.php:137
    492 #: php/pages/class-RP-Option-Page-Builder.php:154
    493 #: php/pages/class-RP-Option-Page-Builder.php:171
    494 #: php/pages/class-RP-Option-Page-Builder.php:188
    495 #: php/pages/class-RP-Option-Page-Builder.php:205
    496 #: php/pages/class-RP-Option-Page-Builder.php:222
    497 #: php/pages/class-RP-Option-Page-Builder.php:239
    498 #: php/pages/class-RP-Option-Page-Builder.php:254
    499 #: php/pages/class-RP-Option-Page-Builder.php:269
    500 #: php/pages/class-RP-Option-Page-Builder.php:285
    501 #: php/pages/class-RP-Option-Page-Builder.php:301
     474#: php/pages/class-RP-Option-Page-Builder.php:120
     475#: php/pages/class-RP-Option-Page-Builder.php:134
     476#: php/pages/class-RP-Option-Page-Builder.php:179
     477#: php/pages/class-RP-Option-Page-Builder.php:196
     478#: php/pages/class-RP-Option-Page-Builder.php:213
     479#: php/pages/class-RP-Option-Page-Builder.php:230
     480#: php/pages/class-RP-Option-Page-Builder.php:247
     481#: php/pages/class-RP-Option-Page-Builder.php:264
     482#: php/pages/class-RP-Option-Page-Builder.php:281
     483#: php/pages/class-RP-Option-Page-Builder.php:296
     484#: php/pages/class-RP-Option-Page-Builder.php:312
     485#: php/pages/class-RP-Option-Page-Builder.php:328
     486#: php/pages/class-RP-Option-Page-Builder.php:345
    502487msgid "Yes"
    503488msgstr "Да"
    504489
    505 #: php/pages/class-RP-Upload-Page-Builder.php:100
     490#: php/pages/class-RP-Upload-Page-Builder.php:117
    506491msgid "You may change the above values in the php.ini file"
    507492msgstr "Вы можете изменить данную настройку в файле php.ini"
    508493
    509 #: php/class-RP-Persona-Site-Mender.php:277
     494#. translators: %s: plugin or context name.
     495#: php/class-RP-Persona-Site-Mender.php:300
     496#, php-format
    510497msgid "Your %s setup is VALID."
    511498msgstr "Ваша %s установка Действительна"
    512499
    513 #: php/pages/class-RP-Upload-Page-Builder.php:84
     500#: php/pages/class-RP-Upload-Page-Builder.php:101
    514501msgid "Your system is configured in "
    515502msgstr "Ваша система настроена в"
    516503
    517 #: rootspersona.php:468
    518 msgid "action not supported"
    519 msgstr "данное действие не поддерживается"
    520 
    521 #: php/class-RP-Persona-Installer.php:215
     504#: php/class-RP-Persona-Installer.php:279
    522505msgid "allowing an admin to manage the display of those pages"
    523506msgstr "позволяет администратору управлять отображением этих страниц"
    524507
    525 #: php/pages/panels/class-RP-Group-Sheet-Panel-Creator.php:168
     508#. translators: Marriage row: before place name (English "at Boston").
     509#: php/pages/panels/class-RP-Group-Sheet-Panel-Creator.php:184
     510msgctxt "marriage: before place"
    526511msgid "at"
    527512msgstr "при"
    528513
    529 #: php/pages/class-RP-Upload-Page-Builder.php:102
     514#: php/pages/class-RP-Upload-Page-Builder.php:119
    530515msgid "or by override the settings in your .htaccess file as defined below"
    531516msgstr "или путем перезаписи настроек в файле .htaccess  как указано ниже"
    532517
    533 #: php/class-RP-Persona-Site-Mender.php:400
     518#. translators: %s: batch id.
     519#: php/class-RP-Persona-Site-Mender.php:418
     520#, php-format
    534521msgid "source page(s) added for batchId %s"
    535522msgstr "страница (ы) источника добавленные для batchId %s"
    536523
    537 #: php/pages/panels/class-RP-Group-Sheet-Panel-Creator.php:157
     524#. translators: Marriage row: short word before the spouse name (English "to Jane Doe").
     525#: php/pages/panels/class-RP-Group-Sheet-Panel-Creator.php:172
     526msgctxt "marriage: before spouse name"
    538527msgid "to"
    539 msgstr "c"
    540 
    541 #: php/pages/class-RP-Upload-Page-Builder.php:71
     528msgstr "с"
     529
     530#: php/pages/class-RP-Upload-Page-Builder.php:86
    542531msgid "version"
    543532msgstr "версия"
    544533
    545 #: php/pages/panels/class-RP-Facts-Panel-Creator.php:48
     534#: php/pages/panels/class-RP-Facts-Panel-Creator.php:56
    546535msgid "with"
    547536msgstr "с"
    548537
    549 #: surname_widget.php:123
     538#: surname_widget.php:122
    550539msgid "How Many:"
    551540msgstr "Сколько"
    552541
    553 #: surname_widget.php:39
     542#: surname_widget.php:41
    554543msgid "Surname Widget"
    555544msgstr "Виджет Фамилий"
    556545
    557 #: php/pages/panels/class-RP-Group-Sheet-Panel-Creator.php:379
     546#: php/pages/panels/class-RP-Group-Sheet-Panel-Creator.php:404
    558547msgid "Link to a Spouse"
    559548msgstr "Связать с Супругом"
    560549
    561 #: php/pages/panels/class-RP-Group-Sheet-Panel-Creator.php:361
     550#: php/pages/panels/class-RP-Group-Sheet-Panel-Creator.php:386
    562551msgid "Unlink from this Spouse"
    563552msgstr "Удалить связь с Супругом"
    564553
    565 #: php/pages/panels/class-RP-Group-Sheet-Panel-Creator.php:322
     554#: php/pages/panels/class-RP-Group-Sheet-Panel-Creator.php:347
    566555msgid "Link to a Father"
    567556msgstr "Связать с Отцом"
    568557
    569 #: php/pages/panels/class-RP-Group-Sheet-Panel-Creator.php:324
     558#: php/pages/panels/class-RP-Group-Sheet-Panel-Creator.php:349
    570559msgid "Link to a Mother"
    571560msgstr "Связать с Матерью"
    572561
    573 #: php/pages/panels/class-RP-Group-Sheet-Panel-Creator.php:320
     562#: php/pages/panels/class-RP-Group-Sheet-Panel-Creator.php:345
    574563msgid "Unlink from Parents"
    575564msgstr "Удалить связь с Родителями"
    576565
    577 #: php/pages/class-RP-Edit-Page-Builder.php:25
     566#: php/pages/class-RP-Edit-Page-Builder.php:42
    578567msgid "New"
    579568msgstr "Новый"
    580569
    581 #: php/pages/class-RP-Edit-Page-Builder.php:23
    582 #: php/pages/class-RP-Edit-Page-Builder.php:61
     570#: php/pages/class-RP-Edit-Page-Builder.php:28
     571#: php/pages/class-RP-Upload-Page-Builder.php:146
    583572msgid "Cancel"
    584573msgstr "Отменить"
    585574
    586 #: php/pages/class-RP-Edit-Page-Builder.php:21
    587 #: php/pages/class-RP-Edit-Page-Builder.php:59
     575#: php/pages/class-RP-Edit-Page-Builder.php:34
     576#: php/pages/class-RP-Edit-Page-Builder.php:73
    588577msgid "Save"
    589578msgstr "Сохранить"
    590579
    591 #: php/class-RP-Persona-Validator.php:79
     580#: php/class-RP-Persona-Validator.php:86
    592581msgid "Surname and/or Given name required."
    593582msgstr "Необходимо указать Фамилию и / или Данное имя."
    594583
    595 #: surname_widget.php:112
     584#: surname_widget.php:111
    596585msgid "Top Surnames"
    597586msgstr "Топ Фамилий"
    598587
    599 #. Description of the plugin/theme
     588#. Description of the plugin
     589#: rootspersona.php
    600590msgid "Build one or more family history pages from a Gedcom file."
    601591msgstr "Создает от одной и более страниц семейной истории из файла GEDCOM"
    602592
    603 #: rootspersona.php:496
     593#: rootspersona.php:1064
    604594msgid "Empty File."
    605595msgstr "Файл пуст"
    606596
    607 #: rootspersona.php:547
     597#: rootspersona.php:1113
    608598msgid "Content reserved for registered members."
    609599msgstr "Контент только для зарегистрированных пользователей."
    610600
    611 #: rootspersona.php:494
     601#: rootspersona.php:1058
     602#: rootspersona.php:1061
    612603msgid "Error Uploading File."
    613604msgstr "Ошибка загрузки файла"
    614605
    615 #: surname_widget.php:33
     606#: surname_widget.php:35
    616607msgid "An widget that displays the top X surnames in rootspersona."
    617608msgstr "Виджет, отображающий Топ Фамилий РутсПерсоны"
    618609
    619 #: surname_widget.php:117
     610#: surname_widget.php:116
    620611msgid "Title:"
    621612msgstr "Заголовок"
    622613
    623 #: php/pages/panels/class-RP-Picture-Panel-Creator.php:22
     614#: php/pages/panels/class-RP-Picture-Panel-Creator.php:95
    624615msgid "Picture Gallery"
    625616msgstr "Фото Галерея"
    626617
    627 #: php/pages/panels/class-RP-Descendancy-Panel-Creator.php:16
     618#: php/pages/panels/class-RP-Descendancy-Panel-Creator.php:20
    628619msgid "Descendancy Chart"
    629620msgstr "Список родственников по нисходящей линии"
    630621
    631 #: php/pages/class-RP-Upload-Page-Builder.php:95
     622#: php/pages/class-RP-Upload-Page-Builder.php:112
    632623msgid "and may need to split your GEDCOM file accordingly"
    633624msgstr "и, возможно, потребуется соответствующим образом разделить файл GEDCOM"
    634625
    635 #: php/pages/class-RP-Upload-Page-Builder.php:93
     626#: php/pages/class-RP-Upload-Page-Builder.php:110
    636627msgid "However you may still run into an issue with server time limit settings (usually 300 seconds)"
    637628msgstr "Тем не менее, вы можете столкнуться с проблемой ограничения выполнения скрипта на сервере (обычно оно составляет около 300 секунд)"
    638629
    639 #: php/pages/class-RP-Upload-Page-Builder.php:90
     630#: php/pages/class-RP-Upload-Page-Builder.php:107
    640631msgid "will try to override your system settings as needed to allow processing of a large GEDCOM file"
    641632msgstr "будет предпринята попытка перезаписи ваших системных настроек - это необходимо для обработки большого GEDCOM-файла"
    642633
    643 #: php/pages/class-RP-Upload-Page-Builder.php:86
     634#: php/pages/class-RP-Upload-Page-Builder.php:103
    644635msgid "will NOT be able to override your system settings to allow processing of a large GEDCOM file."
    645636msgstr "не удалось перезаписать настройки вашей системы для обработки большого GEDCOM-файла"
    646637
    647 #: php/pages/class-RP-Upload-Page-Builder.php:23
     638#: php/pages/class-RP-Upload-Page-Builder.php:26
    648639msgid "resulting in a white screen with no status message"
    649640msgstr "в итоге - белый экран без каких-либо сообщений на нем"
    650641
    651 #: php/pages/class-RP-Upload-Page-Builder.php:22
     642#: php/pages/class-RP-Upload-Page-Builder.php:25
    652643msgid "Many users have reported hitting PHP time limit constraints when processing large files"
    653644msgstr "Многие пользователи жаловались на проблемы связанные с лимитом времени работы скриптов php при обработке больших файлов"
    654645
    655 #: php/pages/class-RP-Upload-Page-Builder.php:20
     646#: php/pages/class-RP-Upload-Page-Builder.php:23
    656647msgid "Please note that GEDCOM processing is the most system taxing processing this plugin performs"
    657648msgstr "Не забывайте, что обработка файла GEDCOM - это самый сильно нагружающий систему процесс из тех, которые выполняет плагин."
    658649
    659 #: php/pages/class-RP-Tools-Page-Builder.php:153
    660 msgid "Perform a bulk deletion of all %s and evidence pages. You will be prompted before the data is deleted."
    661 msgstr "Выполняет массовое удаление всех страниц %s и страниц данных. Перед удалением Вы получите запрос на подтверждение удаления."
    662 
    663 #: php/pages/class-RP-Tools-Page-Builder.php:137
    664 msgid "Identify orphaned %s pages. Includes all pages with %s shortcode and no reference in the database, or reference in the database with no corresponding page."
    665 msgstr "Идентифицировать подвисшие страницы %s, включая все страницы с шоткодами %s и страницы не имеющие ссылок в базе данных или, напротив, ссылки, присутствующие в базе данных, но без соответствующих им страниц."
    666 
    667 #: php/pages/class-RP-Tools-Page-Builder.php:138
     650#: php/pages/class-RP-Tools-Page-Builder.php:166
    668651msgid "Will also identify/sync pages with the wrong parent page assigned."
    669652msgstr "При этом также будут идентифицированы либо синхронизированы страницы которым была присвоена неверная родительская страница "
    670653
    671 #: php/pages/class-RP-Tools-Page-Builder.php:92
     654#: php/pages/class-RP-Tools-Page-Builder.php:110
    672655msgid "If rootspersona is set as the System of record, this screenallows you to add a  new individual from scratch."
    673656msgstr "Если на сайте для РутсПерсоны разрешена Система Записи (в настройках РутсПерсоны), то с помощью этой кнопки можно добавить нового человека с нуля."
    674657
    675 #: php/pages/class-RP-Tools-Page-Builder.php:56
     658#: php/pages/class-RP-Tools-Page-Builder.php:58
    676659msgid "Upload GEDCOM"
    677660msgstr "Загрузить GEDCOM-файл"
    678661
    679 #: php/pages/class-RP-Tools-Page-Builder.php:122
     662#: php/pages/class-RP-Tools-Page-Builder.php:144
    680663msgid "Review people you have previous excluded, and include the ones you select."
    681664msgstr "Можно просмотреть тех людей, которых вы ранее исключили, и включить их вновь по вашему выбору."
    682665
    683 #: php/pages/panels/class-RP-Group-Sheet-Panel-Creator.php:21
     666#: php/pages/panels/class-RP-Group-Sheet-Panel-Creator.php:33
    684667msgid "Family Group Sheet - Child"
    685668msgstr "Семья персоны (Персона здесь - Ребёнок)"
    686669
    687 #: php/class-RP-Persona-Installer.php:51
     670#: php/class-RP-Persona-Installer.php:52
    688671msgid "rootspersona Tree"
    689672msgstr "Дерево РутсПерсоны"
    690673
    691 #: php/class-RP-Persona-Installer.php:212
     674#: php/class-RP-Persona-Installer.php:276
    692675msgid "The default WordPress themes display all first level pages by default"
    693676msgstr "Тема Вордпресс установленная по Умолчанию отображает все страницы первого уровня вложенности"
    694677
    695 #: php/class-RP-Persona-Installer.php:213
     678#: php/class-RP-Persona-Installer.php:277
    696679msgid "When importing a GEDCOM file, hundreds of pages can be created, all first level pages"
    697680msgstr "В результате импорта GEDCOM-файла могут быть созданы сотни страниц - все первого уровня вложенности."
    698681
    699 #: php/class-RP-Persona-Installer.php:46
     682#: php/class-RP-Persona-Installer.php:47
    700683msgid "Evidence Index"
    701684msgstr "Индекс Данных"
    702685
    703 #: php/pages/panels/class-RP-Group-Sheet-Panel-Creator.php:269
     686#: php/pages/panels/class-RP-Group-Sheet-Panel-Creator.php:292
    704687msgid "Family Group Sheet - Spouse"
    705688msgstr "Семья Персоны (Персона здесь - Родитель)"
    706689
    707 #: php/pages/panels/class-RP-Evidence-Panel-Creator.php:15
     690#: php/pages/panels/class-RP-Evidence-Panel-Creator.php:19
    708691msgid "Evidence"
    709692msgstr "Данные"
    710693
    711 #: php/pages/panels/class-RP-Evidence-Panel-Creator.php:27
     694#: php/pages/panels/class-RP-Evidence-Panel-Creator.php:30
    712695msgid "Evidence page has not yet been created for this source"
    713696msgstr "Страница данных не была создана для данного источника"
    714697
    715 #: php/pages/class-RP-Upload-Page-Builder.php:45
     698#: php/pages/class-RP-Upload-Page-Builder.php:50
     699#, php-format
    716700msgid "Please note that %s cannot link persons in different batches"
    717701msgstr "Внимание! Имейте ввиду, что плагин %s не может связывать персон из разных групп!"
    718702
    719 #: php/pages/class-RP-Option-Page-Builder.php:476
    720 msgid "Advanced feature.  Data will be used in place of the RootsPersona shortcode when generating new pages.<br>{%personid%} will be replaced by the actual person id when the page is created.<br>{%batchid%} will be replaced by the current batch id when the page is created.<br>{%picfile1%} will be replaced by the current picfile1 when page is created.<br>The rest is still rendered when the page is requested."
    721 msgstr "Настройка для продвинутых пользователей. Данные будут использоваться при генерации новых страниц в месте расположения шоткодов РутсПерсоны. <br> при создании страницы {%personid%} будет заменен текущим идентификатором (id) персоны, <br>{%batchid%} - текущим идентификатором группы, <br>{%picfile1%} - текущим идентификатором картинки. <br> Все остальное при запросе страницы будет отображаться по-прежнему."
    722 
    723 #: php/pages/class-RP-Persona-Page-Builder.php:83
     703#: php/pages/class-RP-Persona-Page-Builder.php:127
    724704msgid "Page will be removed and supporting data will be deleted.  Proceed?"
    725705msgstr "Страница будет удалена, также как и данные. Продолжить?"
    726706
    727 #: php/pages/class-RP-Option-Page-Builder.php:473
    728 msgid "Custom Page Shortcodes"
    729 msgstr "Шоткоды страницы пользователя"
    730 
    731 #: php/pages/class-RP-Option-Page-Builder.php:437
     707#: php/pages/class-RP-Option-Page-Builder.php:481
    732708msgid "Yes indicates data may be entered from this website. Editing data from this website AND importing GEDCOM data is NOT recommended."
    733709msgstr " \"Да\" - разрешает редактирование данных на сайте. Однако одновременное редактирование данных на сайте и импорт GEDCOM - данных не рекомендуются. На Ваш выбор, не забывайте о бэкапах. "
    734710
    735 #: php/pages/class-RP-Option-Page-Builder.php:423
     711#: php/pages/class-RP-Option-Page-Builder.php:467
    736712msgid "System Of Record"
    737713msgstr "Использовать Систему Записи на сайте."
    738714
    739 #: php/pages/class-RP-Option-Page-Builder.php:420
     715#: php/pages/class-RP-Option-Page-Builder.php:464
    740716msgid "Indicates the number of rows on an index page. Default is 25."
    741717msgstr "Количество строк на индексной странице РутсПерсоны. По умолчанию 25."
    742718
    743 #: php/pages/class-RP-Option-Page-Builder.php:343
     719#: php/pages/class-RP-Option-Page-Builder.php:388
    744720msgid "Group Background Color"
    745721msgstr "Цвет фона для группы"
    746722
    747 #: php/pages/class-RP-Option-Page-Builder.php:313
     723#. translators: %s: context (e.g. persona).
     724#: php/pages/class-RP-Option-Page-Builder.php:358
     725#, php-format
    748726msgid "you want %s pages to be organized under in a menu structure.  0 indicates no parent page"
    749727msgstr "Если вы хотите, чтобы страницы %s были организованы в виде меню. всех персон. Номер идентификатора страницы, где расположено это меню (обычно ее адрес - /rootspersona-tree/). 0 означает отсутствие данной страницы."
    750728
    751 #: php/pages/class-RP-Option-Page-Builder.php:309
     729#: php/pages/class-RP-Option-Page-Builder.php:353
    752730msgid "Parent Page Id"
    753731msgstr "Идентификатор (ID) родительской страницы"
    754732
    755 #: php/pages/class-RP-Option-Page-Builder.php:224
     733#: php/pages/class-RP-Option-Page-Builder.php:266
    756734msgid "Skip the Family Group panel where the person is a Child on persona pages"
    757735msgstr "Не отображать панель Семейной группы (Персона в качестве Ребенка) на страницах Персон"
    758736
    759 #: php/pages/class-RP-Option-Page-Builder.php:241
     737#: php/pages/class-RP-Option-Page-Builder.php:283
    760738msgid "Skip the Family Group panels where the person is a Spouse on persona pages"
    761739msgstr "Не отображать панель Семейная группа (Персона в качестве Супруга) на страницах Персон"
    762740
    763 #: php/pages/class-RP-Option-Page-Builder.php:256
     741#. translators: %s: context (e.g. persona).
     742#: php/pages/class-RP-Option-Page-Builder.php:299
     743#, php-format
    764744msgid "Skip the pictures panel on %s pages. Still displays the image in the Header panel"
    765745msgstr "Не отображать панель Фото на страницах %s. При этом изображение на панели Заголовка будет по-прежнему отображаться."
    766746
    767 #: php/pages/class-RP-Option-Page-Builder.php:258
     747#: php/pages/class-RP-Option-Page-Builder.php:301
    768748msgid "Hide Evidence"
    769749msgstr "Скрыть Данные"
    770750
    771 #: php/pages/class-RP-Option-Page-Builder.php:271
     751#: php/pages/class-RP-Option-Page-Builder.php:314
    772752msgid "Skip the evidence panel in persona pages."
    773753msgstr "Не отображать панель Данных на страницах Персоны"
    774754
    775 #: php/pages/class-RP-Option-Page-Builder.php:207
     755#: php/pages/class-RP-Option-Page-Builder.php:249
    776756msgid "Skip the Descendancy panel on persona pages"
    777757msgstr "Не отображать панель Родство По Нисходящей линии на страницах Персоны"
    778758
    779 #: php/pages/class-RP-Option-Page-Builder.php:194
     759#: php/pages/class-RP-Option-Page-Builder.php:236
    780760msgid "Hide Descendancy"
    781761msgstr "Скрыть область \"Родство По нисходящей\""
    782762
    783 #: php/pages/class-RP-Option-Page-Builder.php:156
     763#: php/pages/class-RP-Option-Page-Builder.php:198
    784764msgid "Skip the Biography panel on persona pages"
    785765msgstr "Не отображать панель Биографии на страницах Персоны"
    786766
    787 #: php/pages/class-RP-Option-Page-Builder.php:173
     767#: php/pages/class-RP-Option-Page-Builder.php:215
    788768msgid "Skip the Facts panel on persona pages"
    789769msgstr "Не отображать панель Фактов на страницах Персоны"
    790770
    791 #: php/pages/class-RP-Option-Page-Builder.php:190
     771#: php/pages/class-RP-Option-Page-Builder.php:232
    792772msgid "Skip the Ancestors panel on persona pages"
    793773msgstr "Не отображать панель Предков на страницах Персоны"
    794774
    795 #: php/pages/class-RP-Option-Page-Builder.php:139
     775#: php/pages/class-RP-Option-Page-Builder.php:181
    796776msgid "Skip the Header Panel on persona pages."
    797777msgstr "Не отображать панель Заголовка на страницах Персоны"
    798778
    799 #: php/pages/class-RP-Option-Page-Builder.php:104
     779#. translators: %s: context (e.g. persona).
     780#: php/pages/class-RP-Option-Page-Builder.php:137
     781#, php-format
    800782msgid "Some people may want to hide locations for privacy purposes.  This is a global flag (impacts all %s pages)."
    801783msgstr "Некоторые люди, возможно захотят скрыть свое местонахождение по личным причинам. Это глобальная настройка (влияющая на все страницы %s)"
    802784
    803 #: php/pages/class-RP-Option-Page-Builder.php:55
     785#: php/pages/class-RP-Option-Page-Builder.php:65
    804786msgid "Privacy Setting to use for living people (no death date and birth date greater than 110 years ago)"
    805787msgstr "Настройки Конфиденциальности для Ныне Живущих Людей (дата рождения или смерти не более 110 лет назад)"
    806788
    807 #: php/pages/class-RP-Option-Page-Builder.php:33
     789#: php/pages/class-RP-Option-Page-Builder.php:43
    808790msgid "Living Privacy Setting"
    809 msgstr "Текущие настройки Конфиденциальности"
    810 
    811 #: php/pages/class-RP-Option-Page-Builder.php:13
     791msgstr "Приватность живых людей"
     792
     793#. translators: %s: context (e.g. persona).
     794#: php/pages/class-RP-Option-Page-Builder.php:17
     795#, php-format
    812796msgid "Edit page is meant to be accessed using the Edit button on a %s page"
    813797msgstr "Переход на страницу редактирования производится с помощью кнопки \"Редактировать\" на странице %s"
    814798
    815 #: php/pages/class-RP-Evidence-Page-Builder.php:46
     799#: php/pages/class-RP-Evidence-Page-Builder.php:49
    816800msgid "Notes"
    817801msgstr "Заметки"
    818802
    819 #: php/pages/class-RP-Edit-Page-Builder.php:113
     803#: php/pages/class-RP-Edit-Page-Builder.php:127
    820804msgid "Delete page and exclude this person FROM future uploads."
    821805msgstr "Удалить страницу и исключить эту персону при будущих загрузках"
    822806
    823 #: php/pages/class-RP-Edit-Page-Builder.php:120
     807#: php/pages/class-RP-Edit-Page-Builder.php:134
    824808msgid "This person will only be visible to the admin role."
    825809msgstr "Эта персона будет видна только пользователю с правами Администратора."
    826810
    827 #: php/class-RP-Persona-Validator.php:27
     811#: php/class-RP-Persona-Validator.php:30
    828812msgid "Batch Id required."
    829813msgstr "Укажите идентификатор (Id) группы"
    830814
    831 #: php/class-RP-Persona-Site-Mender.php:351
     815#. translators: %s: batch id.
     816#: php/class-RP-Persona-Site-Mender.php:372
     817#, php-format
    832818msgid "Data deleted for batchId %s."
    833819msgstr "Данные удалены для группы %s"
    834820
    835 #: php/class-RP-Persona-Site-Mender.php:324
     821#. translators: %s: batch id.
     822#: php/class-RP-Persona-Site-Mender.php:347
     823#, php-format
    836824msgid "pages deleted from batchId %s"
    837825msgstr "страницы будут удалены из группы %s"
    838826
    839 #: php/class-RP-Persona-Site-Mender.php:181
     827#: php/class-RP-Persona-Site-Mender.php:197
    840828msgid "Invalid person page assignment in database (wrong personId)."
    841829msgstr "Неверное сопоставление страницы персоны в базе данных (неверный идентификатор персоны (personid))"
    842830
    843 #: php/class-RP-Persona-Site-Mender.php:169
    844 #: php/class-RP-Persona-Site-Mender.php:177
    845 #: php/class-RP-Persona-Site-Mender.php:228
    846 #: php/class-RP-Persona-Site-Mender.php:236
     831#: php/class-RP-Persona-Site-Mender.php:185
     832#: php/class-RP-Persona-Site-Mender.php:193
     833#: php/class-RP-Persona-Site-Mender.php:247
     834#: php/class-RP-Persona-Site-Mender.php:255
    847835msgid "Database updated to remove reference to invalid page."
    848836msgstr "База данных обновлена, для удаления связи с неверной страницей."
    849837
    850 #: php/class-RP-Persona-Site-Mender.php:163
     838#: php/class-RP-Persona-Site-Mender.php:179
    851839msgid "Invalid person page assignment in database (page missing)."
    852840msgstr "Неверное сопоставление страницы персоны в базе данных (страница отсутствует)"
    853841
    854 #: php/class-RP-Persona-Site-Mender.php:159
    855 #: php/class-RP-Persona-Site-Mender.php:218
     842#: php/class-RP-Persona-Site-Mender.php:175
     843#: php/class-RP-Persona-Site-Mender.php:237
    856844msgid "Database updated to remove reference to missing page."
    857845msgstr "База данных обновлена, для удаления связи с отсутствующей страницей."
    858846
    859 #: php/class-RP-Persona-Site-Mender.php:129
     847#. translators: %s: parent page info.
     848#: php/class-RP-Persona-Site-Mender.php:143
     849#, php-format
    860850msgid "Parent page out of synch %s"
    861851msgstr "Родительская страница не синхронизирована с %s"
    862852
    863 #: php/class-RP-Persona-Site-Mender.php:139
    864 #: php/class-RP-Persona-Site-Mender.php:200
    865 #: php/class-RP-Persona-Site-Mender.php:259
     853#. translators: %s: plugin or context name.
     854#: php/class-RP-Persona-Site-Mender.php:153
     855#: php/class-RP-Persona-Site-Mender.php:217
     856#: php/class-RP-Persona-Site-Mender.php:279
     857#, php-format
    866858msgid "Issues found with your %s pages."
    867859msgstr "Проблемы, обнаруженные с вашими страницами %s"
    868860
    869 #: php/class-RP-Persona-Site-Mender.php:122
     861#. translators: %s: parent page.
     862#: php/class-RP-Persona-Site-Mender.php:136
     863#, php-format
    870864msgid "Updated parent page to %s."
    871865msgstr "Обновляет родительскую страницу для %s."
    872866
    873 #: php/class-RP-Persona-Site-Mender.php:118
    874 msgid "No reference in rp_source."
    875 msgstr "Нет связи в rp_source."
    876 
    877 #: php/class-RP-Persona-Site-Mender.php:114
    878 msgid "Deleted orphaned page with no reference in rp_source."
    879 msgstr "Удалена потерянная страница без связи в rp_source."
    880 
    881 #: php/class-RP-Persona-Site-Mender.php:47
     867#: php/class-RP-Persona-Site-Mender.php:62
    882868msgid "Deleted orphaned page with no reference in"
    883869msgstr "Удаленная потерянная страница с отсутствовавшими связями"
    884870
    885 #: php/class-RP-Persona-Installer.php:222
     871#: php/class-RP-Persona-Installer.php:288
    886872msgid "The parent page is NOT meant for display, as the menu is very large on most sites."
    887873msgstr "Родительская страница не создается с целью отображения всей развертки Древа, ввиду того, что на большинстве сайтов меню будет гигантским."
    888874
    889 #: php/class-RP-Persona-Installer.php:221
     875#: php/class-RP-Persona-Installer.php:287
    890876msgid "will now create and assign a Parent Page by default"
    891877msgstr "создаст и привяжет Родительскую Страницу по умолчанию"
    892878
    893 #: php/class-RP-Persona-Installer.php:218
     879#. translators: %s: plugin name.
     880#: php/class-RP-Persona-Installer.php:284
     881#, php-format
    894882msgid "then emailed me to say that %s has destroyed their site"
    895883msgstr "а после этого маякните мне на мейл, что %s \"положила\" ваш сайт"
    896884
    897 #: php/class-RP-Persona-Installer.php:217
     885#: php/class-RP-Persona-Installer.php:281
    898886msgid "I can not tell you how many new users have skipped the step of assigning the parent page"
    899887msgstr "Даже не могу сказать сколько новых пользователей пропустили шаг на котором назначается родительская страница"
    900888
    901 #: php/class-RP-Persona-Installer.php:214
     889#: php/class-RP-Persona-Installer.php:278
    902890msgid "The intent of assigning a Parent Page is to push persona pages down to a second level"
    903891msgstr "Целью назначения Родительской страницы является перемещение страниц Персон на второй уровень вложенности"
    904892
    905 #: php/class-RP-Persona-Installer.php:43
     893#: php/class-RP-Persona-Installer.php:44
    906894msgid "Person Index"
    907895msgstr "Индекс Персон"
    908896
    909 #: php/class-RP-Persona-Site-Mender.php:51
     897#: php/class-RP-Persona-Site-Mender.php:66
    910898msgid "No reference in"
    911899msgstr "Нет связи"
    912900
    913 #: php/pages/class-RP-Option-Page-Builder.php:15
     901#. translators: %s: context (e.g. persona).
     902#: php/pages/class-RP-Option-Page-Builder.php:19
     903#, php-format
    914904msgid "Accessing outside of a %s page will result in an invalid personId error."
    915905msgstr "Доступ за пределами страницы %s приведет к ошибке неверного идентифмкатора персоны."
     906
     907#. Author of the plugin
     908#: rootspersona.php
     909msgid "Yuriy Fisenko"
     910msgstr "Юрий Фисенко"
     911
     912#. Author URI of the plugin
     913#: rootspersona.php
     914msgid "https://urryes.ru/"
     915msgstr "https://urryes.ru/"
     916
     917#: admin/class-rootspersona-svg-admin.php:35
     918msgid "SVG Tree Export"
     919msgstr "Экспорт дерева в SVG"
     920
     921#: admin/class-rootspersona-svg-admin.php:36
     922msgid "SVG Export"
     923msgstr "Экспорт SVG"
     924
     925#: admin/class-rootspersona-svg-admin.php:63
     926msgid "Clearing..."
     927msgstr "Очистка..."
     928
     929#: admin/class-rootspersona-svg-admin.php:64
     930msgid "✓ Cache cleared."
     931msgstr "✓ Кэш очищен."
     932
     933#: admin/class-rootspersona-svg-admin.php:65
     934msgid "✗ Error clearing cache."
     935msgstr "✗ Ошибка очистки кэша."
     936
     937#: admin/class-rootspersona-svg-admin.php:66
     938msgid "✗ Network error."
     939msgstr "✗ Ошибка сети."
     940
     941#: admin/class-rootspersona-svg-admin.php:98
     942#: admin/class-rootspersona-svg-settings.php:76
     943msgid "You do not have sufficient permissions."
     944msgstr "У вас недостаточно прав."
     945
     946#: admin/class-rootspersona-svg-admin.php:118
     947msgid "Rootspersona SVG Tree Export"
     948msgstr "Экспорт дерева РутсПерсоны в SVG"
     949
     950#: admin/class-rootspersona-svg-admin.php:127
     951msgid "Batch"
     952msgstr "Группа (Batch)"
     953
     954#: admin/class-rootspersona-svg-admin.php:138
     955msgid "GEDCOM batches — choose the tree you want to export."
     956msgstr "Группы GEDCOM — выберите дерево для экспорта."
     957
     958#: admin/class-rootspersona-svg-admin.php:145
     959msgid "Root Person"
     960msgstr "Корневая персона"
     961
     962#: admin/class-rootspersona-svg-admin.php:149
     963msgid "— Select a person —"
     964msgstr "— Выберите персону —"
     965
     966#. translators: %s: batch id.
     967#: admin/class-rootspersona-svg-admin.php:162
     968#, php-format
     969msgid "No persons found for batch \"%s\". Please upload a GEDCOM first."
     970msgstr "В группе «%s» персоны не найдены. Сначала загрузите GEDCOM-файл."
     971
     972#: admin/class-rootspersona-svg-admin.php:172
     973msgid "Options"
     974msgstr "Настройки"
     975
     976#: admin/class-rootspersona-svg-admin.php:177
     977msgid "Include photos (fallback to gendered avatar if missing)"
     978msgstr "Включать фотографии (если фото нет, используется аватар по полу)"
     979
     980#: admin/class-rootspersona-svg-admin.php:182
     981msgid "Vertical tree (ancestor > person > descendant)"
     982msgstr "Вертикальное дерево (предок > персона > потомок)"
     983
     984#: admin/class-rootspersona-svg-admin.php:186
     985msgid "Large trees (>300 people) may take time. Results are cached for 24 hours."
     986msgstr "Большие деревья (более 300 человек) могут строиться долго. Результаты кэшируются на 24 часа."
     987
     988#: admin/class-rootspersona-svg-admin.php:193
     989msgid "Generate & Download SVG"
     990msgstr "Создать и скачать SVG"
     991
     992#: admin/class-rootspersona-svg-admin.php:198
     993msgid "Preview"
     994msgstr "Предпросмотр"
     995
     996#: admin/class-rootspersona-svg-admin.php:200
     997msgid "Select a person above and click \"Generate & Download SVG\" to see the tree. For preview, use the shortcode on a page."
     998msgstr "Выберите персону выше и нажмите «Создать и скачать SVG», чтобы увидеть дерево. Для предпросмотра используйте шоткод на странице."
     999
     1000#: admin/class-rootspersona-svg-admin.php:206
     1001msgid "Cache Management"
     1002msgstr "Управление кэшем"
     1003
     1004#: admin/class-rootspersona-svg-admin.php:209
     1005msgid "Clear SVG Cache"
     1006msgstr "Очистить кэш SVG"
     1007
     1008#: admin/class-rootspersona-svg-admin.php:316
     1009msgid "Access denied. Insufficient permissions."
     1010msgstr "Доступ запрещён. Недостаточно прав."
     1011
     1012#: admin/class-rootspersona-svg-admin.php:321
     1013msgid "Access denied. Security check failed."
     1014msgstr "Доступ запрещён. Проверка безопасности не пройдена."
     1015
     1016#: admin/class-rootspersona-svg-admin.php:337
     1017msgid "Please select a person."
     1018msgstr "Выберите персону."
     1019
     1020#: admin/class-rootspersona-svg-admin.php:343
     1021msgid "SVG generator file not found."
     1022msgstr "Файл генератора SVG не найден."
     1023
     1024#: admin/class-rootspersona-svg-admin.php:361
     1025msgid "Failed to generate SVG. Check person ID and batch data."
     1026msgstr "Не удалось создать SVG. Проверьте ID персоны и данные группы."
     1027
     1028#: admin/class-rootspersona-svg-admin.php:380
     1029msgid "An error occurred while generating SVG. Please check the error logs."
     1030msgstr "При создании SVG произошла ошибка. Проверьте журналы ошибок."
     1031
     1032#: admin/class-rootspersona-svg-admin.php:381
     1033msgid "SVG Generation Error"
     1034msgstr "Ошибка создания SVG"
     1035
     1036#: admin/class-rootspersona-svg-admin.php:410
     1037msgid "Failed to create SVG directory."
     1038msgstr "Не удалось создать каталог для SVG."
     1039
     1040#: admin/class-rootspersona-svg-admin.php:431
     1041msgid "Failed to write SVG file."
     1042msgstr "Не удалось записать файл SVG."
     1043
     1044#: admin/class-rootspersona-svg-admin.php:541
     1045#: rootspersona.php:2021
     1046msgid "Insufficient permissions."
     1047msgstr "Недостаточно прав."
     1048
     1049#. translators: %d: number of deleted transients.
     1050#: admin/class-rootspersona-svg-admin.php:553
     1051#, php-format
     1052msgid "%d transient deleted."
     1053msgid_plural "%d transients deleted."
     1054msgstr[0] "Удалена %d временная запись."
     1055msgstr[1] "Удалено %d временные записи."
     1056msgstr[2] "Удалено %d временных записей."
     1057
     1058#: admin/class-rootspersona-svg-settings.php:27
     1059msgid "SVG Tree Settings"
     1060msgstr "Настройки SVG-дерева"
     1061
     1062#: admin/class-rootspersona-svg-settings.php:28
     1063msgid "SVG Settings"
     1064msgstr "Настройки SVG"
     1065
     1066#: admin/class-rootspersona-svg-settings.php:98
     1067msgid "Rootspersona SVG Tree Settings"
     1068msgstr "Настройки SVG-дерева РутсПерсоны"
     1069
     1070#: admin/class-rootspersona-svg-settings.php:106
     1071msgid "Male Color"
     1072msgstr "Цвет мужчин"
     1073
     1074#: admin/class-rootspersona-svg-settings.php:111
     1075msgid "Background color for male persons"
     1076msgstr "Цвет фона для персон мужского пола"
     1077
     1078#: admin/class-rootspersona-svg-settings.php:117
     1079msgid "Female Color"
     1080msgstr "Цвет женщин"
     1081
     1082#: admin/class-rootspersona-svg-settings.php:122
     1083msgid "Background color for female persons"
     1084msgstr "Цвет фона для персон женского пола"
     1085
     1086#: admin/class-rootspersona-svg-settings.php:128
     1087msgid "Line Color"
     1088msgstr "Цвет линий"
     1089
     1090#: admin/class-rootspersona-svg-settings.php:133
     1091msgid "Color for connection lines"
     1092msgstr "Цвет соединительных линий"
     1093
     1094#: admin/class-rootspersona-svg-settings.php:139
     1095msgid "Node Width"
     1096msgstr "Ширина блока"
     1097
     1098#: admin/class-rootspersona-svg-settings.php:144
     1099msgid "Width of person nodes in pixels"
     1100msgstr "Ширина блока персоны в пикселях"
     1101
     1102#: admin/class-rootspersona-svg-settings.php:150
     1103msgid "Node Height"
     1104msgstr "Высота блока"
     1105
     1106#: admin/class-rootspersona-svg-settings.php:155
     1107msgid "Height of person nodes in pixels"
     1108msgstr "Высота блока персоны в пикселях"
     1109
     1110#: admin/class-rootspersona-svg-settings.php:161
     1111msgid "Generation Spacing"
     1112msgstr "Расстояние между поколениями"
     1113
     1114#: admin/class-rootspersona-svg-settings.php:166
     1115msgid "Distance between generations in pixels"
     1116msgstr "Расстояние между поколениями в пикселях"
     1117
     1118#: admin/class-rootspersona-svg-settings.php:172
     1119msgid "Sibling Spacing"
     1120msgstr "Расстояние между братьями и сёстрами"
     1121
     1122#: admin/class-rootspersona-svg-settings.php:177
     1123msgid "Distance between siblings in pixels"
     1124msgstr "Расстояние между братьями и сёстрами в пикселях"
     1125
     1126#: admin/class-rootspersona-svg-settings.php:183
     1127msgid "Spouse Spacing"
     1128msgstr "Расстояние между супругами"
     1129
     1130#: admin/class-rootspersona-svg-settings.php:188
     1131msgid "Distance between spouses in pixels"
     1132msgstr "Расстояние между супругами в пикселях"
     1133
     1134#: admin/class-rootspersona-svg-settings.php:194
     1135msgid "Font Family"
     1136msgstr "Шрифт"
     1137
     1138#: admin/class-rootspersona-svg-settings.php:199
     1139msgid "Font family for text (e.g., Tahoma, Arial, sans-serif)"
     1140msgstr "Шрифт текста (например, Tahoma, Arial, sans-serif)"
     1141
     1142#: admin/class-rootspersona-svg-settings.php:205
     1143msgid "Font Size"
     1144msgstr "Размер шрифта"
     1145
     1146#: admin/class-rootspersona-svg-settings.php:210
     1147msgid "Font size for person names in pixels"
     1148msgstr "Размер шрифта имён персон в пикселях"
     1149
     1150#: admin/class-rootspersona-svg-settings.php:216
     1151msgid "Title Font Size"
     1152msgstr "Размер шрифта заголовка"
     1153
     1154#: admin/class-rootspersona-svg-settings.php:221
     1155msgid "Font size for tree title in pixels"
     1156msgstr "Размер шрифта заголовка дерева в пикселях"
     1157
     1158#: admin/class-rootspersona-svg-settings.php:227
     1159msgid "Line Width"
     1160msgstr "Толщина линий"
     1161
     1162#: admin/class-rootspersona-svg-settings.php:232
     1163msgid "Width of connection lines in pixels"
     1164msgstr "Толщина соединительных линий в пикселях"
     1165
     1166#: includes/class-rootspersona-svg-export.php:25
     1167#: includes/class-rootspersona-svg-export.php:87
     1168msgid "Imagick extension is not available."
     1169msgstr "Расширение Imagick недоступно."
     1170
     1171#: includes/class-rootspersona-svg-export.php:71
     1172msgid "Failed to export PNG: "
     1173msgstr "Не удалось экспортировать PNG: "
     1174
     1175#: includes/class-rootspersona-svg-export.php:142
     1176msgid "Failed to export PDF: "
     1177msgstr "Не удалось экспортировать PDF: "
     1178
     1179#: includes/class-rootspersona-svg-shortcode.php:70
     1180msgid "Please specify person_id parameter."
     1181msgstr "Укажите параметр person_id."
     1182
     1183#: includes/class-rootspersona-svg-shortcode.php:119
     1184msgid "Loading family tree..."
     1185msgstr "Загрузка родословного дерева..."
     1186
     1187#: includes/class-rootspersona-svg-shortcode.php:138
     1188msgid "SVG generator not found."
     1189msgstr "Генератор SVG не найден."
     1190
     1191#: includes/class-rootspersona-svg-shortcode.php:147
     1192#: rootspersona.php:2060
     1193msgid "Failed to generate SVG tree."
     1194msgstr "Не удалось создать SVG-дерево."
     1195
     1196#: includes/class-rootspersona-svg-shortcode.php:172
     1197#: rootspersona.php:2071
     1198msgid "An error occurred while generating the tree."
     1199msgstr "При построении дерева произошла ошибка."
     1200
     1201#: php/class-RP-Gedcom-Import-Session.php:88
     1202msgid "Could not store the uploaded GEDCOM file for processing."
     1203msgstr "Не удалось сохранить загруженный GEDCOM-файл для обработки."
     1204
     1205#: php/class-RP-Gedcom-Import-Session.php:130
     1206msgid "Import session expired or not found. Please upload the file again."
     1207msgstr "Сеанс импорта истёк или не найден. Загрузите файл заново."
     1208
     1209#: php/class-RP-Gedcom-Import-Session.php:155
     1210msgid "Details were saved to the RootsPersona log: open the Upload GEDCOM File page and use \"Download log\"."
     1211msgstr "Подробности сохранены в журнале РутсПерсоны: откройте страницу «Загрузить GEDCOM-файл» и нажмите «Скачать журнал»."
     1212
     1213#: php/class-RP-Log.php:229
     1214msgid "Import log"
     1215msgstr "Журнал импорта"
     1216
     1217#: php/class-RP-Log.php:230
     1218msgid "If an import fails, download this file and send it to the plugin developer. It records each step and the exact database error."
     1219msgstr "Если импорт завершился ошибкой, скачайте этот файл и отправьте его разработчику плагина. В нём записан каждый шаг и точная ошибка базы данных."
     1220
     1221#: php/class-RP-Log.php:231
     1222msgid "File:"
     1223msgstr "Файл:"
     1224
     1225#: php/class-RP-Log.php:234
     1226msgid "Download log"
     1227msgstr "Скачать журнал"
     1228
     1229#: php/class-RP-Log.php:235
     1230msgid "Clear log"
     1231msgstr "Очистить журнал"
     1232
     1233#: php/class-RP-Log.php:237
     1234msgid "The log is empty — nothing has gone wrong yet."
     1235msgstr "Журнал пуст — ничего плохого пока не случилось."
     1236
     1237#: php/class-RP-Log.php:240
     1238msgid "The log may contain short fragments of your family data (names, notes). Send it only to the developer."
     1239msgstr "Журнал может содержать короткие фрагменты данных вашей семьи (имена, заметки). Отправляйте его только разработчику."
     1240
     1241#: php/class-RP-Log.php:250
     1242#: php/class-RP-Log.php:275
     1243#: rootspersona.php:512
     1244#: rootspersona.php:644
     1245#: rootspersona.php:805
     1246#: rootspersona.php:831
     1247#: rootspersona.php:914
     1248#: rootspersona.php:970
     1249msgid "You do not have permission to perform this action."
     1250msgstr "У вас нет прав на это действие."
     1251
     1252#: php/class-RP-Log.php:255
     1253msgid "The log is empty."
     1254msgstr "Журнал пуст."
     1255
     1256#: php/class-RP-Persona-Site-Mender.php:128
     1257msgid "Deleted orphaned page with no reference in rootspersona_source."
     1258msgstr "Удалена потерянная страница без связи в rootspersona_source."
     1259
     1260#: php/class-RP-Persona-Site-Mender.php:131
     1261msgid "No reference in rootspersona_source."
     1262msgstr "Нет связи в rootspersona_source."
     1263
     1264#. translators: 1: page id, 2: issue description.
     1265#: php/class-RP-Persona-Site-Mender.php:158
     1266#: php/class-RP-Persona-Site-Mender.php:222
     1267#: php/class-RP-Persona-Site-Mender.php:284
     1268#, php-format
     1269msgid "Page %1$s: %2$s"
     1270msgstr "Страница %1$s: %2$s"
     1271
     1272#: php/class-RP-Persona-Site-Mender.php:435
     1273#: php/pages/class-RP-Tools-Page-Builder.php:90
     1274#: rootspersona.php:886
     1275msgid "Sync page titles from database"
     1276msgstr "Обновить заголовки страниц из базы данных"
     1277
     1278#: php/class-RP-Persona-Site-Mender.php:436
     1279msgid "This updates each persona page title and the expected_name meta field to match the primary name currently stored in the genealogy database (for example after re-uploading a GEDCOM with the same Batch Id). It does not change shortcode attributes (e.g. picfile links)."
     1280msgstr "Обновляет заголовок каждой страницы персоны и мета-поле expected_name в соответствии с основным именем, сохранённым сейчас в генеалогической базе данных (например, после повторной загрузки GEDCOM с тем же Batch Id). Атрибуты шоткода (например, ссылки picfile) не изменяются."
     1281
     1282#: php/class-RP-Persona-Site-Mender.php:439
     1283msgid "No persons with linked pages found for this batch."
     1284msgstr "Для этой группы персоны со связанными страницами не найдены."
     1285
     1286#: php/class-RP-Persona-Site-Mender.php:440
     1287#: php/class-RP-Persona-Site-Mender.php:489
     1288msgid "Return to Tools"
     1289msgstr "Вернуться в «Инструменты»"
     1290
     1291#. translators: 1: page ID, 2: old title, 3: new title.
     1292#: php/class-RP-Persona-Site-Mender.php:474
     1293#, php-format
     1294msgid "Updated page %1$d: \"%2$s\" → \"%3$s\"."
     1295msgstr "Обновлена страница %1$d: «%2$s» → «%3$s»."
     1296
     1297#. translators: 1: number of pages updated, 2: number unchanged.
     1298#: php/class-RP-Persona-Site-Mender.php:485
     1299#, php-format
     1300msgid "Finished. Updated: %1$d. Unchanged: %2$d."
     1301msgstr "Готово. Обновлено: %1$d. Без изменений: %2$d."
     1302
     1303#: php/dao/sql/class-RP-Connection-Factory.php:19
     1304msgid "WordPress database layer is not available."
     1305msgstr "Слой базы данных WordPress недоступен."
     1306
     1307#: php/pages/class-RP-Add-Page-Builder.php:27
     1308msgid "No one is waiting for a new page."
     1309msgstr "Нет персон, ожидающих новой страницы."
     1310
     1311#: php/pages/class-RP-Add-Page-Builder.php:28
     1312msgid "This list only shows people without a linked WordPress page. If you re-uploaded a GEDCOM, the database was still updated for the same Batch Id — use Tools → Sync page titles from database to refresh page titles."
     1313msgstr "В этом списке только персоны без связанной страницы WordPress. Если вы повторно загрузили GEDCOM, база данных для того же Batch Id всё равно обновлена — чтобы обновить заголовки страниц, используйте Инструменты → «Обновить заголовки страниц из базы данных»."
     1314
     1315#: php/pages/class-RP-Add-Page-Builder.php:29
     1316msgid "Open sync now"
     1317msgstr "Открыть обновление"
     1318
     1319#: php/pages/class-RP-Edit-Page-Builder.php:27
     1320msgid "View"
     1321msgstr "Просмотр"
     1322
     1323#: php/pages/class-RP-Option-Page-Builder.php:32
     1324#: php/pages/class-RP-Tools-Page-Builder.php:28
     1325msgid "Donate"
     1326msgstr "Поддержать"
     1327
     1328#: php/pages/class-RP-Tools-Page-Builder.php:60
     1329msgid "Upload or re-upload a GEDCOM file. Use the same Batch Id as your existing tree so records are merged into the database (duplicates update). After a re-upload, open “Sync page titles from database” if page titles are out of date."
     1330msgstr "Загрузите или повторно загрузите GEDCOM-файл. Используйте тот же Batch Id, что и у существующего дерева, — тогда записи будут объединены в базе данных (дубликаты обновляются). После повторной загрузки откройте «Обновить заголовки страниц из базы данных», если заголовки страниц устарели."
     1331
     1332#: php/pages/class-RP-Tools-Page-Builder.php:74
     1333msgid "Lists only people who do not have a WordPress page yet. If everyone already has a page, the list is empty — that is normal. Re-uploading GEDCOM still updates the database; use “Sync page titles from database” to refresh titles."
     1334msgstr "Показывает только персон, у которых ещё нет страницы WordPress. Если у всех уже есть страницы, список пуст — это нормально. Повторная загрузка GEDCOM по-прежнему обновляет базу данных; чтобы обновить заголовки, используйте «Обновить заголовки страниц из базы данных»."
     1335
     1336#: php/pages/class-RP-Tools-Page-Builder.php:93
     1337msgid "Run after re-uploading a GEDCOM (same Batch Id): updates each persona page title and expected_name meta from the genealogy database. Choose batch via “Process” if not 1."
     1338msgstr "Запускайте после повторной загрузки GEDCOM (тот же Batch Id): обновляет заголовок каждой страницы персоны и мета-поле expected_name из генеалогической базы данных. Если группа не 1, выберите её через «Process»."
     1339
     1340#. translators: 1: context (e.g. persona), 2: shortcode name. Keep {{1}} and {{2}} as-is in translations.
     1341#: php/pages/class-RP-Tools-Page-Builder.php:164
     1342msgid "Identify orphaned {{1}} pages. Includes all pages with {{2}} shortcode and no reference in the database, or reference in the database with no corresponding page."
     1343msgstr "Найти потерянные страницы {{1}}. Включает все страницы с шоткодом {{2}} без связи в базе данных, а также связи в базе данных без соответствующей страницы."
     1344
     1345#. translators: Keep {{1}} in translation; it is replaced with plugin name.
     1346#: php/pages/class-RP-Tools-Page-Builder.php:186
     1347msgid "Perform a bulk deletion of all {{1}} and evidence pages. You will be prompted before the data is deleted."
     1348msgstr "Массовое удаление всех страниц {{1}} и страниц данных. Перед удалением будет запрошено подтверждение."
     1349
     1350#: php/pages/class-RP-Upload-Page-Builder.php:66
     1351msgid "Step time (sec):"
     1352msgstr "Время шага (сек):"
     1353
     1354#: php/pages/class-RP-Upload-Page-Builder.php:72
     1355msgid "The file is processed in short bursts of this many seconds each, so large files no longer time out. Lower it on slower hosting, raise it to finish faster."
     1356msgstr "Файл обрабатывается короткими порциями по указанному числу секунд, поэтому большие файлы больше не вызывают тайм-аут. Уменьшите значение на медленном хостинге, увеличьте — чтобы закончить быстрее."
     1357
     1358#: php/pages/class-RP-Upload-Page-Builder.php:140
     1359msgid "Uploading and processing your GEDCOM file in short steps so the page never times out."
     1360msgstr "Загрузка и обработка GEDCOM-файла выполняется короткими шагами, чтобы страница не прерывалась по тайм-ауту."
     1361
     1362#: php/pages/class-RP-Upload-Page-Builder.php:145
     1363msgid "Starting…"
     1364msgstr "Запуск…"
     1365
     1366#. translators: %d: seconds spent server-side per processing step.
     1367#: php/pages/class-RP-Upload-Page-Builder.php:149
     1368#, php-format
     1369msgid "Step time: %d sec."
     1370msgstr "Время шага: %d сек."
     1371
     1372#: php/pages/panels/class-RP-Header-Panel-Creator.php:310
     1373msgid "Caption (native description)"
     1374msgstr "Подпись (нативное описание)"
     1375
     1376#: php/pages/panels/class-RP-Header-Panel-Creator.php:313
     1377#: php/pages/panels/class-RP-Picture-Panel-Creator.php:227
     1378msgid "Description"
     1379msgstr "Описание"
     1380
     1381#: php/pages/panels/class-RP-Header-Panel-Creator.php:314
     1382#: php/pages/panels/class-RP-Picture-Panel-Creator.php:228
     1383msgid "Caption"
     1384msgstr "Подпись"
     1385
     1386#: rootspersona.php:51
     1387msgid "This RootsPersona file was not loaded: another copy of RootsPersona already loaded the same PHP code. Deactivate the other plugin, then enable only one."
     1388msgstr "Этот файл РутсПерсоны не загружен: другая копия РутсПерсоны уже загрузила тот же PHP-код. Отключите другой плагин и оставьте включённым только один."
     1389
     1390#: rootspersona.php:77
     1391msgid "This RootsPersona file was not loaded: another copy already loaded the same PHP code. Deactivate the other plugin."
     1392msgstr "Этот файл РутсПерсоны не загружен: другая копия уже загрузила тот же PHP-код. Отключите другой плагин."
     1393
     1394#: rootspersona.php:547
     1395#: rootspersona.php:587
     1396#: rootspersona.php:659
     1397#: rootspersona.php:841
     1398#: rootspersona.php:926
     1399#: rootspersona.php:1048
     1400msgid "Security check failed. Please refresh and try again."
     1401msgstr "Проверка безопасности не пройдена. Обновите страницу и повторите попытку."
     1402
     1403#. translators: %s: missing personId.
     1404#: rootspersona.php:628
     1405#, php-format
     1406msgid "Missing personId: %s"
     1407msgstr "Отсутствует personId: %s"
     1408
     1409#: rootspersona.php:743
     1410msgid "Unknown or unsupported action. Nothing was saved."
     1411msgstr "Неизвестное или неподдерживаемое действие. Ничего не сохранено."
     1412
     1413#: rootspersona.php:881
     1414msgid "GEDCOM uploaded: new or changed records were merged into the database for this batch."
     1415msgstr "GEDCOM-файл загружен: новые и изменённые записи объединены с базой данных для этой группы."
     1416
     1417#: rootspersona.php:883
     1418msgid "The list below only adds WordPress pages for people who do not have one yet; it may be empty if everyone already has a page."
     1419msgstr "Список ниже добавляет страницы WordPress только для персон, у которых их ещё нет; он может быть пустым, если у всех уже есть страницы."
     1420
     1421#: rootspersona.php:885
     1422msgid "To refresh page titles from the updated database, use"
     1423msgstr "Чтобы обновить заголовки страниц из обновлённой базы данных, используйте"
     1424
     1425#: rootspersona.php:887
     1426msgid "Tools menu"
     1427msgstr "меню «Инструменты»"
     1428
     1429#: rootspersona.php:976
     1430msgid "Security check failed. Please refresh the Tools page and try again."
     1431msgstr "Проверка безопасности не пройдена. Обновите страницу «Инструменты» и повторите попытку."
     1432
     1433#. translators: %s: unsupported action name.
     1434#: rootspersona.php:1014
     1435#, php-format
     1436msgid "Action %s not supported."
     1437msgstr "Действие %s не поддерживается."
     1438
     1439#: rootspersona.php:2018
     1440msgid "You must be logged in to view this content."
     1441msgstr "Чтобы просмотреть этот материал, нужно войти в систему."
     1442
     1443#: rootspersona.php:2048
     1444msgid "Person ID is required."
     1445msgstr "Требуется ID персоны."
     1446
     1447#: rootspersona.php:1974
     1448msgid "The RootsPersona SVG tree is disabled (experimental feature). To enable it, set the ROOTSPERSONA_ENABLE_SVG constant to true in wp-config.php."
     1449msgstr "SVG-дерево РутсПерсоны отключено (экспериментальная функция). Чтобы включить его, задайте константе ROOTSPERSONA_ENABLE_SVG значение true в wp-config.php."
     1450
     1451#: php/pages/class-RP-Option-Page-Builder.php:142
     1452msgid "Persona page template"
     1453msgstr "Шаблон страницы персоны"
     1454
     1455#: php/pages/class-RP-Option-Page-Builder.php:149
     1456msgid "Layout of the persona page. Archive card and Chronicle take their colours and fonts from the theme (--rp-* CSS variables) and hide sections that have no data. A page can override it with the template attribute of the shortcode."
     1457msgstr "Раскладка страницы персоны. «Карточка из архива» и «Летопись» берут цвета и шрифты из темы (CSS-переменные --rp-*) и скрывают разделы, для которых нет данных. Для отдельной страницы шаблон можно задать атрибутом template шорткода."
     1458
     1459#: php/templates/class-RP-Persona-Templates.php:29
     1460msgid "Classic (panels)"
     1461msgstr "Классический (панели)"
     1462
     1463#: php/templates/class-RP-Persona-Templates.php:30
     1464msgid "Archive card"
     1465msgstr "Карточка из архива"
     1466
     1467#: php/templates/class-RP-Persona-Templates.php:31
     1468msgid "Chronicle (line of generations)"
     1469msgstr "Летопись (линия поколений)"
     1470
     1471#: php/templates/class-RP-Persona-Templates.php:111
     1472msgid "Information about this person is private."
     1473msgstr "Сведения об этом человеке скрыты."
     1474
     1475#: php/templates/class-RP-Persona-Templates.php:167
     1476msgid "Unknown"
     1477msgstr "Неизвестно"
     1478
     1479#: php/templates/class-RP-Persona-Templates.php:198
     1480msgid "Wife"
     1481msgstr "Жена"
     1482
     1483#: php/templates/class-RP-Persona-Templates.php:201
     1484msgid "Husband"
     1485msgstr "Муж"
     1486
     1487#: php/templates/class-RP-Persona-Templates.php:203
     1488#: php/templates/class-RP-Persona-Templates.php:356
     1489msgid "Spouse"
     1490msgstr "Супруг(а)"
     1491
     1492#: php/templates/class-RP-Persona-Templates.php:233
     1493msgid "Born"
     1494msgstr "Родился(-ась)"
     1495
     1496#: php/templates/class-RP-Persona-Templates.php:241
     1497msgid "Died"
     1498msgstr "Ушёл(-ла) из жизни"
     1499
     1500#: php/templates/class-RP-Persona-Templates.php:258
     1501msgid "Occupation"
     1502msgstr "Род занятий"
     1503
     1504#: php/templates/class-RP-Persona-Templates.php:261
     1505msgid "Places of residence"
     1506msgstr "Места проживания"
     1507
     1508#: php/templates/class-RP-Persona-Templates.php:321
     1509#: php/templates/class-RP-Persona-Templates.php:363
     1510#: php/templates/class-RP-Persona-Templates.php:467
     1511#: php/templates/class-RP-Persona-Templates.php:619
     1512msgid "Children"
     1513msgstr "Дети"
     1514
     1515#: php/templates/class-RP-Persona-Templates.php:346
     1516#: php/templates/class-RP-Persona-Templates.php:538
     1517msgid "Parents"
     1518msgstr "Родители"
     1519
     1520#. translators: %s: marriage date and/or place.
     1521#: php/templates/class-RP-Persona-Templates.php:671
     1522#, php-format
     1523msgid "Married: %s"
     1524msgstr "Брак: %s"
     1525
     1526#: php/templates/class-RP-Persona-Templates.php:356
     1527msgid "Spouses"
     1528msgstr "Супруги"
     1529
     1530#: php/templates/class-RP-Persona-Templates.php:370
     1531#: php/templates/class-RP-Persona-Templates.php:570
     1532msgid "Brothers and sisters"
     1533msgstr "Братья и сёстры"
     1534
     1535#: php/templates/class-RP-Persona-Templates.php:373
     1536msgid "Family"
     1537msgstr "Семья"
     1538
     1539#: php/templates/class-RP-Persona-Templates.php:377
     1540msgid "Place in the family tree"
     1541msgstr "Место в родословной"
     1542
     1543#: php/templates/class-RP-Persona-Templates.php:380
     1544#: php/templates/class-RP-Persona-Templates.php:634
     1545msgid "Descendants"
     1546msgstr "Потомки"
     1547
     1548#: php/templates/class-RP-Persona-Templates.php:385
     1549#: php/templates/class-RP-Persona-Templates.php:585
     1550msgid "Photos"
     1551msgstr "Фотографии"
     1552
     1553#: php/templates/class-RP-Persona-Templates.php:424
     1554#: php/templates/class-RP-Persona-Templates.php:426
     1555#: php/templates/class-RP-Persona-Templates.php:525
     1556msgid "Grandfather"
     1557msgstr "Дедушка"
     1558
     1559#: php/templates/class-RP-Persona-Templates.php:425
     1560#: php/templates/class-RP-Persona-Templates.php:427
     1561#: php/templates/class-RP-Persona-Templates.php:525
     1562msgid "Grandmother"
     1563msgstr "Бабушка"
     1564
     1565#: php/templates/class-RP-Persona-Templates.php:453
     1566msgid "Child"
     1567msgstr "Ребёнок"
     1568
     1569#: php/templates/class-RP-Persona-Templates.php:468
     1570msgid "Grandchildren"
     1571msgstr "Внуки"
     1572
     1573#: php/templates/class-RP-Persona-Templates.php:469
     1574msgid "Great-grandchildren"
     1575msgstr "Правнуки"
     1576
     1577#: php/templates/class-RP-Persona-Templates.php:529
     1578msgid "Grandparents"
     1579msgstr "Бабушки и дедушки"
     1580
     1581#: php/templates/class-RP-Persona-Templates.php:592
     1582msgid "No further details are recorded yet."
     1583msgstr "Других сведений пока нет."
     1584
     1585#. translators: %s: name of the other parent.
     1586#: php/templates/class-RP-Persona-Templates.php:613
     1587#, php-format
     1588msgid "With %s"
     1589msgstr "С %s"
     1590
     1591#: php/templates/class-RP-Persona-View.php:201
     1592#: rootspersona.php:1148
     1593#: rootspersona.php:1173
     1594msgid "Private"
     1595msgstr "Скрыто"
     1596
     1597#. translators: date range of uncertainty, e.g. "between 1852 and 1854".
     1598#: php/class-RP-Date.php:82
     1599#, php-format
     1600msgid "between %1$s and %2$s"
     1601msgstr "между %1$s и %2$s"
     1602
     1603#. translators: period, e.g. "from 1846 to 1890".
     1604#: php/class-RP-Date.php:86
     1605#, php-format
     1606msgid "from %1$s to %2$s"
     1607msgstr "с %1$s по %2$s"
     1608
     1609#. translators: approximate date, e.g. "about 1890".
     1610#: php/class-RP-Date.php:90
     1611#, php-format
     1612msgid "about %s"
     1613msgstr "около %s"
     1614
     1615#. translators: e.g. "before 1900".
     1616#: php/class-RP-Date.php:96
     1617#, php-format
     1618msgid "before %s"
     1619msgstr "до %s"
     1620
     1621#. translators: e.g. "after 1900".
     1622#: php/class-RP-Date.php:98
     1623#, php-format
     1624msgid "after %s"
     1625msgstr "после %s"
     1626
     1627#: php/templates/class-RP-Persona-View.php:265
     1628#: php/templates/class-RP-Persona-View.php:266
     1629msgid "Burial"
     1630msgstr "Погребение"
     1631
     1632#: php/templates/class-RP-Persona-View.php:267
     1633#: php/templates/class-RP-Persona-View.php:268
     1634msgid "Baptism"
     1635msgstr "Крещение"
     1636
     1637#: php/templates/class-RP-Persona-View.php:269
     1638msgid "Education"
     1639msgstr "Образование"
     1640
     1641#: php/templates/class-RP-Persona-View.php:270
     1642msgid "Adoption"
     1643msgstr "Усыновление"
     1644
     1645#: php/templates/class-RP-Persona-View.php:271
     1646msgid "Emigration"
     1647msgstr "Эмиграция"
     1648
     1649#: php/templates/class-RP-Persona-View.php:272
     1650msgid "Immigration"
     1651msgstr "Иммиграция"
     1652
     1653#: php/templates/class-RP-Persona-View.php:273
     1654msgid "Military service"
     1655msgstr "Военная служба"
     1656
     1657#: php/templates/class-RP-Persona-View.php:274
     1658msgid "Religion"
     1659msgstr "Вероисповедание"
     1660
     1661#. translators: %d: number of grandchildren.
     1662#: php/templates/class-RP-Persona-Templates.php:628
     1663#, php-format
     1664msgid "%d grandchild"
     1665msgid_plural "%d grandchildren"
     1666msgstr[0] "%d внук"
     1667msgstr[1] "%d внука"
     1668msgstr[2] "%d внуков"
     1669
     1670#. translators: %d: number of great-grandchildren.
     1671#: php/templates/class-RP-Persona-Templates.php:632
     1672#, php-format
     1673msgid "%d great-grandchild"
     1674msgid_plural "%d great-grandchildren"
     1675msgstr[0] "%d правнук"
     1676msgstr[1] "%d правнука"
     1677msgstr[2] "%d правнуков"
     1678
     1679#: php/class-RP-Persona-Helper.php:784
     1680msgid "« first"
     1681msgstr "« первая"
     1682
     1683#: php/class-RP-Persona-Helper.php:785
     1684msgid "‹ prev"
     1685msgstr "‹ назад"
     1686
     1687#: php/class-RP-Persona-Helper.php:786
     1688msgid "next ›"
     1689msgstr "вперёд ›"
     1690
     1691#: php/class-RP-Persona-Helper.php:787
     1692msgid "last »"
     1693msgstr "последняя »"
     1694
     1695#: php/pages/class-RP-Option-Page-Builder.php:89
     1696msgid "People without dates"
     1697msgstr "Люди без дат"
     1698
     1699#: php/pages/class-RP-Option-Page-Builder.php:91
     1700msgid "Treat as living"
     1701msgstr "Считать живыми"
     1702
     1703#: php/pages/class-RP-Option-Page-Builder.php:92
     1704msgid "Treat as deceased"
     1705msgstr "Считать умершими"
     1706
     1707#: php/pages/class-RP-Option-Page-Builder.php:93
     1708msgid "When neither the person nor the parents, spouses or children have dates that settle it. \"Treat as living\" is the safe choice: living relatives are often entered without dates."
     1709msgstr "Когда ни у самого человека, ни у его родителей, супругов и детей нет дат, по которым это можно определить. «Считать живыми» — безопасный выбор: живых родственников часто вносят без дат."
     1710
     1711#: php/pages/class-RP-Option-Page-Builder.php:97
     1712msgid "Page for private people"
     1713msgstr "Страница для скрытых персон"
     1714
     1715#: php/pages/class-RP-Option-Page-Builder.php:103
     1716msgid "— none: show \"Private\" on the person's page —"
     1717msgstr "— нет: показывать «Скрыто» на странице человека —"
     1718
     1719#: php/pages/class-RP-Option-Page-Builder.php:109
     1720msgid "Visitors who may not see a person are sent to this page, for example a page saying \"Closed. Private access.\""
     1721msgstr "Посетители, которым человек скрыт, будут перенаправлены на эту страницу — например, с текстом «Закрыто. Приватный доступ»."
     1722
     1723#. translators: 1: pages processed so far, 2: people selected.
     1724#: rootspersona.php:1476
     1725#, php-format
     1726msgid "Creating pages: %1$s of %2$s…"
     1727msgstr "Создание страниц: %1$s из %2$s…"
     1728
     1729#. translators: 1: pages created, 2: errors.
     1730#: rootspersona.php:1478
     1731#, php-format
     1732msgid "Done. Pages created: %1$s, errors: %2$s."
     1733msgstr "Готово. Создано страниц: %1$s, ошибок: %2$s."
     1734
     1735#. translators: 1: pages created, 2: people selected.
     1736#: rootspersona.php:1480
     1737#, php-format
     1738msgid "Stopped after %1$s of %2$s pages."
     1739msgstr "Остановлено: создано %1$s из %2$s страниц."
     1740
     1741#: rootspersona.php:1481
     1742msgid "The server did not answer. Check the import log."
     1743msgstr "Сервер не ответил. Проверьте журнал импорта."
     1744
     1745#: php/templates/class-RP-Persona-Templates.php:228
     1746msgctxt "fact label, female person"
     1747msgid "Born"
     1748msgstr "Родилась"
     1749
     1750#: php/templates/class-RP-Persona-Templates.php:231
     1751msgctxt "fact label, male person"
     1752msgid "Born"
     1753msgstr "Родился"
     1754
     1755#: php/templates/class-RP-Persona-Templates.php:236
     1756msgctxt "fact label, female person"
     1757msgid "Died"
     1758msgstr "Ушла из жизни"
     1759
     1760#: php/templates/class-RP-Persona-Templates.php:239
     1761msgctxt "fact label, male person"
     1762msgid "Died"
     1763msgstr "Ушёл из жизни"
     1764
     1765#. translators: date calculated from other data, e.g. "calculated 1890".
     1766#: php/class-RP-Date.php:92
     1767#, php-format
     1768msgid "calculated %s"
     1769msgstr "по расчёту %s"
     1770
     1771#. translators: estimated date, e.g. "estimated 1890".
     1772#: php/class-RP-Date.php:94
     1773#, php-format
     1774msgid "estimated %s"
     1775msgstr "предположительно %s"
     1776
     1777#. translators: open period start, e.g. "from 1846".
     1778#: php/class-RP-Date.php:100
     1779#, php-format
     1780msgid "from %s"
     1781msgstr "с %s"
     1782
     1783#. translators: open period end, e.g. "until 1890".
     1784#: php/class-RP-Date.php:102
     1785#, php-format
     1786msgid "until %s"
     1787msgstr "по %s"
     1788
     1789#. translators: "Old Style" — a date in the Julian calendar.
     1790#: php/class-RP-Date.php:142
     1791#: php/class-RP-Date.php:164
     1792msgid "O.S."
     1793msgstr "ст. ст."
     1794
     1795#: php/class-RP-Date.php:155
     1796msgid "BC"
     1797msgstr "до н. э."
     1798
     1799#: php/pages/panels/class-RP-Descendancy-Panel-Creator.php:52
     1800#: php/pages/panels/class-RP-Descendancy-Panel-Creator.php:98
     1801#: php/pages/panels/class-RP-Header-Panel-Creator.php:194
     1802#: php/pages/panels/class-RP-Header-Panel-Creator.php:252
     1803msgctxt "born: short label before a date"
     1804msgid "b:"
     1805msgstr "р."
     1806
     1807#: php/pages/panels/class-RP-Descendancy-Panel-Creator.php:63
     1808#: php/pages/panels/class-RP-Descendancy-Panel-Creator.php:109
     1809#: php/pages/panels/class-RP-Header-Panel-Creator.php:204
     1810#: php/pages/panels/class-RP-Header-Panel-Creator.php:262
     1811msgctxt "died: short label before a date"
     1812msgid "d:"
     1813msgstr "ум."
     1814
     1815#. translators: %s: surname or full name taken at marriage.
     1816#: php/class-RP-Persona-Helper.php:393
     1817#, php-format
     1818msgid "married name %s"
     1819msgstr "в браке %s"
     1820
     1821#. translators: %s: surname or full name at birth.
     1822#: php/class-RP-Persona-Helper.php:398
     1823#, php-format
     1824msgid "née %s"
     1825msgstr "урожд. %s"
     1826
     1827#. translators: %s: another name of the person.
     1828#: php/class-RP-Persona-Helper.php:402
     1829#, php-format
     1830msgid "also known as %s"
     1831msgstr "также %s"
     1832
     1833#. translators: %s: nickname.
     1834#: php/class-RP-Persona-Helper.php:408
     1835#, php-format
     1836msgid "nickname “%s”"
     1837msgstr "прозвище «%s»"
     1838
     1839#: php/pages/panels/class-RP-Group-Sheet-Panel-Creator.php:195
     1840msgid "Divorce"
     1841msgstr "Развод"
     1842
     1843#. translators: %s: divorce date.
     1844#: php/templates/class-RP-Persona-Templates.php:676
     1845#, php-format
     1846msgid "divorced %s"
     1847msgstr "развод %s"
     1848
     1849#: php/templates/class-RP-Persona-Templates.php:677
     1850msgid "divorced"
     1851msgstr "в разводе"
     1852
     1853#: settings
     1854msgid "Accent (links, markers, years)"
     1855msgstr "Акцент (ссылки, маркеры, годы)"
     1856
     1857#: settings
     1858msgid "Text"
     1859msgstr "Текст"
     1860
     1861#: settings
     1862msgid "Secondary text"
     1863msgstr "Второстепенный текст"
     1864
     1865#: settings
     1866msgid "Card background"
     1867msgstr "Фон карточки"
     1868
     1869#: settings
     1870msgid "Section background"
     1871msgstr "Фон раздела"
     1872
     1873#: settings
     1874msgid "Lines and borders"
     1875msgstr "Линии и рамки"
     1876
     1877#: settings
     1878msgid "Avatar of men"
     1879msgstr "Аватар мужчин"
     1880
     1881#: settings
     1882msgid "Avatar of women"
     1883msgstr "Аватар женщин"
     1884
     1885#: settings
     1886msgid "Do not set — the theme provides the colours"
     1887msgstr "Не задавать — цвета берёт тема"
     1888
     1889#: settings
     1890msgid "Sepia"
     1891msgstr "Сепия"
     1892
     1893#: settings
     1894msgid "Neutral"
     1895msgstr "Нейтральный"
     1896
     1897#: settings
     1898msgid "Dark"
     1899msgstr "Тёмный"
     1900
     1901#: settings
     1902msgid "My own colours"
     1903msgstr "Свои цвета"
     1904
     1905#: settings
     1906msgid "Colour set"
     1907msgstr "Набор цветов"
     1908
     1909#: settings
     1910msgid "Colours of the Archive card and Chronicle templates. \"Do not set\" leaves them to the theme (recommended when the theme defines --rp-* variables, e.g. with a dark mode). Choosing a set fills the colours below; changing any colour switches to \"My own colours\"."
     1911msgstr "Цвета шаблонов «Карточка из архива» и «Летопись». «Не задавать» оставляет их теме (рекомендуется, если тема сама задаёт переменные --rp-*, например с тёмным режимом). Выбор набора заполняет цвета ниже; изменение любого цвета переключает на «Свои цвета»."
     1912
     1913#: settings
     1914msgid "Colours"
     1915msgstr "Цвета"
     1916
     1917#: settings
     1918msgid "Preview (without the theme's own colours)"
     1919msgstr "Предпросмотр (без собственных цветов темы)"
     1920
     1921#: settings
     1922msgid "Peter Smith"
     1923msgstr "Пётр Смирнов"
     1924
     1925#: settings
     1926msgid "Mary Smith"
     1927msgstr "Мария Смирнова"
     1928
     1929#: settings
     1930msgid "The colours below apply to the Classic template only."
     1931msgstr "Цвета ниже действуют только для классического шаблона."
     1932
     1933#: settings
     1934msgid "Choose from Media Library"
     1935msgstr "Выбрать из медиатеки"
     1936
     1937#: settings
     1938msgid "Remove"
     1939msgstr "Убрать"
     1940
     1941#: settings
     1942msgid "Create Page or Post"
     1943msgstr "Создавать страницу или запись"
     1944
     1945#: settings
     1946msgid "Post"
     1947msgstr "Запись"
     1948
     1949#: settings
     1950msgid "Rootspersona can create a Page or Post"
     1951msgstr "Rootspersona может создавать страницу или запись"
     1952
     1953#: settings
     1954msgid "Choose image"
     1955msgstr "Выбор изображения"
     1956
     1957#: settings
     1958msgid "Use this image"
     1959msgstr "Использовать это изображение"
     1960
     1961#: settings
     1962msgid "Privacy"
     1963msgstr "Приватность"
     1964
     1965#: settings
     1966msgid "Persona page"
     1967msgstr "Страница персоны"
     1968
     1969#: settings
     1970msgid "Pages and other"
     1971msgstr "Страницы и прочее"
     1972
     1973#: settings
     1974msgid "Settings sections"
     1975msgstr "Разделы настроек"
     1976
     1977#: settings
     1978msgid "Version %s"
     1979msgstr "Версия %s"
     1980
     1981#: settings
     1982msgid "A fan of RootsPersona? Consider making a donation!"
     1983msgstr "Нравится RootsPersona? Поддержите разработку!"
     1984
     1985#: settings
     1986msgid "Exclude from site"
     1987msgstr "Исключить с сайта"
     1988
     1989#: settings
     1990msgid "Dates and places"
     1991msgstr "Даты и места"
     1992
     1993#: settings
     1994msgid "Show dates"
     1995msgstr "Показывать даты"
     1996
     1997#: settings
     1998msgid "Show places"
     1999msgstr "Показывать места"
     2000
     2001#: settings
     2002msgid "Untick to hide them on all persona pages, for example for privacy."
     2003msgstr "Снимите галочку, чтобы скрыть их на всех страницах персон, например ради приватности."
     2004
     2005#: settings
     2006msgid "Show on the page"
     2007msgstr "Показывать на странице"
     2008
     2009#: settings
     2010msgid "the classic header keeps its picture"
     2011msgstr "в классической шапке фото остаётся"
     2012
     2013#: settings
     2014msgid "Classic page panels"
     2015msgstr "Панели классической страницы"
     2016
     2017#: settings
     2018msgid "Header"
     2019msgstr "Шапка"
     2020
     2021#: settings
     2022msgid "Family where the person is a child"
     2023msgstr "Семья, где человек — ребёнок"
     2024
     2025#: settings
     2026msgid "Families where the person is a spouse"
     2027msgstr "Семьи, где человек — супруг(а)"
     2028
     2029#: settings
     2030msgid "Evidence (sources)"
     2031msgstr "Свидетельства (источники)"
     2032
     2033#: settings
     2034msgid "Silhouettes in place of missing photos"
     2035msgstr "Силуэты вместо отсутствующих фото"
     2036
     2037#: settings
     2038msgid "Edit links at the bottom of the page"
     2039msgstr "Ссылки «Редактировать» внизу страницы"
     2040
     2041#: settings
     2042msgid "These panels exist only in the Classic template."
     2043msgstr "Эти панели есть только в классическом шаблоне."
     2044
     2045#: settings
     2046msgid "Parent page"
     2047msgstr "Родительская страница"
     2048
     2049#: settings
     2050msgid "— none —"
     2051msgstr "— нет —"
     2052
     2053#: settings
     2054msgid "View page"
     2055msgstr "Открыть страницу"
     2056
     2057#: settings
     2058msgid "New persona pages are created as sub-pages of this page. They are kept out of menus and page lists."
     2059msgstr "Новые страницы персон создаются как дочерние этой страницы. В меню и списки страниц они не попадают."
     2060
     2061#: settings
     2062msgid "Editing on the site"
     2063msgstr "Редактирование на сайте"
     2064
     2065#: settings
     2066msgid "Data is entered on this website (system of record)"
     2067msgstr "Данные вводятся на этом сайте (основная база)"
     2068
     2069#: settings
     2070msgid "Donation button"
     2071msgstr "Кнопка пожертвования"
     2072
     2073#: settings
     2074msgid "Show the PayPal donation button on this page"
     2075msgstr "Показывать кнопку пожертвования PayPal на этой странице"
  • rootspersona/trunk/php/Genealogy/Gedcom/Structures/class-RP-Fact.php

    r3572543 r3749166  
    2929class Rootspersona_Fact extends Rootspersona_Fact_Detail
    3030{
    31     var $_TYPES = array('CAST' => 'Caste', 'EDUC' => 'Education', 'NATI' => 'Nationality', 'OCCU' => 'Occupation', 'PROP' => 'Possessions', 'RELI' => 'Religion', 'RESI' => 'Residence', 'TITL' => 'Nobility Title', 'SSN' => 'Social Security Nbr', 'FACT' => 'Fact');
     31    // RESI is parsed as an event (Rootspersona_Event): listing it here too stored every residence twice.
     32    var $_TYPES = array('CAST' => 'Caste', 'EDUC' => 'Education', 'NATI' => 'Nationality', 'OCCU' => 'Occupation', 'PROP' => 'Possessions', 'RELI' => 'Religion', 'TITL' => 'Nobility Title', 'SSN' => 'Social Security Nbr', 'FACT' => 'Fact');
    3233
    3334    /**
  • rootspersona/trunk/php/class-RP-Gedcom-Loader.php

    r3730341 r3749166  
    239239            $event = new Rootspersona_Event_Detail();
    240240            $event->event_type =
    241                     substr(($p_event->tag === 'EVEN' ? $p_event->type : $p_event->_TYPES[$p_event->tag]),0,22);
    242             $event->classification = substr($p_event->descr,0,89);
     241                    substr((string) ($p_event->tag === 'EVEN' ? $p_event->type : $p_event->_TYPES[$p_event->tag]),0,22);
     242            $event->classification = substr((string) $p_event->descr,0,89);
    243243            $event->event_date = $p_event->date;
    244244            $event->place = $p_event->place->name;
    … …  
    279279            $cite->source_batch_id = $batch_id;
    280280            $cite->source_page = $citation->page;
    281             $cite->event_type = substr($citation->event_type,0,22);
     281            $cite->event_type = substr((string) $citation->event_type,0,22);
    282282            $cite->event_role = $citation->role_in_event;
    283283            $cite->quay = $citation->quay;
    … …  
    369369        foreach ($person->names as $p_name) {
    370370            $name = new Rootspersona_Name_Personal();
    371             $name->personal_name = substr($p_name->get_name(),0,119);
     371            $name->personal_name = substr((string) $p_name->get_name(),0,119);
    372372            $name->name_type = $p_name->rp_name->type;
    373373            $name->prefix = $p_name->rp_name->pieces->prefix;
    … …  
    427427            $event = new Rootspersona_Event_Detail();
    428428            $event->event_type =
    429                     substr(($p_event->tag === 'EVEN' ? $p_event->type : $p_event->_TYPES[$p_event->tag]),0,22);
    430             $event->classification = substr($p_event->descr,0,89);
     429                    substr((string) ($p_event->tag === 'EVEN' ? $p_event->type : $p_event->_TYPES[$p_event->tag]),0,22);
     430            $event->classification = substr((string) $p_event->descr,0,89);
    431431            $event->event_date = $p_event->date;
    432432            $event->place = $p_event->place->name;
    … …  
    457457            $event = new Rootspersona_Event_Detail();
    458458            $event->event_type =
    459                     substr(($p_event->tag === 'FACT' ? $p_event->type : $p_event->_TYPES[$p_event->tag]),0,22);
    460             $event->classification = substr($p_event->descr,0,89);
     459                    substr((string) ($p_event->tag === 'FACT' ? $p_event->type : $p_event->_TYPES[$p_event->tag]),0,22);
     460            $event->classification = substr((string) $p_event->descr,0,89);
    461461            $event->event_date = $p_event->date;
    462462            $event->place = $p_event->place->name;
  • rootspersona/trunk/php/class-RP-Log.php

    r3730341 r3749166  
    289289                $dir = self::dir();
    290290                if ( is_dir( $dir ) ) {
    291                         $files = glob( $dir . '/{,.}*', GLOB_BRACE );
    292                         if ( is_array( $files ) ) {
    293                                 foreach ( $files as $file ) {
    294                                         if ( is_file( $file ) ) {
    295                                                 wp_delete_file( $file );
    296                                         }
    297                                 }
     291                        global $wp_filesystem;
     292                        if ( empty( $wp_filesystem ) ) {
     293                                require_once ABSPATH . 'wp-admin/includes/file.php';
     294                                WP_Filesystem();
    298295                        }
    299                         // phpcs:ignore WordPress.WP.AlternativeFunctions.rmdir_rmdir -- removing the plugin's own empty log directory.
    300                         rmdir( $dir );
     296                        if ( ! empty( $wp_filesystem ) ) {
     297                                $wp_filesystem->delete( $dir, true );
     298                        }
    301299                }
    302300                delete_option( self::KEY_OPTION );
  • rootspersona/trunk/php/class-RP-Persona-Factory.php

    r3572543 r3749166  
    6565            }
    6666
     67            // Married name, birth name, also known as.
     68            $persona->other_names = Rootspersona_Dao_Factory::get_rp_persona_dao()
     69                ->get_other_names($id, $batch_id);
     70
    6771            // do I need fact data?
    6872            if ($options['hide_facts'] == 0) {
  • rootspersona/trunk/php/class-RP-Persona-Helper.php

    r3577245 r3749166  
    109109    public static function is_living($persona)
    110110    {
    111         $is_living = false;
    112111        if (isset($persona->is_living)) {
    113             $is_living = $persona->is_living;
     112            return $persona->is_living;
     113        }
     114        if (!empty($persona->death_date)) {
     115            return false;
     116        }
     117        $birth_year = self::year_of(isset($persona->birth_date) ? $persona->birth_date : '');
     118        if ($birth_year) {
     119            if ($birth_year + self::MAX_AGE < (int) gmdate('Y')) {
     120                return false;
     121            }
     122            // Young enough to be alive, unless a burial or similar says otherwise.
     123            return self::family_says_living($persona, true);
     124        }
     125        // No birth year: the person's other events and close family decide.
     126        return self::family_says_living($persona, false);
     127    }
     128
     129    /** People older than this are taken as deceased (years). */
     130    const MAX_AGE = 110;
     131
     132    /**
     133     * Last four-digit year in a GEDCOM date ("ABT 1890", "BET 1852 AND 1854" -> 1854).
     134     *
     135     * @param string $date
     136     * @return int 0 when there is no year.
     137     */
     138    public static function year_of($date)
     139    {
     140        if (!is_string($date) || $date === '') {
     141            return 0;
     142        }
     143        $cnt = preg_match_all('/([1-2][0-9][0-9][0-9])/', $date, $matches);
     144        return $cnt > 0 ? (int) $matches[1][$cnt - 1] : 0;
     145    }
     146
     147    /**
     148     * Living status from the person's own events and close family, for people
     149     * without a birth year (or to catch a burial recorded without a death).
     150     *
     151     * Deceased when: an own death/burial/cremation/probate event; an own event more
     152     * than MAX_AGE years ago; a marriage or a child's birth more than MAX_AGE-15
     153     * years ago; a spouse born more than MAX_AGE+20 years ago; a parent born more
     154     * than MAX_AGE+50 years ago. Living when recent dates point that way. With no
     155     * clue at all, the "People without dates" setting decides (living by default).
     156     *
     157     * @param Rootspersona_Persona $persona
     158     * @param bool                 $has_birth_year Birth year already says "alive".
     159     * @return bool
     160     */
     161    protected static function family_says_living($persona, $has_birth_year)
     162    {
     163        static $cache = array();
     164
     165        if (empty($persona->id) || $persona->id == '0') {
     166            return $has_birth_year || self::unknown_dates_are_living();
     167        }
     168        $batch_id = isset($persona->batch_id) && $persona->batch_id !== '' ? $persona->batch_id : '1';
     169        $key      = $batch_id . '|' . $persona->id;
     170        if (isset($cache[$key])) {
     171            return $cache[$key];
     172        }
     173
     174        $now     = (int) gmdate('Y');
     175        $dead    = false;
     176        $alive   = false;
     177        $rows    = Rootspersona_Dao_Factory::get_rp_persona_dao()->get_living_clues($persona->id, $batch_id);
     178        foreach ($rows as $row) {
     179            $type = strtoupper((string) $row['event_type']);
     180            $year = self::year_of((string) $row['event_date']);
     181            $died = (bool) preg_match('/^(DEAT|BURI|CREM|PROB)/', $type);
     182            $born = strpos($type, 'BIRT') === 0;
     183            switch ($row['rel']) {
     184                case 'self':
     185                    if ($died) {
     186                        $dead = true;
     187                    } elseif ($year && $year < $now - self::MAX_AGE) {
     188                        $dead = true;
     189                    } elseif ($year && $year >= $now - 90) {
     190                        $alive = true;
     191                    }
     192                    break;
     193                case 'fam':
     194                    if ($year && $year < $now - self::MAX_AGE + 15) {
     195                        $dead = true;
     196                    } elseif ($year && $year >= $now - 60) {
     197                        $alive = true;
     198                    }
     199                    break;
     200                case 'child':
     201                    if ($born && $year && $year < $now - self::MAX_AGE + 15) {
     202                        $dead = true;
     203                    } elseif ($born && $year && $year >= $now - 60) {
     204                        $alive = true;
     205                    }
     206                    break;
     207                case 'spouse':
     208                    if ($born && $year && $year < $now - self::MAX_AGE - 20) {
     209                        $dead = true;
     210                    } elseif ($born && $year && $year >= $now - 80) {
     211                        $alive = true;
     212                    }
     213                    break;
     214                case 'parent':
     215                    if ($born && $year && $year < $now - self::MAX_AGE - 50) {
     216                        $dead = true;
     217                    } elseif ($born && $year && $year >= $now - 80) {
     218                        $alive = true;
     219                    }
     220                    break;
     221            }
     222            if ($dead) {
     223                break;
     224            }
     225        }
     226
     227        if ($has_birth_year) {
     228            // Only an explicit death-type event overrides a young birth year.
     229            $living = true;
     230            foreach ($rows as $row) {
     231                if ($row['rel'] === 'self' && preg_match('/^(DEAT|BURI|CREM|PROB)/', strtoupper((string) $row['event_type']))) {
     232                    $living = false;
     233                    break;
     234                }
     235            }
     236        } elseif ($dead) {
     237            $living = false;
     238        } elseif ($alive) {
     239            $living = true;
    114240        } else {
    115             if (!isset($persona->death_date)
    116                 || empty($persona->death_date)
    117             ) {
    118                 if (isset($persona->birth_date)
    119                     && !empty($persona->birth_date)
    120                 ) {
    121                     $matches = array();
    122                     $cnt = preg_match_all('/.*?([1-2][0-9][0-9][0-9]).*?/Us', $persona->birth_date, $matches); // Modified by Horacio (lower case s)
    123                     if ($cnt > 0) {
    124                         $birth_year = $matches[1][$cnt - 1];
    125                         $test_year = ((int)$birth_year) + 110;
    126                         $curr_year = gmdate( 'Y' );
    127                         if ($test_year >= $curr_year) {
    128                             $is_living = true;
    129                         }
    130                     }
    131                 }
    132             }
    133         }
    134         return $is_living;
     241            $living = self::unknown_dates_are_living();
     242        }
     243
     244        $cache[$key] = $living;
     245        return $living;
     246    }
     247
     248    /**
     249     * "People without dates" setting: true = treat as living (default, safer for privacy).
     250     *
     251     * @return bool
     252     */
     253    public static function unknown_dates_are_living()
     254    {
     255        $options = get_option('rootspersona_plugin');
     256        return !(is_array($options) && isset($options['living_unknown']) && $options['living_unknown'] === 'dead');
    135257    }
    136258
    … …  
    199321        }
    200322        return '';
     323    }
     324
     325    /**
     326     * Whether the current visitor may NOT see the person embedded in this persona page.
     327     *
     328     * Uses the same scoring as the persona panels, so a page shown as "Private"
     329     * also gets a private title (page title, document title, menus, lists).
     330     *
     331     * @param WP_Post|int $post Post object or ID.
     332     * @return bool
     333     */
     334    public static function is_persona_post_restricted( $post ) {
     335        static $cache = array();
     336
     337        $post = get_post( $post );
     338        if ( ! $post || ! is_string( $post->post_content ) || stripos( $post->post_content, '[rootsPersona' ) === false ) {
     339            return false;
     340        }
     341        if ( isset( $cache[ $post->ID ] ) ) {
     342            return $cache[ $post->ID ];
     343        }
     344
     345        $restricted = false;
     346        if ( preg_match( '/\[rootsPersona\s[^\]]*personId\s*=\s*["\']([^"\']+)["\'][^\]]*\]/i', $post->post_content, $m ) ) {
     347            $batch_id = '1';
     348            if ( preg_match( '/batchId\s*=\s*["\']\D*(\d+)/i', $m[0], $bm ) ) {
     349                $batch_id = $bm[1];
     350            }
     351            $persona = Rootspersona_Dao_Factory::get_rp_persona_dao()->get_persona( $m[1], $batch_id );
     352            if ( $persona && isset( $persona->id ) && $persona->id != '0' ) {
     353                $options = get_option( 'rootspersona_plugin' );
     354                $options = is_array( $options ) ? $options : array();
     355                $options += array(
     356                    'privacy_living'  => false,
     357                    'privacy_default' => false,
     358                );
     359                $pscore     = isset( $persona->pscore ) ? $persona->pscore : self::score_persona( $persona, $options );
     360                $restricted = self::is_restricted( self::score_user(), $pscore );
     361            }
     362        }
     363
     364        $cache[ $post->ID ] = $restricted;
     365        return $restricted;
     366    }
     367
     368    /**
     369     * Other names of a person as short phrases: "née Sokolova", "married name Petrova",
     370     * "also known as ...", "nickname «Petrusha»". Shared by the classic header and templates.
     371     *
     372     * @param Rootspersona_Persona $persona
     373     * @return string[] Plain text.
     374     */
     375    public static function other_names_text($persona)
     376    {
     377        $out   = array();
     378        $given = isset($persona->given) ? trim((string) $persona->given) : '';
     379        $names = isset($persona->other_names) && is_array($persona->other_names) ? $persona->other_names : array();
     380        foreach ($names as $n) {
     381            $n_given   = trim((string) $n['given']);
     382            $n_surname = trim((string) $n['surname']);
     383            // Same given name: the surname alone says it ("née Sokolova").
     384            $text = ($n_given === $given && $n_surname !== '')
     385                ? $n_surname
     386                : trim(preg_replace('/\s+/', ' ', str_replace('/', ' ', (string) $n['personal_name'])));
     387            if ($text === '') {
     388                continue;
     389            }
     390            switch (strtolower(trim((string) $n['name_type']))) {
     391                case 'married':
     392                    /* translators: %s: surname or full name taken at marriage. */
     393                    $out[] = Rootspersona_I18n::sprintf(__('married name %s', 'rootspersona'), $text);
     394                    break;
     395                case 'birth':
     396                case 'maiden':
     397                    /* translators: %s: surname or full name at birth. */
     398                    $out[] = Rootspersona_I18n::sprintf(__('née %s', 'rootspersona'), $text);
     399                    break;
     400                default:
     401                    /* translators: %s: another name of the person. */
     402                    $out[] = Rootspersona_I18n::sprintf(__('also known as %s', 'rootspersona'), $text);
     403            }
     404        }
     405        $nick = isset($persona->nickname) ? trim((string) $persona->nickname) : '';
     406        if ($nick !== '') {
     407            /* translators: %s: nickname. */
     408            $out[] = Rootspersona_I18n::sprintf(__('nickname “%s”', 'rootspersona'), $nick);
     409        }
     410        return $out;
     411    }
     412
     413    /** Transient with the IDs of persona pages an anonymous visitor may not see. */
     414    const GUEST_PRIVATE_PAGES = 'rootspersona_guest_private_pages';
     415
     416    /**
     417     * IDs of persona pages an anonymous visitor may not see (living or private people).
     418     * Used to keep those pages, and the names in their URLs, out of XML sitemaps.
     419     * Cached for a few hours; cleared when pages are created or settings change.
     420     *
     421     * @return int[]
     422     */
     423    public static function get_guest_private_page_ids()
     424    {
     425        $ids = get_transient(self::GUEST_PRIVATE_PAGES);
     426        if (is_array($ids)) {
     427            return $ids;
     428        }
     429
     430        $options = get_option('rootspersona_plugin');
     431        $options = is_array($options) ? $options : array();
     432        $options += array(
     433            'privacy_living'  => false,
     434            'privacy_default' => false,
     435        );
     436        $ids = array();
     437        try {
     438            foreach (Rootspersona_Dao_Factory::get_rp_persona_dao()->get_paged_personas_for_privacy() as $persona) {
     439                $restricted = $persona->privacy === self::EXC
     440                    || self::is_restricted(self::GUEST, self::score_persona($persona, $options));
     441                if ($restricted && (int) $persona->page > 0) {
     442                    $ids[] = (int) $persona->page;
     443                }
     444            }
     445        } catch (Exception $e) {
     446            return array();
     447        }
     448        set_transient(self::GUEST_PRIVATE_PAGES, $ids, 6 * HOUR_IN_SECONDS);
     449        return $ids;
    201450    }
    202451
    … …  
    457706                    && !empty($options['banner_fcolor'])
    458707                ) {
    459                     $ban_style = ' style="color:' . $options['banner_fcolor']
     708                    $ban_style = ' style="color:' . esc_attr( $options['banner_fcolor'] )
    460709                        . ';background-image:none;background-color:'
    461                         . $options['banner_bcolor'] . ';" ';
     710                        . esc_attr( $options['banner_bcolor'] ) . ';" ';
    462711                } else {
    463712                    $ban_style = ' style="background-image:none;background-color:'
    464                         . $options['banner_bcolor'] . ';" ';
     713                        . esc_attr( $options['banner_bcolor'] ) . ';" ';
    465714                }
    466715            } else if (isset($options['banner_image'])
    … …  
    470719                    && !empty($options['banner_fcolor'])
    471720                ) {
    472                     $ban_style = ' style="color:' . $options['banner_fcolor']
    473                         . ';background-image:url(\'' . $options['banner_image']
     721                    $ban_style = ' style="color:' . esc_attr( $options['banner_fcolor'] )
     722                        . ';background-image:url(\'' . esc_url( $options['banner_image'] )
    474723                        . '\');" ';
    475724                } else {
    476725                    $ban_style = ' style="background-image:none;background-color:'
    477                         . $options['banner_bcolor'] . ';" ';
     726                        . esc_attr( $options['banner_bcolor'] ) . ';" ';
    478727                }
    479728            } elseif (isset($options['banner_fcolor'])
    480729                && !empty($options['banner_fcolor'])
    481730            ) {
    482                 $ban_style = ' style="color:' . $options['banner_fcolor'] . ';" ';
     731                $ban_style = ' style="color:' . esc_attr( $options['banner_fcolor'] ) . ';" ';
    483732            }
    484733
    … …  
    515764        }
    516765
     766        // Link to the page's permalink (not "?page_id=", which redirects and may drop the page
     767        // number) and build every URL with add_query_arg() so it is always well formed.
     768        $base = (string) $targetpage;
     769        if ( preg_match( '/[?&]page_id=(\d+)/', $base, $m ) ) {
     770            $permalink = get_permalink( (int) $m[1] );
     771            if ( $permalink ) {
     772                $base = $permalink;
     773            }
     774        }
     775        $base = remove_query_arg( 'rootsvar', $base );
     776        $link = function ( $page, $label ) use ( $base ) {
     777            $url = ( $page > 1 ) ? add_query_arg( 'rootsvar', (int) $page, $base ) : $base;
     778            return "<a href='" . esc_url( $url ) . "'>" . esc_html( $label ) . '</a>';
     779        };
     780        $span = function ( $class, $label ) {
     781            return "<span class='" . esc_attr( $class ) . "'>" . esc_html( $label ) . '</span>';
     782        };
     783
     784        $first_label = __( '« first', 'rootspersona' );
     785        $prev_label  = __( '‹ prev', 'rootspersona' );
     786        $next_label  = __( 'next ›', 'rootspersona' );
     787        $last_label  = __( 'last »', 'rootspersona' );
     788
    517789        $pagination = "<div class='pagination'>";
    518 
    519         if ($curr_page != 1) {
    520             $pagination .= "<a href='"
    521                 . $targetpage . "page=1'>&lt;&lt; first</a>";
    522         } else {
    523             $pagination .= "<span class='disabled'>&lt;&lt; first</span>";
    524         }
    525 
    526         if ($curr_page > 1) {
    527             $pagination .= "<a href='"
    528                 . $targetpage . "&rootsvar=$prev'>&lt; prev</a>";
    529         } else {
    530             $pagination .= "<span class='disabled'>&lt; prev</span>";
    531         }
    532 
    533         for ($p = $start; $p <= $finish; $p++) {
    534             if ($p == $curr_page) {
    535                 $pagination .= "<span class='current'>$p</span>";
    536             } else {
    537                 $pagination .= "<a href='" . $targetpage
    538                     . "&rootsvar=" . $p . "'>" . $p . "</a>";
    539             }
    540         }
    541         if ($curr_page != $finish) {
    542             $pagination .= "<a href='" . $targetpage
    543                 . "&rootsvar=" . $next . "'>next &gt;</a>";
    544         } else {
    545             $pagination .= "<span class='disabled'>next &gt;</span>";
    546         }
    547         if ($curr_page != $lastpage) {
    548             $pagination .= "<a href='" . $targetpage
    549                 . "&rootsvar=" . $lastpage . "'>last &gt;&gt;</a>";
    550         } else {
    551             $pagination .= "<span class='disabled'>last &gt;&gt;</span>";
    552         }
     790        $pagination .= ( $curr_page > 1 ) ? $link( 1, $first_label ) : $span( 'disabled', $first_label );
     791        $pagination .= ( $curr_page > 1 ) ? $link( $prev, $prev_label ) : $span( 'disabled', $prev_label );
     792        for ( $p = $start; $p <= $finish; $p++ ) {
     793            $pagination .= ( $p == $curr_page ) ? $span( 'current', (string) $p ) : $link( $p, (string) $p );
     794        }
     795        $pagination .= ( $curr_page < $lastpage ) ? $link( $next, $next_label ) : $span( 'disabled', $next_label );
     796        $pagination .= ( $curr_page < $lastpage ) ? $link( $lastpage, $last_label ) : $span( 'disabled', $last_label );
    553797        $pagination .= "</div>";
    554798        return $pagination;
  • rootspersona/trunk/php/dao/mysql/class-RP-Persona-Mysql-DAO.php

    r3577245 r3749166  
    275275        $sql_query->set($privacy);
    276276        $sql_query->set($privacy == 'Exc' ? $name : '');
    277         return $this->execute_update($sql_query);
     277        $result = $this->execute_update($sql_query);
     278        delete_transient(Rootspersona_Persona_Helper::GUEST_PRIVATE_PAGES);
     279        return $result;
    278280    }
    279281
    … …  
    538540        $sql_query->set_number($batch_id);
    539541        $rows = Rootspersona_Query_Executor::execute($sql_query);
     542
     543        // Years of birth and death for the whole batch in one query, so people with
     544        // the same name can be told apart in the list.
     545        $sql = "SELECT rie.indi_id AS id, red.event_type AS event_type, red.event_date AS event_date"
     546            . " FROM rootspersona_indi_event rie"
     547            . " JOIN rootspersona_event_detail red ON red.id = rie.event_id"
     548            . " WHERE rie.indi_batch_id = ?"
     549            . " AND (UPPER(red.event_type) LIKE 'BIRT%' OR UPPER(red.event_type) LIKE 'DEAT%')";
     550        $years_query = new Rootspersona_Sql_Query($sql);
     551        $years_query->set_number($batch_id);
     552        $years = array();
     553        foreach (Rootspersona_Query_Executor::execute($years_query) as $y) {
     554            $kind = strpos(strtoupper($y['event_type']), 'DEAT') === 0 ? 'death_year' : 'birth_year';
     555            $year = Rootspersona_Persona_Helper::year_of((string) $y['event_date']);
     556            if ($year && empty($years[$y['id']][$kind])) {
     557                $years[$y['id']][$kind] = $year;
     558            }
     559        }
     560
    540561        $persons = array();
    541562        if ($rows > 0) {
    … …  
    549570                $person['given'] = $rows[$idx]['given'];
    550571                $person['surname'] = $rows[$idx]['surname'];
     572                $person['birth_year'] = isset($years[$person['id']]['birth_year']) ? $years[$person['id']]['birth_year'] : 0;
     573                $person['death_year'] = isset($years[$person['id']]['death_year']) ? $years[$person['id']]['death_year'] : 0;
    551574                $persons[$idx] = $person;
    552575            }
    … …  
    671694
    672695    /**
     696     * Dated events that tell whether a person without own dates can still be alive:
     697     * the person's own events, events of the families they head (marriage...),
     698     * and the events of spouses, children and parents.
     699     *
     700     * @param string     $id
     701     * @param int|string $batch_id
     702     * @return array[] rows: [rel (self|fam|spouse|child|parent), event_type, event_date]
     703     */
     704    public function get_living_clues($id, $batch_id)
     705    {
     706        $sql = "SELECT 'self' AS rel, red.event_type AS event_type, red.event_date AS event_date"
     707            . " FROM rootspersona_indi_event rie"
     708            . " JOIN rootspersona_event_detail red ON red.id = rie.event_id"
     709            . " WHERE rie.indi_id = ? AND rie.indi_batch_id = ?"
     710            . " UNION ALL"
     711            . " SELECT 'fam', red.event_type, red.event_date"
     712            . " FROM rootspersona_fam f"
     713            . " JOIN rootspersona_fam_event fe ON fe.fam_id = f.id AND fe.fam_batch_id = f.batch_id"
     714            . " JOIN rootspersona_event_detail red ON red.id = fe.event_id"
     715            . " WHERE f.batch_id = ? AND (f.spouse1 = ? OR f.spouse2 = ?)"
     716            . " UNION ALL"
     717            . " SELECT 'spouse', red.event_type, red.event_date"
     718            . " FROM rootspersona_fam f"
     719            . " JOIN rootspersona_indi_event rie ON rie.indi_batch_id = f.batch_id"
     720            . " AND rie.indi_id = CASE WHEN f.spouse1 = ? THEN f.spouse2 ELSE f.spouse1 END"
     721            . " JOIN rootspersona_event_detail red ON red.id = rie.event_id"
     722            . " WHERE f.batch_id = ? AND (f.spouse1 = ? OR f.spouse2 = ?)"
     723            . " UNION ALL"
     724            . " SELECT 'child', red.event_type, red.event_date"
     725            . " FROM rootspersona_fam f"
     726            . " JOIN rootspersona_fam_child fc ON fc.fam_id = f.id AND fc.fam_batch_id = f.batch_id"
     727            . " JOIN rootspersona_indi_event rie ON rie.indi_id = fc.child_id AND rie.indi_batch_id = fc.indi_batch_id"
     728            . " JOIN rootspersona_event_detail red ON red.id = rie.event_id"
     729            . " WHERE f.batch_id = ? AND (f.spouse1 = ? OR f.spouse2 = ?)"
     730            . " UNION ALL"
     731            . " SELECT 'parent', red.event_type, red.event_date"
     732            . " FROM rootspersona_fam_child fc"
     733            . " JOIN rootspersona_fam f ON f.id = fc.fam_id AND f.batch_id = fc.fam_batch_id"
     734            . " JOIN rootspersona_indi_event rie ON rie.indi_batch_id = f.batch_id"
     735            . " AND (rie.indi_id = f.spouse1 OR rie.indi_id = f.spouse2)"
     736            . " JOIN rootspersona_event_detail red ON red.id = rie.event_id"
     737            . " WHERE fc.child_id = ? AND fc.indi_batch_id = ?";
     738        $sql_query = new Rootspersona_Sql_Query($sql);
     739        // self
     740        $sql_query->set($id);
     741        $sql_query->set_number($batch_id);
     742        // fam
     743        $sql_query->set_number($batch_id);
     744        $sql_query->set($id);
     745        $sql_query->set($id);
     746        // spouse
     747        $sql_query->set($id);
     748        $sql_query->set_number($batch_id);
     749        $sql_query->set($id);
     750        $sql_query->set($id);
     751        // child
     752        $sql_query->set_number($batch_id);
     753        $sql_query->set($id);
     754        $sql_query->set($id);
     755        // parent
     756        $sql_query->set($id);
     757        $sql_query->set_number($batch_id);
     758        return Rootspersona_Query_Executor::execute($sql_query);
     759    }
     760
     761    /**
     762     * Every person that has a WordPress page, with what privacy scoring needs
     763     * (privacy code, birth and death dates) — in two queries instead of one per person.
     764     *
     765     * @return Rootspersona_Persona[]
     766     */
     767    public function get_paged_personas_for_privacy()
     768    {
     769        $sql = "SELECT ri.id AS id, ri.batch_id AS batch_id, ri.wp_page_id AS page,"
     770            . " IFNULL(rio.privacy_code,'Def') AS privacy"
     771            . " FROM rootspersona_indi ri"
     772            . " LEFT OUTER JOIN rootspersona_indi_option rio ON rio.indi_id = ri.id AND rio.indi_batch_id = ri.batch_id"
     773            . " WHERE ri.wp_page_id IS NOT NULL AND ri.wp_page_id <> '' AND ri.wp_page_id <> '0'";
     774        $rows = Rootspersona_Query_Executor::execute(new Rootspersona_Sql_Query($sql));
     775
     776        $sql = "SELECT rie.indi_id AS id, rie.indi_batch_id AS batch_id, red.event_type AS event_type, red.event_date AS event_date"
     777            . " FROM rootspersona_indi_event rie"
     778            . " JOIN rootspersona_event_detail red ON red.id = rie.event_id"
     779            . " WHERE UPPER(red.event_type) LIKE 'BIRT%' OR UPPER(red.event_type) LIKE 'DEAT%'";
     780        $dates = array();
     781        foreach (Rootspersona_Query_Executor::execute(new Rootspersona_Sql_Query($sql)) as $d) {
     782            $key  = $d['batch_id'] . '|' . $d['id'];
     783            $kind = strpos(strtoupper($d['event_type']), 'DEAT') === 0 ? 'death_date' : 'birth_date';
     784            if (empty($dates[$key][$kind])) {
     785                $dates[$key][$kind] = (string) $d['event_date'];
     786            }
     787        }
     788
     789        $personas = array();
     790        foreach ($rows as $r) {
     791            $p             = new Rootspersona_Persona();
     792            $p->id         = $r['id'];
     793            $p->batch_id   = $r['batch_id'];
     794            $p->page       = $r['page'];
     795            $p->privacy    = $r['privacy'];
     796            $key           = $r['batch_id'] . '|' . $r['id'];
     797            $p->birth_date = isset($dates[$key]['birth_date']) ? $dates[$key]['birth_date'] : '';
     798            $p->death_date = isset($dates[$key]['death_date']) ? $dates[$key]['death_date'] : '';
     799            $personas[]    = $p;
     800        }
     801        return $personas;
     802    }
     803
     804    /**
    673805     * @todo Description of function getPersonaEvents
    674806     * @param  $id
    … …  
    690822        if ($rows > 0) {
    691823            $events = array();
     824            $seen = array();
    692825            for ($idx = 0;
    693826                 $idx < $cnt;
    694827                 $idx++) {
     828                // Imports before 4.2.4 stored each residence twice; show identical facts once.
     829                $key = implode("\x1f", array($rows[$idx]['event_type'], $rows[$idx]['event_date'], $rows[$idx]['place'], $rows[$idx]['classification'], $rows[$idx]['cause']));
     830                if (isset($seen[$key])) {
     831                    continue;
     832                }
     833                $seen[$key] = true;
    695834                $event = array();
    696835                $event['type'] = $rows[$idx]['event_type'];
    … …  
    700839                $event['cause'] = $rows[$idx]['cause'];
    701840                $event['associated_person'] = null; //array('name'=>'', 'isPrivate'=> false);
    702                 $events[$idx] = $event;
     841                $events[] = $event;
    703842            }
    704843        }
    … …  
    721860            $families = &self::$_fcache[$batch_id][$id];
    722861        } else {
    723             $sql = "SELECT rf.id as id, rf.spouse1 AS s1, rf.spouse2 AS s2,"
    724                 . "red.event_date AS event_date, red.place AS place"
     862            // Every family of the person. (Joining the events here and keeping only
     863            // "Marriage" rows dropped families whose only event was e.g. a divorce.)
     864            $sql = "SELECT rf.id as id, rf.spouse1 AS s1, rf.spouse2 AS s2"
    725865                . " FROM rootspersona_fam rf"
    726                 . " LEFT OUTER JOIN rootspersona_fam_event rfe ON rf.id = rfe.fam_id"
    727                 . " AND rf.batch_id = rfe.fam_batch_id"
    728                 . " LEFT OUTER JOIN rootspersona_event_detail red ON red.id = rfe.event_id"
    729866                . " WHERE ((rf.spouse1 = ? AND rf.indi_batch_id_1 = ?)"
    730                 . " OR (rf.spouse2 = ? AND rf.indi_batch_id_2 = ?))"
    731                 . " AND (red.event_type = ? OR red.event_type IS null)";
     867                . " OR (rf.spouse2 = ? AND rf.indi_batch_id_2 = ?))";
    732868            $sql_query = new Rootspersona_Sql_Query($sql);
    733869            $sql_query->set($id);
    … …  
    735871            $sql_query->set($id);
    736872            $sql_query->set_number($batch_id);
    737             $sql_query->set('Marriage');
    738873            $rows = Rootspersona_Query_Executor::execute($sql_query);
    739874            $cnt = count($rows);
    … …  
    747882                    $event['fams'] = $rows[$idx]['id'];
    748883                    $event['type'] = 'Marriage';
    749                     $event['date'] = $rows[$idx]['event_date'];
    750                     $event['place'] = $rows[$idx]['place'];
     884                    $event['date'] = null;
     885                    $event['place'] = null;
     886                    $event['divorced'] = false;
     887                    $event['divorce_date'] = '';
     888                    foreach ($this->get_family_events($rows[$idx]['id'], $batch_id) as $fe) {
     889                        $type = strtoupper((string) $fe['event_type']);
     890                        if ($type === 'MARRIAGE' && $event['date'] === null) {
     891                            $event['date'] = $fe['event_date'];
     892                            $event['place'] = $fe['place'];
     893                        } elseif (strpos($type, 'DIVORCE') === 0 || $type === 'ANNULMENT') {
     894                            $event['divorced'] = true;
     895                            if ('' === $event['divorce_date']) {
     896                                $event['divorce_date'] = (string) $fe['event_date'];
     897                            }
     898                        }
     899                    }
    751900                    $id1 = $rows[$idx]['s1'];
    752901                    $id2 = $rows[$idx]['s2'];
    … …  
    769918
    770919    /**
     920     * Marriage / divorce events of one family.
     921     *
     922     * @param string     $fam_id
     923     * @param int|string $batch_id
     924     * @return array[] rows: [event_type, event_date, place]
     925     */
     926    public function get_family_events($fam_id, $batch_id)
     927    {
     928        $sql = "SELECT red.event_type AS event_type, red.event_date AS event_date, red.place AS place"
     929            . " FROM rootspersona_fam_event rfe"
     930            . " JOIN rootspersona_event_detail red ON red.id = rfe.event_id"
     931            . " WHERE rfe.fam_id = ? AND rfe.fam_batch_id = ?";
     932        $sql_query = new Rootspersona_Sql_Query($sql);
     933        $sql_query->set($fam_id);
     934        $sql_query->set_number($batch_id);
     935        return Rootspersona_Query_Executor::execute($sql_query);
     936    }
     937
     938    /**
     939     * Names after the first one (married name, birth name, also known as).
     940     *
     941     * @param string     $id
     942     * @param int|string $batch_id
     943     * @return array[] rows: [name_type, given, surname, personal_name]
     944     */
     945    public function get_other_names($id, $batch_id)
     946    {
     947        $sql = "SELECT rnp.name_type AS name_type, rnp.given AS given, rnp.surname AS surname,"
     948            . " rnp.personal_name AS personal_name"
     949            . " FROM rootspersona_indi_name rip"
     950            . " JOIN rootspersona_name_personal rnp ON rip.name_id = rnp.id"
     951            . " WHERE rip.indi_id = ? AND rip.indi_batch_id = ? AND rip.seq_nbr > 1"
     952            . " ORDER BY rip.seq_nbr";
     953        $sql_query = new Rootspersona_Sql_Query($sql);
     954        $sql_query->set($id);
     955        $sql_query->set_number($batch_id);
     956        return Rootspersona_Query_Executor::execute($sql_query);
     957    }
     958
     959    /**
    771960     * @todo Description of function getChildren
    772961     * @param  $id
    … …  
    804993    public function get_persona_sources($id, $batch_id)
    805994    {
    806         $sql = 'SELECT DISTINCT id,page,title,abbr FROM'
    807             . ' (SELECT rs.id AS id, rs.wp_page_id AS page,rs.source_title AS title,rs.abbr AS abbr'
     995        $sql = 'SELECT DISTINCT id,page,title,abbr,cite_page FROM'
     996            . ' (SELECT rs.id AS id, rs.wp_page_id AS page,rs.source_title AS title,rs.abbr AS abbr,rsc.source_page AS cite_page'
    808997            . ' FROM rootspersona_indi_cite ric'
    809998            . ' JOIN rootspersona_source_cite rsc ON ric.cite_id = rsc.id'
    … …  
    8111000            . ' WHERE ric.indi_id = ? AND ric.indi_batch_id = ?'
    8121001            . ' UNION'
    813             . ' SELECT rs.id AS id, rs.wp_page_id AS page,rs.source_title AS title,rs.abbr AS abbr'
     1002            . ' SELECT rs.id AS id, rs.wp_page_id AS page,rs.source_title AS title,rs.abbr AS abbr,rsc.source_page AS cite_page'
    8141003            . ' FROM rootspersona_indi_event rie'
    8151004            . ' JOIN rootspersona_event_cite rec ON rie.event_id = rec.event_id'
    … …  
    8181007            . ' WHERE rie.indi_id = ? AND rie.indi_batch_id = ?'
    8191008            . ' UNION'
    820             . ' SELECT rs.id AS id, rs.wp_page_id AS page,rs.source_title AS title,rs.abbr AS abbr'
     1009            . ' SELECT rs.id AS id, rs.wp_page_id AS page,rs.source_title AS title,rs.abbr AS abbr,rsc.source_page AS cite_page'
    8211010            . ' FROM rootspersona_indi_fam rif'
    8221011            . ' JOIN rootspersona_fam_cite rfc ON rfc.fam_id = rif.fam_id AND rfc.fam_batch_id = rif.fam_batch_id'
    … …  
    8251014            . ' WHERE rif.indi_id = ? AND rif.indi_batch_id = ?'
    8261015            . ' UNION'
    827             . ' SELECT rs.id AS id, rs.wp_page_id AS page,rs.source_title AS title,rs.abbr AS abbr'
     1016            . ' SELECT rs.id AS id, rs.wp_page_id AS page,rs.source_title AS title,rs.abbr AS abbr,rsc.source_page AS cite_page'
    8281017            . ' FROM rootspersona_indi_name rin'
    8291018            . ' JOIN rootspersona_name_cite rnc ON rnc.name_id = rin.name_id'
    … …  
    8441033        $sources = null;
    8451034        if ($cnt > 0) {
    846             $sources = array();
     1035            // One entry per source; the cited pages ("Folio 12, entry 34") collected under it.
     1036            $by_id = array();
    8471037            for ($idx = 0;
    8481038                 $idx < $cnt;
    8491039                 $idx++) {
    850                 $src = array();
    851                 $src['src_id'] = $rows[$idx]['id'];
    852                 $src['src_title'] = $rows[$idx]['title'];
    853                 $src['src_abbr'] = $rows[$idx]['abbr'];
    854                 $src['page_id'] = $rows[$idx]['page'];
    855                 $sources[$idx] = $src;
    856             }
     1040                $sid = $rows[$idx]['id'];
     1041                if (!isset($by_id[$sid])) {
     1042                    $by_id[$sid] = array(
     1043                        'src_id'    => $sid,
     1044                        'src_title' => $rows[$idx]['title'],
     1045                        'src_abbr'  => $rows[$idx]['abbr'],
     1046                        'page_id'   => $rows[$idx]['page'],
     1047                        'src_pages' => array(),
     1048                    );
     1049                }
     1050                $cite_page = trim((string) $rows[$idx]['cite_page']);
     1051                if ($cite_page !== '' && !in_array($cite_page, $by_id[$sid]['src_pages'], true)) {
     1052                    $by_id[$sid]['src_pages'][] = $cite_page;
     1053                }
     1054            }
     1055            $sources = array_values($by_id);
    8571056        }
    8581057        return $sources;
  • rootspersona/trunk/php/model/class-RP-Persona.php

    r3572543 r3749166  
    2121    var $prefix = "";
    2222    var $nickname = "";
     23    var $other_names = array();
    2324    var $suffix = "";
    2425    var $facts;
  • rootspersona/trunk/php/pages/class-RP-Add-Page-Builder.php

    r3572543 r3749166  
    2222
    2323        $block = "<div style='overflow:hidden;width:60%;margin:40px;'><div class='personBanner'>&#160;</div>";
    24         if (count($persons) == 0) {
     24        if (count($persons) == 0 && $msg === '') {
    2525            $sync_url = esc_url( admin_url( 'tools.php?page=rootsPersona_tools&rootspage=util&utilityAction=syncPersonaDbToPages&batch_id=' . rawurlencode( (string) $default ) ) );
    2626            $block   .= "<br/><div style='text-align:left;padding:12px;border:1px solid #c3c4c7;background:#f6f7f7;max-width:42em;margin:0 auto;'>"
    … …  
    2929                . ' <a href="' . $sync_url . '">' . esc_html__( 'Open sync now', 'rootspersona' ) . '</a>.</div>';
    3030        }
    31         $block .= "<form action='" . esc_url( $action ) . "' method='POST'>"
     31        $block .= "<form id='rp-add-pages-form' action='" . esc_url( $action ) . "' method='POST'>"
    3232            . wp_nonce_field('rootspersona_add_page_nonce', '_wpnonce', true, false)
    3333            . "<div style='overflow:hidden;margin:10px;'>"
    … …  
    5757        $cnt = count($persons);
    5858        for ($i = 0; $i < $cnt; $i++) {
    59             $opt_value = esc_attr( $persons[$i]['id'] . '|' . $persons[$i]['surname'] . ', ' . $persons[$i]['given'] );
    60             $opt_text  = esc_html( $persons[$i]['surname'] . ', ' . $persons[$i]['given'] );
     59            $opt_value = esc_attr( self::option_value( $persons[$i] ) );
     60            $opt_text  = esc_html( self::option_label( $persons[$i] ) );
    6161            $block .= "<option value='" . $opt_value . "'>" . $opt_text . "</option>";
    6262        }
    6363        $block .= "</select></td>"
    6464            . "</tr><tr><td>"
    65             . "<div style='text-align:center;color:red;font-weight:bold'>"
    66             . esc_html( $msg ) . "</div>"
     65            . "<div id='rp-add-pages-msg' style='text-align:left;max-height:20em;overflow:auto;'>"
     66            . wp_kses( $msg, array( 'br' => array() ) ) . "</div>"
    6767            . "<br/><input type='submit' class='js-rp-submit-loading' name='submitAddPageForm' value='"
    6868            . __('Add', 'rootspersona') . "'/>"
    … …  
    7474        return $block;
    7575    }
     76    /**
     77     * Submitted value: "ID|Surname, Given" (the name becomes the page title).
     78     *
     79     * @param array $p Row from get_persons_no_page().
     80     * @return string
     81     */
     82    public static function option_value( $p )
     83    {
     84        return $p['id'] . '|' . $p['surname'] . ', ' . $p['given'];
     85    }
     86
     87    /**
     88     * Visible text: "Surname, Given (1850–1920) [I123]", so namesakes can be told apart.
     89     *
     90     * @param array $p Row from get_persons_no_page().
     91     * @return string
     92     */
     93    public static function option_label( $p )
     94    {
     95        $label = $p['surname'] . ', ' . $p['given'];
     96        $born  = ! empty( $p['birth_year'] ) ? (string) $p['birth_year'] : '';
     97        $died  = ! empty( $p['death_year'] ) ? (string) $p['death_year'] : '';
     98        if ( '' !== $born || '' !== $died ) {
     99            $label .= ' (' . $born . '–' . $died . ')';
     100        }
     101        return $label . ' [' . $p['id'] . ']';
     102    }
    76103}
  • rootspersona/trunk/php/pages/class-RP-Evidence-Page-Builder.php

    r3572543 r3749166  
    3939                    . esc_html( $p->surname . ', ' . $p->given )
    4040                    . ' <span style="font-size:smaller">'
    41                     . ($options['hide_dates'] == 1 ? ' ' : esc_html( '(' . $p->birth_date . ' - '
    42                         . $p->death_date . ')' ))
     41                    . ($options['hide_dates'] == 1 ? ' ' : esc_html( '(' . Rootspersona_Date::format( $p->birth_date ) . ' - '
     42                        . Rootspersona_Date::format( $p->death_date ) . ')' ))
    4343                    . '</span></a></li>';
    4444            }
  • rootspersona/trunk/php/pages/class-RP-Index-Page-Builder.php

    r3572543 r3749166  
    3737                . esc_html( $entry->surname ) . ', ' . esc_html( $entry->given )
    3838                . ($hide_dates == 1 ? ' ' : ('&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;('
    39                         . esc_html( $entry->birth_date ) . ' - '
    40                         . esc_html( $entry->death_date )) . ')')
     39                        . esc_html( Rootspersona_Date::format( $entry->birth_date ) ) . ' - '
     40                        . esc_html( Rootspersona_Date::format( $entry->death_date ) )) . ')')
    4141                . "</option>";
    4242        }
    … …  
    5959        $hide_dates = $options['hide_dates'];
    6060        $hdrcolor = ((isset($options['index_hdr_color']) && !empty($options['index_hdr_color']))
    61             ? $options['index_hdr_color'] : '#CCCCCC');
     61            ? esc_attr( $options['index_hdr_color'] ) : '#CCCCCC');
    6262        $block = $pagination . $xofy;
    6363        $block .= "<table id='personaIndexTable' cellpadding='0' cellspacing='0'>"
    … …  
    6868        $evenodd = 'even';
    6969        $evencolor = ((isset($options['index_even_color']) && !empty($options['index_even_color']))
    70             ? $options['index_even_color'] : 'white');
     70            ? esc_attr( $options['index_even_color'] ) : 'white');
    7171        $oddcolor = ((isset($options['index_odd_color']) && !empty($options['index_odd_color']))
    72             ? $options['index_odd_color'] : '#DDDDDD');
     72            ? esc_attr( $options['index_odd_color'] ) : '#DDDDDD');
    7373        $evenoddcolor = $evencolor;
    7474        foreach ($index AS $persona) {
    … …  
    7676                . esc_html( $persona->surname ) . "</td><td style='background-color:" . esc_attr( $evenoddcolor ) . "' class='given'>"
    7777                . esc_html( $persona->given ) . "</td><td style='background-color:" . esc_attr( $evenoddcolor ) . "' class='rp_dates'>"
    78                 . ($hide_dates == 1 ? ' ' : (esc_html( $persona->birth_date ) . ' - '
    79                     . esc_html( $persona->death_date )))
     78                . ($hide_dates == 1 ? ' ' : (esc_html( Rootspersona_Date::format( $persona->birth_date ) ) . ' - '
     79                    . esc_html( Rootspersona_Date::format( $persona->death_date ) )))
    8080                . "</td><td style='background-color:" . esc_attr( $evenoddcolor ) . "' class='page'><a href='" . esc_url( $home_url . "?page_id="
    8181                . $persona->page ) . "'>" . esc_html( $persona->page ) . "</a>"
  • rootspersona/trunk/php/pages/class-RP-Option-Page-Builder.php

    r3746101 r3749166  
    44}
    55
     6/**
     7 * Settings page (Settings → RootsPersona): one form split into tabs, WordPress form-table markup.
     8 *
     9 * The "hide_*" options are stored as before, but shown as "Show …" checkboxes: a hidden field
     10 * sends the "hidden" value and the checkbox, when ticked, overrides it with the "shown" value
     11 * (PHP keeps the last field of the same name).
     12 */
    613class Rootspersona_Option_Page_Builder
    714{
    8 
    9     /**
    10      * @param $options
    11      *
    12      *
    13      */
    14     function build($options)
    15     {
    16         /* translators: %s: context (e.g. persona). */
    17         $win1 = Rootspersona_I18n::sprintf( __( 'Edit page is meant to be accessed using the Edit button on a %s page', 'rootspersona' ), 'persona' );
    18         /* translators: %s: context (e.g. persona). */
    19         $win1 .= '. ' . Rootspersona_I18n::sprintf( __( 'Accessing outside of a %s page will result in an invalid personId error.', 'rootspersona' ), 'persona' );
    20         $plugin_images = isset( $options['plugin_url'] )
    21             ? trailingslashit( $options['plugin_url'] ) . 'images/bannerstartwithwp.png'
    22             : ( defined( 'ROOTSPERSONA_PLUGIN_URL' )
    23                 ? trailingslashit( ROOTSPERSONA_PLUGIN_URL ) . 'images/bannerstartwithwp.png'
    24                 : plugins_url( 'images/bannerstartwithwp.png', ROOTSPERSONA_PLUGIN_FILE ) );
    25         $banner = esc_url( $plugin_images );
    26         $block = "<div class='wrap'><a href='https://rootspersona.com'><img src='" . $banner . "' alt=''/></a>"
    27             . "<div style='padding-left:10px;padding-top:5px;font-size:larger;'>Version " . esc_html( isset( $options['version'] ) ? $options['version'] : '' );
    28 
    29         if ( empty( $options['hide_donation'] ) ) {
    30             $block .= " <span style='display:inline-block;float:right;padding-right:1em;'>"
    31                 . '<a href="' . esc_url( 'https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=ANLEXXNXKFN9N' ) . '">'
    32                 . esc_html__( 'Donate', 'rootspersona' ) . '</a>'
    33                 . "<span style='display:inline-block;float:right;width:15em;padding-right:20em;padding-left:2em;font-size:smaller;'>A Fan of <i>RootsPersona?</i><br>Consider making a donation!</span>";
    34         }
    35         echo wp_kses_post( $block . "</div>" );
    36         echo '<form method="post" action="' . esc_url( admin_url( 'options.php' ) ) . '">';
    37         // Nonce + hidden option_page must be inside the form before any submit (broken markup below used to break saves).
    38         settings_fields( 'rootspersona_plugin' );
    39         echo "<p class='submit'><input type='submit' name='Submit' value=' " . esc_attr__( 'Save Changes', 'rootspersona' ) . " '/></p>";
    40         echo "<table class='form-table'>";
    41         echo "<tr><td colspan='3'><span class='optionsHdr'>" . esc_html__( 'Privacy Options', 'rootspersona' ) . "</span><hr class='optionsHdr'></span></td></tr>";
    42         echo "<tr valign='top'>";
    43         echo "<td scope='row' class='left-label'><label for='rootspersona_plugin[privacy_living]'>" . esc_html__( 'Living Privacy Setting', 'rootspersona' ) . "</label></td>";
    44         $opt = $options['privacy_living'];
    45         $exc = '';
    46         $pvt = 'selected';
    47         $mbr = '';
    48         $pub = '';
    49         if ( isset( $opt ) ) {
    50             if ( $opt == 'Pub' ) {
    51                 $pub = 'selected';
    52                 $pvt = '';
    53             } elseif ( $opt == 'Mbr' ) {
    54                 $mbr = 'selected';
    55                 $pvt = '';
    56             } elseif ( $opt == 'Exc' ) {
    57                 $exc = 'selected';
    58                 $pvt = '';
    59             }
    60         }
    61         echo "<td><select id='rootspersona_plugin[privacy_living]' name='rootspersona_plugin[privacy_living]'><option value='Exc' " . esc_attr( $exc ) . ">Exclude FROM site</option>";
    62         echo "<option value='Pvt' " . esc_attr( $pvt ) . ">" . esc_html__( 'Private, Admins Only', 'rootspersona' ) . "&#160;</option>";
    63         echo "<option value='Mbr' " . esc_attr( $mbr ) . ">" . esc_html__( 'Members Only', 'rootspersona' ) . "</option>";
    64         echo "<option value='Pub' " . esc_attr( $pub ) . ">" . esc_html__( 'Public', 'rootspersona' ) . "</option></select></td>";
    65         echo "<td>" . esc_html__( 'Privacy Setting to use for living people (no death date and birth date greater than 110 years ago)', 'rootspersona' ) . ".</td></tr>";
    66         echo "<tr valign='top'>";
    67         echo "<td scope='row' class='left-label'><label for='rootspersona_plugin[privacy_default]'>" . esc_html__( 'Default Privacy Setting', 'rootspersona' ) . "</label></td>";
    68         $opt = $options['privacy_default'];
    69         $pub = 'selected';
    70         $mbr = '';
    71         $pvt = '';
    72         if ( isset( $opt ) ) {
    73             if ( $opt == 'Pvt' ) {
    74                 $pvt = 'selected';
    75                 $pub = '';
    76             } elseif ( $opt == 'Mbr' ) {
    77                 $mbr = 'selected';
    78                 $pub = '';
    79             }
    80         }
    81         echo "<td><select id='rootspersona_plugin[privacy_default]' name='rootspersona_plugin[privacy_default]'><option value='Pvt' " . esc_attr( $pvt ) . ">" . esc_html__( 'Private, Admins Only', 'rootspersona' ) . "&#160;</option>";
    82         echo "<option value='Mbr' " . esc_attr( $mbr ) . ">" . esc_html__( 'Members Only', 'rootspersona' ) . "</option>";
    83         echo "<option value='Pub' " . esc_attr( $pub ) . ">" . esc_html__( 'Public', 'rootspersona' ) . "</option></select></td>";
    84         echo "<td>" . esc_html__( 'Privacy Setting to use when nothing else applies', 'rootspersona' ) . ".</td></tr>";
    85         echo "<tr valign='top' class='left-label'>";
    86         echo "<td scope='row' class='left=label' ><label for='rootspersona_plugin[hide_dates]'>" . esc_html__( 'Hide Dates', 'rootspersona' ) . "?</label></td>";
    87         $yes = $options['hide_dates'];
    88         if ( isset( $yes ) && $yes == '1' ) {
    89             $yes = 'checked';
    90             $no = '';
    91         } else {
    92             $yes = '';
    93             $no = 'checked';
    94         }
    95         echo "<td><input type='radio' id='rootspersona_plugin[hide_dates]' name='rootspersona_plugin[hide_dates]' value='1' " . esc_attr( $yes ) . ">" . esc_html__( 'Yes', 'rootspersona' );
    96         echo "&#160;<input type='radio' name='rootspersona_plugin[hide_dates]' value='0' " . esc_attr( $no ) . ">" . esc_html__( 'No', 'rootspersona' ) . "</td>";
    97         /* translators: %s: context (e.g. persona). */
    98         echo "<td>" . Rootspersona_I18n::sprintf( esc_html__( 'Some people may want to hide dates for privacy purposes.  This is a global flag (impacts all %s pages).', 'rootspersona' ), 'persona' ) . "</td></tr>";
    99         echo "<tr valign='top' class='left-label'>";
    100         echo "<td scope='row' class='left=label' ><label for='rootspersona_plugin[hide_places]'>" . esc_html__( 'Hide Locations', 'rootspersona' ) . "?</label></td>";
    101         $yes = $options['hide_places'];
    102         if ( isset( $yes ) && $yes == '1' ) {
    103             $yes = 'checked';
    104             $no = '';
    105         } else {
    106             $yes = '';
    107             $no = 'checked';
    108         }
    109         echo "<td><input type='radio' id='rootspersona_plugin[hide_places]' name='rootspersona_plugin[hide_places]' value='1' " . esc_attr( $yes ) . ">" . esc_html__( 'Yes', 'rootspersona' );
    110         echo "&#160;<input type='radio' name='rootspersona_plugin[hide_places]' value='0' " . esc_attr( $no ) . ">" . esc_html__( 'No', 'rootspersona' ) . "</td>";
    111         /* translators: %s: context (e.g. persona). */
    112         echo "<td>" . Rootspersona_I18n::sprintf( esc_html__( 'Some people may want to hide locations for privacy purposes.  This is a global flag (impacts all %s pages).', 'rootspersona' ), 'persona' ) . "</td></tr>";
    113         echo "<tr><td colspan='3'><span class='optionsHdr'>" . esc_html__( 'Display Options', 'rootspersona' ) . "</span><hr class='optionsHdr'></span></td></tr>";
    114 
    115         // Header Style
    116         echo "<tr valign='top' class='left-label'>";
    117         echo "<td scope='row' class='left=label' ><label for='rootspersona_plugin[header_style]'>" . esc_html__( 'Header Style', 'rootspersona' ) . "?</label></td>";
    118         $opt1 = $options['header_style'];
    119         if ( isset( $opt1 ) && $opt1 == '1' ) {
    120             $opt1 = 'checked';
    121             $opt2 = '';
    122         } else {
    123             $opt1 = '';
    124             $opt2 = 'checked';
    125         }
    126         echo "<td><input type='radio' id='rootspersona_plugin[header_style]' name='rootspersona_plugin[header_style]' value='1' " . esc_attr( $opt1 ) . ">" . esc_html__( 'Original', 'rootspersona' );
    127         echo "&#160;<input type='radio' name='rootspersona_plugin[header_style]' value='2' " . esc_attr( $opt2 ) . ">" . esc_html__( 'Bio', 'rootspersona' ) . "</td>";
    128         echo "<td>" . esc_html__( 'Choose a style for the header panel', 'rootspersona' ) . ".</td></tr>";
    129 
    130         // Header
    131         echo "<tr valign='top'>";
    132         echo "<td scope='row' class='left-label'><label for='rootspersona_plugin[hide_header]'>" . esc_html__( 'Hide Header', 'rootspersona') . "?</label></td>";
    133         $yes = $options['hide_header'];
    134         if (isset($yes)
    135             && $yes == '1'
    136         ) {
    137             $yes = 'checked';
    138             $no = '';
    139         } else {
    140             $yes = '';
    141             $no = 'checked';
    142         }
    143         echo "<td><input type='radio' id='rootspersona_plugin[hide_header]' name='rootspersona_plugin[hide_header]' value='1' " . esc_attr( $yes ) . ">" . esc_html__( 'Yes', 'rootspersona');
    144         echo "&#160;<input type='radio' name='rootspersona_plugin[hide_header]' value='0' " . esc_attr( $no ) . ">" . esc_html__( 'No', 'rootspersona') . "</td>";
    145         echo "<td>" . esc_html__( 'Skip the Header Panel on persona pages.', 'rootspersona') . "</td></tr>";
    146 
    147         // Bio
    148         echo "<tr valign='top'>";
    149         echo "<td scope='row' class='left-label'><label for='rootspersona_plugin[hide_bio]'>" . esc_html__( 'Hide Bio', 'rootspersona') . "?</label></td>";
    150         $yes = $options['hide_bio'];
    151         if (isset($yes)
    152             && $yes == '1'
    153         ) {
    154             $yes = 'checked';
    155             $no = '';
    156         } else {
    157             $yes = '';
    158             $no = 'checked';
    159         }
    160         echo "<td><input type='radio' id='rootspersona_plugin[hide_bio]' name='rootspersona_plugin[hide_bio]' value='1' " . esc_attr( $yes ) . ">" . esc_html__( 'Yes', 'rootspersona');
    161         echo "&#160;<input type='radio' name='rootspersona_plugin[hide_bio]' value='0' " . esc_attr( $no ) . ">" . esc_html__( 'No', 'rootspersona') . "</td>";
    162         echo "<td>" . esc_html__( 'Skip the Biography panel on persona pages', 'rootspersona') . ".</td></tr>";
    163 
    164         // Facts
    165         echo "<tr valign='top'>";
    166         echo "<td scope='row' class='left-label'><label for='rootspersona_plugin[hide_facts]'>" . esc_html__( 'Hide Facts', 'rootspersona') . "?</label></td>";
    167         $yes = $options['hide_facts'];
    168         if (isset($yes)
    169             && $yes == '1'
    170         ) {
    171             $yes = 'checked';
    172             $no = '';
    173         } else {
    174             $yes = '';
    175             $no = 'checked';
    176         }
    177         echo "<td><input type='radio' id='rootspersona_plugin[hide_facts]' name='rootspersona_plugin[hide_facts]' value='1' " . esc_attr( $yes ) . ">" . esc_html__( 'Yes', 'rootspersona');
    178         echo "&#160;<input type='radio' name='rootspersona_plugin[hide_facts]' value='0' " . esc_attr( $no ) . ">" . esc_html__( 'No', 'rootspersona') . "</td>";
    179         echo "<td>" . esc_html__( 'Skip the Facts panel on persona pages', 'rootspersona') . ".</td></tr>";
    180 
    181         // Ancestors
    182         echo "<tr valign='top'>";
    183         echo "<td scope='row' class='left-label'><label for='rootspersona_plugin[hide_ancestors]'>" . esc_html__( 'Hide Ancestors', 'rootspersona') . "?</label></td>";
    184         $yes = $options['hide_ancestors'];
    185         if (isset($yes)
    186             && $yes == '1'
    187         ) {
    188             $yes = 'checked';
    189             $no = '';
    190         } else {
    191             $yes = '';
    192             $no = 'checked';
    193         }
    194         echo "<td><input type='radio' id='rootspersona_plugin[hide_ancestors]' name='rootspersona_plugin[hide_ancestors]' value='1' " . esc_attr( $yes ) . ">" . esc_html__( 'Yes', 'rootspersona');
    195         echo "&#160;<input type='radio' name='rootspersona_plugin[hide_ancestors]' value='0' " . esc_attr( $no ) . ">" . esc_html__( 'No', 'rootspersona') . "</td>";
    196         echo "<td>" . esc_html__( 'Skip the Ancestors panel on persona pages', 'rootspersona') . ".</td></tr>";
    197 
    198         // Descendancy
    199         echo "<tr valign='top'>";
    200         echo "<td scope='row' class='left-label'><label for='rootspersona_plugin[hide_descendancy]'>" . esc_html__( 'Hide Descendancy', 'rootspersona') . "?</label></td>";
    201         $yes = $options['hide_descendancy'];
    202         if (isset($yes)
    203             && $yes == '1'
    204         ) {
    205             $yes = 'checked';
    206             $no = '';
    207         } else {
    208             $yes = '';
    209             $no = 'checked';
    210         }
    211         echo "<td><input type='radio' id='rootspersona_plugin[hide_descendancy]' name='rootspersona_plugin[hide_descendancy]' value='1' " . esc_attr( $yes ) . ">" . esc_html__( 'Yes', 'rootspersona');
    212         echo "&#160;<input type='radio' name='rootspersona_plugin[hide_descendancy]' value='0' " . esc_attr( $no ) . ">" . esc_html__( 'No', 'rootspersona') . "</td>";
    213         echo "<td>" . esc_html__( 'Skip the Descendancy panel on persona pages', 'rootspersona') . ".</td></tr>";
    214 
    215         // Family - Child
    216         echo "<tr valign='top'>";
    217         echo "<td scope='row' class='left-label'><label for='rootspersona_plugin[hide_family_c]'>" . esc_html__( 'Hide Child Family Group', 'rootspersona') . "?</label></td>";
    218         $yes = $options['hide_family_c'];
    219         if (isset($yes)
    220             && $yes == '1'
    221         ) {
    222             $yes = 'checked';
    223             $no = '';
    224         } else {
    225             $yes = '';
    226             $no = 'checked';
    227         }
    228         echo "<td><input type='radio' id='rootspersona_plugin[hide_family_c]' name='rootspersona_plugin[hide_family_c]' value='1' " . esc_attr( $yes ) . ">" . esc_html__( 'Yes', 'rootspersona');
    229         echo "&#160;<input type='radio' name='rootspersona_plugin[hide_family_c]' value='0' " . esc_attr( $no ) . ">" . esc_html__( 'No', 'rootspersona') . "</td>";
    230         echo "<td>" . esc_html__( 'Skip the Family Group panel where the person is a Child on persona pages', 'rootspersona') . ".</td></tr>";
    231 
    232         // Family - Spouse
    233         echo "<tr valign='top'>";
    234         echo "<td scope='row' class='left-label'><label for='rootspersona_plugin[hide_family_s]'>" . esc_html__( 'Hide Spousal Family Groups', 'rootspersona') . "?</label></td>";
    235         $yes = $options['hide_family_s'];
    236         if (isset($yes)
    237             && $yes == '1'
    238         ) {
    239             $yes = 'checked';
    240             $no = '';
    241         } else {
    242             $yes = '';
    243             $no = 'checked';
    244         }
    245         echo "<td><input type='radio' id='rootspersona_plugin[hide_family_s]' name='rootspersona_plugin[hide_family_s]' value='1' " . esc_attr( $yes ) . ">" . esc_html__( 'Yes', 'rootspersona');
    246         echo "&#160;<input type='radio' name='rootspersona_plugin[hide_family_s]' value='0' " . esc_attr( $no ) . ">" . esc_html__( 'No', 'rootspersona') . "</td>";
    247         echo "<td>" . esc_html__( 'Skip the Family Group panels where the person is a Spouse on persona pages', 'rootspersona') . ".</td></tr>";
    248         echo "<tr valign='top'>";
    249         echo "<td scope='row' class='left-label'><label for='rootspersona_plugin[hide_pictures]'>" . esc_html__( 'Hide Picture', 'rootspersona') . "?</label></td>";
    250         $yes = $options['hide_pictures'];
    251         if (isset($yes)
    252             && $yes == '1'
    253         ) {
    254             $yes = 'checked';
    255             $no = '';
    256         } else {
    257             $yes = '';
    258             $no = 'checked';
    259         }
    260         echo "<td><input type='radio' id='rootspersona_plugin[hide_pictures]' name='rootspersona_plugin[hide_pictures]' value='1' " . esc_attr( $yes ) . ">" . esc_html__( 'Yes', 'rootspersona');
    261         echo "&#160;<input type='radio' name='rootspersona_plugin[hide_pictures]' value='0' " . esc_attr( $no ) . ">" . esc_html__( 'No', 'rootspersona') . "</td>";
    262         /* translators: %s: context (e.g. persona). */
    263         echo "<td>" . Rootspersona_I18n::sprintf( esc_html__( 'Skip the pictures panel on %s pages. Still displays the image in the Header panel', 'rootspersona' ), 'persona' ) . ".</td></tr>";
    264         echo "<tr valign='top'>";
    265         echo "<td scope='row' class='left-label'><label for='rootspersona_plugin[hide_evidence]'>" . esc_html__( 'Hide Evidence', 'rootspersona') . "?</label></td>";
    266         $yes = $options['hide_evidence'];
    267         if (isset($yes)
    268             && $yes == '1'
    269         ) {
    270             $yes = 'checked';
    271             $no = '';
    272         } else {
    273             $yes = '';
    274             $no = 'checked';
    275         }
    276         echo "<td><input type='radio' id='rootspersona_plugin[hide_evidence]' name='rootspersona_plugin[hide_evidence]' value='1' " . esc_attr( $yes ) . ">" . esc_html__( 'Yes', 'rootspersona');
    277         echo "&#160;<input type='radio' name='rootspersona_plugin[hide_evidence]' value='0' " . esc_attr( $no ) . ">" . esc_html__( 'No', 'rootspersona') . "</td>";
    278         echo "<td>" . esc_html__( 'Skip the evidence panel in persona pages.', 'rootspersona') . "</td></tr>";
    279 
    280         echo "<tr valign='top'>";
    281         echo "<td scope='row' class='left-label'><label for='rootspersona_plugin[hide_edit_links]'>" . esc_html__( 'Hide Edit Links', 'rootspersona') . "?</label></td>";
    282         $yes = $options['hide_edit_links'];
    283         if (isset($yes)
    284             && $yes == '1'
    285         ) {
    286             $yes = 'checked';
    287             $no = '';
    288         } else {
    289             $yes = '';
    290             $no = 'checked';
    291         }
    292         echo "<td><input type='radio' id='rootspersona_plugin[hide_edit_links]' name='rootspersona_plugin[hide_edit_links]' value='1' " . esc_attr( $yes ) . ">" . esc_html__( 'Yes', 'rootspersona');
    293         echo "&#160;<input type='radio' name='rootspersona_plugin[hide_edit_links]' value='0' " . esc_attr( $no ) . ">" . esc_html__( 'No', 'rootspersona') . "</td>";
    294         /* translators: %s: context (e.g. persona). */
    295         echo "<td>" . Rootspersona_I18n::sprintf( esc_html__( 'Some people may want to hide the edit links at the bottom of the %s page', 'rootspersona' ), 'persona' ) . ".</td></tr>";
    296 
    297         echo "<tr valign='top'>";
    298         echo "<td scope='row' class='left-label'><label for='rootspersona_plugin[hide_undef_pics]'>" . esc_html__( 'Hide Silhouettes', 'rootspersona') . "?</label></td>";
    299         $yes = $options['hide_undef_pics'];
    300         if (isset($yes)
    301             && $yes == '1'
    302         ) {
    303             $yes = 'checked';
    304             $no = '';
    305         } else {
    306             $yes = '';
    307             $no = 'checked';
    308         }
    309         echo "<td><input type='radio' id='rootspersona_plugin[hide_undef_pics]' name='rootspersona_plugin[hide_undef_pics]' value='1' " . esc_attr( $yes ) . ">" . esc_html__( 'Yes', 'rootspersona');
    310         echo "&#160;<input type='radio' name='rootspersona_plugin[hide_undef_pics]' value='0' " . esc_attr( $no ) . ">" . esc_html__( 'No', 'rootspersona') . "</td>";
    311         echo "<td>" . esc_html__( 'Some people may want to hide the silhouette placeholders in the picture panel', 'rootspersona') . ".</td></tr>";
    312 
    313         // Misc
    314         echo "<tr><td colspan='3'><span class='optionsHdr'>" . esc_html__( 'Utility Pages', 'rootspersona') . "</span><hr class='optionsHdr'></span></td></tr>";
    315 
    316         echo "<tr valign='top'>";
    317         echo "<td scope='row' class='left-label'><label for='parent_page'>" . esc_html__( 'Parent Page Id', 'rootspersona') . "</label></td>";
    318         echo "<td><input type='text' size='5' name='rootspersona_plugin[parent_page]' id='parent_page'";
    319         echo " value='" . esc_attr( isset( $options['parent_page'] ) ? $options['parent_page'] : '0' ) . "'/></td>";
    320         echo "<td><a href='" . esc_url( $options['home_url'] . '?page_id=' . $options['parent_page'] ) . "'>" . esc_html__( 'Page', 'rootspersona' ) . "</a> ";
    321         /* translators: %s: context (e.g. persona). */
    322         echo Rootspersona_I18n::sprintf( esc_html__( 'you want %s pages to be organized under in a menu structure.  0 indicates no parent page', 'rootspersona' ), 'persona' ) . ".</td></tr>";
    323 
    324         echo "<tr><td colspan='3'><span class='optionsHdr'>" . esc_html__( 'Style Options', 'rootspersona') . "</span><hr class='optionsHdr'></span></td></tr>";
    325 
    326         echo "<tr valign='top'>";
    327         echo "<td scope='row' class='left-label'><label for='banner_fcolor'>" . esc_html__( 'Banner Foreground Color', 'rootspersona') . "</label></td>";
    328         echo "<td colspan='2'>color:<br/><input type='text' size='8' name='rootspersona_plugin[banner_fcolor]' id='banner_fcolor'";
    329         echo " value='" . esc_attr( isset( $options['banner_fcolor'] ) ? $options['banner_fcolor'] : '' ) . "'/>;</td>";
    330         //echo "<td>" . Rootspersona_I18n::sprintf( __( 'Overrides the default foreground color for the banners between %s panels.', 'rootspersona' ), "persona" ) . "</td></tr>";
    331 
    332         echo "<tr valign='top'>";
    333         echo "<td scope='row' class='left-label'><label for='banner_bcolor'>" . esc_html__( 'Banner Background Color', 'rootspersona') . "</label></td>";
    334         echo "<td colsspan='2'>background-color:<br/><input type='text' size='8' name='rootspersona_plugin[banner_bcolor]' id='banner_bcolor'";
    335         echo " value='" . esc_attr( isset( $options['banner_bcolor'] ) ? $options['banner_bcolor'] : '' ) . "'/>;</td>";
    336         // echo "<td>" . Rootspersona_I18n::sprintf( __( 'Overrides the default background image for the banners between %s panels. If both color and image are provided, color will take precedence', 'rootspersona' ), "persona" ) . ".</td></tr>";
    337 
    338         echo "<tr valign='top'>";
    339         echo "<td scope='row' class='left-label'><label for='banner_image'>" . esc_html__( 'Banner Image', 'rootspersona') . "</label></td>";
    340         echo "<td colspan='2'>background-image:<br/>url('<input type='text' size='65' name='rootspersona_plugin[banner_image]' id='banner_image'";
    341         echo " value='" . esc_attr( isset( $options['banner_image'] ) ? $options['banner_image'] : '' ) . "'/>');</td>";
    342         //echo "<td>" . Rootspersona_I18n::sprintf( __( 'Overrides the default background image for the banners between %s panels. If both color and image are provided, color will take precedence', 'rootspersona' ), "persona" ) . ".</td></tr>";
    343 
    344 
    345         echo "<tr valign='top'>";
    346         echo "<td scope='row' class='left-label'><label for='group_fcolor'>" . esc_html__( 'Group Foreground Color', 'rootspersona') . "</label></td>";
    347         echo "<td colspan='2'>color:<br/><input type='text' size='8' name='rootspersona_plugin[group_fcolor]' id='group_fcolor'";
    348         echo " value='" . esc_attr( isset( $options['group_fcolor'] ) ? $options['group_fcolor'] : '' ) . "'/>;</td>";
    349         //echo "<td>" . Rootspersona_I18n::sprintf( __( 'Overrides the default foreground color for the banners between %s panels.', 'rootspersona' ), "persona" ) . "</td></tr>";
    350 
    351         echo "<tr valign='top'>";
    352         echo "<td scope='row' class='left-label'><label for='group_bcolor'>" . esc_html__( 'Group Background Color', 'rootspersona') . "</label></td>";
    353         echo "<td colsspan='2'>background-color:<br/><input type='text' size='8' name='rootspersona_plugin[group_bcolor]' id='group_bcolor'";
    354         echo " value='" . esc_attr( isset( $options['group_bcolor'] ) ? $options['group_bcolor'] : '' ) . "'/>;</td>";
    355         // echo "<td>" . Rootspersona_I18n::sprintf( __( 'Overrides the default background image for the banners between %s panels. If both color and image are provided, color will take precedence', 'rootspersona' ), "persona" ) . ".</td></tr>";
    356 
    357         echo "<tr valign='top'>";
    358         echo "<td scope='row' class='left-label'><label for='group_image'>" . esc_html__( 'Group Image', 'rootspersona') . "</label></td>";
    359         echo "<td colspan='2'>background-image:<br/>url('<input type='text' size='65' name='rootspersona_plugin[group_image]' id='group_image'";
    360         echo " value='" . esc_attr( isset( $options['group_image'] ) ? $options['group_image'] : '' ) . "'/>');</td>";
    361         //echo "<td>" . Rootspersona_I18n::sprintf( __( 'Overrides the default background image for the banners between %s panels. If both color and image are provided, color will take precedence', 'rootspersona' ), "persona" ) . ".</td></tr>";
    362 
    363         echo "<tr valign='top'>";
    364         echo "<td scope='row' class='left-label'><label for='pframe_color'>" . esc_html__( 'Persona Frames', 'rootspersona') . "</label></td>";
    365         echo "<td colspan='2'>border-color:<br/><input type='text' size='8' name='rootspersona_plugin[pframe_color]' id='pframe_color'";
    366         echo " value='" . esc_attr( isset( $options['pframe_color'] ) ? $options['pframe_color'] : '' ) . "'/>;</td>";
    367         //echo "<td>" . Rootspersona_I18n::sprintf( __( 'Overrides the default background image for the banners between %s panels. If both color and image are provided, color will take precedence', 'rootspersona' ), "persona" ) . ".</td></tr>";
    368 
    369         echo "<tr valign='top'>";
    370         echo "<td scope='row' class='left-label'><label for='index_even_color'>" . esc_html__( 'Index Even Row Color', 'rootspersona') . "</label></td>";
    371         echo "<td colsspan='2'>background-color:<br/><input type='text' size='8' name='rootspersona_plugin[index_even_color]' id='index_even_color'";
    372         echo " value='" . esc_attr( isset( $options['index_even_color'] ) ? $options['index_even_color'] : '' ) . "'/>;</td>";
    373         // echo "<td>" . Rootspersona_I18n::sprintf( __( 'Overrides the default background image for the banners between %s panels. If both color and image are provided, color will take precedence', 'rootspersona' ), "persona" ) . ".</td></tr>";
    374 
    375         echo "<tr valign='top'>";
    376         echo "<td scope='row' class='left-label'><label for='index_odd_color'>" . esc_html__( 'Index Odd Row Color', 'rootspersona') . "</label></td>";
    377         echo "<td colsspan='2'>background-color:<br/><input type='text' size='8' name='rootspersona_plugin[index_odd_color]' id='index_odd_color'";
    378         echo " value='" . esc_attr( isset( $options['index_odd_color'] ) ? $options['index_odd_color'] : '' ) . "'/>;</td>";
    379         // echo "<td>" . Rootspersona_I18n::sprintf( __( 'Overrides the default background image for the banners between %s panels. If both color and image are provided, color will take precedence', 'rootspersona' ), "persona" ) . ".</td></tr>";
    380 
    381         echo "<tr valign='top'>";
    382         echo "<td scope='row' class='left-label'><label for='index_hdr_color'>" . esc_html__( 'Index Header Row Color', 'rootspersona') . "</label></td>";
    383         echo "<td colsspan='2'>background-color:<br/><input type='text' size='8' name='rootspersona_plugin[index_hdr_color]' id='index_hdr_color'";
    384         echo " value='" . esc_attr( isset( $options['index_hdr_color'] ) ? $options['index_hdr_color'] : '' ) . "'/>;</td>";
    385         // echo "<td>" . Rootspersona_I18n::sprintf( __( 'Overrides the default background image for the banners between %s panels. If both color and image are provided, color will take precedence', 'rootspersona' ), "persona" ) . ".</td></tr>";
    386 
    387         // Raw custom CSS input (custom_style) was removed for security. Page colours are
    388         // configured through the dedicated, validated colour fields above; use the
    389         // WordPress Customizer/Additional CSS for any further site-wide styling.
    390 
    391         echo "<tr><td colspan='3'><span class='optionsHdr'>" . esc_html__( 'Misc Options', 'rootspersona') . "</span><hr class='optionsHdr'></span></td></tr>";
    392 
    393         echo "<tr valign='top'>";
    394         echo "<td scope='row' class='left=label' ><label for='rootspersona_plugin[hide_donation]'>" . esc_html__( 'Hide Donation Button', 'rootspersona') . "?</label></td>";
    395         $yes = isset($options['hide_donation']) ? $options['hide_donation'] : 0;
    396         if (isset($yes)
    397             && $yes == 1
    398         ) {
    399             $yes = 'checked';
    400             $no = '';
    401         } else {
    402             $yes = '';
    403             $no = 'checked';
    404         }
    405         echo "<td><input type='checkbox' id='rootspersona_plugin[hide_donation]' name='rootspersona_plugin[hide_donation]' value='true' " . esc_attr( $yes ) . ">&#160;&#160;Yes </td>";
    406 
    407         echo "<td>" . esc_html__( 'Hide the PayPal Donation button', 'rootspersona') . ".</td></tr>";
    408         /*
    409                 echo "<tr valign='top'>";
    410                 echo "<td scope='row' class='left-label'><label for='rootspersona_plugin[post_page]'>" . __( 'Create Page or Post', 'rootspersona' ) . "?</label></td>";
    411                 $yes = $options['post_page'];
    412                 if ( isset( $yes )
    413                     && $yes == '1' ) {
    414                     $yes = 'checked';
    415                     $no = '';
    416                 } else {
    417                     $yes = '';
    418                     $no = 'checked';
    419                 }
    420                 echo "<td><input type='radio' id='rootspersona_plugin[post_page]' name='rootspersona_plugin[post_page]' value='1' " . esc_attr( $yes ) . ">" . esc_html__( 'Post','rootspersona');
    421                 echo "&#160;<input type='radio' name='rootspersona_plugin[post_page]' value='0' " . esc_attr( $no ) . ">" . esc_html__( 'Page','rootspersona') . "</td>";
    422                 echo "<td>" . Rootspersona_I18n::sprintf( __( 'Rootspersona can create a Page or Post', 'rootspersona' ), "persona" ) . ".</td></tr>";
    423         */
    424         echo "<tr valign='top'>";
    425         echo "<td scope='row' class='left=label' ><label for='per_page'>" . esc_html__( 'Entries Per Page', 'rootspersona') . "</label></td>";
    426         echo "<td><input type='text' size='5' name='rootspersona_plugin[per_page]' id='per_page'";
    427         echo " value='" . esc_attr( isset( $options['per_page'] ) ? $options['per_page'] : '' ) . "'/></td>";
    428         echo "<td>" . esc_html__( 'Indicates the number of rows on an index page. Default is 25.', 'rootspersona') . ".</td></tr>";
    429 
    430         echo "<tr valign='top'>";
    431         echo "<td scope='row' class='left=label' ><label for='rootspersona_plugin[is_system_of_record]'>" . esc_html__( 'System Of Record', 'rootspersona') . "?</label></td>";
    432         $yes = isset( $options['is_system_of_record'] ) ? $options['is_system_of_record'] : 0;
    433         if (isset($yes)
    434             && $yes == 1
    435         ) {
    436             $yes = 'checked';
    437             $no = '';
    438         } else {
    439             $yes = '';
    440             $no = 'checked';
    441         }
    442         echo "<td><input type='radio' id='rootspersona_plugin[is_system_of_record]' name='rootspersona_plugin[is_system_of_record]' value='true' " . esc_attr( $yes ) . ">Yes ";
    443         echo "&#160;<input type='radio' name='rootspersona_plugin[is_system_of_record]' value='false' " . esc_attr( $no ) . ">No </td>";
    444         echo "<td>"
    445             . esc_html__( 'Yes indicates data may be entered from this website. Editing data from this website AND importing GEDCOM data is NOT recommended.', 'rootspersona')
    446             . ".</td></tr>";
    447 
    448         echo "<tr valign='top'>";
    449         echo "<td scope='row' class='left=label' ><label for='rootspersona_plugin[is_editor]'>" . esc_html__( 'Allow Editor', 'rootspersona') . "?</label></td>";
    450         $yes = isset($options['is_editor']) ? $options['is_editor'] : 0;
    451         if (isset($yes)
    452             && $yes == 1
    453         ) {
    454             $yes = 'checked';
    455             $no = '';
    456         } else {
    457             $yes = '';
    458             $no = 'checked';
    459         }
    460         echo "<td><input type='checkbox' id='rootspersona_plugin[is_editor]' name='rootspersona_plugin[is_editor]' value='true' " . esc_attr( $yes ) . ">&#160;&#160;Yes </td>";
    461 
    462         echo "<td>" . esc_html__( 'Allow Editor to edit persona pages', 'rootspersona') . ".</td></tr>";
    463 
    464         // Custom page shortcode/raw content input (custom_page) was removed for security:
    465         // persona pages are generated only from the plugin's built-in, structured template.
    466 
    467         echo "</table><p class='submit'>";
    468         echo "<input type='submit' name='Submit' value=' " . esc_html__( 'Save Changes', 'rootspersona') . " '/>";
    469         echo '</p></form></div>';
    470     }
     15        /**
     16         * @param array $options
     17         */
     18        function build( $options ) {
     19                if ( ! is_array( $options ) ) {
     20                        $options = array();
     21                }
     22                echo "<div class='wrap rp-settings'>";
     23                $this->header( $options );
     24
     25                echo '<form method="post" action="' . esc_url( admin_url( 'options.php' ) ) . '">';
     26                // Nonce + hidden option_page must be inside the form before any submit.
     27                settings_fields( 'rootspersona_plugin' );
     28
     29                $tabs = array(
     30                        'privacy' => __( 'Privacy', 'rootspersona' ),
     31                        'display' => __( 'Persona page', 'rootspersona' ),
     32                        'colours' => __( 'Colours', 'rootspersona' ),
     33                        'pages'   => __( 'Pages and other', 'rootspersona' ),
     34                );
     35                echo "<nav class='nav-tab-wrapper rp-tabs' aria-label='" . esc_attr__( 'Settings sections', 'rootspersona' ) . "'>";
     36                foreach ( $tabs as $slug => $label ) {
     37                        echo "<a href='#rp-tab-" . esc_attr( $slug ) . "' class='nav-tab' data-tab='" . esc_attr( $slug ) . "'>" . esc_html( $label ) . '</a>';
     38                }
     39                echo '</nav>';
     40
     41                $this->open_panel( 'privacy', $tabs['privacy'] );
     42                $this->privacy_rows( $options );
     43                $this->close_panel();
     44
     45                $this->open_panel( 'display', $tabs['display'] );
     46                $this->display_rows( $options );
     47                $this->close_panel();
     48
     49                $this->open_panel( 'colours', $tabs['colours'] );
     50                $this->colour_rows( $options );
     51                $this->close_panel();
     52
     53                $this->open_panel( 'pages', $tabs['pages'] );
     54                $this->page_rows( $options );
     55                $this->close_panel();
     56
     57                submit_button();
     58                echo '</form></div>';
     59        }
     60
     61        /**
     62         * Logo, version and the donation link.
     63         */
     64        private function header( $options ) {
     65                $banner = isset( $options['plugin_url'] )
     66                        ? trailingslashit( $options['plugin_url'] ) . 'images/bannerstartwithwp.png'
     67                        : plugins_url( 'images/bannerstartwithwp.png', ROOTSPERSONA_PLUGIN_FILE );
     68                echo "<h1 class='screen-reader-text'>RootsPersona</h1>";
     69                echo "<div class='rp-settings__head'>";
     70                echo "<a href='https://rootspersona.com'><img src='" . esc_url( $banner ) . "' alt='RootsPersona'/></a>";
     71                /* translators: %s: plugin version. */
     72                echo "<span class='rp-settings__version'>" . esc_html( Rootspersona_I18n::sprintf( __( 'Version %s', 'rootspersona' ), isset( $options['version'] ) ? $options['version'] : '' ) ) . '</span>';
     73                if ( empty( $options['hide_donation'] ) ) {
     74                        echo "<span class='rp-settings__donate'>" . esc_html__( 'A fan of RootsPersona? Consider making a donation!', 'rootspersona' )
     75                                . " <a class='button button-secondary' href='" . esc_url( 'https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=ANLEXXNXKFN9N' ) . "'>"
     76                                . esc_html__( 'Donate', 'rootspersona' ) . '</a></span>';
     77                }
     78                echo '</div>';
     79        }
     80
     81        private function open_panel( $slug, $title ) {
     82                echo "<div class='rp-tab-panel' id='rp-tab-" . esc_attr( $slug ) . "'>";
     83                echo "<h2 class='rp-tab-panel__title'>" . esc_html( $title ) . '</h2>';
     84                echo "<table class='form-table' role='presentation'>";
     85        }
     86
     87        private function close_panel() {
     88                echo '</table></div>';
     89        }
     90
     91        /**
     92         * Start of a row: <tr><th><label/></th><td>.
     93         */
     94        private function row_start( $label, $for = '', $class = '' ) {
     95                echo '<tr' . ( $class ? " class='" . esc_attr( $class ) . "'" : '' ) . "><th scope='row'>";
     96                if ( $for ) {
     97                        echo "<label for='" . esc_attr( $for ) . "'>" . esc_html( $label ) . '</label>';
     98                } else {
     99                        echo esc_html( $label );
     100                }
     101                echo '</th><td>';
     102        }
     103
     104        private function row_end( $description = '' ) {
     105                if ( '' !== $description ) {
     106                        echo "<p class='description'>" . esc_html( $description ) . '</p>';
     107                }
     108                echo '</td></tr>';
     109        }
     110
     111        /**
     112         * "Show …" checkbox for an inverted hide_* option.
     113         *
     114         * @param array  $options
     115         * @param string $key         Option name (hide_*).
     116         * @param string $label       Checkbox label.
     117         * @param string $description Optional hint under the label.
     118         * @param string $hidden      Value stored when hidden.
     119         * @param string $shown       Value stored when shown.
     120         */
     121        private function show_checkbox( $options, $key, $label, $description = '', $hidden = '1', $shown = '0' ) {
     122                $is_hidden = ! empty( $options[ $key ] ) && 'false' !== $options[ $key ];
     123                $name      = 'rootspersona_plugin[' . $key . ']';
     124                $id        = 'rp_show_' . $key;
     125                echo "<input type='hidden' name='" . esc_attr( $name ) . "' value='" . esc_attr( $hidden ) . "'/>";
     126                echo "<label for='" . esc_attr( $id ) . "'><input type='checkbox' id='" . esc_attr( $id ) . "' name='" . esc_attr( $name ) . "' value='" . esc_attr( $shown ) . "'" . checked( ! $is_hidden, true, false ) . '/> '
     127                        . esc_html( $label ) . '</label>';
     128                if ( '' !== $description ) {
     129                        echo " <span class='description'>— " . esc_html( $description ) . '</span>';
     130                }
     131                echo '<br/>';
     132        }
     133
     134        /**
     135         * Plain yes/no checkbox (value "true" when ticked, "false" otherwise).
     136         */
     137        private function yes_checkbox( $options, $key, $label ) {
     138                $on   = ! empty( $options[ $key ] ) && 'false' !== $options[ $key ];
     139                $name = 'rootspersona_plugin[' . $key . ']';
     140                $id   = 'rp_' . $key;
     141                echo "<input type='hidden' name='" . esc_attr( $name ) . "' value='false'/>";
     142                echo "<label for='" . esc_attr( $id ) . "'><input type='checkbox' id='" . esc_attr( $id ) . "' name='" . esc_attr( $name ) . "' value='true'" . checked( $on, true, false ) . '/> '
     143                        . esc_html( $label ) . '</label>';
     144        }
     145
     146        /**
     147         * Ordinary pages for a dropdown: persona pages (thousands on a big tree) are left out,
     148         * the page currently chosen is always kept.
     149         *
     150         * @param int $keep Page ID to keep in the list.
     151         * @return array id => title
     152         */
     153        private function page_choices( $keep = 0 ) {
     154                global $wpdb;
     155                $rows = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- admin-only list, one query.
     156                        $wpdb->prepare(
     157                                "SELECT ID, post_title FROM $wpdb->posts WHERE post_type = 'page' AND post_status IN ('publish','private','draft')"
     158                                . ' AND ( post_content NOT LIKE %s OR ID = %d ) ORDER BY post_title',
     159                                '%' . $wpdb->esc_like( '[rootsPersona ' ) . '%',
     160                                (int) $keep
     161                        )
     162                );
     163                $out = array();
     164                foreach ( (array) $rows as $row ) {
     165                        $out[ (int) $row->ID ] = '' !== $row->post_title ? $row->post_title : '#' . $row->ID;
     166                }
     167                return $out;
     168        }
     169
     170        private function page_select( $name, $id, $selected, $none_label, $choices ) {
     171                echo "<select name='" . esc_attr( $name ) . "' id='" . esc_attr( $id ) . "'>";
     172                echo "<option value='0'>" . esc_html( $none_label ) . '</option>';
     173                foreach ( $choices as $page_id => $title ) {
     174                        echo "<option value='" . esc_attr( $page_id ) . "'" . selected( (int) $selected, $page_id, false ) . '>' . esc_html( $title ) . '</option>';
     175                }
     176                echo '</select>';
     177        }
     178
     179        private function privacy_rows( $options ) {
     180                $levels = array(
     181                        'Pvt' => __( 'Private, Admins Only', 'rootspersona' ),
     182                        'Mbr' => __( 'Members Only', 'rootspersona' ),
     183                        'Pub' => __( 'Public', 'rootspersona' ),
     184                );
     185
     186                $this->row_start( __( 'Living Privacy Setting', 'rootspersona' ), 'rp_privacy_living' );
     187                $living = isset( $options['privacy_living'] ) ? $options['privacy_living'] : 'Pvt';
     188                echo "<select id='rp_privacy_living' name='rootspersona_plugin[privacy_living]'>";
     189                echo "<option value='Exc'" . selected( $living, 'Exc', false ) . '>' . esc_html__( 'Exclude from site', 'rootspersona' ) . '</option>';
     190                foreach ( $levels as $value => $label ) {
     191                        echo "<option value='" . esc_attr( $value ) . "'" . selected( $living, $value, false ) . '>' . esc_html( $label ) . '</option>';
     192                }
     193                echo '</select>';
     194                $this->row_end( __( 'Privacy Setting to use for living people (no death date and birth date greater than 110 years ago)', 'rootspersona' ) . '.' );
     195
     196                $this->row_start( __( 'Default Privacy Setting', 'rootspersona' ), 'rp_privacy_default' );
     197                $default = isset( $options['privacy_default'] ) ? $options['privacy_default'] : 'Pub';
     198                echo "<select id='rp_privacy_default' name='rootspersona_plugin[privacy_default]'>";
     199                foreach ( $levels as $value => $label ) {
     200                        echo "<option value='" . esc_attr( $value ) . "'" . selected( $default, $value, false ) . '>' . esc_html( $label ) . '</option>';
     201                }
     202                echo '</select>';
     203                $this->row_end( __( 'Privacy Setting to use when nothing else applies', 'rootspersona' ) . '.' );
     204
     205                // People with no dates at all (and no telling dates in close family).
     206                $unknown = ( isset( $options['living_unknown'] ) && 'dead' === $options['living_unknown'] ) ? 'dead' : 'living';
     207                $this->row_start( __( 'People without dates', 'rootspersona' ), 'rp_living_unknown' );
     208                echo "<select id='rp_living_unknown' name='rootspersona_plugin[living_unknown]'>"
     209                        . "<option value='living'" . selected( $unknown, 'living', false ) . '>' . esc_html__( 'Treat as living', 'rootspersona' ) . '</option>'
     210                        . "<option value='dead'" . selected( $unknown, 'dead', false ) . '>' . esc_html__( 'Treat as deceased', 'rootspersona' ) . '</option></select>';
     211                $this->row_end( __( 'When neither the person nor the parents, spouses or children have dates that settle it. "Treat as living" is the safe choice: living relatives are often entered without dates.', 'rootspersona' ) );
     212
     213                // Page shown instead of a person the visitor may not see.
     214                $private = isset( $options['private_page'] ) ? (int) $options['private_page'] : 0;
     215                $this->row_start( __( 'Page for private people', 'rootspersona' ), 'rootspersona_private_page' );
     216                $this->page_select( 'rootspersona_plugin[private_page]', 'rootspersona_private_page', $private, __( '— none: show "Private" on the person\'s page —', 'rootspersona' ), $this->page_choices( $private ) );
     217                $this->row_end( __( 'Visitors who may not see a person are sent to this page, for example a page saying "Closed. Private access."', 'rootspersona' ) );
     218
     219                $this->row_start( __( 'Dates and places', 'rootspersona' ) );
     220                echo '<fieldset>';
     221                $this->show_checkbox( $options, 'hide_dates', __( 'Show dates', 'rootspersona' ) );
     222                $this->show_checkbox( $options, 'hide_places', __( 'Show places', 'rootspersona' ) );
     223                echo '</fieldset>';
     224                /* translators: %s: context (e.g. persona). */
     225                $this->row_end( __( 'Untick to hide them on all persona pages, for example for privacy.', 'rootspersona' ) );
     226        }
     227
     228        private function display_rows( $options ) {
     229                $current_template = Rootspersona_Persona_Templates::sanitize( isset( $options['persona_template'] ) ? $options['persona_template'] : '' );
     230                $this->row_start( __( 'Persona page template', 'rootspersona' ), 'rootspersona_plugin_persona_template' );
     231                echo "<select id='rootspersona_plugin_persona_template' name='rootspersona_plugin[persona_template]'>";
     232                foreach ( Rootspersona_Persona_Templates::available() as $slug => $label ) {
     233                        echo "<option value='" . esc_attr( $slug ) . "'" . selected( $current_template, $slug, false ) . '>' . esc_html( $label ) . '</option>';
     234                }
     235                echo '</select>';
     236                $this->row_end( __( 'Layout of the persona page. Archive card and Chronicle take their colours and fonts from the theme (--rp-* CSS variables) and hide sections that have no data. A page can override it with the template attribute of the shortcode.', 'rootspersona' ) );
     237
     238                $this->row_start( __( 'Show on the page', 'rootspersona' ) );
     239                echo '<fieldset>';
     240                $this->show_checkbox( $options, 'hide_bio', __( 'Biography', 'rootspersona' ) );
     241                $this->show_checkbox( $options, 'hide_pictures', __( 'Photos', 'rootspersona' ), __( 'the classic header keeps its picture', 'rootspersona' ) );
     242                echo '</fieldset>';
     243                $this->row_end();
     244
     245                // Panels of the classic page only.
     246                $this->row_start( __( 'Header Style', 'rootspersona' ), '', 'rp-classic-only' );
     247                $style = ( isset( $options['header_style'] ) && 2 === (int) $options['header_style'] ) ? 2 : 1;
     248                echo "<fieldset><label><input type='radio' name='rootspersona_plugin[header_style]' value='1'" . checked( $style, 1, false ) . '/> ' . esc_html__( 'Original', 'rootspersona' ) . '</label><br/>';
     249                echo "<label><input type='radio' name='rootspersona_plugin[header_style]' value='2'" . checked( $style, 2, false ) . '/> ' . esc_html__( 'Bio', 'rootspersona' ) . '</label></fieldset>';
     250                $this->row_end( __( 'Choose a style for the header panel', 'rootspersona' ) . '.' );
     251
     252                $this->row_start( __( 'Classic page panels', 'rootspersona' ), '', 'rp-classic-only' );
     253                echo '<fieldset>';
     254                $this->show_checkbox( $options, 'hide_header', __( 'Header', 'rootspersona' ) );
     255                $this->show_checkbox( $options, 'hide_facts', __( 'Facts', 'rootspersona' ) );
     256                $this->show_checkbox( $options, 'hide_ancestors', __( 'Ancestors', 'rootspersona' ) );
     257                $this->show_checkbox( $options, 'hide_descendancy', __( 'Descendants', 'rootspersona' ) );
     258                $this->show_checkbox( $options, 'hide_family_c', __( 'Family where the person is a child', 'rootspersona' ) );
     259                $this->show_checkbox( $options, 'hide_family_s', __( 'Families where the person is a spouse', 'rootspersona' ) );
     260                $this->show_checkbox( $options, 'hide_evidence', __( 'Evidence (sources)', 'rootspersona' ) );
     261                $this->show_checkbox( $options, 'hide_undef_pics', __( 'Silhouettes in place of missing photos', 'rootspersona' ) );
     262                $this->show_checkbox( $options, 'hide_edit_links', __( 'Edit links at the bottom of the page', 'rootspersona' ) );
     263                echo '</fieldset>';
     264                $this->row_end( __( 'These panels exist only in the Classic template.', 'rootspersona' ) );
     265        }
     266
     267        private function colour_rows( $options ) {
     268                // Colours of the Archive card / Chronicle templates (--rp-* custom properties).
     269                echo Rootspersona_Template_Colors::render_settings_rows( $options ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- every value is escaped inside render_settings_rows().
     270
     271                // Colours and images of the classic panels: only shown while the classic template is selected.
     272                $classic_colors = array(
     273                        'banner_fcolor'    => __( 'Banner Foreground Color', 'rootspersona' ),
     274                        'banner_bcolor'    => __( 'Banner Background Color', 'rootspersona' ),
     275                        'group_fcolor'     => __( 'Group Foreground Color', 'rootspersona' ),
     276                        'group_bcolor'     => __( 'Group Background Color', 'rootspersona' ),
     277                        'pframe_color'     => __( 'Persona Frames', 'rootspersona' ),
     278                        'index_even_color' => __( 'Index Even Row Color', 'rootspersona' ),
     279                        'index_odd_color'  => __( 'Index Odd Row Color', 'rootspersona' ),
     280                        'index_hdr_color'  => __( 'Index Header Row Color', 'rootspersona' ),
     281                );
     282                echo "<tr class='rp-classic-only'><td colspan='2'><p class='description'>" . esc_html__( 'The colours below apply to the Classic template only.', 'rootspersona' ) . '</p></td></tr>';
     283                foreach ( $classic_colors as $key => $label ) {
     284                        $this->row_start( $label, $key, 'rp-classic-only' );
     285                        echo "<input type='text' class='rp-color-field' name='rootspersona_plugin[" . esc_attr( $key ) . "]' id='" . esc_attr( $key ) . "'"
     286                                . " value='" . esc_attr( isset( $options[ $key ] ) ? $options[ $key ] : '' ) . "'/>";
     287                        $this->row_end();
     288                }
     289                $classic_images = array(
     290                        'banner_image' => __( 'Banner Image', 'rootspersona' ),
     291                        'group_image'  => __( 'Group Image', 'rootspersona' ),
     292                );
     293                foreach ( $classic_images as $key => $label ) {
     294                        $this->row_start( $label, $key, 'rp-classic-only' );
     295                        echo "<input type='url' class='regular-text' name='rootspersona_plugin[" . esc_attr( $key ) . "]' id='" . esc_attr( $key ) . "'"
     296                                . " value='" . esc_attr( isset( $options[ $key ] ) ? $options[ $key ] : '' ) . "'/> ";
     297                        echo "<button type='button' class='button rp-media-pick' data-target='" . esc_attr( $key ) . "'>" . esc_html__( 'Choose from Media Library', 'rootspersona' ) . '</button> ';
     298                        echo "<button type='button' class='button-link rp-media-clear' data-target='" . esc_attr( $key ) . "'>" . esc_html__( 'Remove', 'rootspersona' ) . '</button>';
     299                        $this->row_end();
     300                }
     301                // Raw custom CSS input (custom_style) was removed for security; use the
     302                // WordPress Customizer/Additional CSS for any further site-wide styling.
     303        }
     304
     305        private function page_rows( $options ) {
     306                $parent = isset( $options['parent_page'] ) ? (int) $options['parent_page'] : 0;
     307                $this->row_start( __( 'Parent page', 'rootspersona' ), 'rp_parent_page' );
     308                $this->page_select( 'rootspersona_plugin[parent_page]', 'rp_parent_page', $parent, __( '— none —', 'rootspersona' ), $this->page_choices( $parent ) );
     309                if ( $parent > 0 && get_permalink( $parent ) ) {
     310                        echo " <a href='" . esc_url( get_permalink( $parent ) ) . "'>" . esc_html__( 'View page', 'rootspersona' ) . '</a>';
     311                }
     312                /* translators: %s: context (e.g. persona). */
     313                $this->row_end( __( 'New persona pages are created as sub-pages of this page. They are kept out of menus and page lists.', 'rootspersona' ) );
     314
     315                $this->row_start( __( 'Entries Per Page', 'rootspersona' ), 'per_page' );
     316                echo "<input type='number' min='1' max='500' class='small-text' name='rootspersona_plugin[per_page]' id='per_page' value='" . esc_attr( isset( $options['per_page'] ) ? $options['per_page'] : 25 ) . "'/>";
     317                $this->row_end( __( 'Indicates the number of rows on an index page. Default is 25.', 'rootspersona' ) );
     318
     319                $this->row_start( __( 'Editing on the site', 'rootspersona' ) );
     320                echo '<fieldset>';
     321                $this->yes_checkbox( $options, 'is_system_of_record', __( 'Data is entered on this website (system of record)', 'rootspersona' ) );
     322                echo "<p class='description'>" . esc_html__( 'Yes indicates data may be entered from this website. Editing data from this website AND importing GEDCOM data is NOT recommended.', 'rootspersona' ) . '</p>';
     323                $this->yes_checkbox( $options, 'is_editor', __( 'Allow Editor to edit persona pages', 'rootspersona' ) );
     324                echo '</fieldset>';
     325                $this->row_end();
     326
     327                $this->row_start( __( 'Donation button', 'rootspersona' ) );
     328                $this->show_checkbox( $options, 'hide_donation', __( 'Show the PayPal donation button on this page', 'rootspersona' ), '', 'true', 'false' );
     329                $this->row_end();
     330        }
    471331}
  • rootspersona/trunk/php/pages/class-RP-Persona-Page-Builder.php

    r3577169 r3749166  
    2323     * @return string
    2424     */
    25     public function build($persona, $options, $page_id)
    26     {
     25    public function build($persona, $options, $page_id, $part = 'all')
     26    {
     27        // Parts (see Rootspersona_Persona_Templates::resolve_part()) switch whole panels off.
     28        $want_intro  = in_array( $part, array( 'all', 'intro' ), true );
     29        $want_family = in_array( $part, array( 'all', 'family', 'rest' ), true );
     30        $want_photos = in_array( $part, array( 'all', 'photos', 'rest' ), true );
     31        if ( ! $want_intro ) {
     32            $options['hide_header'] = 1;
     33            $options['hide_bio']    = 1;
     34            $options['hide_facts']  = 1;
     35        }
     36        if ( ! $want_family ) {
     37            $options['hide_ancestors']   = 1;
     38            $options['hide_family_c']    = 1;
     39            $options['hide_family_s']    = 1;
     40            $options['hide_descendancy'] = 1;
     41            $options['hide_evidence']    = 1;
     42        }
     43        if ( ! $want_photos ) {
     44            $options['hide_pictures'] = 1;
     45        }
    2746        $block = '';
    2847        if ($options['hide_header'] == 0) {
    … …  
    4463        //    $block .= $creator->create( $persona, $options );
    4564        //}
    46         if ($options['hide_family_c'] == 0 && isset($persona->ancestors)) {
     65        if ($options['hide_family_c'] == 0 && isset($persona->ancestors) && self::has_parental_family($persona)) {
    4766            $block .= Rootspersona_Group_Sheet_Panel_Creator::create_group_child('c0', $persona->ancestors, $persona->siblings, $options);
    4867        }
    … …  
    6483            $block .= $creator->create($persona, $options);
    6584        } // Moved from line 36 by Horacio to show descendancy at the bottom [End]
    66         $block .= Rootspersona_Persona_Helper::get_banner($options, '');
    67         $block .= Rootspersona_Persona_Page_Builder::create_end_of_page($persona->id, $persona->batch_id, $page_id, $options);
     85        if ( $want_family || 'all' === $part ) {
     86            $block .= Rootspersona_Persona_Helper::get_banner($options, '');
     87            $block .= Rootspersona_Persona_Page_Builder::create_end_of_page($persona->id, $persona->batch_id, $page_id, $options);
     88        }
    6889        return $block;
     90    }
     91
     92    /**
     93     * Whether the "Family Group Sheet - Child" panel has anything to show: a known
     94     * parent or a sibling. Without either it was a table of "?" placeholders.
     95     *
     96     * @param Rootspersona_Persona $persona
     97     * @return bool
     98     */
     99    protected static function has_parental_family($persona)
     100    {
     101        foreach (array(2, 3) as $i) {
     102            $parent = isset($persona->ancestors[$i]) ? $persona->ancestors[$i] : null;
     103            if (!empty($parent) && isset($parent->id) && $parent->id !== '' && $parent->id != '0') {
     104                return true;
     105            }
     106        }
     107        // The list of siblings always contains the person; more than one means real siblings.
     108        return isset($persona->siblings) && is_array($persona->siblings) && count($persona->siblings) > 1;
    69109    }
    70110
  • rootspersona/trunk/php/pages/panels/class-RP-Ancestors-Panel-Creator.php

    r3577169 r3749166  
    124124                $tmpDate = '<span itemprop="birth" itemscope itemtype="http://historical-data.org/HistoricalEvent.html">'
    125125                    . (strlen($year) == 4 ? '<span itemprop="startDate" content="' . esc_attr( $year ) . '">' : '')
    126                     . esc_html( preg_replace('/@.*@(.*)/Us', '$1', $bd) ) // Modified by Horacio (lower case s)
     126                    . esc_html( Rootspersona_Date::format( $bd ) )
    127127                . (strlen($year) == 4 ? '</span>' : '')
    128128                . '</span>';
    … …  
    135135                $tmpDate = '<span itemprop="death" itemscope itemtype="http://historical-data.org/HistoricalEvent.html">'
    136136                    . (strlen($year) == 4 ? '<span itemprop="startDate" content="' . esc_attr( $year ) . '">' : '')
    137                     . esc_html( preg_replace('/@.*@(.*)/Us', '$1', $dd) ) // Modified by Horacio (lower case s)
     137                    . esc_html( Rootspersona_Date::format( $dd ) )
    138138                . (strlen($year) == 4 ? '</span>' : '')
    139139                . '</span>';
  • rootspersona/trunk/php/pages/panels/class-RP-Descendancy-Panel-Creator.php

    r3577245 r3749166  
    4747                $tmpDate = '<span itemprop="birth" itemscope itemtype="http://historical-data.org/HistoricalEvent.html">'
    4848                    . (strlen($year) == 4 ? '<span itemprop="startDate" content="' . esc_attr( $year ) . '">' : '')
    49                     . esc_html( preg_replace('/@.*@(.*)/Us', '$1', $bd) )  // Modified by Horacio (lower case s)
     49                    . esc_html( Rootspersona_Date::format( $bd ) )
    5050                    . (strlen($year) == 4 ? '</span>' : '')
    5151                    . '</span>';
    52                 $block .= ' b: ' . $tmpDate;
     52                $block .= ' ' . esc_html_x( 'b:', 'born: short label before a date', 'rootspersona' ) . ' ' . $tmpDate;
    5353            }
    5454            $dd = (string) ( $persona->death_date ?? '' );
    … …  
    5858                $tmpDate = '<span itemprop="death" itemscope itemtype="http://historical-data.org/HistoricalEvent.html">'
    5959                    . (strlen($year) == 4 ? '<span itemprop="startDate" content="' . esc_attr( $year ) . '">' : '')
    60                     . esc_html( preg_replace('/@.*@(.*)/Us', '$1', $dd) )  // Modified by Horacio (lower case s)
     60                    . esc_html( Rootspersona_Date::format( $dd ) )
    6161                    . (strlen($year) == 4 ? '</span>' : '')
    6262                    . '</span>';
    63                 $block .= ' d: ' . $tmpDate;
     63                $block .= ' ' . esc_html_x( 'd:', 'died: short label before a date', 'rootspersona' ) . ' ' . $tmpDate;
    6464            }
    6565            $block .= '</span>';
    … …  
    9393                    $tmpDate = '<span itemprop="birth" itemscope itemtype="http://historical-data.org/HistoricalEvent.html">'
    9494                        . (strlen($year) == 4 ? '<span itemprop="startDate" content="' . esc_attr( $year ) . '">' : '')
    95                         . esc_html( preg_replace('/@.*@(.*)/Us', '$1', $bd) )  // Modified by Horacio (lower case s)
     95                        . esc_html( Rootspersona_Date::format( $bd ) )
    9696                        . (strlen($year) == 4 ? '</span>' : '')
    9797                        . '</span>';
    98                     $block .= ' b: ' . $tmpDate;
     98                    $block .= ' ' . esc_html_x( 'b:', 'born: short label before a date', 'rootspersona' ) . ' ' . $tmpDate;
    9999                }
    100100                $dd = (string) ( $associated->death_date ?? '' );
    … …  
    104104                    $tmpDate = '<span itemprop="death" itemscope itemtype="http://historical-data.org/HistoricalEvent.html">'
    105105                        . (strlen($year) == 4 ? '<span itemprop="startDate" content="' . esc_attr( $year ) . '">' : '')
    106                         . esc_html( preg_replace('/@.*@(.*)/Us', '$1', $dd) )  // Modified by Horacio (lower case s)
     106                        . esc_html( Rootspersona_Date::format( $dd ) )
    107107                        . (strlen($year) == 4 ? '</span>' : '')
    108108                        . '</span>';
    109                     $block .= ' d: ' . $tmpDate;
     109                    $block .= ' ' . esc_html_x( 'd:', 'died: short label before a date', 'rootspersona' ) . ' ' . $tmpDate;
    110110                }
    111111                $block .= '</span>';
  • rootspersona/trunk/php/pages/panels/class-RP-Evidence-Panel-Creator.php

    r3746101 r3749166  
    2525                && !empty($persona->sources[$idx]['page_id'])
    2626            ) {
    27                 $link = '<a href="' . $options['home_url'] . "?page_id="
    28                     . $persona->sources[$idx]['page_id'] . '">'
    29                     . '[' . $persona->sources[$idx]['src_id'] . ']</a>';
     27                $link = '<a href="' . esc_url( $options['home_url'] . '?page_id=' . (int) $persona->sources[$idx]['page_id'] ) . '">'
     28                    . '[' . esc_html( $persona->sources[$idx]['src_id'] ) . ']</a>';
    3029            } else {
    3130                $win1 = Rootspersona_I18n::sprintf(__('Evidence page has not yet been created for this source',
    … …  
    4140                . $link
    4241                . '</sup></td><td><span style="padding-left:10px;display:inline-block;">'
    43                 . $persona->sources[$idx]['src_title']
     42                . esc_html( $persona->sources[$idx]['src_title'] )
     43                . ( ! empty( $persona->sources[$idx]['src_pages'] )
     44                    ? ' <span class="rp_cite_page">(' . esc_html( implode( '; ', $persona->sources[$idx]['src_pages'] ) ) . ')</span>'
     45                    : '' )
    4446                . '</span></td></tr>';
    4547        }
  • rootspersona/trunk/php/pages/panels/class-RP-Facts-Panel-Creator.php

    r3577245 r3749166  
    3737                && !empty($facts[$idx]['date'])
    3838            ) {
    39                 $block .= esc_html( preg_replace('/@.*@(.*)/Us', '$1', $facts[$idx]['date']) ) . ' - '; // Modified by Horacio (lower case s)
     39                $block .= esc_html( Rootspersona_Date::format( $facts[$idx]['date'] ) ) . ' - ';
    4040            }
    41             $block .= esc_html( $facts[$idx]['type'] ) . ' - ' . esc_html( $facts[$idx]['classification'] ) . ' ' . esc_html( $facts[$idx]['cause'] );
     41            $block .= esc_html( $facts[$idx]['type'] );
     42            $details = trim( ( isset( $facts[$idx]['classification'] ) ? $facts[$idx]['classification'] : '' ) . ' ' . ( isset( $facts[$idx]['cause'] ) ? $facts[$idx]['cause'] : '' ) );
     43            if ( '' !== $details ) {
     44                $block .= ' - ' . esc_html( $details );
     45            }
    4246            if (!$options['hide_places']
    4347                && isset($facts[$idx]['place'])
    4448                && !empty($facts[$idx]['place'])
    4549            ) {
    46                 $block .= '; <span class="rp_place">' . esc_html( $facts[$idx]['place'] ) . '</span>';
     50                $block .= ', <span class="rp_place">' . esc_html( $facts[$idx]['place'] ) . '</span>';
    4751            }
    4852            $p = $facts[$idx]['associated_person'];
    … …  
    8589                && !empty($facts[$idx]['date'])
    8690            ) {
    87                 $block .= esc_attr( preg_replace('/@.*@(.*)/Us', '$1', $facts[$idx]['date']) ); // Modified by Horacio (lower case s)
     91                // Raw GEDCOM date: stripping the calendar escape here lost it on save.
     92                $block .= esc_attr( $facts[$idx]['date'] );
    8893            }
    8994            $block .= '"/></td>';
  • rootspersona/trunk/php/pages/panels/class-RP-Group-Sheet-Panel-Creator.php

    r3577169 r3749166  
    5757            ? esc_attr( $options['pframe_color'] ) : 'brown');
    5858        $color = ((isset($options['group_fcolor']) && !empty($options['group_fcolor']))
    59             ? $options['group_fcolor'] : 'black');
     59            ? esc_attr( $options['group_fcolor'] ) : 'black');
    6060        $bcolor = ((isset($options['group_bcolor']) && !empty($options['group_bcolor']))
    61             ? $options['group_bcolor'] : null);
     61            ? esc_attr( $options['group_bcolor'] ) : null);
    6262        $img = ((isset($options['group_image']) && !empty($options['group_image']))
    63             ? $options['group_image'] : ROOTSPERSONA_PLUGIN_URL . 'images/familyGroupSidebar.jpg');
     63            ? esc_url( $options['group_image'] ) : ROOTSPERSONA_PLUGIN_URL . 'images/familyGroupSidebar.jpg');
    6464
    6565        $fill = 'color:' . esc_attr( $color ) . ';'
    … …  
    6969
    7070        $row_span = 4 + (isset($parent->marriages) ? count($parent->marriages) : 0);
    71         $birth_date = isset($parent->birth_date) ? preg_replace('/@.*@(.*)/Us', '$1', $parent->birth_date) : ''; // Modified by Horacio (lower case s)
     71        $birth_date = isset($parent->birth_date) ? Rootspersona_Date::format( $parent->birth_date ) : '';
    7272        $birth_place = isset($parent->birth_place) ? $parent->birth_place : '';
    73         $death_date = isset($parent->death_date) ? preg_replace('/@.*@(.*)/Us', '$1', $parent->death_date) : ''; // Modified by Horacio (lower case s)
     73        $death_date = isset($parent->death_date) ? Rootspersona_Date::format( $parent->death_date ) : '';
    7474        $death_place = isset($parent->death_place) ? $parent->death_place : '';
    7575
    … …  
    187187                . $pframe_color . ' !important">' . esc_html__( 'Marriage', 'rootspersona' )
    188188                . '</td><td id="' . $ppfx . '_mdate" class="rp_date" style="border-color:' . $pframe_color . ' !important" itemprop="startDate">'
    189                 . ($options['hide_dates'] == 1 ? '' : esc_html( $marriage['date'] ))
     189                . ($options['hide_dates'] == 1 ? '' : esc_html( Rootspersona_Date::format( $marriage['date'] ) ))
    190190                . '</td><td class="notes" style="border-color:' . $pframe_color . ' !important">'
    191191                . $spouse . '<span  id="' . $ppfx . '_mloc" itemprop="location" itemscope itemtype="http://schema.org/Place">'
    192192                . '<span itemprop="name">' . $place . '</span></span></td></tr>';
     193            if (!empty($marriage['divorced'])) {
     194                $block .= '<tr id="' . $ppfx . '_divorce"><td class="label" style="border-color:'
     195                    . $pframe_color . ' !important">' . esc_html__( 'Divorce', 'rootspersona' )
     196                    . '</td><td class="rp_date" style="border-color:' . $pframe_color . ' !important">'
     197                    . ($options['hide_dates'] == 1 ? '' : esc_html( Rootspersona_Date::format( $marriage['divorce_date'] ) ))
     198                    . '</td><td class="notes" style="border-color:' . $pframe_color . ' !important"></td></tr>';
     199            }
    193200        }
    194201        return $block;
    … …  
    206213            ? esc_attr( $options['pframe_color'] ) : 'brown');
    207214        $color = ((isset($options['group_fcolor']) && !empty($options['group_fcolor']))
    208             ? $options['group_fcolor'] : 'black');
     215            ? esc_attr( $options['group_fcolor'] ) : 'black');
    209216        $bcolor = ((isset($options['group_bcolor']) && !empty($options['group_bcolor']))
    210             ? $options['group_bcolor'] : null);
     217            ? esc_attr( $options['group_bcolor'] ) : null);
    211218        $img = ((isset($options['group_image']) && !empty($options['group_image']))
    212             ? $options['group_image'] : ROOTSPERSONA_PLUGIN_URL . 'images/familyGroupSidebar.jpg');
     219            ? esc_url( $options['group_image'] ) : ROOTSPERSONA_PLUGIN_URL . 'images/familyGroupSidebar.jpg');
    213220
    214221        $fill = 'color:' . esc_attr( $color ) . ';'
    … …  
    230237        for ($idx = 0; $idx < $cnt; $idx++) {
    231238            $ppfx = $pfx . $idx . $children[$idx]->id;
    232             $birth_date = isset($children[$idx]->birth_date) ? preg_replace('/@.*@(.*)/Us', '$1', $children[$idx]->birth_date) : ''; // Modified by Horacio (lower case s)
     239            $birth_date = isset($children[$idx]->birth_date) ? Rootspersona_Date::format( $children[$idx]->birth_date ) : '';
    233240            $birth_place = isset($children[$idx]->birth_place) ? $children[$idx]->birth_place : '';
    234             $death_date = isset($children[$idx]->death_date) ? preg_replace('/@.*@(.*)/Us', '$1', $children[$idx]->death_date) : ''; // Modified by Horacio (lower case s)
     241            $death_date = isset($children[$idx]->death_date) ? Rootspersona_Date::format( $children[$idx]->death_date ) : '';
    235242            $death_place = isset($children[$idx]->death_place) ? $children[$idx]->death_place : '';
    236243
  • rootspersona/trunk/php/pages/panels/class-RP-Header-Panel-Creator.php

    r3577245 r3749166  
    140140        $cnt = isset($persona->notes)?count($persona->notes):0;
    141141        $pframe_color = ((isset($options['pframe_color']) && !empty($options['pframe_color']))
    142             ? ("style='border-color:" . $options['pframe_color'] . " !important;'") : '');
     142            ? ("style='border-color:" . esc_attr( $options['pframe_color'] ) . " !important;'") : '');
    143143
    144144        if (!isset ($options['header_style']) || $options['header_style'] == '1' || $cnt == 0) {
    … …  
    155155                . '<span class="rp_headerbox" id="hdr_name" itemprop="name">'
    156156                . esc_html( $persona->full_name ) . '</span>'
     157                . self::other_names_html( $persona )
    157158                . '<span class="rp_headerbox" style="padding-left:15px;text-align:right;color:#EBDDE2;display:none;">'
    158159                . esc_html( $persona->id ) . '</span>';
    … …  
    187188                $tmpDate = '<span itemprop="birth" itemscope itemtype="http://historical-data.org/HistoricalEvent.html">'
    188189                    . (strlen($year) == 4 ? '<span itemprop="startDate" content="' . $year . '">' : '')
    189                     . preg_replace('/@.*@(.*)/Us', '$1', $bd) // Modified by Horacio (lower case s)
     190                    . esc_html( Rootspersona_Date::format( $bd ) )
    190191                    . (strlen($year) == 4 ? '</span>' : '')
    191192                    . '</span>';
    192193
    193                 $block .= '<br/>b: ' . $tmpDate;
     194                $block .= '<br/>' . esc_html_x( 'b:', 'born: short label before a date', 'rootspersona' ) . ' ' . $tmpDate;
    194195
    195196                $d = preg_replace('/@.*@(.*)/Us', '$1', $dd); // Modified by Horacio (lower case s)
    … …  
    197198                $tmpDate = '<span itemprop="death" itemscope itemtype="http://historical-data.org/HistoricalEvent.html">'
    198199                    . (strlen($year) == 4 ? '<span itemprop="startDate" content="' . $year . '">' : '')
    199                     . preg_replace('/@.*@(.*)/Us', '$1', $dd) // Modified by Horacio (lower case s)
     200                    . esc_html( Rootspersona_Date::format( $dd ) )
    200201                    . (strlen($year) == 4 ? '</span>' : '')
    201202                    . '</span>';
    202203
    203                 $block .= '<br/>d: ' . $tmpDate;
     204                $block .= '<br/>' . esc_html_x( 'd:', 'died: short label before a date', 'rootspersona' ) . ' ' . $tmpDate;
    204205            }
    205206
    … …  
    217218                . ' src="'
    218219                . esc_url($thumb) . '"/></a><span class="rp_headerbox"  itemprop="name" style="margin-bottom:5px !important;">'
    219                 . esc_html( $persona->full_name ) . '</span><br/>';
     220                . esc_html( $persona->full_name ) . '</span>' . self::other_names_html( $persona ) . '<br/>';
    220221            if (!$options['hide_dates']) {
    221222                $bd = (string) ( $persona->birth_date ?? '' );
    … …  
    245246                $tmpDate = '<span itemprop="birth" itemscope itemtype="http://historical-data.org/HistoricalEvent.html">'
    246247                    . (strlen($year) == 4 ? '<span itemprop="startDate" content="' . $year . '">' : '')
    247                     . preg_replace('/@.*@(.*)/Us', '$1', $bd) // Modified by Horacio (lower case s)
     248                    . esc_html( Rootspersona_Date::format( $bd ) )
    248249                    . (strlen($year) == 4 ? '</span>' : '')
    249250                    . '</span>';
    250251
    251                 $block .= '<br/>b: ' . $tmpDate;
     252                $block .= '<br/>' . esc_html_x( 'b:', 'born: short label before a date', 'rootspersona' ) . ' ' . $tmpDate;
    252253
    253254                $d = preg_replace('/@.*@(.*)/Us', '$1', $dd); // Modified by Horacio (lower case s)
    … …  
    255256                $tmpDate = '<span itemprop="death" itemscope itemtype="http://historical-data.org/HistoricalEvent.html">'
    256257                    . (strlen($year) == 4 ? '<span itemprop="startDate" content="' . $year . '">' : '')
    257                     . preg_replace('/@.*@(.*)/Us', '$1', $dd) // Modified by Horacio (lower case s)
     258                    . esc_html( Rootspersona_Date::format( $dd ) )
    258259                    . (strlen($year) == 4 ? '</span>' : '')
    259260                    . '</span>';
    260261
    261                 $block .= '<br/>d: ' . $tmpDate;
     262                $block .= '<br/>' . esc_html_x( 'd:', 'died: short label before a date', 'rootspersona' ) . ' ' . $tmpDate;
    262263            }
    263264            $cnt = isset($persona->notes) ? count($persona->notes) : 0;
    … …  
    307308        $header_caption_value = isset($persona->piccaps[$idx0]) ? $persona->piccaps[$idx0] : '';
    308309
    309         $block .= '<div class="rp_caption" style="margin-top:6px;"><label for="cap_1" style="display:block;font-size:11px;margin-bottom:2px;">' . __('Подпись (нативное описание)', 'rootspersona') . '</label>'
     310        $block .= '<div class="rp_caption" style="margin-top:6px;"><label for="cap_1" style="display:block;font-size:11px;margin-bottom:2px;">' . __('Caption (native description)', 'rootspersona') . '</label>'
    310311            . '<textarea id="cap_1" name="cap_1" cols="24" rows="2" style="width:100%;max-width:280px;box-sizing:border-box;">' . esc_textarea($header_caption_value) . '</textarea></div>'
    311312            . '<div style="margin-top:6px;text-align:left;font-size:11px;border-top:1px solid #ddd;padding-top:4px;">'
    312             . '<div style="margin-bottom:2px;"><label><input type="checkbox" name="rp_show_picdesc_1" value="1"' . ($show_desc_1 ? ' checked="checked"' : '') . ' /> ' . __('Описание', 'rootspersona') . '</label></div>'
    313             . '<div style="margin-bottom:2px;"><label><input type="checkbox" name="rp_show_piccaption_1" value="1"' . ($show_cap_1 ? ' checked="checked"' : '') . ' /> ' . __('Подпись', 'rootspersona') . '</label></div>'
     313            . '<div style="margin-bottom:2px;"><label><input type="checkbox" name="rp_show_picdesc_1" value="1"' . ($show_desc_1 ? ' checked="checked"' : '') . ' /> ' . __('Description', 'rootspersona') . '</label></div>'
     314            . '<div style="margin-bottom:2px;"><label><input type="checkbox" name="rp_show_piccaption_1" value="1"' . ($show_cap_1 ? ' checked="checked"' : '') . ' /> ' . __('Caption', 'rootspersona') . '</label></div>'
    314315            . '<div style="margin-top:4px;">'
    315316            . '<label style="margin-right:4px;"><input type="radio" name="rp_pictext_format_1" value="txt"' . ($pictext_format_1 === 'txt' ? ' checked="checked"' : '') . ' /> TXT</label>'
    … …  
    390391        return $block;
    391392    }
     393
     394    /**
     395     * "née Sokolova · nickname “Petrusha”" under the name, or ''.
     396     *
     397     * @param Rootspersona_Persona $persona
     398     * @return string HTML.
     399     */
     400    protected static function other_names_html($persona)
     401    {
     402        $names = Rootspersona_Persona_Helper::other_names_text($persona);
     403        return $names ? '<br/><span class="rp_headerbox rp_other_names">' . esc_html( implode( ' · ', $names ) ) . '</span>' : '';
     404    }
    392405}
    393 
  • rootspersona/trunk/php/pages/panels/class-RP-Picture-Panel-Creator.php

    r3577245 r3749166  
    225225                . '</textarea></div>'
    226226                . '<div style="margin-top:6px;text-align:left;font-size:11px;border-top:1px solid #ddd;padding-top:4px;">'
    227                 . '<div style="margin-bottom:2px;"><label><input type="checkbox" name="rp_show_picdesc_' . $i . '" value="1"' . ($show_desc ? ' checked="checked"' : '') . ' /> ' . __('Описание', 'rootspersona') . '</label></div>'
    228                 . '<div style="margin-bottom:2px;"><label><input type="checkbox" name="rp_show_piccaption_' . $i . '" value="1"' . ($show_cap ? ' checked="checked"' : '') . ' /> ' . __('Подпись', 'rootspersona') . '</label></div>'
     227                . '<div style="margin-bottom:2px;"><label><input type="checkbox" name="rp_show_picdesc_' . $i . '" value="1"' . ($show_desc ? ' checked="checked"' : '') . ' /> ' . __('Description', 'rootspersona') . '</label></div>'
     228                . '<div style="margin-bottom:2px;"><label><input type="checkbox" name="rp_show_piccaption_' . $i . '" value="1"' . ($show_cap ? ' checked="checked"' : '') . ' /> ' . __('Caption', 'rootspersona') . '</label></div>'
    229229                . '<div style="margin-top:4px;">'
    230230                . '<label style="margin-right:4px;"><input type="radio" name="rp_pictext_format_' . $i . '" value="txt"' . ($pictext_format === 'txt' ? ' checked="checked"' : '') . ' /> TXT</label>'
  • rootspersona/trunk/readme.txt

    r3746101 r3749166  
    55Requires PHP: 7.4
    66Tested up to: 7.1
    7 Stable tag: 4.2.2
     7Stable tag: 4.2.4
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    104104Currently you will lose any links to pictures if you do that.
    105105
     106= How can I help translate RootsPersona or correct a translation? =
     107
     108RootsPersona is translated by the community at [translate.wordpress.org](https://translate.wordpress.org/projects/wp-plugins/rootspersona/). Open your language there and suggest or correct a string. Once the language team approves it, WordPress delivers it to all sites automatically - no plugin update is needed.
     109
    106110== Screenshots ==
    107111
    … …  
    115119
    116120
     121 = 4.2.4 =
     122* New: two persona page templates besides the classic layout, "Archive card" and "Chronicle", with colour sets (Sepia, Neutral, Dark or the theme's colours), colour pickers and a live preview. A template page can be printed in parts with [rootsPersona part="intro|family|photos"], so your own content (contents, memoirs, galleries) can sit between them.
     123* New: dates are shown in the site language instead of raw GEDCOM: "15 March 1822", "about 1890", "between 1852 and 1854", "from 1846 to 1890". Julian (Old Style) dates show the New Style in brackets, e.g. "15 (27) March 1822". The edit form keeps the raw date, so saving no longer drops the Old Style mark.
     124* New: divorce, other names (birth name, married name, also known as), nickname and the cited page of each source are now shown.
     125* New: "Page for private people" option — visitors who may not see a person are redirected to a page you choose (for example "Closed. Private access.").
     126* New: "People without dates" option. A person without a birth year is now judged by their other events and by their parents, spouses and children (a child born in 1850 means the parent is deceased; a spouse born in 1990 means living). With no clue at all the option decides; the default is "living" (such people used to be public). A burial without a death date now means deceased.
     127* New: the Settings page is organised in tabs, with "Show …" checkboxes and page dropdowns.
     128* Privacy: persona pages are no longer listed in automatic page lists and menus (block theme navigation, Pages widget), which showed thousands of entries — including names of living people — on every page.
     129* Privacy: pages of living/private people get "Private" as page, browser and Yoast / Rank Math title, a noindex tag, and are left out of the WordPress and Yoast XML sitemaps.
     130* Fix: "Add Uploaded Persons" works for any number of people. Selecting more than about 1000 at once used to create nothing (PHP max_input_vars); pages are now created in chunks with a progress line. Entries show years and the GEDCOM id, so namesakes can be told apart.
     131* Fix: every residence was stored and shown twice. Pages show it once now; reloading the GEDCOM cleans the database.
     132* Fix: families with events but no marriage date no longer disappear from the person page.
     133* Fix: "@#DJULIAN@" no longer shows in marriage dates; stray " - ;" in the facts list removed; the empty "Family Group Sheet - Child" table of "?" is not shown for people without parents.
     134* Performance: public pages load only the scripts they need (about 40 fewer files per page); the jQuery UI widgets stay on admin screens.
     135* Fix: PHP 8.1+ deprecation notices during GEDCOM import.
     136* The experimental SVG tree module is off by default (enable with the ROOTSPERSONA_ENABLE_SVG constant).
     137* Russian translation completed.
    117138 = 4.2.2 =
    118139* Fix: the Settings page and plugin activation no longer stop with a PHP 8 fatal error ("Unknown format specifier" / "ArgumentCountError" in sprintf) when a translation has a damaged placeholder such as "% pages" instead of "%s pages". Translated messages are now built through a safe formatter that repairs or neutralises a broken format instead of failing.
  • rootspersona/trunk/rootspersona.php

    r3746101 r3749166  
    33 Plugin Name: RootsPersona
    44 Description: Build one or more family history pages from a Gedcom file.
    5  Version: 4.2.2
     5 Version: 4.2.4
    66 Author: Yuriy Fisenko
    77 Author URI: https://urryes.ru/
    … …  
    3434
    3535if ( defined( 'ROOTSPERSONA_PLUGIN_FILE' ) ) {
    36         $rp_same_main = false;
     36        $rootspersona_same_main = false;
    3737        if ( function_exists( 'wp_normalize_path' ) ) {
    38                 $rp_same_main = ( wp_normalize_path( ROOTSPERSONA_PLUGIN_FILE ) === wp_normalize_path( __FILE__ ) );
     38                $rootspersona_same_main = ( wp_normalize_path( ROOTSPERSONA_PLUGIN_FILE ) === wp_normalize_path( __FILE__ ) );
    3939        } else {
    40                 $rp_same_main = ( str_replace( '\\', '/', ROOTSPERSONA_PLUGIN_FILE ) === str_replace( '\\', '/', __FILE__ ) );
     40                $rootspersona_same_main = ( str_replace( '\\', '/', ROOTSPERSONA_PLUGIN_FILE ) === str_replace( '\\', '/', __FILE__ ) );
    4141        }
    42         if ( ! $rp_same_main ) {
     42        if ( ! $rootspersona_same_main ) {
    4343                $GLOBALS['roots_persons_skipped_basename'] = plugin_basename( __FILE__ );
    4444                if ( ! function_exists( 'roots_persons_duplicate_install_notice' ) ) {
    … …  
    9393}
    9494if ( ! defined( 'ROOTSPERSONA_PLUGIN_DIR' ) ) {
    95         define( 'ROOTSPERSONA_PLUGIN_DIR', ROOTSPERSONA_PLUGIN_DIR );
     95        define( 'ROOTSPERSONA_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
    9696}
    9797if ( ! defined( 'ROOTSPERSONA_PLUGIN_URL' ) ) {
    … …  
    9999}
    100100if ( ! defined( 'ROOTSPERSONA_VERSION' ) ) {
    101         define( 'ROOTSPERSONA_VERSION', '4.2.2' );
     101        define( 'ROOTSPERSONA_VERSION', '4.2.4' );
    102102}
    103103if ( ! defined( 'ROOTS_PERSONA_UNINSTALL_MODE_OPTION' ) ) {
    … …  
    246246require_once ROOTSPERSONA_PLUGIN_DIR . 'php/class-RP-Persona-Helper.php';
    247247require_once ROOTSPERSONA_PLUGIN_DIR . 'php/class-RP-I18n.php';
     248require_once ROOTSPERSONA_PLUGIN_DIR . 'php/class-RP-Date.php';
    248249require_once ROOTSPERSONA_PLUGIN_DIR . 'php/class-RP-Persona-Installer.php';
    249250require_once ROOTSPERSONA_PLUGIN_DIR . 'php/class-RP-Persona-Site-Mender.php';
    … …  
    256257require_once ROOTSPERSONA_PLUGIN_DIR . 'php/pages/class-RP-Index-Page-Builder.php';
    257258require_once ROOTSPERSONA_PLUGIN_DIR . 'php/pages/class-RP-Persona-Page-Builder.php';
     259require_once ROOTSPERSONA_PLUGIN_DIR . 'php/templates/class-RP-Persona-Templates.php';
     260require_once ROOTSPERSONA_PLUGIN_DIR . 'php/class-RP-Template-Colors.php';
    258261require_once ROOTSPERSONA_PLUGIN_DIR . 'php/pages/class-RP-Evidence-Page-Builder.php';
    259262require_once ROOTSPERSONA_PLUGIN_DIR . 'php/class-RP-Log.php';
    … …  
    277280         * @var string
    278281         */
    279         var $persona_version = '4.2.2';
     282        var $persona_version = '4.2.4';
    280283
    281284        /**
    … …  
    387390                    $builder = new Rootspersona_Persona_Page_Builder();
    388391                    $options = $builder->get_persona_options( $atts, $callback, $options );
     392                    // Page templates apply to the full [rootsPersona] page only, not to single-panel shortcodes.
     393                    $template = 'classic';
     394                    if ( empty( $callback ) || 'rootspersona' === strtolower( (string) $callback ) ) {
     395                        $template = Rootspersona_Persona_Templates::resolve( $atts, $options );
     396                    }
    389397                    $factory = new Rootspersona_Persona_Factory();
    390                     $persona = $factory->get_with_options( $persona_id, $batch_id, $options );
    391                     //if($persona->full_name == 'Private')
    392                     //    $post->post_title = 'Private';
    393                     $block = $builder->build( $persona, $options, Rootspersona_Persona_Helper::get_page_id() );
     398                    $part    = Rootspersona_Persona_Templates::resolve_part( $atts );
     399                    // Edit links once per page: with the last part, or with the whole page.
     400                    $edit_links = in_array( $part, array( 'all', 'family', 'rest' ), true );
     401                    if ( 'classic' === $template ) {
     402                        $persona = $factory->get_with_options( $persona_id, $batch_id, $options );
     403                        $block   = $builder->build( $persona, $options, Rootspersona_Persona_Helper::get_page_id(), $part );
     404                    } else {
     405                        $persona = $factory->get_with_options( $persona_id, $batch_id, Rootspersona_Persona_Templates::loading_options( $options ) );
     406                        $block   = Rootspersona_Persona_Templates::render( $template, $persona, $options, $part )
     407                            . ( $edit_links ? Rootspersona_Persona_Page_Builder::create_end_of_page( $persona->id, $persona->batch_id, Rootspersona_Persona_Helper::get_page_id(), $options ) : '' );
     408                    }
    394409                } else {
    395410                    $msg = __('Invalid person id.', 'rootspersona');
    … …  
    669684                                $persons[ $p_idx ]['given'] = sanitize_text_field( (string) $p_row['given'] );
    670685                            }
     686                            $persons[ $p_idx ]['value'] = Rootspersona_Add_Page_Builder::option_value( $persons[ $p_idx ] );
     687                            $persons[ $p_idx ]['label'] = Rootspersona_Add_Page_Builder::option_label( $persons[ $p_idx ] );
    671688                        }
    672689                    }
    … …  
    742759
    743760        /**
     761         * Create persona pages for "ID|Surname, Given" values from the Add Uploaded Persons list.
     762         *
     763         * @param string[] $persons  Sanitized option values.
     764         * @param string   $batch_id Batch.
     765         * @param array    $options  Plugin options.
     766         * @return array { lines: string[] (escaped HTML), created: int, errors: int }
     767         */
     768        function create_persona_pages( $persons, $batch_id, $options ) {
     769            $result = array(
     770                'lines'   => array(),
     771                'created' => 0,
     772                'errors'  => 0,
     773            );
     774            foreach ( $persons as $person_raw ) {
     775                $parts = explode( '|', $person_raw, 2 );
     776                $p     = $parts[0];
     777                $name  = isset( $parts[1] ) ? $parts[1] : '';
     778                $page_id = Rootspersona_Persona_Helper::add_page( $p, $name, $options, $batch_id, null, null );
     779                if ( $page_id != false ) {
     780                    Rootspersona_Dao_Factory::get_rp_indi_dao()->update_page( $p, $batch_id, $page_id );
     781                    $result['created']++;
     782                    $result['lines'][] =
     783                        /* translators: %s: page ID created by RootsPersona. */
     784                        Rootspersona_I18n::sprintf( __( 'Page %s created for', 'rootspersona' ), esc_html( $page_id ) )
     785                        . ' ' . esc_html( $p ) . ': ' . esc_html( $name );
     786                } else {
     787                    $result['errors']++;
     788                    $result['lines'][] = esc_html__( 'Error creating page for', 'rootspersona' ) . ' ' . esc_html( $p );
     789                }
     790                // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged
     791                set_time_limit( 60 );
     792            }
     793            if ( $result['created'] > 0 ) {
     794                delete_transient( Rootspersona_Persona_Helper::GUEST_PRIVATE_PAGES );
     795            }
     796            return $result;
     797        }
     798
     799        /**
     800         * AJAX: create pages for one chunk of the Add Uploaded Persons selection.
     801         * The browser sends the selection in small chunks, so neither PHP's max_input_vars
     802         * (1000 on most hosts) nor the request time limit is hit on big trees.
     803         */
     804        function add_pages_ajax() {
     805            if ( ! current_user_can( 'manage_options' ) ) {
     806                wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'rootspersona' ) ), 403 );
     807            }
     808            check_ajax_referer( 'rootspersona_add_page_nonce', '_wpnonce' );
     809
     810            $persons = array();
     811            if ( isset( $_POST['persons'] ) && is_array( $_POST['persons'] ) ) {
     812                $raw_persons = wp_unslash( $_POST['persons'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized below.
     813                $persons     = array_map( 'sanitize_text_field', $raw_persons );
     814            }
     815            $batch_id = isset( $_POST['batch_id'] ) ? sanitize_text_field( wp_unslash( $_POST['batch_id'] ) ) : '1';
     816            if ( empty( $persons ) ) {
     817                wp_send_json_error( array( 'message' => __( 'No people selected.', 'rootspersona' ) ) );
     818            }
     819
     820            wp_send_json_success( $this->create_persona_pages( $persons, $batch_id, get_option( 'rootspersona_plugin' ) ) );
     821        }
     822
     823        /**
    744824         *
    745825         * @global wpdb $wpdb
    … …  
    776856                        $msg = __( 'No people selected.', 'rootspersona' );
    777857                    } else {
    778                         foreach ( $persons as $person_raw ) {
    779                                                         list( $p, $name ) = explode( '|', $person_raw, 2 ); // Modified by Horacio (Begin)
    780                             //$name = RP_Dao_Factory::get_rp_persona_dao()
    781                             //        ->get_fullname( $p, $batch_id ); // Modified by Horacio (End)
    782                             $pageId = Rootspersona_Persona_Helper::add_page( $p, $name, $options, $batch_id, null, null );
    783                             if ( $pageId != false ) {
    784                                 Rootspersona_Dao_Factory::get_rp_indi_dao()
    785                                         ->update_page( $p, $batch_id, $pageId );
    786                                 $msg = $msg . '<br/>'
    787                                     /* translators: %s: page ID created by RootsPersona. */   
    788                                                                         . Rootspersona_I18n::sprintf( __( 'Page %s created for', 'rootspersona' ),
    789                                                 esc_html( $pageId ) )
    790                                         . ' ' . esc_html( $p ) . ': ' . esc_html( $name ); // Modified by Horacio
    791                             }
    792                             else {
    793                                 $msg .= '<br/>'
    794                                         . __( 'Error creating page for', 'rootspersona' )
    795                                         . ' ' . esc_html( $p );
    796                             }
    797                             // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged
    798                             set_time_limit( 60 );
    799                         }
     858                        $result = $this->create_persona_pages( $persons, $batch_id, $options );
     859                        $msg    = implode( '<br/>', $result['lines'] );
    800860                    }
    801861                }
    … …  
    803863                $batch_ids = Rootspersona_Dao_Factory::get_rp_persona_dao()->get_batch_ids();
    804864
    805                 if ( isset( $_GET['batch_id'] ) ) {
     865                if ( isset( $batch_id ) ) {
     866                    // Keep showing the batch that was just processed.
     867                    $batch_ids[0] = $batch_id;
     868                } elseif ( isset( $_GET['batch_id'] ) ) {
    806869                    $batch_ids[0] = sanitize_text_field( wp_unslash( $_GET['batch_id'] ) );
    807870                } elseif ( count( $batch_ids ) === 0 ) {
    … …  
    813876                // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only success flag.
    814877                if ( isset( $_GET['rootspersona_gedcom_uploaded'] ) && '1' === sanitize_text_field( wp_unslash( $_GET['rootspersona_gedcom_uploaded'] ) ) ) {
     878                    // New dates may change who counts as living.
     879                    delete_transient( Rootspersona_Persona_Helper::GUEST_PRIVATE_PAGES );
    815880                    $sync_url             = admin_url( 'tools.php?page=rootsPersona_tools&rootspage=util&utilityAction=syncPersonaDbToPages&batch_id=' . rawurlencode( (string) ( $batch_ids[0] ?? '1' ) ) );
    816881                    $gedcom_upload_notice = '<div style="padding:12px 14px;margin:0 0 16px;border:1px solid #46b450;background:#ecf7ed;color:#1e1e1e;text-align:left;font-weight:normal;">'
    … …  
    10801145            }
    10811146
     1147            // Living/private people: the panels say "Private", so must the title.
     1148            if ( Rootspersona_Persona_Helper::is_persona_post_restricted( $post ) ) {
     1149                return esc_html__( 'Private', 'rootspersona' );
     1150            }
     1151
    10821152            // If there is a single comma between surname block and given names, replace it with a space.
    10831153            if ( strpos( $title, ',' ) !== false ) {
    … …  
    10881158
    10891159            return esc_html( $title );
     1160        }
     1161
     1162        /**
     1163         * Document <title> (single_post_title does not run the_title).
     1164         *
     1165         * @param string  $title
     1166         * @param WP_Post $post
     1167         * @return string
     1168         */
     1169        function filter_persona_document_title( $title, $post = null ) {
     1170            if ( is_admin() || ! $post || strpos( $post->post_content, '[rootsPersona' ) === false ) {
     1171                return $title;
     1172            }
     1173            if ( Rootspersona_Persona_Helper::is_persona_post_restricted( $post ) ) {
     1174                return __( 'Private', 'rootspersona' );
     1175            }
     1176            // Same "Surname Given" form as the visible title.
     1177            if ( strpos( $title, ',' ) !== false ) {
     1178                $parts = explode( ',', $title, 2 );
     1179                $title = trim( $parts[0] ) . ' ' . trim( $parts[1] );
     1180            }
     1181            return $title;
     1182        }
     1183
     1184        /**
     1185         * Keep search engines away from pages of living/private people.
     1186         *
     1187         * @param array $robots
     1188         * @return array
     1189         */
     1190        function filter_persona_robots( $robots ) {
     1191            if ( is_singular() && Rootspersona_Persona_Helper::is_persona_post_restricted( get_queried_object_id() ) ) {
     1192                $robots['noindex']  = true;
     1193                $robots['nofollow'] = true;
     1194            }
     1195            return $robots;
     1196        }
     1197
     1198        /**
     1199         * SEO plugins (Yoast, Rank Math) build titles from the raw post title, bypassing
     1200         * the_title. Swap the raw "Surname, Given" inside their title for the same text
     1201         * the page shows: "Surname Given", or "Private" for restricted people.
     1202         *
     1203         * @param string $title
     1204         * @return string
     1205         */
     1206        function filter_persona_seo_title( $title ) {
     1207            if ( ! is_string( $title ) || ! is_singular() ) {
     1208                return $title;
     1209            }
     1210            $post = get_queried_object();
     1211            if ( ! $post instanceof WP_Post || strpos( $post->post_content, '[rootsPersona' ) === false || $post->post_title === '' ) {
     1212                return $title;
     1213            }
     1214            return str_replace( $post->post_title, $this->filter_persona_document_title( $post->post_title, $post ), $title );
     1215        }
     1216
     1217        /**
     1218         * Yoast prints its own robots meta (string form).
     1219         *
     1220         * @param string $robots
     1221         * @return string
     1222         */
     1223        function filter_persona_seo_robots( $robots ) {
     1224            if ( is_singular() && Rootspersona_Persona_Helper::is_persona_post_restricted( get_queried_object_id() ) ) {
     1225                return 'noindex, nofollow';
     1226            }
     1227            return $robots;
     1228        }
     1229
     1230        /**
     1231         * Rank Math prints its own robots meta (array form).
     1232         *
     1233         * @param array $robots
     1234         * @return array
     1235         */
     1236        function filter_persona_rank_math_robots( $robots ) {
     1237            if ( is_singular() && Rootspersona_Persona_Helper::is_persona_post_restricted( get_queried_object_id() ) ) {
     1238                $robots['index']  = 'noindex';
     1239                $robots['follow'] = 'nofollow';
     1240            }
     1241            return $robots;
     1242        }
     1243
     1244        /**
     1245         * Visitors who may not see a person are sent to the page chosen in
     1246         * Options → "Page for private people" (if any), instead of an empty persona page.
     1247         */
     1248        function redirect_private_persona() {
     1249            if ( ! is_singular() ) {
     1250                return;
     1251            }
     1252            $options = get_option( 'rootspersona_plugin' );
     1253            $target  = isset( $options['private_page'] ) ? (int) $options['private_page'] : 0;
     1254            $current = get_queried_object_id();
     1255            if ( $target <= 0 || $target === $current || 'publish' !== get_post_status( $target ) ) {
     1256                return;
     1257            }
     1258            if ( Rootspersona_Persona_Helper::is_persona_post_restricted( $current ) ) {
     1259                nocache_headers();
     1260                // 302: the person becomes visible later (e.g. after a death date is recorded).
     1261                wp_safe_redirect( get_permalink( $target ), 302 );
     1262                exit;
     1263            }
     1264        }
     1265
     1266        /**
     1267         * Keep living/private people out of the WordPress core XML sitemap:
     1268         * the sitemap is public and would otherwise list their names (in the URLs).
     1269         *
     1270         * @param array  $args
     1271         * @param string $post_type
     1272         * @return array
     1273         */
     1274        function exclude_private_from_core_sitemap( $args, $post_type ) {
     1275            $options = get_option( 'rootspersona_plugin' );
     1276            $type    = empty( $options['post_page'] ) ? 'page' : 'post';
     1277            if ( $post_type === $type ) {
     1278                $ids = Rootspersona_Persona_Helper::get_guest_private_page_ids();
     1279                if ( $ids ) {
     1280                    $args['post__not_in'] = array_merge( isset( $args['post__not_in'] ) ? (array) $args['post__not_in'] : array(), $ids );
     1281                }
     1282            }
     1283            return $args;
     1284        }
     1285
     1286        /**
     1287         * Same for the Yoast SEO sitemap.
     1288         *
     1289         * @param int[] $ids
     1290         * @return int[]
     1291         */
     1292        function exclude_private_from_yoast_sitemap( $ids ) {
     1293            return array_merge( is_array( $ids ) ? $ids : array(), Rootspersona_Persona_Helper::get_guest_private_page_ids() );
     1294        }
     1295
     1296        /**
     1297         * Privacy settings changed: recompute the list of private pages on next use.
     1298         */
     1299        function flush_private_pages_cache() {
     1300            delete_transient( Rootspersona_Persona_Helper::GUEST_PRIVATE_PAGES );
     1301        }
     1302
     1303        /**
     1304         * Hide persona and evidence pages (children of the parent page) from automatic
     1305         * page lists: block themes' Page List / navigation fallback, wp_list_pages(),
     1306         * the Pages widget. Otherwise every page of the site lists thousands of people,
     1307         * living ones included. Menus built by hand are not affected.
     1308         *
     1309         * @param array $query_args WP_Query args built by get_pages().
     1310         * @return array
     1311         */
     1312        function exclude_persona_pages_query( $query_args, $parsed_args = array() ) {
     1313            $parent = $this->get_persona_parent_page_id();
     1314            if ( $parent && ! is_admin() && ! $this->is_persona_pages_request( $parsed_args, $parent ) ) {
     1315                $query_args['post_parent__not_in'] = array_merge(
     1316                    isset( $query_args['post_parent__not_in'] ) ? (array) $query_args['post_parent__not_in'] : array(),
     1317                    array( $parent )
     1318                );
     1319                $query_args['post__not_in'] = array_merge(
     1320                    isset( $query_args['post__not_in'] ) ? (array) $query_args['post__not_in'] : array(),
     1321                    array( $parent )
     1322                );
     1323            }
     1324            return $query_args;
     1325        }
     1326
     1327        /**
     1328         * Same as above for get_pages() results (WordPress before 6.3 has no query filter).
     1329         *
     1330         * @param WP_Post[] $pages
     1331         * @return WP_Post[]
     1332         */
     1333        function exclude_persona_pages( $pages, $parsed_args = array() ) {
     1334            $parent = $this->get_persona_parent_page_id();
     1335            if ( ! $parent || is_admin() || ! is_array( $pages ) || $this->is_persona_pages_request( $parsed_args, $parent ) ) {
     1336                return $pages;
     1337            }
     1338            return array_values(
     1339                array_filter(
     1340                    $pages,
     1341                    function ( $page ) use ( $parent ) {
     1342                        return (int) $page->ID !== $parent && (int) $page->post_parent !== $parent;
     1343                    }
     1344                )
     1345            );
     1346        }
     1347
     1348        /**
     1349         * A caller that explicitly asks for the persona branch gets it unfiltered.
     1350         *
     1351         * @param array $parsed_args get_pages() arguments.
     1352         * @param int   $parent      Persona parent page.
     1353         * @return bool
     1354         */
     1355        function is_persona_pages_request( $parsed_args, $parent ) {
     1356            foreach ( array( 'child_of', 'parent' ) as $key ) {
     1357                if ( isset( $parsed_args[ $key ] ) && (int) $parsed_args[ $key ] === $parent ) {
     1358                    return true;
     1359                }
     1360            }
     1361            return false;
     1362        }
     1363
     1364        /**
     1365         * @return int Parent page of persona pages, 0 when not set.
     1366         */
     1367        function get_persona_parent_page_id() {
     1368            $options = get_option( 'rootspersona_plugin' );
     1369            return isset( $options['parent_page'] ) ? (int) $options['parent_page'] : 0;
    10901370        }
    10911371
    … …  
    11081388            wp_enqueue_style( 'rootsPersona-4' );
    11091389            wp_register_style(
     1390                'rootspersona-templates',
     1391                plugins_url( 'css/persona-templates.css', __FILE__ ),
     1392                array(),
     1393                $this->persona_version
     1394            );
     1395            wp_enqueue_style( 'rootspersona-templates' );
     1396            $template_colors = Rootspersona_Template_Colors::inline_css( get_option( 'rootspersona_plugin', array() ) );
     1397            if ( '' !== $template_colors ) {
     1398                wp_add_inline_style( 'rootspersona-templates', $template_colors );
     1399            }
     1400            wp_register_style(
    11101401                'jquery-ui-autocomplete-style',
    11111402                plugins_url( 'css/jquery.ui.autocomplete.css', __FILE__ ),
    … …  
    11211412         */
    11221413        function insert_persona_scripts() {
    1123             $this->insert_admin_scripts();
     1414            // Public pages need only the photo lightbox and the plugin's own scripts;
     1415            // the jQuery UI widgets and media uploader are for the admin screens.
     1416            wp_enqueue_script( 'thickbox' );
     1417            $this->register_core_scripts();
     1418            wp_enqueue_script( 'rootsUtilities' );
     1419            wp_enqueue_script( 'rootspersona-inline' );
    11241420        }
    11251421
    … …  
    11661462            wp_enqueue_script('jquery-ui-tooltip');
    11671463
     1464            $this->register_core_scripts();
     1465            wp_enqueue_script( 'rootsUtilities' );
     1466            // Add persons: Ctrl/Shift multi-select must account for list scroll (see js file).
     1467            wp_register_script(
     1468                'rp-persons-add-multiselect',
     1469                plugins_url( 'js/rp-persons-add-multiselect.js', __FILE__ ),
     1470                array(),
     1471                $this->persona_version,
     1472                true
     1473            );
     1474            wp_localize_script(
     1475                'rp-persons-add-multiselect',
     1476                'rootspersonaAddPages',
     1477                array(
     1478                    'ajaxurl' => admin_url( 'admin-ajax.php' ),
     1479                    'i18n'    => array(
     1480                        /* translators: 1: pages processed so far, 2: people selected. */
     1481                        'progress' => __( 'Creating pages: %1$s of %2$s…', 'rootspersona' ),
     1482                        /* translators: 1: pages created, 2: errors. */
     1483                        'finished' => __( 'Done. Pages created: %1$s, errors: %2$s.', 'rootspersona' ),
     1484                        /* translators: 1: pages created, 2: people selected. */
     1485                        'stopped'  => __( 'Stopped after %1$s of %2$s pages.', 'rootspersona' ),
     1486                        'failed'   => __( 'The server did not answer. Check the import log.', 'rootspersona' ),
     1487                    ),
     1488                )
     1489            );
     1490            wp_enqueue_script( 'rp-persons-add-multiselect' );
     1491            wp_enqueue_script( 'rootspersona-inline' );
     1492            // Registered here so it's available on the Tools page; only enqueued by
     1493            // upload_gedcom_handler() once a chunked GEDCOM import has actually started.
     1494            wp_register_script(
     1495                'rootspersona-gedcom-import',
     1496                plugins_url( 'js/rootspersona-gedcom-import.js', __FILE__ ),
     1497                array( 'jquery', 'rootsUtilities' ),
     1498                $this->persona_version,
     1499                true
     1500            );
     1501        }
     1502
     1503        /**
     1504         * Scripts shared by the admin screens and the public persona pages.
     1505         */
     1506        function register_core_scripts() {
     1507            if ( wp_script_is( 'rootsUtilities', 'registered' ) ) {
     1508                return;
     1509            }
    11681510            wp_register_script(
    11691511                'rootsUtilities',
    … …  
    11821524                                )
    11831525                        );
    1184             wp_enqueue_script( 'rootsUtilities' );
    1185             // Add persons: Ctrl/Shift multi-select must account for list scroll (see js file).
    1186             wp_register_script(
    1187                 'rp-persons-add-multiselect',
    1188                 plugins_url( 'js/rp-persons-add-multiselect.js', __FILE__ ),
    1189                 array(),
    1190                 $this->persona_version,
    1191                 true
    1192             );
    1193             wp_enqueue_script( 'rp-persons-add-multiselect' );
    11941526            wp_register_script(
    11951527                'rootspersona-inline',
    11961528                plugins_url( 'js/rootspersona-inline.js', __FILE__ ),
    11971529                array(),
    1198                 $this->persona_version,
    1199                 true
    1200             );
    1201             wp_enqueue_script( 'rootspersona-inline' );
    1202             // Registered here so it's available on the Tools page; only enqueued by
    1203             // upload_gedcom_handler() once a chunked GEDCOM import has actually started.
    1204             wp_register_script(
    1205                 'rootspersona-gedcom-import',
    1206                 plugins_url( 'js/rootspersona-gedcom-import.js', __FILE__ ),
    1207                 array( 'jquery', 'rootsUtilities' ),
    12081530                $this->persona_version,
    12091531                true
    … …  
    13211643                        $self->insert_persona_styles();
    13221644                        $self->insert_admin_scripts();
     1645                        $self->insert_settings_scripts();
    13231646                    }
    13241647                }
    … …  
    13441667           
    13451668            // Use standard WordPress capability checks (`manage_options`) for admin pages.
     1669        }
     1670
     1671        /**
     1672         * Colour pickers, template colour sets with live preview and Media Library pickers
     1673         * on the settings page.
     1674         */
     1675        function insert_settings_scripts() {
     1676            wp_enqueue_style( 'wp-color-picker' );
     1677            wp_enqueue_media();
     1678            wp_enqueue_script(
     1679                'rootspersona-settings',
     1680                plugins_url( 'js/rootspersona-settings.js', __FILE__ ),
     1681                array( 'jquery', 'wp-color-picker' ),
     1682                $this->persona_version,
     1683                true
     1684            );
     1685            wp_localize_script(
     1686                'rootspersona-settings',
     1687                'rootspersonaSettings',
     1688                array(
     1689                    'presets'     => Rootspersona_Template_Colors::presets_for_js(),
     1690                    'chooseImage' => __( 'Choose image', 'rootspersona' ),
     1691                    'useImage'    => __( 'Use this image', 'rootspersona' ),
     1692                )
     1693            );
     1694            wp_add_inline_style(
     1695                'wp-color-picker',
     1696                '.rp-colors-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(14rem,1fr));gap:.8rem 1.2rem}'
     1697                . '.rp-colors-grid.is-disabled{opacity:.45;pointer-events:none}'
     1698                . '.rp-colors-preview{max-width:22rem;padding:.9rem;gap:.6rem;border:1px dashed #c3c4c7;background:var(--rp-surface,#f6f1e8)}'
     1699                . '.rp-colors-preview__caption{margin:0;font-size:12px;color:#646970}'
     1700                . '.rp-colors-row{display:flex;flex-wrap:wrap;gap:1.2rem;align-items:flex-start}.rp-colors-row .rp-colors-grid{flex:1 1 28rem}.rp-colors-row .rp-colors-preview{flex:0 1 20rem}'
     1701                . '.rp-settings__head{display:flex;flex-wrap:wrap;align-items:center;gap:.6rem 1.2rem;margin:1em 0}.rp-settings__head img{max-width:100%;height:auto}'
     1702                . '.rp-settings__version{font-size:14px;color:#50575e}.rp-settings__donate{margin-left:auto;color:#50575e}'
     1703                . '.rp-settings.has-tabs .rp-tab-panel__title{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(1px,1px,1px,1px)}'
     1704                . '.rp-tab-panel[hidden]{display:none}.rp-settings fieldset label{line-height:2}'
     1705            );
    13461706        }
    13471707
    … …  
    14711831
    14721832        /**
     1833         * A yes/no option as 1 or 0. The settings form sends "true"/"false"; the plugin
     1834         * itself saves the stored options back (plugin upgrade) with 1/0 — both count.
     1835         * A key the input does not carry keeps its previous value: only reading "true"
     1836         * used to switch stored flags off on every upgrade (system of record, editors).
     1837         *
     1838         * @param array  $i    Submitted options.
     1839         * @param array  $prev Stored options.
     1840         * @param string $key
     1841         * @return int
     1842         */
     1843        private function flag_option( $i, $prev, $key ) {
     1844            $on = array( 1, '1', true, 'true', 'on' );
     1845            if ( array_key_exists( $key, $i ) ) {
     1846                return in_array( $i[ $key ], $on, true ) ? 1 : 0;
     1847            }
     1848            return ( isset( $prev[ $key ] ) && in_array( $prev[ $key ], $on, true ) ) ? 1 : 0;
     1849        }
     1850
     1851        /**
    14731852         * Sanitize and validate input. Accepts an array, return a sanitized array.
    14741853         *
    … …  
    14871866            $options['parent_page']         = isset( $i['parent_page'] ) ? intval( $i['parent_page'] ) : ( isset( $prev['parent_page'] ) ? intval( $prev['parent_page'] ) : 0 );
    14881867            $options['per_page']            = isset( $i['per_page'] ) ? intval( $i['per_page'] ) : ( isset( $prev['per_page'] ) ? intval( $prev['per_page'] ) : 25 );
    1489             $options['is_system_of_record'] = isset( $i['is_system_of_record'] )
    1490                 ? ( $i['is_system_of_record'] === 'true' ? 1 : 0 )
    1491                 : ( isset( $prev['is_system_of_record'] ) ? (int) $prev['is_system_of_record'] : 0 );
     1868            $options['is_system_of_record'] = $this->flag_option( $i, $prev, 'is_system_of_record' );
    14921869            // Colours: store as validated hex colours (sanitize_hex_color), never via esc_attr (an output-escaping function).
    14931870            $options['index_even_color']    = $this->sanitize_color_option( isset( $i['index_even_color'] ) ? $i['index_even_color'] : null, isset( $prev['index_even_color'] ) ? $prev['index_even_color'] : '' );
    … …  
    15021879            $options['group_image']         = isset( $i['group_image'] ) ? esc_url_raw( trim( wp_unslash( $i['group_image'] ) ) ) : ( isset( $prev['group_image'] ) ? $prev['group_image'] : '' );
    15031880            $options['pframe_color']        = $this->sanitize_color_option( isset( $i['pframe_color'] ) ? $i['pframe_color'] : null, isset( $prev['pframe_color'] ) ? $prev['pframe_color'] : '' );
     1881            $options = array_merge( $options, Rootspersona_Template_Colors::sanitize( $i, $prev ) );
     1882            $options['persona_template']    = Rootspersona_Persona_Templates::sanitize(
     1883                isset( $i['persona_template'] ) ? wp_unslash( $i['persona_template'] ) : ( isset( $prev['persona_template'] ) ? $prev['persona_template'] : Rootspersona_Persona_Templates::DEFAULT_SLUG )
     1884            );
    15041885            $options['header_style']        = isset( $i['header_style'] ) ? ( (int) $i['header_style'] === 2 ? 2 : 1 ) : ( isset( $prev['header_style'] ) ? (int) $prev['header_style'] : 1 );
    15051886            $options['hide_header']         = isset( $i['hide_header'] ) ? ( (int) $i['hide_header'] === 1 ? 1 : 0 ) : ( isset( $prev['hide_header'] ) ? (int) $prev['hide_header'] : 0 );
    … …  
    15161897            $options['hide_dates']          = isset( $i['hide_dates'] ) ? ( (int) $i['hide_dates'] === 1 ? 1 : 0 ) : ( isset( $prev['hide_dates'] ) ? (int) $prev['hide_dates'] : 0 );
    15171898            $options['hide_places']         = isset( $i['hide_places'] ) ? ( (int) $i['hide_places'] === 1 ? 1 : 0 ) : ( isset( $prev['hide_places'] ) ? (int) $prev['hide_places'] : 0 );
    1518             $options['hide_donation']       = ( ! empty( $i['hide_donation'] ) && ( $i['hide_donation'] === 'true' || $i['hide_donation'] === true ) ) ? 1 : 0;
    1519             $options['is_editor']           = ( ! empty( $i['is_editor'] ) && ( $i['is_editor'] === 'true' || $i['is_editor'] === true ) ) ? 1 : 0;
    1520             $options['post_page']           = ( ! empty( $i['post_page'] ) && ( $i['post_page'] === 'true' || $i['post_page'] === true ) ) ? 1 : 0;
     1899            $options['hide_donation']       = $this->flag_option( $i, $prev, 'hide_donation' );
     1900            $options['is_editor']           = $this->flag_option( $i, $prev, 'is_editor' );
     1901            $options['post_page']           = $this->flag_option( $i, $prev, 'post_page' );
    15211902            $options['privacy_default']     = ( isset( $i['privacy_default'] ) && in_array( $i['privacy_default'], array( 'Pub', 'Pvt', 'Mbr' ), true ) )
    15221903                ? $i['privacy_default']
    … …  
    15251906                ? $i['privacy_living']
    15261907                : ( isset( $prev['privacy_living'] ) ? $prev['privacy_living'] : 'Mbr' );
     1908            $options['living_unknown']      = ( isset( $i['living_unknown'] ) && in_array( $i['living_unknown'], array( 'living', 'dead' ), true ) )
     1909                ? $i['living_unknown']
     1910                : ( isset( $prev['living_unknown'] ) ? $prev['living_unknown'] : 'living' );
     1911            $options['private_page']        = isset( $i['private_page'] ) ? absint( $i['private_page'] ) : ( isset( $prev['private_page'] ) ? absint( $prev['private_page'] ) : 0 );
    15271912            // Raw custom CSS (custom_style) and custom shortcode/page content (custom_page)
    15281913            // were removed for security; any previously stored values are dropped on save.
    … …  
    15931978    add_filter( 'the_content', array( $rootspersona_plugin, 'check_permissions' ), 2 );
    15941979    add_filter( 'query_vars', array( $rootspersona_plugin, 'parameter_queryvars' ) );
     1980    // Yoast SEO's "Remove unregistered URL parameters" strips the index page number otherwise.
     1981    add_filter(
     1982        'Yoast\\WP\\SEO\\allowlist_permalink_vars',
     1983        function ( $vars ) {
     1984            $vars   = is_array( $vars ) ? $vars : array();
     1985            $vars[] = 'rootsvar';
     1986            $vars[] = 'override-surname';
     1987            return array_values( array_unique( $vars ) );
     1988        }
     1989    );
    15951990    add_filter( 'wp_nav_menu_args', array( $rootspersona_plugin, 'person_menu_filter' ) );
    15961991    add_action('wp_ajax_rootspersona_action', array( $rootspersona_plugin, 'rootspersona_action_callback' ) );
    … …  
    15981993    add_action( 'admin_post_' . Rootspersona_Log::CLEAR_ACTION, array( 'Rootspersona_Log', 'handle_clear' ) );
    15991994    add_filter( 'the_title', array( $rootspersona_plugin, 'filter_persona_title' ), 10, 2 );
     1995    add_filter( 'single_post_title', array( $rootspersona_plugin, 'filter_persona_document_title' ), 10, 2 );
     1996    add_filter( 'wp_robots', array( $rootspersona_plugin, 'filter_persona_robots' ) );
     1997    foreach ( array( 'wpseo_title', 'wpseo_opengraph_title', 'wpseo_twitter_title', 'rank_math/frontend/title' ) as $rootspersona_seo_hook ) {
     1998        add_filter( $rootspersona_seo_hook, array( $rootspersona_plugin, 'filter_persona_seo_title' ) );
     1999    }
     2000    add_filter( 'wpseo_robots', array( $rootspersona_plugin, 'filter_persona_seo_robots' ) );
     2001    add_filter( 'rank_math/frontend/robots', array( $rootspersona_plugin, 'filter_persona_rank_math_robots' ) );
     2002    add_action( 'template_redirect', array( $rootspersona_plugin, 'redirect_private_persona' ) );
     2003    add_filter( 'wp_sitemaps_posts_query_args', array( $rootspersona_plugin, 'exclude_private_from_core_sitemap' ), 10, 2 );
     2004    add_filter( 'wpseo_exclude_from_sitemap_by_post_ids', array( $rootspersona_plugin, 'exclude_private_from_yoast_sitemap' ) );
     2005    add_action( 'update_option_rootspersona_plugin', array( $rootspersona_plugin, 'flush_private_pages_cache' ) );
     2006    add_filter( 'get_pages_query_args', array( $rootspersona_plugin, 'exclude_persona_pages_query' ), 10, 2 );
     2007    add_filter( 'get_pages', array( $rootspersona_plugin, 'exclude_persona_pages' ), 10, 2 );
     2008    add_action( 'wp_ajax_rootspersona_add_pages', array( $rootspersona_plugin, 'add_pages_ajax' ) );
    16002009}
    16012010
    … …  
    16042013// SVG Tree Export Module — safely integrated (load only if files exist to avoid 500 on other sites)
    16052014if ( ! defined( 'ROOTSPERSONA_PLUGIN_DIR' ) ) {
    1606         define( 'ROOTSPERSONA_PLUGIN_DIR', ROOTSPERSONA_PLUGIN_DIR );
     2015        define( 'ROOTSPERSONA_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
    16072016}
    16082017
    1609 $rootspersona_svg_loaded = false;
    1610 try {
    1611     $rootspersona_svg_gen = ROOTSPERSONA_PLUGIN_DIR . 'includes/class-rootspersona-svg-generator.php';
    1612     if ( is_readable( $rootspersona_svg_gen ) ) {
    1613         require_once $rootspersona_svg_gen;
     2018/**
     2019 * The SVG tree module is an unfinished experiment and is OFF by default.
     2020 * Enable it with define( 'ROOTSPERSONA_ENABLE_SVG', true ); in wp-config.php, or with the
     2021 * 'rootspersona_enable_svg_tree' filter. Runs on init so themes and mu-plugins can filter.
     2022 */
     2023if ( ! function_exists( 'rootspersona_load_svg_module' ) ) {
     2024function rootspersona_load_svg_module() {
     2025    $enabled = defined( 'ROOTSPERSONA_ENABLE_SVG' ) ? (bool) ROOTSPERSONA_ENABLE_SVG : false;
     2026    $enabled = (bool) apply_filters( 'rootspersona_enable_svg_tree', $enabled );
     2027
     2028    if ( ! $enabled ) {
     2029        // Keep the shortcode registered so existing pages do not print "[rootspersona_tree]" as text.
     2030        add_shortcode(
     2031            'rootspersona_tree',
     2032            function () {
     2033                if ( current_user_can( 'manage_options' ) ) {
     2034                    return '<p class="rootspersona-notice">'
     2035                        . esc_html__( 'The RootsPersona SVG tree is disabled (experimental feature). To enable it, set the ROOTSPERSONA_ENABLE_SVG constant to true in wp-config.php.', 'rootspersona' )
     2036                        . '</p>';
     2037                }
     2038                return '';
     2039            }
     2040        );
     2041        return;
     2042    }
     2043
     2044    try {
     2045        $generator = ROOTSPERSONA_PLUGIN_DIR . 'includes/class-rootspersona-svg-generator.php';
     2046        if ( ! is_readable( $generator ) ) {
     2047            return;
     2048        }
     2049        require_once $generator;
    16142050        // Do NOT load SVG admin pages (Export / Settings) so they stay hidden from menus.
    1615         $rootspersona_svg_shortcode = ROOTSPERSONA_PLUGIN_DIR . 'includes/class-rootspersona-svg-shortcode.php';
    1616         $rootspersona_svg_export    = ROOTSPERSONA_PLUGIN_DIR . 'includes/class-rootspersona-svg-export.php';
    1617         if ( is_readable( $rootspersona_svg_shortcode ) ) {
    1618             require_once $rootspersona_svg_shortcode;
    1619         }
    1620         if ( is_readable( $rootspersona_svg_export ) ) {
    1621             require_once $rootspersona_svg_export;
    1622         }
    1623         $rootspersona_svg_loaded = true;
     2051        $shortcode = ROOTSPERSONA_PLUGIN_DIR . 'includes/class-rootspersona-svg-shortcode.php';
     2052        $export    = ROOTSPERSONA_PLUGIN_DIR . 'includes/class-rootspersona-svg-export.php';
     2053        if ( is_readable( $shortcode ) ) {
     2054            require_once $shortcode;
     2055        }
     2056        if ( is_readable( $export ) ) {
     2057            require_once $export;
     2058        }
     2059        // nopriv hook intentionally absent: the SVG tree contains genealogy data (names, dates,
     2060        // family relationships) that must not be exposed to anonymous visitors.
     2061        if ( function_exists( 'rootspersona_ajax_load_svg_tree' )
     2062            && false === has_action( 'wp_ajax_rootspersona_load_svg_tree', 'rootspersona_ajax_load_svg_tree' ) ) {
     2063            add_action( 'wp_ajax_rootspersona_load_svg_tree', 'rootspersona_ajax_load_svg_tree' );
     2064        }
     2065    } catch ( Throwable $e ) {
     2066        return;
    16242067    }
    1625 } catch ( Throwable $e ) {
    16262068}
     2069}
     2070add_action( 'init', 'rootspersona_load_svg_module', 1 );
    16272071
    16282072/**
    … …  
    16912135}
    16922136
    1693 if ( ! empty( $rootspersona_svg_loaded ) && function_exists( 'rootspersona_ajax_load_svg_tree' ) ) {
    1694         if ( false === has_action( 'wp_ajax_rootspersona_load_svg_tree', 'rootspersona_ajax_load_svg_tree' ) ) {
    1695                 add_action( 'wp_ajax_rootspersona_load_svg_tree', 'rootspersona_ajax_load_svg_tree' );
    1696         }
    1697         // nopriv hook intentionally removed: SVG tree contains genealogy data (names, dates,
    1698         // family relationships) that must not be exposed to anonymous visitors.
    1699         // Anonymous users fall back to inline rendering in the shortcode (render_tree()).
    1700 }
     2137
  • rootspersona/trunk/uninstall.php

    r3730341 r3749166  
    188188
    189189if ( function_exists( 'is_multisite' ) && is_multisite() ) {
    190         $site_ids = function_exists( 'get_sites' )
     190        $roots_persons_site_ids = function_exists( 'get_sites' )
    191191                ? get_sites( array( 'fields' => 'ids' ) )
    192192                : $wpdb->get_col( "SELECT blog_id FROM {$wpdb->blogs}" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
    193         foreach ( $site_ids as $site_id ) {
    194                 switch_to_blog( (int) $site_id );
     193        foreach ( $roots_persons_site_ids as $roots_persons_site_id ) {
     194                switch_to_blog( (int) $roots_persons_site_id );
    195195                roots_persons_uninstall_blog( $roots_persons_uninstall_dir );
    196196                restore_current_blog();
Note: See TracChangeset for help on using the changeset viewer.