The User Interface Language and the i18n modified Plugin in Cotonti: Full Guide

How $usr['lang'] works in Cotonti and the i18n modified plugin: locales, page and category translation, extrafields, hreflang, multilingual URLs and troubleshooting.

0 Published Filed under: Cotonti Siena CMF > Cotonti - reading materials. Documentation.

The $usr['lang'] Variable in Cotonti and the i18n modified Plugin: A Complete Reference Guide

Table of Contents

  1. Introduction
  2. Purpose of the $usr['lang'] Variable
  3. Value Sources and Priority
  4. Related Variables and Constants
  5. Usage in PHP Extensions
  6. Usage in XTemplate Templates
  7. Architecture of the i18n modified Plugin
  8. Key Entities of the Plugin
  9. Settings of the i18n modified Plugin
  10. Working with Locales
  11. Structure (Category) Translation
  12. Page Translation
  13. Integration with Extrafields
  14. Language Switcher in the Site Header
  15. Page Tags and Title Overrides
  16. Integration with Tags and Trashcan
  17. Access Rights and Security
  18. Multilingual URLs
  19. SEO Capabilities
  20. Logging and Messages
  21. Practical Scenarios
  22. Templates and Plugin Tags
  23. Related Core Functions
  24. Common Patterns
  25. Troubleshooting
  26. Conclusion
  27. Useful Links

1. Introduction

The $usr['lang'] variable is a key element of the multilingual architecture of Cotonti CMF. It stores the language code applied to the current site visitor and determines which language packs are loaded by the engine when processing a request. Understanding how this variable works is essential when developing and localizing themes, modules, and plugins.

However, in modern Cotonti, the $usr['lang'] variable alone is no longer sufficient for building a fully multilingual site. Interface language is only part of the task. The second, equally important part is content language: article titles, category descriptions, page texts, and extra fields. To solve this task, the i18n modified plugin is used — a modified version of the stock multilingual plugin for Cotonti.

This guide combines two levels of knowledge:

  • the core level — the $usr['lang'] variable, its sources, priority, and usage in PHP and templates;
  • the extension level — the architecture, settings, and capabilities of the i18n modified plugin.

The material is based on the analysis of Cotonti V.1 source code, the system/common.php file, the system/functions.php file, as well as the files of the i18n modified plugin: the language file, the functions file, page handlers, extrafields integration, tags integration, template tags, and configuration.


2. Purpose of the $usr['lang'] Variable

$usr['lang'] is part of the global $usr array, which is created in system/common.php when the user is initialized. The variable is responsible for identifying the interface language for the current request.

The $usr['lang'] value is used by the engine in several places:

  • when loading core language files (main, users, admin, message);
  • when loading the theme language file ({theme}.{lang}.lang.php);
  • when loading extension language files (modules and plugins);
  • when generating user tags in templates;
  • when localizing dates, declensions, and other formatted data.

The value of $usr['lang'] determines which interface strings the user sees: button labels, menu item names, system messages, month and weekday names, numeral declension forms.

Additionally, $usr['lang'] is used in the browser language detection mechanism via the cot_lang_determine() function, which parses the HTTP_ACCEPT_LANGUAGE header and matches it against available language packs.


3. Value Sources and Priority

The value of $usr['lang'] is calculated in system/common.php in several stages. Each stage has its own priority and conditions of application.

3.1. Default Value

When initializing the $usr array, a default value is set:

$usr = [
    'id' => 0,
    'name' => '',
    'ip' => cot_getCurrentUserIp(),
    'level' => 0,
    'sessionid' => '',
    'lastvisit' => 30000000000,
    'lastlog' => 0,
    'timezone' => cot_timezone_offset($cfg['defaulttimezone'], true),
    'timezonename' => $cfg['defaulttimezone'],
    'newpm' => 0,
    'messages' => 0,
    'theme' => $cfg['defaulttheme'],
    'scheme' => $cfg['defaultscheme'],
    'lang' => $cfg['defaultlang'],
    'maingrp' => COT_GROUP_GUESTS,
    'groups' => [COT_GROUP_GUESTS]
];

$cfg['defaultlang'] is taken from datas/config.php and represents the default site language code. For guests and unauthenticated visitors, this value remains unchanged throughout the request.

3.2. Value for an Authenticated User

Upon successful authorization, the value of $usr['lang'] is overridden:

$usr['lang'] = $cfg['forcedefaultlang'] ? $cfg['defaultlang'] : $row['user_lang'];

The selection logic is as follows:

  • if forced language setting is enabled in the configuration (forcedefaultlang = true), then $cfg['defaultlang'] is applied to all users regardless of their personal preferences;
  • if forced setting is disabled, the user's personal language saved in the user_lang field of the cot_users table is used.

3.3. Priority of Sources

