Implement Translation Tabs Component: Added TranslationTabs component for managing translations in Blog and Page resources. Integrated translation fields into forms, ensuring localization support for titles, content, and metadata. Updated existing pages to save and load translations, enhancing the universal translation system. Added relevant localization keys for English and Turkish.

This commit is contained in:
Ümit Tunç
2025-10-01 04:43:27 -03:00
parent 105e14b48d
commit 9cc36297ea
12 changed files with 365 additions and 3 deletions
@@ -3,6 +3,7 @@
namespace App\Filament\Admin\Resources\Blogs\Pages;
use App\Filament\Admin\Resources\Blogs\BlogResource;
use App\Filament\Admin\Resources\Components\TranslationTabs;
use Filament\Resources\Pages\CreateRecord;
class CreateBlog extends CreateRecord
@@ -23,4 +24,10 @@ class CreateBlog extends CreateRecord
{
return __('blog.created_successfully');
}
protected function afterCreate(): void
{
// Save translations
TranslationTabs::saveTranslations($this->record, $this->form->getState());
}
}
@@ -3,6 +3,7 @@
namespace App\Filament\Admin\Resources\Blogs\Pages;
use App\Filament\Admin\Resources\Blogs\BlogResource;
use App\Filament\Admin\Resources\Components\TranslationTabs;
use Filament\Actions\DeleteAction;
use Filament\Actions\ForceDeleteAction;
use Filament\Actions\RestoreAction;
@@ -38,4 +39,28 @@ class EditBlog extends EditRecord
{
return __('blog.updated_successfully');
}
protected function mutateFormDataBeforeFill(array $data): array
{
// Load existing translations
$translationData = TranslationTabs::fillFromRecord($this->record);
\Log::info('Loading translations for edit', [
'blog_id' => $this->record->id,
'translations' => $translationData
]);
return array_merge($data, $translationData);
}
protected function afterSave(): void
{
\Log::info('Saving blog translations', [
'blog_id' => $this->record->id,
'form_state' => $this->form->getState()
]);
// Save translations
TranslationTabs::saveTranslations($this->record, $this->form->getState());
}
}
@@ -2,6 +2,7 @@
namespace App\Filament\Admin\Resources\Blogs\Schemas;
use App\Filament\Admin\Resources\Components\TranslationTabs;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\FileUpload;
@@ -149,6 +150,47 @@ class BlogForm
->columnSpanFull()
->collapsible(true)
->collapsed(true),
// Çeviri Sekmesi
Section::make('🌍 ' . __('blog.translations_section'))
->schema([
TranslationTabs::make([
'title' => [
'type' => 'text',
'label' => __('blog.title_field'),
'required' => false,
'maxLength' => 255,
],
'content' => [
'type' => 'richtext',
'label' => __('blog.content_field'),
'required' => false,
],
'excerpt' => [
'type' => 'textarea',
'label' => __('blog.excerpt_field'),
'required' => false,
'maxLength' => 500,
'rows' => 3,
],
'meta_title' => [
'type' => 'text',
'label' => __('blog.meta_title_field'),
'required' => false,
'maxLength' => 60,
],
'meta_description' => [
'type' => 'textarea',
'label' => __('blog.meta_description_field'),
'required' => false,
'maxLength' => 160,
'rows' => 3,
],
]),
])
->columnSpanFull()
->collapsible(true)
->collapsed(false),
]);
}
}
@@ -0,0 +1,217 @@
<?php
namespace App\Filament\Admin\Resources\Components;
use App\Models\Language;
use Filament\Forms\Components\RichEditor;
use Filament\Schemas\Components\Tabs;
use Filament\Schemas\Components\Tabs\Tab;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
class TranslationTabs
{
public static function make(array $fields = []): Tabs
{
$languages = Language::active()->ordered()->get();
if ($languages->isEmpty()) {
return Tabs::make('translations');
}
$tabs = [];
foreach ($languages as $language) {
// User permission kontrolü
$canManage = auth()->user()?->canManageLanguage($language->code) ?? true;
if (!$canManage) {
continue;
}
$tabFields = [];
foreach ($fields as $fieldName => $fieldConfig) {
$fieldType = $fieldConfig['type'] ?? 'text';
$label = $fieldConfig['label'] ?? ucfirst($fieldName);
$required = $fieldConfig['required'] ?? false;
$maxLength = $fieldConfig['maxLength'] ?? null;
$rows = $fieldConfig['rows'] ?? 3;
$fieldKey = "translations.{$language->code}.{$fieldName}";
switch ($fieldType) {
case 'richtext':
$field = RichEditor::make($fieldKey)
->label($label)
->fileAttachmentsDisk('public')
->fileAttachmentsDirectory('translations')
->fileAttachmentsVisibility('public')
->toolbarButtons([
'attachFiles',
'blockquote',
'bold',
'bulletList',
'codeBlock',
'h2',
'h3',
'italic',
'link',
'orderedList',
'redo',
'strike',
'underline',
'undo',
])
->columnSpanFull()
->required($required);
break;
case 'textarea':
$field = Textarea::make($fieldKey)
->label($label)
->rows($rows)
->columnSpanFull();
break;
case 'text':
default:
$field = TextInput::make($fieldKey)
->label($label);
break;
}
if ($required) {
$field->required();
}
if ($maxLength) {
$field->maxLength($maxLength);
}
$tabFields[] = $field;
}
// Convert country code to flag emoji
$code = strtoupper($language->code);
$flag = '';
for ($i = 0; $i < strlen($code); $i++) {
$flag .= mb_chr(127397 + ord($code[$i]));
}
$tabs[] = Tab::make($language->name)
->label($flag . ' ' . $language->native_name)
->schema($tabFields)
->badge(fn ($record) => $record
? self::getTranslationBadge($record, $language->code)
: null
)
->badgeColor(fn ($record) => $record
? self::getTranslationBadgeColor($record, $language->code)
: 'gray'
);
}
return Tabs::make('translations')
->tabs($tabs)
->columnSpanFull()
->persistTabInQueryString();
}
protected static function getTranslationBadge($record, string $languageCode): ?string
{
if (!method_exists($record, 'getTranslationProgress')) {
return null;
}
$progress = $record->getTranslationProgress();
$percentage = $progress[$languageCode] ?? 0;
if ($percentage === 100) {
return '✓';
} elseif ($percentage > 0) {
return $percentage . '%';
}
return null;
}
protected static function getTranslationBadgeColor($record, string $languageCode): string
{
if (!method_exists($record, 'getTranslationProgress')) {
return 'gray';
}
$progress = $record->getTranslationProgress();
$percentage = $progress[$languageCode] ?? 0;
if ($percentage === 100) {
return 'success';
} elseif ($percentage >= 50) {
return 'warning';
} elseif ($percentage > 0) {
return 'danger';
}
return 'gray';
}
public static function fillFromRecord($record): array
{
$data = ['translations' => []];
if (!method_exists($record, 'getTranslatableFields')) {
return $data;
}
$languages = Language::active()->ordered()->get();
$fields = $record->getTranslatableFields();
foreach ($languages as $language) {
$data['translations'][$language->code] = [];
foreach ($fields as $field) {
$translation = $record->getTranslation($field, $language->code, false);
if ($translation) {
$data['translations'][$language->code][$field] = $translation->field_value;
}
}
}
return $data;
}
public static function saveTranslations($record, array $data): void
{
if (!method_exists($record, 'setTranslation')) {
return;
}
$translations = $data['translations'] ?? [];
\Log::info('Saving translations', ['data' => $translations]);
foreach ($translations as $languageCode => $fields) {
if (!is_array($fields)) {
continue;
}
foreach ($fields as $fieldName => $value) {
// Boş değerleri de kaydet (silme işlemi için)
$record->setTranslation(
$fieldName,
$languageCode,
$value ?? '',
'published', // Default status
auth()->id()
);
\Log::info('Translation saved', [
'language' => $languageCode,
'field' => $fieldName,
'value' => substr($value ?? '', 0, 50)
]);
}
}
}
}
@@ -2,6 +2,7 @@
namespace App\Filament\Admin\Resources\Pages\Pages;
use App\Filament\Admin\Resources\Components\TranslationTabs;
use App\Filament\Admin\Resources\Pages\PageResource;
use Filament\Resources\Pages\CreateRecord;
@@ -23,4 +24,10 @@ class CreatePage extends CreateRecord
{
return __('pages.created_successfully');
}
protected function afterCreate(): void
{
// Save translations
TranslationTabs::saveTranslations($this->record, $this->form->getState());
}
}
@@ -2,6 +2,7 @@
namespace App\Filament\Admin\Resources\Pages\Pages;
use App\Filament\Admin\Resources\Components\TranslationTabs;
use App\Filament\Admin\Resources\Pages\PageResource;
use Filament\Actions\DeleteAction;
use Filament\Actions\ForceDeleteAction;
@@ -38,4 +39,18 @@ class EditPage extends EditRecord
{
return __('pages.updated_successfully');
}
protected function mutateFormDataBeforeFill(array $data): array
{
// Load existing translations
$translationData = TranslationTabs::fillFromRecord($this->record);
return array_merge($data, $translationData);
}
protected function afterSave(): void
{
// Save translations
TranslationTabs::saveTranslations($this->record, $this->form->getState());
}
}
@@ -2,6 +2,7 @@
namespace App\Filament\Admin\Resources\Pages\Schemas;
use App\Filament\Admin\Resources\Components\TranslationTabs;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\FileUpload;
@@ -162,6 +163,47 @@ class PageForm
->columnSpanFull()
->collapsible(true)
->collapsed(true),
// Çeviri Sekmesi
Section::make('🌍 ' . __('pages.translations_section'))
->schema([
TranslationTabs::make([
'title' => [
'type' => 'text',
'label' => __('pages.title_field'),
'required' => false,
'maxLength' => 255,
],
'content' => [
'type' => 'richtext',
'label' => __('pages.content_field'),
'required' => false,
],
'excerpt' => [
'type' => 'textarea',
'label' => __('pages.excerpt_field'),
'required' => false,
'maxLength' => 500,
'rows' => 3,
],
'meta_title' => [
'type' => 'text',
'label' => __('pages.meta_title_field'),
'required' => false,
'maxLength' => 60,
],
'meta_description' => [
'type' => 'textarea',
'label' => __('pages.meta_description_field'),
'required' => false,
'maxLength' => 160,
'rows' => 3,
],
]),
])
->columnSpanFull()
->collapsible(true)
->collapsed(false),
]);
}
}