Multilingual Sites
How to build a multilingual site: settings, URL prefixes, _t object, Core\I18n class.
Multilingual Sites
apidcms supports building sites in multiple languages through a URL prefix system, cookies, and the _t translation object.
How to Enable
In the admin panel, go to Settings (/admin/settings). Find these two options:
| Setting | Description | Example |
|---|---|---|
site_language |
Default site language | ru |
site_languages |
Additional languages (JSON array) | ["en", "de"] |
After saving, a language switcher appears on the frontend, and all pages become available with a language prefix in the URL.
Important: the system is opt-in — multilingual support is inactive until site_languages is populated.
How It Works
URL Prefixes
Each additional language gets a prefix in the URL:
| Base language (ru) | English (en) |
|---|---|
/changelog |
/en/changelog |
/docs/install |
/en/docs/install |
/plugins |
/en/plugins |
The base language (site_language) does not get a prefix — its URLs remain bare. This preserves backward compatibility with existing links.
The site_lang Cookie
When visiting a page with a language prefix, the system sets a site_lang cookie for 1 year. If the user navigates to a bare URL (without prefix), the system checks the cookie and serves the stored language.
Switching to the base language is done via the ?lang=ru query parameter — it clears the cookie and returns to the base locale.
The _t Object in Templates
All translations are stored in the base.html.twig template as a _t object:
{% set _t = {
ru: {
home: "Главная",
docs: "Документация",
changelog: "CHANGELOG"
},
en: {
home: "Home",
docs: "Documentation",
changelog: "Changelog"
}
} %}
Usage in any template:
<h1>{{ _t[site_lang].changelog_heading }}</h1>
<a href="{{ url('/docs') }}">{{ _t[site_lang].docs }}</a>
The site_lang variable is automatically passed to all templates — it contains the current language (ru or en).
The url() Function
All internal links must use url(), not hardcoded paths:
{# Correct — prefix is added automatically #}
<a href="{{ url('/changelog') }}">Changelog</a>
{# Wrong — link always goes to the base language #}
<a href="/changelog">Changelog</a>
url() checks the current locale and, if it differs from the base language, adds the prefix:
- When
site_lang = en:url('/changelog')→/en/changelog - When
site_lang = ru:url('/changelog')→/changelog
System paths (/admin, /storage, /static) are never prefixed.
Language Switcher
A minimal switcher in base.html.twig:
{% if get_setting('site_languages')|default('') is not empty %}
<div class="lang-switch">
<a href="/{{ current_path }}?lang=ru"
class="{% if site_lang == 'ru' %}active{% endif %}">RU</a>
<a href="/en/{{ current_path }}"
class="{% if site_lang == 'en' %}active{% endif %}">EN</a>
</div>
{% endif %}
- Base language (RU):
/{current_path}?lang=ru— query parameter signals explicit switch - Additional language (EN):
/en/{current_path}— URL prefix
The current_path variable contains the current path without the locale prefix.
current_path
The current_path variable is automatically stripped of the locale prefix:
| Page URL | site_lang |
current_path |
|---|---|---|
/changelog |
ru |
changelog |
/en/changelog |
en |
changelog |
/en/changelog/v2-1-1 |
en |
changelog/v2-1-1 |
This allows the language switcher to always generate correct links.
Core\I18n Class
For advanced translation handling in PHP, use the Core\I18n class.
resolve()
Extracts the value for a locale from an i18n object:
use Core\I18n;
$title = I18n::resolve($item['title'], 'ru'); // {"ru":"Hello","en":"Привет"} → "Hello"
$title = I18n::resolve($item['title'], 'en'); // → "Привет"
resolveArray()
Recursively walks an array and resolves all i18n fields:
$items = I18n::resolveArray($items, 'en');
// All fields like {"ru":"...","en":"..."} are replaced with English values
Used in FrontController::resolveI18nData() for automatic translation of data before rendering.
searchExpr()
Escapes a search query for different locales:
$expr = I18n::searchExpr($query, 'en');
Useful when implementing search on multilingual sites.
encode()
Encodes a value with locale awareness:
$encoded = I18n::encode($value, 'en');
External Pages and contenturl{locale}
For external pages (changelog, plugins), you can specify different content URLs for different languages.
Instead of a single content_url, use:
| Field | Purpose |
|---|---|
content_url |
Default URL (base language) |
content_url_ru |
URL for Russian version |
content_url_en |
URL for English version |
If content_url_{locale} is not set, content_url is used as fallback.
Setting <html lang>
The base.html.twig template should use site_lang for the lang attribute:
<html lang="{{ site_lang }}">
This is important for SEO and accessibility (screen readers).
Known Limitations
- No auto-redirect. The system does not redirect from a bare URL to a prefixed one. If the user has a
site_lang=encookie and visits/changelog, the page renders in English but the URL stays bare. - Prefix method only. Subdomain-based (
en.example.com) or parameter-based (?lang=en) persistent routing is not supported. - Translations in templates. All
_tstrings live inbase.html.twig. For larger projects, consider extracting translations to a separate Twig file viainclude. - No content translation. The system only translates the UI (navigation, buttons, headings). Page content (articles, text) requires manual duplication for each locale.