SourceCondition of applicationValue
$row['user_lang']User is authenticated, forcedefaultlang = falsePersonal language choice
$cfg['defaultlang']forcedefaultlang = true or guestDefault site language

3.4. Forced Language Setting

The forcedefaultlang parameter is managed from the administration panel: Administration Panel → Configuration → Localization.

When the option is enabled, the user_lang value is ignored, and all users receive the default site language. This is convenient for sites that do not assume language selection but at the same time must support the ability to switch content language via the i18n plugin.

3.5. Saving the Personal Language

When updating the user profile, the value is saved in the cot_users table:

$db->update($db_users, ['user_lang' => $newLang], "user_id = {$usr['id']}");

After saving, the new value takes effect on the next request, since $usr['lang'] is recalculated at each initialization.


4.1. The $lang Variable

The global $lang variable is a short synonym for $usr['lang']. It is set in common.php after $usr is defined:

$lang = $usr['lang'];

It is used in core functions and extensions where there is no direct access to the $usr array. For example, in the cot_langfile() function:

function cot_langfile($name, $type = ExtensionsDictionary::TYPE_PLUGIN, $default = 'en', $lang = null): ?string
{
    global $cfg;
    if (!is_string($lang)) {
        global $lang;
    }
    // ...
}

This allows functions to accept the language as an explicit parameter or as a default value from the global variable.

4.2. Tags in XTemplate Templates

TagPurpose
{PHP.usr.lang}Language code from the $usr array
{PHP.lang}Synonym for the previous one
{HTML_LANG}Custom tag set by the i18n modified plugin
{PHP.i18n_locale}Content locale code (i18n modified plugin)

4.3. i18n modified Plugin Variables

The i18n modified plugin introduces a number of its own variables available in PHP handlers and templates:

VariablePurpose
$i18n_localeActive content locale
$i18n_localesArray of available locales
$i18n_fallbackFallback locale (usually the default language)
$i18n_adminFlag: is the user an i18n administrator
$i18n_writeFlag: does the user have write permission for translations
$i18n_readFlag: does the user have read permission for translations
$i18n_editFlag: does the user have edit permission for translations
$i18n_notmainFlag: the active locale is not the default language

4.4. Cotonti Core Constants

The group constants (COT_GROUP_GUESTS, COT_GROUP_MEMBERS, COT_GROUP_ADMINS, and others) define access rights that are closely related to i18n rights. A user with i18n administrator rights can translate the structure and manage any translations.


5. Usage in PHP Extensions

5.1. Loading an Extension Language File

The standard way to load a module or plugin localization is the cot_langfile() function:

require_once cot_langfile('myplugin', 'plug');
require_once cot_langfile('mymodule', 'module');

The function automatically substitutes the current language ($lang, i.e. $usr['lang']) and selects the appropriate file:

  • plugins/myplugin/lang/myplugin.{lang}.lang.php;
  • modules/mymodule/lang/mymodule.{lang}.lang.php.

5.2. Conditional Language Constructs

Checking the current language is used to display different content to different audiences:

if ($usr['lang'] == 'ru') {
    $t->assign('WELCOME_MESSAGE', 'Welcome');
} elseif ($usr['lang'] == 'en') {
    $t->assign('WELCOME_MESSAGE', 'Welcome');
} else {
    $t->assign('WELCOME_MESSAGE', 'Hello');
}

5.3. Dynamic Tag Generation

The language prefix can be used in template tag names:

$tagName = 'INDEX_NEWS_' . strtoupper($usr['lang']);
$t->assign($tagName, $newsContent);

5.4. Generating URLs with a Language Prefix

When working with the i18n modified plugin, the URL is built with the l parameter containing the language code:

$url = cot_url('page', ['c' => 'news', 'l' => $usr['lang']]);

If the language matches the default language and the omitmain option is enabled, the l parameter is not added.

5.5. Working with the cot_lang_determine() Function

The cot_lang_determine() function parses the HTTP_ACCEPT_LANGUAGE header and returns the language code that best suits the user. It is used during the initial installation of Cotonti and in the installer:

$lang = cot_import('lang', 'P', 'ALP');
if (empty($lang)) {
    $lang = cot_lang_determine();
}

The function returns a language code only if a file main.{lang}.lang.php exists for it in the /lang/ or /system/lang/ directory.


6. Usage in XTemplate Templates

6.1. Direct Output of the Language Code

<html lang="{PHP.usr.lang}">

However, for the lang attribute in HTML, it is recommended to use full IETF tags (uk-UA, en-US), so the standard code output is often insufficient.

6.2. Conditional Block Display

<!-- IF {PHP.usr.lang} == 'ru' -->
    <div class="block-ru">
        <p>This block is visible only to Russian-speaking users.</p>
    </div>
<!-- ENDIF -->

