Интеграция Extrafields в плагин I18n в Cotonti CMF

подробное руководство по внесенным изменениям, описывающее каждый файл, логику работы и примеры использования. Материал предназначен для разработчиков, желающих понять принципы интеграции Extrafields в собственные плагины Cotonti.

Полное руководство по интеграции Extrafields в плагин I18n для Cotonti CMF

Введение

Штатный плагин I18n (Content Internationalization) для Cotonti CMF предоставляет возможность создавать многоязычные версии страниц и категорий. Однако изначально он не поддерживал механизм Extrafields (дополнительных полей), который широко используется в других модулях Cotonti (например, в стандартном модуле Pages) для динамического добавления произвольных полей к сущностям без изменения кода.

Целью данной работы является полная интеграция Extrafields в мною модифицированный плагин I18n, а именно:

  • Добавление поддержки дополнительных полей для переводов страниц (cot_i18n_pages).
  • Реализация форм добавления и редактирования переводов с динамическими полями.
  • Корректное сохранение и загрузка значений Extrafields.
  • Вывод значений дополнительных полей в шаблонах:
    • На странице просмотра статьи.
    • В списках статей.
    • В «шапке» сайта (например, для SEO-тегов).
  • Предоставление гибких инструментов для ручного и автоматического вывода полей.

В результате мы получили полноценное расширение функциональности плагина I18n, позволяющее администраторам создавать любые дополнительные поля для переводов (текстовые, текстовые области, даты, файлы и т.д.) и использовать их наравне с обычными полями страниц.

Данный документ представляет собой подробное руководство по внесенным изменениям, описывающее каждый файл, логику работы и примеры использования. Материал предназначен для разработчиков, желающих понять принципы интеграции Extrafields в собственные плагины Cotonti.Тема обсуждения в этом разделе форума.


Часть 1. Архитектура и подготовка

1.1. Обзор механизма Extrafields в Cotonti

Extrafields (дополнительные поля) в Cotonti управляются через API, описанный в файле system/extrafields.php. Основные функции:

  • cot_extrafield_add() – регистрирует новое поле в таблице cot_extra_fields и добавляет соответствующую колонку в целевую таблицу.
  • cot_extrafield_remove() – удаляет поле.
  • cot_build_extrafields() – генерирует HTML-элемент формы для ввода значения поля.
  • cot_import_extrafields() – валидирует и возвращает значение поля из HTTP-запроса.
  • cot_build_extrafields_data() – форматирует значение для вывода в шаблоне.

 

Для того чтобы таблица БД стала доступной для управления Extrafields через административную панель, 

в 

https://cotonti.local/ru/admin/extrafields?n=cot_i18n_pages
Управление сайтом ⇒ Прочее ⇒ Экстраполя ⇒ Таблица cot_i18n_pages - Переводы страниц

необходимо добавить её в «белый список» ($extra_whitelist) и “повесть” на хук admin.extrafields.first

Именно так сделано, например, в модуле Pages (файл 

modules/page/page.extrafields.php

ссылка на код файла).

 

1.2. Структура таблицы переводов

Исходная таблица cot_i18n_pages содержит следующие поля:

  • ipage_id – идентификатор оригинальной страницы.
  • ipage_locale – код языка (например, en, ua).
  • ipage_translatorid, ipage_translatorname – информация о переводчике.
  • ipage_date, ipage_title, ipage_desc, ipage_text – переведённые данные.

Для поддержки Extrafields необходимо, чтобы администратор мог добавлять новые колонки в эту таблицу через админ-интерфейс. При этом API Extrafields автоматически создаст колонки с префиксом ipage_, как того требует логика плагина (аналогично page_ для таблицы cot_pages).


Часть 2. Регистрация таблицы в админ-панели Extrafields

2.1. Файл plugins/i18n/i18n.extrafields.php

Для того чтобы таблица cot_i18n_pages появилась в списке доступных для управления Extrafields, необходимо создать файл, подключаемый через хук admin.extrafields.first.

Путь: plugins/i18n/i18n.extrafields.php

Содержимое:

 

<?php
/* ====================
  [BEGIN_COT_EXT]
  Hooks=admin.extrafields.first
  [END_COT_EXT]
==================== */

// файл новый i18n.extrafields.php
// Путь: plugins/i18n/i18n.extrafields.php
defined('COT_CODE') or die('Wrong URL.');

require_once cot_incfile('i18n', 'plug');

$extra_whitelist[$db_i18n_pages] = [
    'name'    => $db_i18n_pages,
    'caption' => $L['i18n_pages'],
    'type'    => 'plug',
    'code'    => 'i18n',
    'tags'    => [
        'i18n.page.tpl' => '{I18N_PAGE_FORM_XXXXX}, {I18N_PAGE_FORM_XXXXX_TITLE}',
    ]
];

 

Про себя всегдя читаю так: 

создать файл, (обычно, но невсегда это: префикс расширения и суфикс с именем самого хука, на который будем вешать код логики этого файла).

В корне плагина создаем файл i18n.extrafields.php

в файл i18n.extrafields.php помещаем код логики:

defined('COT_CODE') or die('Wrong URL.');
require_once cot_incfile('i18n', 'plug');
$extra_whitelist[$db_i18n_pages] = [
 .....
];

затем обьявляем, 

<?php
/* ====================
  [BEGIN_COT_EXT]
  Hooks=admin.extrafields.first
  [END_COT_EXT]
==================== */

что код этой логики “вешаем на крючок” - подключаем к хуку admin.extrafields.first

где этот крючок “прибит, завинчен, закреплен, расположен, что бы на него что-то вешали”, а именно в файле

/system/admin/admin.extrafields.php

а конкретно в участке 

/* === Hook === */
foreach (cot_getextplugins('admin.extrafields.first') as $pl) {
	include $pl;
}
/* ===== */

 

Как это работает в Cotonti (размещение хука и подключение кода к нему):

Файл, в котором непосредственно происходит вызов хука (висит крючок) с именем
'admin.extrafields.first' 
и, соответственно, подключение файла с нужным нам кодом в i18n.extrafields.php, — это 

/system/admin/admin.extrafields.php

Файл /system/extrafields.php не содержит вызовов хуков, он является библиотекой функций API для работы с дополнительными полями и подключается в admin.extrafields.php через require_once. Сам он не инициирует загрузку плагинов.
Таким образом, последовательность:
Открывается admin.php?m=extrafields.
Загружается /system/admin/admin.extrafields.php.
Внутри него выполняется require_once cot_incfile('extrafields'); (подключается /system/extrafields.php).
Затем в admin.extrafields.php срабатывает хук admin.extrafields.first, который и подключает i18n.extrafields.php (и другие файлы расширений).
 


