Переводы и конфигурация
Flute CMS поддерживает многоязычность и гибкую систему настроек модулей. В этом разделе — как создавать переводы и конфигурацию.
Система переводов
Переводы позволяют показывать текст на разных языках без изменения кода.
Как это работает
Создаёте файлы переводов
Для каждого языка создаётся папка с PHP-файлами, где ключ → перевод.
Используете в коде
Функция __('ключ') возвращает текст на текущем языке пользователя.
Flute определяет язык
По параметру URL, cookie или настройкам пользователя.
Структура файлов
- messages.php
- validation.php
- Каждый язык — отдельная папка (
ru,en,de) - Внутри — PHP-файлы с массивами переводов
- Имя файла = домен перевода (например,
messages)
Создание файлов переводов
Основные сообщения
<?php
// Resources/lang/ru/messages.php
return [
// Общие
'welcome' => 'Добро пожаловать в блог',
'home' => 'Главная',
'back' => 'Назад',
// Статьи
'articles' => 'Статьи',
'article' => 'Статья',
'no_articles' => 'Статей пока нет',
'read_more' => 'Читать далее',
// CRUD
'create' => 'Создать',
'edit' => 'Редактировать',
'delete' => 'Удалить',
'save' => 'Сохранить',
'cancel' => 'Отмена',
// Сообщения об успехе
'article_created' => 'Статья успешно создана',
'article_updated' => 'Статья обновлена',
'article_deleted' => 'Статья удалена',
// Ошибки
'article_not_found' => 'Статья не найдена',
'access_denied' => 'Доступ запрещён',
// С параметрами (используется :name для подстановки)
'hello_user' => 'Привет, :name!',
'articles_count' => 'Найдено статей: :count',
];<?php
// Resources/lang/en/messages.php
return [
// General
'welcome' => 'Welcome to the blog',
'home' => 'Home',
'back' => 'Back',
// Articles
'articles' => 'Articles',
'article' => 'Article',
'no_articles' => 'No articles yet',
'read_more' => 'Read more',
// CRUD
'create' => 'Create',
'edit' => 'Edit',
'delete' => 'Delete',
'save' => 'Save',
'cancel' => 'Cancel',
// Success messages
'article_created' => 'Article created successfully',
'article_updated' => 'Article updated',
'article_deleted' => 'Article deleted',
// Errors
'article_not_found' => 'Article not found',
'access_denied' => 'Access denied',
// With parameters
'hello_user' => 'Hello, :name!',
'articles_count' => 'Found :count articles',
];Сообщения валидации
<?php
// Resources/lang/ru/validation.php
return [
// Общие правила
'required' => 'Это поле обязательно',
'email' => 'Введите корректный email',
'min' => 'Минимум :min символов',
'max' => 'Максимум :max символов',
// Для конкретных полей
'title' => [
'required' => 'Введите заголовок статьи',
'min-str-len' => 'Заголовок слишком короткий (минимум :min символов)',
'max-str-len' => 'Заголовок слишком длинный (максимум :max символов)',
],
'content' => [
'required' => 'Напишите содержание статьи',
'min-str-len' => 'Статья слишком короткая',
],
'category_id' => [
'required' => 'Выберите категорию',
'exists' => 'Такой категории не существует',
],
];Важно: Имя файла становится “доменом” перевода. Если у вас и в модуле Blog, и в модуле News есть файл messages.php, ключи могут пересечься. Используйте уникальные имена файлов или ключей.
Использование переводов
В PHP-коде
<?php
namespace Flute\Modules\Blog\Http\Controllers;
use Flute\Core\Support\BaseController;
class ArticleController extends BaseController
{
public function index()
{
// Простой перевод
$title = __('messages.articles');
// Результат (ru): "Статьи"
// Результат (en): "Articles"
// Перевод с параметрами
$greeting = __('messages.hello_user', ['name' => user()->name]);
// Результат: "Привет, Иван!"
// Перевод с количеством
$count = count($articles);
$countText = __('messages.articles_count', ['count' => $count]);
// Результат: "Найдено статей: 15"
return response()->view('blog::index', compact('articles'));
}
public function store()
{
// Валидация прошла успешно
$this->flash(__('messages.article_created'), 'success');
return redirect(route('blog.index'));
}
public function show(int $id)
{
$article = rep(Article::class)->findByPK($id);
if (!$article) {
// Показываем ошибку на языке пользователя
return $this->error(__('messages.article_not_found'), 404);
}
return response()->view('blog::show', compact('article'));
}
}В Blade-шаблонах
{{-- Простой перевод --}}
<h1>{{ __('messages.articles') }}</h1>
{{-- С параметрами --}}
<p>{{ __('messages.hello_user', ['name' => $user->name]) }}</p>
{{-- Через директиву --}}
<button>@lang('messages.save')</button>
{{-- В атрибутах --}}
<input type="text" placeholder="{{ __('messages.search') }}">
{{-- Условный перевод --}}
@if($articles->isEmpty())
<p>{{ __('messages.no_articles') }}</p>
@endif
{{-- В ссылках --}}
<a href="{{ route('blog.create') }}">
{{ __('messages.create') }}
</a>В JavaScript
Иногда нужны переводы на клиенте (в JavaScript):
{{-- В шаблоне передаём нужные переводы --}}
@push('scripts')
<script>
// Создаём объект с переводами
window.translations = {
confirmDelete: @json(__('messages.confirm_delete')),
loading: @json(__('messages.loading')),
success: @json(__('messages.success')),
error: @json(__('messages.error')),
};
</script>
@endpush// В JS-файле
function deleteArticle(id) {
if (confirm(window.translations.confirmDelete)) {
// Удаляем статью
}
}
function showNotification(type, message) {
const text = message || window.translations[type];
// Показываем уведомление
}Множественное число
Для склонения слов в зависимости от количества:
<?php
// Resources/lang/ru/messages.php
return [
// Формат: единственное|множественное
// Или: один|несколько|много (для русского)
'comments' => '{0} Нет комментариев|{1} :count комментарий|[2,4] :count комментария|[5,*] :count комментариев',
// Упрощённый вариант
'items' => ':count товар|:count товара|:count товаров',
];// Использование (если функция trans_choice доступна)
$text = trans_choice('messages.comments', 0); // "Нет комментариев"
$text = trans_choice('messages.comments', 1); // "1 комментарий"
$text = trans_choice('messages.comments', 3); // "3 комментария"
$text = trans_choice('messages.comments', 10); // "10 комментариев"
// Альтернатива через __() с логикой в коде
$text = __('messages.comments_count', ['count' => $count]);Проверьте доступность функции trans_choice() в вашей версии Flute CMS. Если её нет, используйте обычную функцию __() и реализуйте логику множественного числа в коде.
Управление языком
// Получить текущий язык
$locale = app()->getLocale(); // 'ru', 'en' и т.д.
// Установить язык
app()->setLocale('en');
// Проверить, какой язык
if (app()->getLocale() === 'ru') {
// Русский
}Как Flute определяет язык пользователя:
Параметр в URL
?lang=en — высший приоритет
Cookie
current_lang — если пользователь выбрал язык ранее
Настройки пользователя
app()->getLang() — если пользователь авторизован
Настройки по умолчанию
config('lang.locale') — язык сайта по умолчанию
Система конфигурации
Конфигурация позволяет хранить настройки модуля в отдельных файлах и легко их изменять.
Структура файлов
- blog.php
- cache.php
- features.php
Создание конфигурации
<?php
// Resources/config/blog.php
return [
/*
|--------------------------------------------------------------------------
| Основные настройки
|--------------------------------------------------------------------------
*/
// Включён ли модуль
'enabled' => true,
// Название раздела
'title' => 'Блог',
/*
|--------------------------------------------------------------------------
| Настройки отображения
|--------------------------------------------------------------------------
*/
// Сколько статей на странице
'per_page' => 10,
// Длина превью статьи в символах
'excerpt_length' => 200,
// Показывать автора
'show_author' => true,
// Показывать дату публикации
'show_date' => true,
// Формат даты
'date_format' => 'd.m.Y',
/*
|--------------------------------------------------------------------------
| Настройки изображений
|--------------------------------------------------------------------------
*/
'images' => [
// Максимальный размер файла в КБ
'max_size' => 2048,
// Разрешённые форматы
'allowed_types' => ['jpg', 'jpeg', 'png', 'webp'],
// Качество сжатия (для JPEG)
'quality' => 85,
// Размеры превью
'thumbnail' => [
'width' => 300,
'height' => 200,
],
],
/*
|--------------------------------------------------------------------------
| Настройки комментариев
|--------------------------------------------------------------------------
*/
'comments' => [
// Включены ли комментарии
'enabled' => true,
// Модерация (комментарии проверяются перед публикацией)
'moderation' => false,
// Могут ли гости комментировать
'guests_allowed' => false,
// Максимальная вложенность ответов
'max_depth' => 3,
],
/*
|--------------------------------------------------------------------------
| Кеширование
|--------------------------------------------------------------------------
*/
'cache' => [
// Время кеширования списка статей (секунды)
'list_ttl' => 3600, // 1 час
// Время кеширования отдельной статьи
'article_ttl' => 1800, // 30 минут
],
];Использование конфигурации
В контроллерах
<?php
namespace Flute\Modules\Blog\Http\Controllers;
use Flute\Core\Support\BaseController;
class ArticleController extends BaseController
{
public function index()
{
// Получаем значение из конфига
$perPage = config('blog.per_page', 10);
// Если 'blog.per_page' не задан, вернётся 10
$articles = rep(Article::class)
->select()
->where('published', true)
->paginate($perPage);
return response()->view('blog::index', [
'articles' => $articles,
'showAuthor' => config('blog.show_author'),
'showDate' => config('blog.show_date'),
]);
}
public function store()
{
// Проверяем, включён ли модуль
if (!config('blog.enabled')) {
return $this->error('Блог временно отключён', 503);
}
// Проверяем настройки изображений
$image = request()->files->get('image');
if ($image) {
$maxSize = config('blog.images.max_size') * 1024; // В байтах
if ($image->getSize() > $maxSize) {
return $this->error('Изображение слишком большое', 422);
}
$allowedTypes = config('blog.images.allowed_types');
$extension = strtolower($image->getClientOriginalExtension());
if (!in_array($extension, $allowedTypes)) {
return $this->error('Недопустимый формат изображения', 422);
}
}
// Создаём статью...
}
}В сервисах
<?php
namespace Flute\Modules\Blog\Services;
class ArticleService
{
public function getRecentArticles(): array
{
$cacheKey = 'blog_recent_articles';
$cacheTtl = config('blog.cache.list_ttl', 3600);
// Пробуем получить из кеша
$cached = cache()->get($cacheKey);
if ($cached) {
return $cached;
}
// Загружаем из БД
$articles = rep(Article::class)
->select()
->where('published', true)
->orderBy('created_at', 'DESC')
->limit(config('blog.per_page', 10))
->fetchAll();
// Сохраняем в кеш
cache()->set($cacheKey, $articles, $cacheTtl);
return $articles;
}
public function canComment(): bool
{
// Проверяем настройки комментариев
if (!config('blog.comments.enabled')) {
return false;
}
if (!user()->isLoggedIn() && !config('blog.comments.guests_allowed')) {
return false;
}
return true;
}
}В шаблонах
{{-- Получаем настройки --}}
@php
$showAuthor = config('blog.show_author');
$showDate = config('blog.show_date');
$dateFormat = config('blog.date_format', 'd.m.Y');
@endphp
<article class="article">
<h1>{{ $article->title }}</h1>
<div class="article-meta">
@if($showAuthor)
<span class="author">{{ $article->author->name }}</span>
@endif
@if($showDate)
<time>{{ $article->created_at->format($dateFormat) }}</time>
@endif
</div>
<div class="article-content">
{!! $article->content !!}
</div>
</article>
{{-- Проверяем, включены ли комментарии --}}
@if(config('blog.comments.enabled'))
<section class="comments">
@include('blog::components.comments', ['article' => $article])
</section>
@endifПолучение всей конфигурации
// Один ключ
$value = config('blog.per_page');
// С значением по умолчанию
$value = config('blog.per_page', 10);
// Вся секция как массив
$imagesConfig = config('blog.images');
// ['max_size' => 2048, 'allowed_types' => [...], ...]
// Весь файл конфигурации
$blogConfig = config('blog');Кеширование
Переводы
- Переводы кешируются автоматически на 1 час
- В режиме performance компилируются в
storage/app/translations
Сброс кеша переводов:
translation()->flushLocaleCache('ru'); // Для конкретного языка
translation()->flushLocaleCache(); // Для всех языковКонфигурация
- Конфигурация кешируется провайдером модуля
- Время кеширования по умолчанию: 1 час
Управление кешем:
// В провайдере — изменить время кеширования
public function boot(\DI\Container $container): void
{
$this->setCacheDuration(7200); // 2 часа
$this->bootstrapModule();
}
// Сбросить весь кеш модуля
$provider->clearFileCache();
// Сбросить кеш конкретного конфига
cache()->delete('flute.config.blog');Советы
Организация переводов
- Группируйте по смыслу — отдельные файлы для messages, validation, emails
- Используйте префиксы —
article_created,article_deletedвместоcreated,deleted - Документируйте параметры — комментарий
// :count - количествопоможет переводчикам
Организация конфигурации
- Разделяйте по областям — отдельные секции для images, cache, comments
- Всегда указывайте значения по умолчанию —
config('key', 'default') - Используйте env() для изменяемых настроек — то, что может меняться между окружениями
Поддержка нескольких языков
- Создавайте переводы сразу — не откладывайте на потом
- Тестируйте на всех языках — некоторые тексты могут быть длиннее
- Учитывайте RTL — арабский и иврит читаются справа налево