<!-- IF {PHP.usr.lang} != 'en' -->
    <div class="block-non-en">
        <p>This block is hidden from English-speaking users.</p>
    </div>
<!-- ENDIF -->

6.3. Language Switching

The standard switcher provided by the i18n modified plugin uses its own set of tags:

<!-- BEGIN: I18N_LANG -->
<div class="dropdown">
    <a class="btn-icon dropdown-toggle" data-bs-toggle="dropdown" title="{PHP.i18n_locale}">
        <i class="fa-solid fa-language me-2"></i>
        <small>
            <!-- IF {PHP.i18n_locale} == 'ru' -->RU<!-- ENDIF -->
            <!-- IF {PHP.i18n_locale} == 'en' -->EN<!-- ENDIF -->
            <!-- IF {PHP.i18n_locale} == 'ua' -->UA<!-- ENDIF -->
        </small>
    </a>
    <ul class="dropdown-menu dropdown-menu-end">
        <!-- BEGIN: I18N_LANG_ROW -->
        <li>
            <a class="dropdown-item" href="{I18N_LANG_ROW_URL}" title="{I18N_LANG_ROW_TITLE}">
                {I18N_LANG_ROW_TITLE}
            </a>
        </li>
        <!-- END: I18N_LANG_ROW -->
    </ul>
</div>
<!-- END: I18N_LANG -->