Конкретная последовательность выполнения файлов и работа хуков:

(это описывает принцип работы хуков в Cotonti, поэтому пишу здесь как шпаргалку, особенно для новичков)

   Пользователь открывает админку доп. полей:
   URL: admin.php?m=extrafields

   Система Cotonti загружает файл:
   /system/admin/admin.extrafields.php

   В начале этого файла выполняется:
   require_once cot_incfile('extrafields');
   → это подключает /system/extrafields.php (библиотеку функций API).

   Затем в admin.extrafields.php инициализируется массив $extra_whitelist
   (изначально в нём только $db_structure – категории).

   Сразу после этого идёт хук:

   foreach (cot_getextplugins('admin.extrafields.first') as $pl) {
       include $pl;
   }

    
Система ищет все файлы, зарегистрированные на хук admin.extrafields.first.
Один из них новый файл i18n.extrafields.php (из папки плагина i18n который создали).

   Файл i18n.extrafields.php подключается прямо в этом месте.
   Внутри него выполняется код: 

$extra_whitelist[$db_i18n_pages] = [ ... ];

    
   Таким образом, в глобальный массив $extra_whitelist добавляется информация о таблице $db_i18n_pages (локализация контента страницы).
   После завершения цикла хуков admin.extrafields.php продолжает работу.
   Далее идёт построение списка таблиц БД для управления Экстраполями (дополнительными полями), и в этом списке уже присутствуют страницы.
Итог:
   /system/extrafields.php загружается первым, но он лишь предоставляет функции и не влияет на хуки.
   /system/admin/admin.extrafields.php — основной файл, который инициирует хук admin.extrafields.first.
   i18n.extrafields.php срабатывает в момент выполнения хука, дополняя список разрешённых таблиц.
Таким образом, код из i18n.extrafields.php выполняется до отображения интерфейса админки, но после загрузки основных функций и начальной инициализации admin.extrafields.php.

 

Пояснения:

  • $db_i18n_pages – глобальная переменная, содержащая полное имя таблицы (например, cot_i18n_pages).
  • caption – локализованное название, берётся из языкового файла.
  • tags – подсказка для администратора, какие теги можно использовать в шаблонах (в данном случае для формы редактирования перевода).

Языковой файл (plugins/i18n/lang/i18n.ru.lang.php) дополнен строкой:

 

$L['i18n_pages']='Переводы страниц';

После добавления этого файла таблица cot_i18n_pages становится доступной в разделе «Администрирование → Extrafields», и администратор может создавать для неё любые дополнительные поля.

я вот тупанул, и мне выдало: 

Warning: Undefined array key "i18n_pages" 
in /home/..../public_html/plugins/i18n/i18n.extrafields.php on line 16 

никогда не бойтесь таких ошибок - критичного ничего нет, 

мы добавили строку локизации $L['i18n_pages'] в файл i18n.extrafields но сам ключ забыли прописать в языковых файлах.

Отрываем папку с файлами строк локализации

/plugins/i18n/lang/

и в файл нужного языка, например i18n.ru.lang.php

 добавляем строку 

$L['i18n_pages'] = 'Переводы страниц';

и так для каждого языка локализации определяем ключ для элемента 'caption' => $L['i18n_pages'], в массиве $extra_whitelist[$db_i18n_pages] = []; нашего файла i18n.extrafields 

 


Часть 3. Модификация основного скрипта переводов

3.1. Файл plugins/i18n/inc/i18n.page.php

Этот файл отвечает за добавление (a=add), редактирование (a=edit) и удаление (a=delete) переводов страниц. Внесённые изменения обеспечивают загрузку, отображение, валидацию и сохранение Extrafields.

3.1.1. Подключение API Extrafields

В начале файла после стандартных require_once 

а именно, после строки:

require_once cot_incfile('forms');

добавляем

// === EXTRAFIELDS: Подключаем API дополнительных полей ===
require_once cot_incfile('extrafields');

 

3.1.2. Загрузка конфигурации полей

Перед строкой