6.4. Using `

The i18n modified plugin generates a full IETF tag for the lang attribute in the <html> tag. This allows using:

<html lang="{HTML_LANG}">

The HTML_LANG value is formed based on a map of correspondences between short codes and full IETF tags. For example, ua is converted to uk-UA.


7. Architecture of the i18n modified Plugin

The i18n modified plugin has a modular structure. Several layers can be distinguished in it.

7.1. Language File

Contains information about the plugin, settings, and interface strings. The language file includes a name, description, note, as well as strings for configuration and the user interface. Strings for page translation, structure, deletion, addition, editing, locale selection, error messages, and successful actions are listed separately.

7.2. API Functions

The functions file defines the main operations:

  • loading locales;
  • loading structure translations;
  • getting a category translation;
  • getting a page translation;
  • list of locales for a category and page;
  • checking whether internationalization is enabled for a category;
  • building a category path taking translation into account;
  • saving a translation;
  • integration with tags.

These functions form the core of the plugin and are used by other parts.

7.3. Page Handlers

A separate file for page translation handles adding, editing, and deleting translations. There is a file for structure translation that works in the administrative part. There are files that connect to hooks and add tags to templates: header.tpl, page.tpl, and also override page tags in the generation function.

7.4. Configuration

The installation file lists the parameters: cats, locales, omitmain, rewrite, cookie. These parameters determine which categories participate in multilingualism, which locales are available, how the URL is built, and whether the language should be remembered in a cookie.

7.5. Integration with Extrafields

There is a file that adds the page translations table to the whitelist of extra fields. This allows the administrator to create extra fields specifically for translations.

7.6. Integration with Tags

If the tags plugin is installed, i18n modified adds a locale column to the tag relations table and changes the primary key. This allows tags to be stored separately for each language.

7.7. Integration with Trashcan

If the trashcan plugin is active and configured for the page trash can, then when a translation is deleted, it is first placed in the trash can.

This architecture makes the plugin quite flexible. It is not monolithic: each part is responsible for its own task. At the same time, all parts are connected through common functions and tables.


8. Key Entities of the Plugin

To understand how the plugin works, one must understand several key entities.

  • Locale — this is a language code, for example, ru, en, ua, pl. In the plugin settings, locales are specified as a list, each line of which has the format code|Name. For example, en|English. The name is used for display in the language switcher. The code is used in the URL and in the database.
  • Default language — this is the main language of the site. It is taken from the general Cotonti configuration. The plugin automatically adds it to the list of locales if it is not there. Special rules may apply to the default language: for example, the language parameter may be omitted in the URL if the corresponding setting is enabled.
  • Fallback language — this is the language used if there is no translation for the current locale. In the files it is mentioned as i18n_fallback. Judging by the logic, if there is no translation, the original in the default language is displayed. This is standard behavior for multilingual systems.
  • Active locale — this is the language selected by the user at the moment. It is passed through the l parameter in the URL and can be stored in a cookie if the corresponding setting is enabled.
  • Page translation — this is a record in a separate table that contains the title, description, text, and extra fields for a specific page and a specific locale. Page translations can be added, edited, and deleted. Each translation has an author, date, and locale.
  • Structure translation — this is a record in another table that contains the title and description of a category for a specific locale. Structure translations are managed only by the administrator.
  • Extra translation fields — these are fields created through the extrafields system for the page translations table. They allow storing arbitrary data in different languages.
  • Categories participating in i18n — these are the root categories listed in the cats setting. If a category is not included in this list, then multilingual mechanisms may not apply to it. The check is performed by determining the category's parents.
  • Original — this is the source page or category in the default language. The original is not overwritten by the translation. The translation exists in parallel.
  • Localized value — this is the translation of a page or category. In the translation interface, both variants are shown: the original and the localized value.

9. Settings of the i18n modified Plugin

The installation file lists five main settings. Let us consider them in detail.

9.1. cats

"Category codes". This is a list of category codes for which internationalization is enabled. In the Russian language file, the hint clarifies: "Category codes separated by commas". That is, the administrator specifies which categories should support translations. If a category is not specified, it probably will not participate in multilingualism. This allows not overloading with translation those sections where it is not needed.

9.2. locales

"Site locales". This is the list of site locales. In the Russian file, the hint is: "Each locale on a new line, format: locale_code|Locale title". That is, each line contains a code and a display name separated by a vertical bar. For example, en|English. This list is used to build the language switcher, to select a locale during translation, and to validate the locale.

9.3. omitmain

"Omit the language parameter in the URL if pointing to main language". This is a toggle. If it is enabled, then for the main language the language parameter may not be added to the URL. This makes links in the main language cleaner. If it is disabled, the language parameter is always added. In the Russian file, the setting is described as "Omit the language parameter in the URL if it points to the main language".

9.4. rewrite

"Enable URL overwrite for language parameter". Enables SEO-friendly URLs for the language parameter. In the Russian file, the hint is: "Requires manual updating of .htaccess". That is, if the administrator wants the language in the URL to look like part of the path rather than a parameter, they need to enable this setting and manually update the redirect rules on the server. This is an important point: the plugin does not do this automatically.

"Remember language selection in cookie". If enabled, the selected language is remembered in a cookie. Then on the next visit the user will automatically see the site in the selected language. In the Russian file: "Remember the selected language in a cookie".

These five settings form the basic configuration. From the language file it is also clear that there are strings for explanations of these settings, that is, the administrator sees hints in Russian.


10. Working with Locales

Locales are loaded by a special function. It splits the string into lines, splits each line by the vertical bar, trims spaces, checks that the code and name are not empty, and adds the locale to the array. If the default language is not in the list, it is added automatically. In this case, the name is taken from the general Cotonti language list if present there; otherwise, the code itself is used.

This means that the administrator may not specify the default language in the settings, and the plugin will still know about its existence. But for a complete picture, it is better to specify all languages used on the site.

The list of locales is used in several places. First, for the language switcher in the header. Second, for selecting a locale when adding or editing a page translation. Third, for selecting a locale when translating the structure. Fourth, for checking that the passed locale is valid.

In the language switcher, a link is generated for each locale while preserving the current GET parameters. This is important: if the user is on a page with filters or parameters, they are not lost when switching languages. If the omitmain setting is enabled and the current locale matches the fallback language, the l parameter is removed from the URL. If cookie is enabled and the language is saved in a cookie, the omitmain logic may not apply. This is done to avoid URL duplication and conflicts.

The switcher also determines the selected class for the active language. This allows styling the current language in the template.


11. Structure (Category) Translation

Structure translation is available only to the administrator. This follows from the rights check: before executing the scenario, a block is triggered if the user does not have i18n administrator rights.

The structure translation workflow looks like this. First, the administrator selects a locale. If no locale is selected or it is equal to the default language, a list of locales is displayed for selection. If a locale is selected, a table of categories with fields for translation is displayed.

In the table, for each category, the original title and description are shown, as well as fields for entering the translation of the title and description. The administrator can change the translation and save. When saving, the plugin iterates over all passed category codes and compares the new values with the old ones. If the translation was empty and remained empty, nothing is done. If the translation was empty and became filled, an insert is performed. If the translation was filled and became empty, a delete is performed. If the translation changed, an update is performed.

After saving, messages are generated about the number of added, updated, and deleted elements. These messages use language file strings with number substitution. Logging is also maintained: addition, editing, and deletion of category translations.

The structure translation table has pagination. The number of elements per page is taken from the general maxrowsperpage setting if it is set and positive; otherwise, the default value of 15 is used. This allows comfortable work with a large number of categories.

Before displaying the table, the plugin loads structure translations from the database and saves them to cache if caching is enabled. This speeds up subsequent requests.

If no categories are specified in the cats setting, the administrator is shown a warning with a link to the plugin settings page. This helps to quickly fix the configuration.

Thus, structure translation is a full-fledged administrative tool with pagination, bulk saving, messages, and logging.


12. Page Translation

Page translation is a more complex process because it is available not only to the administrator, but also to translators and authors. In the page handler file, there are three main branches: adding, editing, and deleting.

12.1. Adding a Translation

First, it checks that the page exists and the identifier is correct. If the page does not exist, a 404 is issued. Then the page data is loaded. If the action is adding, the translation is not loaded; an empty array is created. If the action is editing, the existing translation for the specified locale is loaded.

When adding a translation, the request method is checked first. If it is POST, the selected locale is imported. It checks that the locale is in the list of available ones. If not — the error "Invalid locale". Then it checks whether a translation already exists for this page and this locale. If yes — the error "Translation already exists". Then an array of translation data is formed: page identifier, locale, translator identifier, translator name, date, title, description, text. Extra fields are also imported if they exist. The title length is checked: if it is less than two characters — the error "Title is too short". If there are no errors, the record is inserted into the database. Then hooks are executed, the message "Added" is displayed, logging is performed, the page URL is formed taking the locale into account, and a redirect is performed.

If the request is not POST or there are errors, the add translation form is displayed. In the form there is a locale selector that excludes the default language and already existing translations. The original title, description, and text of the page are shown, as well as fields for entering the translation. A text editor is used for the text. Extra fields are output into the template as separate blocks.

12.2. Editing a Translation

Editing is available if the translation exists and the user is an i18n administrator, or has edit rights, or is the author of the translation. On POST, the new locale is imported. Its validity is checked. If the locale has changed, it checks whether a translation already exists for the new locale. If yes — error. Then the date, title, description, and text are updated. If the locale has changed, it is also updated. Extra fields are imported with old values passed. If there are errors, the form is displayed again with filled fields. If there are no errors, the database record is updated. Then hooks, the message "Updated", logging, URL formation, and redirect.

The edit form also has a locale selector. It excludes the default language and occupied locales except the current one. The selector value is taken from POST on error or from the current locale. The form fields are filled with the entered data on error or with the current translation data. Extra fields are output similarly to adding.

12.3. Deleting a Translation

Deletion is available to the administrator or the author of the translation. If the trashcan plugin is active and configured for the page trash can, the translation is first placed in the trash can. Then the record is deleted from the database. Hooks are executed, the message "Deleted" is displayed, logging is performed, the page URL is formed, and a redirect is performed.

If the action is not recognized or rights are insufficient, an error message is issued.

Thus, page translation is a full-fledged CRUD interface with checks, messages, logging, support for extra fields, and integration with the trash can.


13. Integration with Extrafields

One of the key features of the modification is deep integration with extra fields. In the page handler file, it can be seen that when adding and editing a translation, the configuration of extra fields for the page translations table is loaded. Then, in a loop over each field, a field name with a prefix is formed, the value is imported from POST taking the old value into account, and saved into the translation data array.

In the translation form, an input element and a title are generated for each extra field. This data is passed to the template. For each field, tags with the field name in uppercase are formed, as well as general EXTRAFLD tags. This allows outputting extra fields in the template both individually and in a loop.

In the file that adds the translations table to the whitelist of extra fields, it is stated that extra fields can be created for the i18n_pages table. The description states that the template tags are I18N_PAGE_FORM_XXXXX and I18N_PAGE_FORM_XXXXX_TITLE. This means that the administrator can create an extra field, for example, "material", and in the translation template use the tag I18N_PAGE_FORM_MATERIAL.

In addition, extra translation fields are output in the header and in page.tags. In the header, the tags I18N_HEADER_XXXXX, I18N_HEADER_XXXXX_TITLE, I18N_HEADER_XXXXX_VALUE are formed. In page.tags, the tags I18N_XXXXX_TITLE, I18N_XXXXX, I18N_XXXXX_VALUE are formed, as well as the dynamic EXTRAFLD block.

In pagetags.main, extra translation fields are added to the page tags array with the prefix I18N_PAGE_. If there is no translation, the tags are reset to empty values.

Thus, extra fields can be used not only in the translation form, but also anywhere in the template: in the header, in the page card, in lists, in SEO tags. This makes the system very flexible.


14. Language Switcher in the Site Header

In the header.tags.php file, the language switcher and SEO tag generation are implemented. Let us consider its capabilities.

First, a drop-down list of languages is built. For each locale, the selected class is determined if it matches the current one. All GET parameters are preserved. If the omitmain setting is enabled and the locale matches the fallback language, the l parameter is removed. If cookie is enabled and the language is saved in a cookie, the omitmain condition may not apply. It determines whether we are inside the plugin and forms the URL. If we are in the admin panel, the URL is built with the admin prefix. This fix is important because without it, switching the language in the admin panel could lead to an incorrect URL.

Then tags are passed to the template: URL, locale code, flag (for English, the UK flag is used), title, class, selected. These tags can be used in header.tpl to render the switcher.

Next, the full IETF tag for the lang attribute is set. For this, a map of correspondence between short codes and full tags is used. For example, ua is converted to uk-UA. If there is no correspondence, the short code is used. The resulting value is passed to the template as HTML_LANG. This allows using <html lang="{HTML_LANG}"> in header.tpl.

Then alternate hreflang tags are generated for translated pages. If we are on a page of the page module and there is an identifier, the list of locales into which the page has been translated is obtained. For each locale except the default language, a URL with the l parameter and a link tag with the hreflang attribute are generated. An x-default tag is also generated. If omitmain is enabled, x-default points to the URL without the language prefix. If disabled, it points to the URL with the default language prefix. All tags are passed to the template as ALTERNATE_TAGS.

Finally, extra translation fields are passed to the header. If a translation exists and there are extra fields, then for each field the tags I18N_HEADER_XXXXX, I18N_HEADER_XXXXX_TITLE, I18N_HEADER_XXXXX_VALUE are formed. The value is processed through the parser and escaped. If there is no translation, the tags are reset.

Thus, the header receives everything necessary: the switcher, the correct lang, hreflang, x-default, and extra fields.


15. Page Tags and Title Overrides

In the page.tags.php file, tags for the page are assigned. If internationalization is enabled, a list of locales for the page is formed. If there are translations, a language switcher for the page is built. For each locale, a URL is formed taking the alias or identifier into account, the l parameter is added if necessary. The tags URL, code, title, class, selected are passed.

If the user has write permission, tags for translation are added. If a translation exists and the user can edit it, an edit link is added. If there is no translation and the number of locales is less than the total number, a "Translate" button is added.

If the user is an administrator, a delete translation button with confirmation is added.

Extra translation fields are also output: for each field, the tags I18N_XXXXX_TITLE, I18N_XXXXX, I18N_XXXXX_VALUE are formed, as well as the dynamic EXTRAFLD block.

This allows page.tpl to output the language switcher, translation management buttons, and extra fields.

In the pagetags.main.php file, page tags are overridden in the generation function. If internationalization is enabled and the current language is not the main one, the category translation is loaded. If there is a category translation, the category URL, validation URL, edit URL, category path, breadcrumbs, category title, category description are formed. Administrator links are also added: edit, approve, submit for approval. If there is no category translation, the original is used.

If there is a page translation, the page URL, title, breadcrumbs, description, text, trimmed text, trim flag, "Read more" link, update date are formed. Extra translation fields are also added.

If there is no translation, the extra field tags are reset.

If the user has write permission and a translation exists, an edit translation link is added.

All these tags are merged with the main page tags. This allows page templates to use translated values automatically.

In the page.main.php file, it is shown how the page title, subtitle, and description are overridden. If internationalization is enabled and the current language is not the main one, the page translation and category translation are loaded. If there is a page translation, title parameters are formed taking the translated title and translated category into account. The subtitle and description are set. Then the translation data is merged with the page data. This means that all subsequent handlers see already translated values.

This is an important point: the plugin does not just add separate tags, it replaces the page data with the translation if one exists. This ensures consistency across all modules and templates.


16. Integration with Tags and Trashcan

16.1. Integration with Tags

If the tags plugin is installed, i18n modified adds locale support to tags. In the integration installation function, it checks whether tags is installed. If yes, the tags API is connected. Then it checks whether the tag_locale column exists in the tag relations table. If not, it is added. After that, the primary key is dropped and a new one is created that includes tag_locale. This allows tags to be stored separately for each language.

This means that on a multilingual site, tags can be translated or at least tied to a language. This is useful for SEO and navigation.

16.2. Integration with Trashcan

If the trashcan plugin is active and configured for the page trash can, when deleting a page translation it is first placed in the trash can. To do this, the translation record is loaded, a description is formed, and the trash can service is called. Only after that is the record deleted from the translations table. This allows restoring a deleted translation if it was deleted by mistake.


17. Access Rights and Security

The plugin uses several levels of rights. The variables i18n_admin, i18n_write, i18n_read, i18n_edit, i18n_notmain, i18n_locale, i18n_fallback determine what the user can do.

The i18n administrator can translate the structure, edit and delete any translations. A user with write permission can add translations and edit their own translations. The author of a translation can edit and delete their own translation. A guest can only read.

Rights checks are performed before executing actions. If rights are insufficient, an error message or redirect is issued.

The validity of the locale, duplicate translations, and title length are also checked. This prevents incorrect data.


18. Multilingual URLs

The plugin supports several URL modes. The language parameter can be passed as l in the query string. If the rewrite setting is enabled, the language can be part of a SEO-friendly URL. If the omitmain setting is enabled, the parameter may be omitted for the main language. If cookie is enabled, the selected language is remembered.

This gives flexibility: you can make simple URLs for the main language and language prefixes for the others. You can use SEO-friendly URLs, but for this you need to manually update .htaccess.

Example of a URL with a parameter:

https://example.com/page.php?c=news&l=en

Example of a URL with a SEO prefix (with rewrite enabled):

https://example.com/en/news

Example of a URL with omitmain for the main language:

https://example.com/news

19. SEO Capabilities

The plugin generates a full IETF tag for the lang attribute, alternate hreflang tags for translated pages, and the x-default tag. This improves indexing of multilingual pages by search engines. Titles, descriptions, and meta tags are also translated, which has a positive effect on SEO.

The hreflang generation is performed based on the list of locales into which the current page has been translated. For each locale, a URL with the l parameter and a <link rel="alternate" hreflang="..."> tag are generated. The x-default tag points to the version that will be shown to users whose language preferences do not match any of the specified locales.


20. Logging and Messages

The plugin logs actions: adding, editing, and deleting page and category translations. Messages are displayed to the user: "Added", "Updated", "Deleted", "Invalid locale", "Translation already exists", "Title is too short", "No products", and others. The number of added, updated, and deleted structure elements is displayed with number substitution.

This helps the administrator monitor changes and quickly respond to errors.


21. Practical Scenarios

  • Adding a new language. The administrator adds a locale in the settings, specifies the code and name. The language appears in the switcher. Then they translate the structure and pages.
  • Translating an article. The author opens the page, clicks "Translate", selects a locale, fills in the title, description, text, and extra fields. Saves. The translation appears on the site.
  • Translating a category. The administrator goes to structure translation, selects a locale, fills in category titles and descriptions. Saves.
  • Configuring SEO-friendly URLs. The administrator enables rewrite and updates .htaccess. The language becomes part of the URL.
  • Remembering the language. The administrator enables cookie. The user selects a language, and it is saved.
  • Deleting a translation. The administrator or author deletes the translation. If trashcan is active, the translation goes to the trash can.

22. Templates and Plugin Tags

The plugin uses several templates: i18n.page.tpl for the page translation form, i18n.structure.tpl for the structure translation table, i18n.locales.tpl for locale selection. It also adds tags to header.tpl and page.tpl.

22.1. Tags in header.tpl

  • I18N_LANG_ROW_URL
  • I18N_LANG_ROW_CODE
  • I18N_LANG_ROW_TITLE
  • I18N_LANG_ROW_CLASS
  • I18N_LANG_ROW_SELECTED
  • HTML_LANG
  • ALTERNATE_TAGS
  • I18N_HEADER_XXXXX

22.2. Tags in page.tpl

  • I18N_LANG_ROW_*
  • PAGE_I18N_TRANSLATE
  • PAGE_I18N_DELETE
  • I18N_XXXXX
  • I18N_EXTRAFIELD_TITLE
  • I18N_EXTRAFIELD_VALUE

22.3. Tags in the Page Translation Template

  • I18N_ACTION
  • I18N_TITLE
  • I18N_ORIGINAL_LANG
  • I18N_LOCALIZED_LANG
  • I18N_PAGE_TITLE
  • I18N_PAGE_DESC
  • I18N_PAGE_TEXT
  • I18N_IPAGE_TITLE
  • I18N_IPAGE_DESC
  • I18N_IPAGE_TEXT
  • I18N_PAGE_FORM_XXXXX
  • I18N_PAGE_FORM_XXXXX_TITLE
  • I18N_PAGE_FORM_EXTRAFLD
  • I18N_PAGE_FORM_EXTRAFLD_TITLE

22.4. Tags in the Structure Translation Template

  • I18N_ACTION
  • I18N_ORIGINAL_LANG
  • I18N_TARGET_LANG
  • I18N_CATEGORY_ROW_TITLE
  • I18N_CATEGORY_ROW_DESC
  • I18N_CATEGORY_ROW_CODE_NAME
  • I18N_CATEGORY_ROW_CODE_VALUE
  • I18N_CATEGORY_ROW_ITITLE_NAME
  • I18N_CATEGORY_ROW_ITITLE_VALUE
  • I18N_CATEGORY_ROW_IDESC_NAME
  • I18N_CATEGORY_ROW_IDESC_VALUE
  • I18N_CATEGORY_ROW_ODDEVEN
  • I18N_PAGINATION_PREV
  • I18N_PAGNAV
  • I18N_PAGINATION_NEXT

This allows you to fully customize the appearance for a specific template.


FunctionPurposeRelation to language
cot_langfile()Returns the path to a language fileUses $lang
cot_lang_determine()Determines browser language from HTTP_ACCEPT_LANGUAGEReturns language code
cot_declension()Word declension by numbersTakes $lang into account
cot_get_plural()Determines the plural formTakes $lang into account
cot_date()Date formattingLocalizes month and day names
cot_translit_encode()String transliterationUses language tables
cot_translit_decode()Reverse transliterationUses language tables

23.1. The cot_langfile() Function

function cot_langfile($name, $type = 'plug', $default = 'en', $lang = null): ?string

Search order:

  1. lang/{$lang}/modules/{$name}.{$lang}.lang.php (for a module);
  2. modules/{$name}/lang/{$name}.{$lang}.lang.php;
  3. modules/{$name}/lang/{$name}.{$default}.lang.php.

If $lang is not passed explicitly, the global $lang variable is used (synonym for $usr['lang']).

23.2. The cot_lang_determine() Function

Automatically determines the user's preferred language by parsing the HTTP_ACCEPT_LANGUAGE header. It is used during the initial installation of Cotonti, as well as in the installer.


24. Common Patterns

24.1. Multilingual Tags

Creating dynamic tags in PHP and outputting them in a template:

PHP:

$tagBase = 'WELCOME_' . strtoupper($usr['lang']);
$t->assign($tagBase, $welcomeText);

TPL:

<!-- IF {PHP.usr.lang} == 'ru' -->
    {WELCOME_RU}
<!-- ENDIF -->
<!-- IF {PHP.usr.lang} == 'en' -->
    {WELCOME_EN}
<!-- ENDIF -->

24.2. Conditional Category Output

An example from the pages module, where the category depends on the language:

if ($usr['lang'] != $cfg['defaultlang']) {
    $category = $usr['lang'] . '_news';
} else {
    $category = 'news';
}

24.3. Working with the i18n modified Plugin

When using the i18n modified plugin for content, $usr['lang'] remains the interface language, while $i18n_locale is the content language. Both tags are available in the template:

<html lang="{HTML_LANG}">
    <body data-lang="{PHP.usr.lang}" data-content-lang="{PHP.i18n_locale}">

24.4. Localizing the Administration Panel

The administrative part uses the same $usr['lang'] to load the admin language file:

if (defined('COT_ADMIN')) {
    require_once cot_langfile('admin', 'core');
}

The admin language file is system/lang/{lang}/admin.{lang}.lang.php.


25. Troubleshooting

25.1. Language Does Not Switch

Cause: the forcedefaultlang parameter is enabled, so the user's personal choice is ignored.

Solution: disable forcedefaultlang in the section Administration Panel → Configuration → Localization.

25.2. Theme Is Not Translated

Cause: the theme localization file themes/{theme}/{theme}.{lang}.lang.php is missing.

Solution: create a language file for the required language or use the English file as a fallback.

25.3. Plugin Is Not Translated

Cause: the file plugins/{plugin}/lang/{plugin}.{lang}.lang.php is missing, and the cot_langfile() function returns only the English variant.

Solution: create a localization file or contact the plugin developer.

25.4. The lang Attribute in <html> Contains a Short Code

Cause: the template uses {PHP.usr.lang} directly without converting it to a full IETF tag.

Solution: use the i18n modified plugin, which generates {HTML_LANG} in the format uk-UA, en-US, etc.

25.5. Difference between $usr['lang'] and $i18n_locale

Cause: the user expects that when choosing the content language, the interface language will also change.

Solution: in Cotonti, interface language and content language are two independent variables. To change the interface language, the user must select a language in their profile. The i18n modified plugin manages only the content language.

25.6. Translation Does Not Appear on the Site

Cause: the category is not included in the cats setting of the i18n modified plugin.

Solution: add the category code to the cats setting in the plugin configuration.

25.7. SEO-Friendly URLs Do Not Work

Cause: the rewrite setting is enabled, but .htaccess has not been updated.

Solution: manually update the redirect rules in .htaccess in accordance with the plugin requirements.


26. Conclusion

The $usr['lang'] variable is a fundamental element of Cotonti CMF localization. It determines which language files are loaded by the engine, is used in PHP extensions and XTemplate templates, and is related to the global $lang variable, the cot_langfile() function, and other i18n tools.

The i18n modified plugin extends the capabilities of Cotonti by adding a full multilingual content system. It includes:

  • a language switcher in the site header;
  • structure (category) translation;
  • page translation;
  • extra field support;
  • SEO tags hreflang and x-default;
  • integration with the tags and trashcan plugins;
  • flexible URL and cookie settings;
  • an access rights system.

When developing themes and extensions, one must take into account the duality: the interface language is managed by $usr['lang'], and the content language is managed by the i18n modified plugin. For SEO-correct markup, it is recommended to use full IETF tags formed on the basis of a correspondence map.

All data presented in the article is based exclusively on the analysis of Cotonti V.1 source code, the system/common.php and system/functions.php files, as well as the files of the i18n modified plugin: the language file, the functions file, page handlers, extrafields integration, tags integration, template tags, and configuration.


No comments yet
Only registered users can post new comments