if ($a == 'add' && empty($pag_i18n)) {

добавляем

 

    // === EXTRAFIELDS: Загружаем конфигурацию дополнительных полей для таблицы переводов ===
    $extrafields = Cot::$extrafields[$db_i18n_pages] ?? [];

После определения $pag_i18n (загрузки существующего перевода).Этот массив содержит все зарегистрированные дополнительные поля для таблицы переводов.

3.1.3. Режим добавления ($a == 'add')

Дальше, сразу после массива (примерно строки 65-78):

            $pag_i18n = [
                'ipage_id' => $id,
                'ipage_locale' => $selected_locale,
                'ipage_translatorid' => Cot::$usr['id'],
                'ipage_translatorname' => Cot::$usr['name'],
                'ipage_date' => Cot::$sys['now'],
                'ipage_title' => cot_import('title', 'P', 'TXT'),
                'ipage_desc' => cot_import('desc', 'P', 'TXT'),
                'ipage_text' => cot_import('translate_text', 'P', 'HTM')
            ];

добавляем импорт, вставляем код

            // === EXTRAFIELDS: Импортируем значения дополнительных полей из POST ===
            foreach ($extrafields as $exfld) {
                $fieldName = 'ipage_' . $exfld['field_name'];
                $pag_i18n[$fieldName] = cot_import_extrafields(
                    'ri18n' . $exfld['field_name'],
                    $exfld,
                    'P',
                    '',
                    'i18n_'
                );
            }

Импорт и сохранение

При POST-запросе после сбора основных полей происходит импорт Extrafields:

Функция cot_import_extrafields() корректно обрабатывает все типы полей, включая файлы, чекбоксы и даты.

 

Отображение полей в форме

В шаблон передаются теги для каждого поля:

после массива:

        $t->assign([
            'I18N_ACTION' => cot_url('plug', "e=i18n&m=page&a=add&id=$id"),
            'I18N_TITLE' => Cot::$L['i18n_adding'],
            'I18N_ORIGINAL_LANG' => $i18n_locales[Cot::$cfg['defaultlang']],
            'I18N_LOCALIZED_LANG' => cot_selectbox($selected_in_selector, 'locale', $lc_values, $lc_names, false),
            'I18N_PAGE_TITLE' => htmlspecialchars($pag['page_title']),
            'I18N_PAGE_DESC' => htmlspecialchars($pag['page_desc']),
            'I18N_PAGE_TEXT' => cot_parse($pag['page_text'], Cot::$cfg['page']['markup']),
            'I18N_IPAGE_TITLE' => htmlspecialchars($title_val),
            'I18N_IPAGE_DESC' => htmlspecialchars($desc_val),
            'I18N_IPAGE_TEXT' => cot_textarea('translate_text', $text_val, 32, 80, '', 'input_textarea_editor')
        ]);

вставляем код

        // === EXTRAFIELDS: Генерируем и передаём в шаблон поля для ввода дополнительных данных ===
        if (!empty($extrafields)) {
            foreach ($extrafields as $exfld) {
                $uname = strtoupper($exfld['field_name']);
                $fieldValue = $pag_i18n['ipage_' . $exfld['field_name']] ?? null;
                $extrafieldElement = cot_build_extrafields(
                    'ri18n' . $exfld['field_name'],
                    $exfld,
                    $fieldValue
                );
                $extrafieldTitle = cot_extrafield_title($exfld, 'i18n_');

                // === FIX: Добавлены общие теги EXTRAFLD для шаблона ===
                $t->assign([
                    'I18N_PAGE_FORM_' . $uname            => $extrafieldElement,
                    'I18N_PAGE_FORM_' . $uname . '_TITLE' => $extrafieldTitle,
                    'I18N_PAGE_FORM_EXTRAFLD'             => $extrafieldElement,
                    'I18N_PAGE_FORM_EXTRAFLD_TITLE'       => $extrafieldTitle,
                ]);
                $t->parse('MAIN.EXTRAFLD');
            }
        }

Здесь используются два типа тегов:

  • Специфичные для каждого поля (I18N_PAGE_FORM_ИМЯПОЛЯ).
  • Общие I18N_PAGE_FORM_EXTRAFLD и _TITLE, которые позволяют вывести все поля в цикле через один блок <!-- BEGIN: EXTRAFLD -->.

 

3.1.4. Режим редактирования ($a == 'edit')

Находим строку:

if ($_SERVER['REQUEST_METHOD'] == 'POST') {

Перед ней, добавить код:

        // === EXTRAFIELDS: Сохраняем старые данные для корректной обработки файловых полей ===
        $pag_i18n_old = $pag_i18n;

Сохранение старых данных

Для корректной работы файловых полей (чтобы при удалении файла он физически удалялся с сервера) необходимо сохранить старые значения:

$pag_i18n_old=$pag_i18n;

 

Импорт при POST

При сохранении изменений Extrafields импортируются с передачей старых значений:

находим строки (примерно 200):

            $pag_i18n['ipage_locale'] = $new_locale;

            if (cot_error_found()) {

 

и именно между ними вставляем

            // === EXTRAFIELDS: Импортируем значения дополнительных полей из POST, передавая старые значения ===
            foreach ($extrafields as $exfld) {
                $fieldName = 'ipage_' . $exfld['field_name'];
                $oldValue = $pag_i18n_old[$fieldName] ?? '';
                $pag_i18n[$fieldName] = cot_import_extrafields(
                    'ri18n' . $exfld['field_name'],
                    $exfld,
                    'P',
                    $oldValue,
                    'i18n_'
                );
            }

Критическое исправление сохранения

 

В моем модифицированном плагине обновление записи выполнялось с явным перечислением полей:

                Cot::$db->update(Cot::$db->i18n_pages,
                    [
                        'ipage_locale' => $new_locale,
                        'ipage_date' => $pag_i18n['ipage_date'],
                        'ipage_title' => $pag_i18n['ipage_title'],
                        'ipage_desc' => $pag_i18n['ipage_desc'],
                        'ipage_text' => $pag_i18n['ipage_text']
                    ],
                    "ipage_id = ? AND ipage_locale = ?",
                    [$id, $i18n_locale]
                );

Такой подход игнорирует любые дополнительные поля. Чтобы они сохранялись, необходимо передать весь массив $pag_i18n, предварительно удалив из него первичный ключ ipage_id:

поэтому этот блок кода заменить на код ниже:

unset($pag_i18n['ipage_id']);
// старый ДиБи-update заменили на новый
Cot::$db->update(Cot::$db->i18n_pages, $pag_i18n, "ipage_id = ? AND ipage_locale = ?", [$id, $i18n_locale]);

Это позволяет API базы данных автоматически обновить все колонки, включая динамически добавленные.

Отображение полей в форме редактирования

Аналогично режиму добавления, поля генерируются с учётом уже введённых значений (в том числе после ошибок валидации). Значение берётся либо из $pag_i18n, либо из POST.

находим массив для вывода тегов в шаблон: 

        $t->assign([
            'I18N_ACTION' => cot_url('plug', "e=i18n&m=page&a=edit&id=$id&l=$i18n_locale"),
            'I18N_TITLE' => Cot::$L['i18n_editing'],
            'I18N_ORIGINAL_LANG' => $i18n_locales[Cot::$cfg['defaultlang']],
            'I18N_LOCALIZED_LANG' => cot_selectbox($selected_in_selector, 'locale', $lc_values, $lc_names, false),
            'I18N_PAGE_TITLE' => htmlspecialchars($pag['page_title']),
            'I18N_PAGE_DESC' => htmlspecialchars($pag['page_desc']),
            'I18N_PAGE_TEXT' => cot_parse($pag['page_text'], Cot::$cfg['page']['markup']),
            'I18N_IPAGE_TITLE' => htmlspecialchars($title_val),
            'I18N_IPAGE_DESC' => htmlspecialchars($desc_val),
            'I18N_IPAGE_TEXT' => cot_textarea('translate_text', $text_val, 32, 80, '', 'input_textarea_editor')
        ]);

и сразу после него добавляем вывод в шаблон наших экстраполей:

        // === EXTRAFIELDS: Генерируем и передаём в шаблон поля для ввода дополнительных данных (редактирование) ===
        if (!empty($extrafields)) {
            foreach ($extrafields as $exfld) {
                $uname = strtoupper($exfld['field_name']);
                $fieldValue = $pag_i18n['ipage_' . $exfld['field_name']] ?? null;
                $extrafieldElement = cot_build_extrafields(
                    'ri18n' . $exfld['field_name'],
                    $exfld,
                    $fieldValue
                );
                $extrafieldTitle = cot_extrafield_title($exfld, 'i18n_');

                // === FIX: Добавлены общие теги EXTRAFLD для шаблона ===
                $t->assign([
                    'I18N_PAGE_FORM_' . $uname            => $extrafieldElement,
                    'I18N_PAGE_FORM_' . $uname . '_TITLE' => $extrafieldTitle,
                    'I18N_PAGE_FORM_EXTRAFLD'             => $extrafieldElement,
                    'I18N_PAGE_FORM_EXTRAFLD_TITLE'       => $extrafieldTitle,
                ]);
                $t->parse('MAIN.EXTRAFLD');
            }
        }

 

Всё! 

на этом правка файла /plugins/i18n/inc/i18n.page.php закончена. 

Сам файл полностью на github тут.

Теперь открваем сам шаблон заполнения и редактирования переводов контента на другие языки 

стандартный шабло лежит в 

/plugins/i18n/tpl/i18n.page.tpl

но мы используем свой.

и докидуем динамический вывод полей

<!-- ===== БЛОК ДОПОЛНИТЕЛЬНЫХ ПОЛЕЙ (EXTRAFIELDS) ===== -->
<!-- Каждое поле выводится в цикле через блок EXTRAFLD -->
<!-- BEGIN: EXTRAFLD -->
<div class="py-2">
	<label class="form-label fw-semibold">{I18N_PAGE_FORM_EXTRAFLD_TITLE}</label>
	{I18N_PAGE_FORM_EXTRAFLD}
</div>
<!-- END: EXTRAFLD -->
<!-- ===== КОНЕЦ БЛОКА EXTRAFIELDS ===== -->

 


Часть 4. Вывод Extrafields в шаблонах модуля страниц

 

Открываем файл /plugins/i18n/i18n.pagetags.php

 

Чтобы не рассусоливать, находим код:

	if (!empty($page_data['ipage_title'])) {
		$text = cot_parse($page_data['ipage_text'], Cot::$cfg['page']['markup'], $page_data['page_parser']);
		$text_cut = ((int) $textLength > 0) ? cot_string_truncate($text, $textLength) : cot_cut_more($text);
		$cutted = mb_strlen($text) > mb_strlen($text_cut);

        $pageDescription = !empty($page_data['ipage_desc'])
            ? htmlspecialchars($page_data['ipage_desc'])
            : '';

		$page_link = array(array(cot_url('page', $urlparams), $page_data['ipage_title']));
		$i18n_array = array_merge(
            $i18n_array,
            [
                'URL' => cot_url('page', $urlparams),
                'TITLE' => htmlspecialchars($page_data['ipage_title']),
                'BREADCRUMBS' => cot_breadcrumbs(array_merge($pagepath, $page_link), $pagepath_home),
                'DESCRIPTION' => $pageDescription,
                'TEXT' => $text,
                'TEXT_CUT' => $text_cut,
                'TEXT_IS_CUT' => $cutted,
                'DESCRIPTION_OR_TEXT' => $pageDescription !== '' ? $pageDescription : $text,
                'DESCRIPTION_OR_TEXT_CUT' => $pageDescription !== '' ? $pageDescription : $text_cut,
                'MORE' => $cutted ? cot_rc_link($page_data['page_pageurl'], Cot::$L['ReadMore']) : '',
                'UPDATED_STAMP' => $page_data['ipage_date'],
            ]
        );
        if (isset(Cot::$cfg['legacyMode']) && Cot::$cfg['legacyMode']) {
            $i18n_array = array_merge(
                $i18n_array,
                [
                    // @deprecated in 0.9.24
                    'SHORTTITLE' => htmlspecialchars($page_data['ipage_title']),
                    'DESC' => $pageDescription,
                    'DESC_OR_TEXT' => $pageDescription !== '' ? $pageDescription : $text,
                ]
            );
        }
	}

меняем его на код ниже

	if (!empty($page_data['ipage_title'])) {
		$text = cot_parse($page_data['ipage_text'], Cot::$cfg['page']['markup'], $page_data['page_parser']);
		$text_cut = ((int) $textLength > 0) ? cot_string_truncate($text, $textLength) : cot_cut_more($text);
		$cutted = mb_strlen($text) > mb_strlen($text_cut);

        $pageDescription = !empty($page_data['ipage_desc'])
            ? htmlspecialchars($page_data['ipage_desc'])
            : '';

		$page_link = array(array(cot_url('page', $urlparams), $page_data['ipage_title']));
		$i18n_array = array_merge(
            $i18n_array,
            [
                'URL' => cot_url('page', $urlparams),
                'TITLE' => htmlspecialchars($page_data['ipage_title']),
                'BREADCRUMBS' => cot_breadcrumbs(array_merge($pagepath, $page_link), $pagepath_home),
                'DESCRIPTION' => $pageDescription,
                'TEXT' => $text,
                'TEXT_CUT' => $text_cut,
                'TEXT_IS_CUT' => $cutted,
                'DESCRIPTION_OR_TEXT' => $pageDescription !== '' ? $pageDescription : $text,
                'DESCRIPTION_OR_TEXT_CUT' => $pageDescription !== '' ? $pageDescription : $text_cut,
                'MORE' => $cutted ? cot_rc_link($page_data['page_pageurl'], Cot::$L['ReadMore']) : '',
                'UPDATED_STAMP' => $page_data['ipage_date'],
            ]
        );

		// === EXTRAFIELDS: Формируем теги для каждого дополнительного поля перевода ===
		if (!empty(Cot::$extrafields[Cot::$db->i18n_pages])) {
			foreach (Cot::$extrafields[Cot::$db->i18n_pages] as $exfld) {
				$tag = 'I18N_PAGE_' . strtoupper($exfld['field_name']);
				$value = $page_data['ipage_' . $exfld['field_name']] ?? null;
				$i18n_array[$tag] = cot_build_extrafields_data('i18n', $exfld, $value, $page_data['page_parser']);
				$i18n_array[$tag . '_TITLE'] = cot_extrafield_title($exfld, 'i18n_');
				$i18n_array[$tag . '_VALUE'] = $value;
			}
		}
		// === END EXTRAFIELDS ===
	} else {
		// === Сброс тегов экстраполей для страниц без перевода ===
		if (!empty(Cot::$extrafields[Cot::$db->i18n_pages])) {
			foreach (Cot::$extrafields[Cot::$db->i18n_pages] as $exfld) {
				$tag = 'I18N_PAGE_' . strtoupper($exfld['field_name']);
				$i18n_array[$tag] = '';
				$i18n_array[$tag . '_TITLE'] = '';
				$i18n_array[$tag . '_VALUE'] = '';
			}
		}
		// === END СБРОС ===
	}

 

Обратите внимание, что я никогда не использую @deprecated in 0.9.24 - устаревшее в мусорку

 

Теперь, кому интересно, - вчитываемся, что здесь происходит.

После сохранения данных необходимо отобразить их посетителям сайта. Cotonti предоставляет несколько уровней шаблонизации, и мы реализовали поддержку Extrafields для всех основных сценариев.

 

4.1. Индивидуальные теги через cot_generate_pagetags() (ручной вывод)

Функция cot_generate_pagetags() вызывается для каждой страницы (в списке или при одиночном просмотре) и возвращает массив тегов с префиксом. Плагин I18n переопределяет её через хук pagetags.main.

Файл: plugins/i18n/inc/i18n.pagetags.php

Внутри условия, когда перевод существует (!empty($page_data['ipage_title'])), добавляется цикл по Extrafields:

		if (!empty(Cot::$extrafields[Cot::$db->i18n_pages])) {
			foreach (Cot::$extrafields[Cot::$db->i18n_pages] as $exfld) {
				$tag = 'I18N_PAGE_' . strtoupper($exfld['field_name']);
				$value = $page_data['ipage_' . $exfld['field_name']] ?? null;
				$i18n_array[$tag] = cot_build_extrafields_data('i18n', $exfld, $value, $page_data['page_parser']);
				$i18n_array[$tag . '_TITLE'] = cot_extrafield_title($exfld, 'i18n_');
				$i18n_array[$tag . '_VALUE'] = $value;
			}
		}

Эти теги затем сливаются с основным массивом $temp_array и становятся доступными в шаблонах с соответствующим префиксом.

Пример использования в шаблоне page.tpl:

имя полей тупо для примера, использовалось для тестирования с успешным финалом

<!-- IF {I18N_META_TITLE} --> 
<div class="news-item">
    <span class="label">{I18N_META_TITLE_TITLE}:</span>
    <span class="value">{I18N_META_TITLE}</span>
</div>	
<!-- ENDIF -->

<!-- IF {I18N_META_DESCRIPTION} --> 
<div class="news-item">
    <span class="label">{I18N_META_DESCRIPTION_TITLE}:</span>
    <span class="value">{I18N_META_DESCRIPTION}</span>
</div>	
<!-- ENDIF -->

 

где META_DESCRIPTION - это имя экстраполя в нижнем регистре 'meta_description' при создания экстраполей для плагина i18n
где META_TITLE - это имя экстраполя в нижнем регистре 'meta_title' при создания экстрfполей для плагина i18n

смотрите скрин

 

Проблема дублирования в списках и её решение (немного забегая вперед, но чтоб не возращаться снова сюда)

При использовании в цикле списка статей (page.list.tpl) XTemplate сохраняет значения тегов между итерациями, если они не переопределены. Поэтому для статей без перевода теги Extrafields содержали данные от предыдущей статьи.

Решение: добавлен блок else после проверки if (!empty($page_data['ipage_title'])), который сбрасывает теги в пустые строки:

 

   if (!empty($page_data['ipage_title'])) {
      ... 
		// === END EXTRAFIELDS ===
	} else {
		// === Сброс тегов экстраполей для страниц без перевода ===
		if (!empty(Cot::$extrafields[Cot::$db->i18n_pages])) {
			foreach (Cot::$extrafields[Cot::$db->i18n_pages] as $exfld) {
				$tag = 'I18N_PAGE_' . strtoupper($exfld['field_name']);
				$i18n_array[$tag] = '';
				$i18n_array[$tag . '_TITLE'] = '';
				$i18n_array[$tag . '_VALUE'] = '';
			}
		}
		// === END СБРОС ===
	}

Теперь для каждой статьи без перевода поля гарантированно пусты.

 

4.2. Динамический вывод всех полей на странице просмотра

Для вывода всех полей сразу без ручного перечисления используется хук page.tags и блок <!-- BEGIN: EXTRAFLD -->.

Файл: plugins/i18n/inc/i18n.page.tags.php

В этом файле, который выполняется уже после создания объекта шаблона $t,  

находим код

	if ($i18n_admin) {
		// Control tags
		if (!empty($pag_i18n)) {
			// Delete translation button and URL
            $i18nDeleteUrl = cot_url(
                'plug',
                ['e' => 'i18n', 'm' => 'page', 'a' => 'delete', 'id' => $id, 'l' => $i18n_locale]);
            $i18nDeleteConfirmUrl = cot_confirm_url($i18nDeleteUrl, 'i18n', 'i18n_confirm_delete');
			$t->assign([
				'PAGE_I18N_DELETE' => cot_rc_link($i18nDeleteConfirmUrl, Cot::$L['i18n_delete'], 'class="confirmLink"'),
				'PAGE_I18N_DELETE_URL' => $i18nDeleteConfirmUrl,
			]);
		}
	}

и сразу после него вставляем следующий код:

	// === Extrafields for i18n pages — вывод в шаблон ===
	if (!empty($pag_i18n) && !empty(Cot::$extrafields[Cot::$db->i18n_pages])) {
		foreach (Cot::$extrafields[Cot::$db->i18n_pages] as $exfld) {
			$tag = mb_strtoupper($exfld['field_name']);
			$exfld_title = cot_extrafield_title($exfld, 'i18n_');

			$temp_value = null;
			if (isset($pag_i18n['ipage_' . $exfld['field_name']])) {
				$temp_value = $pag_i18n['ipage_' . $exfld['field_name']];
			}

			// Индивидуальные теги (ручной вывод)
			$t->assign([
				'I18N_' . $tag . '_TITLE' => $exfld_title,
				'I18N_' . $tag           => cot_build_extrafields_data('i18n', $exfld, $temp_value, $pag['page_parser']),
				'I18N_' . $tag . '_VALUE' => $temp_value,
			]);

			// Динамический вывод через блок EXTRAFLD
			$t->assign([
				'I18N_EXTRAFIELD_TITLE' => $exfld_title,
				'I18N_EXTRAFIELD_VALUE' => cot_build_extrafields_data('i18n', $exfld, $temp_value, $pag['page_parser']),
			]);
			$t->parse('MAIN.EXTRAFLD');
		}
	}

 

Шаблон page.tpl должен содержать:

для динамического вывода

<!-- BEGIN: EXTRAFLD -->
<div class="form-group">
<label>{I18N_EXTRAFIELD_TITLE}</label>
<div>{I18N_EXTRAFIELD_VALUE}</div>
</div>
<!-- END: EXTRAFLD -->

или индивидуально, как это было в примере выше с полями

{I18N_META_TITLE} и {I18N_META_DESCRIPTION}

При парсинге для каждого поля будет выведен отдельный блок.

 

4.3. Передача Extrafields в шаблон «шапки» сайта (header.tpl)

Теперь переходим к самому интересному, из-за чего собственно и начался “сыр-бор” и движуха с экстраполями в плагине локализации контента, - это именно выдать отличимые от стандартных полей, именно свои заголовки и описание в мета-теги страницы.

Часто SEO-поля (мета-заголовок, описание) должны быть выведены в тегах <title> и <meta name="description">, которые находятся в header.tpl. Для этого мы расширили хук header.tags.

Файл: plugins/i18n/inc/i18n.header.tags.php

открываем его, и в самый конец добавляем


// ========== 4. ПЕРЕДАЧА ЭКСТРАПОЛЕЙ ПЕРЕВОДА В HEADER ==========
if ($env['ext'] == 'page' && isset($id) && $id > 0) {
    // Загружаем перевод для текущего языка, если ещё не загружен
    if (empty($pag_i18n) && !empty($i18n_locale)) {
        $pag_i18n = cot_i18n_get_page($id, $i18n_locale);
    }
    
    if (!empty($pag_i18n) && !empty(Cot::$extrafields[Cot::$db->i18n_pages])) {
        // Загружаем данные оригинальной страницы для парсера
        $page_data = Cot::$db->query("SELECT page_parser FROM " . Cot::$db->pages . " WHERE page_id = ?", array($id))->fetch();
        $parser = $page_data['page_parser'] ?? Cot::$cfg['page']['parser'];
        
        foreach (Cot::$extrafields[Cot::$db->i18n_pages] as $exfld) {
            $field_name = $exfld['field_name'];
            $tag = 'I18N_HEADER_' . strtoupper($field_name);
            $value = $pag_i18n['ipage_' . $field_name] ?? '';
            
            // Присваиваем теги для header.tpl
            $t->assign([
             // без экранирования
             //   $tag                     => cot_build_extrafields_data('i18n', $exfld, $value, $parser), 
             // экранируем кавычки и другие специальные символы
				$tag                     => htmlspecialchars(cot_build_extrafields_data('i18n', $exfld, $value, $parser), ENT_QUOTES, 'UTF-8'), 
                $tag . '_TITLE'          => cot_extrafield_title($exfld, 'i18n_'),
                $tag . '_VALUE'          => $value,
            ]);
        }
    } else {
        // Сброс тегов, если перевод отсутствует
        if (!empty(Cot::$extrafields[Cot::$db->i18n_pages])) {
            foreach (Cot::$extrafields[Cot::$db->i18n_pages] as $exfld) {
                $tag = 'I18N_HEADER_' . strtoupper($exfld['field_name']);
                $t->assign([
                    $tag             => '',
                    $tag . '_TITLE'  => '',
                    $tag . '_VALUE'  => '',
                ]);
            }
        }
    }
}

мой полный файл /plugins/i18n/i18n.header.tags.php лежит в репозитории

обратите внимание, здесь пришлось применить экранирование, и вместо спецсимволов будут выводится их коды, например вместо  будет &quot;  - это потому, что в шаблоне мы вставляем {I18N_HEADER_META_DESCRIPTION} непосредственно в атрибут content="". Если значение содержит кавычки ( " ), это может сломать HTML. Поэтому лучше экранировать спецсимволы на стороне PHP перед передачей в шаблон, что мы и сделали, заменив строку

$tag                     => cot_build_extrafields_data('i18n', $exfld, $value, $parser),

на 

$tag                     => htmlspecialchars(cot_build_extrafields_data('i18n', $exfld, $value, $parser), ENT_QUOTES, 'UTF-8'), 

 

Использование в header.tpl:

добавляем смело

	<!-- IF {I18N_HEADER_META_TITLE} --> 
		<title>{I18N_HEADER_META_TITLE}</title>
	<!-- ELSE -->
		<title>{HEADER_TITLE}</title>
	<!-- ENDIF -->

	<!-- IF {I18N_HEADER_META_DESCRIPTION} --> 
		<meta name="description" content="{I18N_HEADER_META_DESCRIPTION}" />
	<!-- ELSE -->
		<!-- IF {HEADER_META_DESCRIPTION} -->
		<meta name="description" content="{HEADER_META_DESCRIPTION}" />
		<!-- ENDIF -->
	<!-- ENDIF -->

Таким образом, при наличии перевода с заполненными Extrafields meta_title и meta_description они заменяют стандартные мета-теги страницы.


Часть 5. Структура базы данных и примеры

5.1. Таблица cot_extra_fields

В ней хранятся определения всех дополнительных полей. Для нашей задачи были добавлены две записи для cot_i18n_pages:

sql

INSERTINTO`cot_extra_fields`(`field_location`,`field_name`,`field_type`,`field_html`,`field_variants`,`field_params`,`field_default`,`field_required`,`field_enabled`,`field_parse`,`field_description`)VALUES
('cot_i18n_pages','meta_title','input','<input class=\"form-control\" type=\"text\" name=\"{$name}\" value=\"{$value}\" maxlength=\"255\" />','','','',0,1,'HTML','Мета-заголовк'),
('cot_i18n_pages','meta_description','textarea','<textarea class=\"form-control\" name=\"{$name}\" rows=\"{$rows}\" cols=\"{$cols}\" maxlength=\"255\">{$value}</textarea>','','','',0,1,'HTML','Мета описание');

5.2. Таблица cot_i18n_pages

При создании полей через админ-панель API Extrafields автоматически добавил колонки ipage_meta_title и ipage_meta_description:

sql

ALTERTABLE`cot_i18n_pages`ADD`ipage_meta_title`VARCHAR(255)DEFAULT'';
ALTERTABLE`cot_i18n_pages`ADD`ipage_meta_description`TEXT;

Теперь таблица имеет следующую структуру (фрагмент):

sql

CREATETABLE`cot_i18n_pages`(
`ipage_id`intUNSIGNEDNOTNULL,
`ipage_locale`varchar(8)NOTNULLDEFAULT'en',
...
`ipage_meta_title`varchar(255)DEFAULT'',
`ipage_meta_description`text,
PRIMARYKEY(`ipage_id`,`ipage_locale`),
...
);

5.3. Пример данных перевода с заполненными Extrafields

 

INSERTINTO`cot_i18n_pages`
(`ipage_id`,`ipage_locale`,`ipage_translatorid`,`ipage_translatorname`,`ipage_date`,`ipage_title`,`ipage_desc`,`ipage_text`,`ipage_meta_title`,`ipage_meta_description`)VALUES
(63,'en',3,'webitproff',1776872330,'Calculate Stupid Efficiency...','learn how to calculate...','<h1>...</h1>','Formulas, Examples','Conclusion...');

Часть 6. Шаблоны и их модификация

 

Пройдемся снова по шаблонам, закрепим, что бы уже было в одном месте по финалу.

6.1. Форма перевода i18n.page.tpl

В форму были добавлены блоки для вывода Extrafields:

динамический вывод:

<!-- BEGIN: EXTRAFLD -->
<div class="py-2">
<label class="form-label fw-semibold">{I18N_PAGE_FORM_EXTRAFLD_TITLE}</label>
 {I18N_PAGE_FORM_EXTRAFLD}
</div>
<!-- END: EXTRAFLD -->

 

6.2. Страница просмотра page.tpl

Используются два подхода:

 

Ручной вывод конкретных полей:

<!-- IF {I18N_META_TITLE} -->
<div class="news-item">
<span class="label">{I18N_META_TITLE_TITLE}:</span>
<span class="value">{I18N_META_TITLE}</span>
</div>
<!-- ENDIF -->

 

Динамический вывод всех полей:

<!-- BEGIN: EXTRAFLD -->
<div class="form-group">
<label>{I18N_EXTRAFIELD_TITLE}</label>
<div>{I18N_EXTRAFIELD_VALUE}</div>
</div>
<!-- END: EXTRAFLD -->

 

6.3. Список статей page.list.tpl

Для вывода в списке доступны теги с префиксом LIST_ROW_I18N_PAGE_...

!!! Отвечает файл i18n.pagetags.php

Работает только индивидуальный (ручное прописывание) полей в шаблоне

Пример:

<!-- IF {LIST_ROW_I18N_PAGE_META_TITLE} --> 
<div class="news-item">
    <span class="label">{LIST_ROW_I18N_PAGE_META_TITLE_TITLE}:</span>
    <span class="value">{LIST_ROW_I18N_PAGE_META_TITLE}</span>
</div>	
<!-- ENDIF -->
<!-- IF {LIST_ROW_I18N_PAGE_META_DESCRIPTION} --> 
<div class="news-item">
    <span class="label">{LIST_ROW_I18N_PAGE_META_DESCRIPTION_TITLE}:</span>
    <span class="value">{LIST_ROW_I18N_PAGE_META_DESCRIPTION}</span>
</div>	
<!-- ENDIF -->

 

6.4. Шапка сайта header.tpl

короткий вариант был выше, а здесь для полноты картиын:

<!--
	/********************************************************************************
	* File: header.tpl
	* Extension: Core'
	* Description: HTML template for header.tpl.
	* Compatibility: CMF/CMS Cotonti Siena v0.9.26[](https://github.com/Cotonti/Cotonti)
	* Dependencies: 
	* 		 Bootstrap 5.3.+[](https://getbootstrap.com/); 
	* 		 Font Awesome Free 7.1[](https://fontawesome.com/)
	* Theme: Index36  
	* Version: 1.0.2 
	* Created: 01 Feb 2026 
	* Updated: 22 Apr 2026 
	* Copyright (c) 2026 webitproff | https://github.com/webitproff
	* Source: https://github.com/webitproff/index36-cotonti-theme
	* Demo : https://freelance-script.abuyfile.com/ 
	* Help and support: https://abuyfile.com/ru/forums/cotonti/original/skins/index36
	* License: BSD (Free distribution with saving Copyright (c) 2026 webitproff)  
	********************************************************************************/
-->
<!-- BEGIN: HEADER -->
<!DOCTYPE html>
	<!-- IF {HTML_LANG} -->
	<html lang="{HTML_LANG}" data-bs-theme="light">
	<!-- ELSE -->
	<html lang="{PHP.usr.lang}" data-bs-theme="light">
	<!-- ENDIF -->

	<head>
		<meta charset="UTF-8">
		<meta name="viewport" content="width=device-width, initial-scale=1">
		<!-- IF {I18N_HEADER_META_TITLE} --> 
		<title>{I18N_HEADER_META_TITLE}</title>
		<!-- ELSE -->
		<title>{HEADER_TITLE}</title>
		<!-- ENDIF -->

	<!-- IF {I18N_HEADER_META_DESCRIPTION} --> 
		<meta name="description" content="{I18N_HEADER_META_DESCRIPTION}" />
	<!-- ELSE -->
		<!-- IF {HEADER_META_DESCRIPTION} -->
		<meta name="description" content="{HEADER_META_DESCRIPTION}" />
		<!-- ENDIF -->
	<!-- ENDIF -->
		<!-- IF {HEADER_BASEHREF} -->
		{HEADER_BASEHREF}
		<!-- ENDIF -->
		<!-- IF {HEADER_CANONICAL_URL} -->
		<link rel="canonical" href="{HEADER_CANONICAL_URL}" />
		<!-- ENDIF -->
		<!-- IF {ALTERNATE_TAGS} -->
		{ALTERNATE_TAGS}
		<!-- ENDIF -->
		<link rel="shortcut icon" href="favicon.ico" />
		<link rel="icon" href="{PHP.cfg.themes_dir}/{PHP.theme}/img/icon.webp" type="image/svg+xml">
		<link rel="apple-touch-icon" href="apple-touch-icon.png" />
		<!-- IF {PHP.out.meta} -->
		{PHP.out.meta}
		<!-- ENDIF -->
		<script>
			(function () {
				const storedTheme = localStorage.getItem('theme');
				const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
				const defaultTheme = storedTheme || (prefersDark ? 'dark' : 'light');
				document.documentElement.setAttribute('data-bs-theme', defaultTheme);
			})();
		</script>
		{HEADER_HEAD}
	</head>

Часть 7. Итоги и проверка работоспособности

После внесения всех описанных изменений плагин I18n получил полную поддержку Extrafields, сравнимую с модулем Pages.

Проверка:

  1. Админ-панель: В разделе «Extrafields» появилась таблица cot_i18n_pages. Администратор может создавать и редактировать поля любых типов.
  2. Форма перевода: При добавлении/редактировании перевода отображаются все активные дополнительные поля. Введённые значения корректно сохраняются.
  3. Страница перевода: При просмотре страницы на языке, отличном от основного, поля выводятся в соответствии с настройками шаблона (индивидуально или группой).
  4. Список статей: В списках статей значения полей отображаются только для тех записей, у которых есть перевод; для остальных поля пусты (нет «наследования» от предыдущей итерации).
  5. SEO-теги в header: Мета-заголовок и описание успешно заменяются на переведённые версии, что подтверждается просмотром исходного кода страницы.

Заключение

Проделанная работа демонстрирует глубокую интеграцию механизма Extrafields в существующий плагин I18n с соблюдением всех архитектурных принципов Cotonti. Полученное решение может служить образцом для расширения других плагинов дополнительными полями. Все изменения задокументированы и готовы к использованию в production-окружении.

Ушло у меня на реализацию два дня. По накатанной вам сделать это самостоятельно по этой инструкции на может час максимум.

Список файлов, которые редактировались:

1. - i18n.extrafields.php - новый 
|- /plugins/i18n/i18n.extrafields.php 

2. - i18n.page.php 
|- /plugins/i18n/inc/i18n.page.php
  
3. - i18n.ru.lang.php
|- plugins/i18n/lang/i18n.ru.lang.php

4 - i18n.page.tags.php 
|- /plugins/i18n/i18n.page.tags.php

5 - i18n.pagetags.php
|- /plugins/i18n/i18n.pagetags.php

6 - i18n.pagetags.php
|- /plugins/i18n/i18n.pagetags.php

7. - i18n.page.tpl
|- /themes/index36/plugins/i18n/i18n.page.tpl

8. - header.tpl
|- /themes/index36/header.tpl

9. - page.tpl
|- /themes/index36/modules/page/page.tpl

10. - page.list.tpl
; |- /themes/index36/modules/page/page.list.tpl

 

И так, вот так было:

<!DOCTYPE html>
<!-- header.pages.tpl -->
<html lang="en" data-bs-theme="light">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>The effectiveness of the link in the text of the content or in the widget.</title>
    <meta name="description" content="description of the differences between the two methods of placing links on a web page: inserting a link directly into the text of an article and displaying a link through a plugin or widget " Recommended products". The purpose of the document is to explain" />
    <meta property="og:title" content="The effectiveness of the link in the text of the content or in the widget.">
    <meta property="og:description" content="description of the differences between the two methods of placing links on a web page inserting a link directly into the text of an article and displaying a">
    <meta property="og:type" content="article">
    <meta property="og:url" content="https://abuyfile.com/en/usersblog/comparison-effectiveness-links-to-page-complete-guide">
    <meta property="og:image" content="https://abuyfile.com/attacher/page/765/att_1080.webp">
    <meta property="og:image:alt" content="The effectiveness of the link in the text of the content or in the widget.">
    <meta property="og:site_name" content="aBuyFile - online marketplace">
    <meta property="og:locale" content="en">
    <meta name="twitter:card" content="summary_large_image">
    <meta name="twitter:title" content="The effectiveness of the link in the text of the content or in the widget.">
    <meta name="twitter:description" content="description of the differences between the two methods of placing links on a web page inserting a link directly into the text of an article and displaying a">
    <meta name="twitter:image" content="https://abuyfile.com/attacher/page/765/att_1080.webp">
    <meta http-equiv="content-type" content="text/html; charset=UTF-8" />

 

Результат всей работы и что получилось:


<!DOCTYPE html>
<!-- header.pages.tpl -->
<html lang="en" data-bs-theme="light">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Where does a link have greater SEO effectiveness?</title>
    <meta name="description" content="A complete guide: Comparing the effectiveness of links in the main content versus the “Recommended Products” section for search engine optimization (SEO)" />
    <base href="https://abuyfile.com/" />
    <link rel="canonical" href="https://abuyfile.com/en/usersblog/comparison-effectiveness-links-to-page-complete-guide" />
    <link rel="alternate" hreflang="en" href="https://abuyfile.com/en/usersblog/comparison-effectiveness-links-to-page-complete-guide">
    <link rel="alternate" hreflang="x-default" href="https://abuyfile.com/ru/usersblog/comparison-effectiveness-links-to-page-complete-guide">
    <link rel="shortcut icon" href="favicon.ico" />
    <link rel="icon" href="themes/2waydeal/img/icon.webp" type="image/svg+xml">
    <link rel="apple-touch-icon" href="apple-touch-icon.png" />
    <meta property="og:title" content="The effectiveness of the link in the text of the content or in the widget.">
    <meta property="og:description" content="description of the differences between the two methods of placing links on a web page inserting a link directly into the text of an article and displaying a">
    <meta property="og:type" content="article">
    <meta property="og:url" content="https://abuyfile.com/en/usersblog/comparison-effectiveness-links-to-page-complete-guide">
    <meta property="og:image" content="https://abuyfile.com/attacher/page/765/att_1080.webp">
    <meta property="og:image:alt" content="The effectiveness of the link in the text of the content or in the widget.">
    <meta property="og:site_name" content="aBuyFile - online marketplace">
    <meta property="og:locale" content="en">
    <meta name="twitter:card" content="summary_large_image">
    <meta name="twitter:title" content="The effectiveness of the link in the text of the content or in the widget.">
    <meta name="twitter:description" content="description of the differences between the two methods of placing links on a web page inserting a link directly into the text of an article and displaying a">
    <meta name="twitter:image" content="https://abuyfile.com/attacher/page/765/att_1080.webp">

 

Автор: совместная работа webitproff и AI-ассистента.
Дата: 22 апреля 2026 г.
Актуальный код модификаций плагина на GitHub совместимо с Cotonti 0.9.26 и PHP 8.4.

21 минут чтения Sodium Carbonate

Комментарии (0)

Комментарии отсутствуют
Добавление комментариев доступно только зарегистрированным пользователям

Автор контента

webitproff

Оффлайн

Sodium Carbonate

Последняя авторизация: 20.07.2026 00:04

  • Страница размещена: 23.04.2026 02:24
  • Последнее обновление: 23.04.2026 17:30
  • Язык:

Похожие страницы

i18n - устанавливаем локаль перевода только по селектору
1 [title]Мультиязычность статей в Cotonti[/title]Статья: Модификация плагина i18n для Cotonti – корректная работа
Дизайн, верстка, интеграция в движок сайта. Обзор этапов как руководство.
2  Полный путь создания дизайна сайта: от идеи до готовой «шкурки»Создание дизайна сайта для новичков часто выглядит
Файл admin.extrafields.php в Cotonti CMF
3 Файл admin.extrafields.php в движке Cotonti CMF предназначен для управления дополнительными полями (extra fields) в
Модуль "Users", - users.extrafields.php в Cotonti CMF
4 Основное назначение файла Файл users.extrafields.php в Cotonti CMF представляет собой часть модуля для управления
Файл "page.extrafields.php" в Cotonti CMF
5 Файл "page.extrafields.php" в системе Cotonti CMF является частью модуля "page", и он отвечает за