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:
+6
-3
@@ -101,8 +101,12 @@ use Filament\Forms\Components\FileUpload;
|
||||
// Layout components (ÖNEMLİ: Schemas namespace!)
|
||||
use Filament\Schemas\Components\Section; // ✅ Doğru
|
||||
use Filament\Schemas\Components\Grid; // ✅ Doğru
|
||||
use Filament\Schemas\Components\Tabs; // ✅ Doğru
|
||||
use Filament\Schemas\Components\Fieldset; // ✅ Doğru
|
||||
|
||||
// Tabs components (ÖNEMLİ: Forms namespace!)
|
||||
use Filament\Forms\Components\Tabs; // ✅ Doğru
|
||||
// Tab kullanımı: Tabs\Tab::make() şeklinde kullan
|
||||
// Import gerekmez, Tabs namespace'i üzerinden erişilir
|
||||
```
|
||||
|
||||
#### **YANLIŞ KULLANIM (Eski Filament 3.x):**
|
||||
@@ -127,7 +131,6 @@ $table
|
||||
- ✅ **Table Methods:** `->recordActions()` ve `->toolbarActions()`
|
||||
|
||||
### Dil Dosyası Yapısı
|
||||
```php
|
||||
<?php
|
||||
|
||||
return [
|
||||
@@ -158,7 +161,7 @@ return [
|
||||
'field_required' => 'Bu alan zorunludur.',
|
||||
'field_unique' => 'Bu değer zaten kullanılıyor.',
|
||||
];
|
||||
```
|
||||
|
||||
|
||||
### Model Kuralları
|
||||
- `$fillable` array'ini tanımla
|
||||
|
||||
@@ -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),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ return [
|
||||
'content_section' => 'Content',
|
||||
'settings_section' => 'Blog Settings',
|
||||
'seo_section' => 'SEO Settings',
|
||||
'translations_section' => 'Translations',
|
||||
|
||||
// Helper texts
|
||||
'slug_helper' => 'The part that will appear in the URL. Example: blog-post',
|
||||
|
||||
@@ -26,6 +26,7 @@ return [
|
||||
'form_section_content' => 'Content',
|
||||
'form_section_page_settings' => 'Page Settings',
|
||||
'form_section_seo_settings' => 'SEO Settings',
|
||||
'translations_section' => 'Translations',
|
||||
|
||||
// Form fields
|
||||
'title_field' => 'Title',
|
||||
|
||||
@@ -34,6 +34,7 @@ return [
|
||||
'content_section' => 'İçerik',
|
||||
'settings_section' => 'Blog Ayarları',
|
||||
'seo_section' => 'SEO Ayarları',
|
||||
'translations_section' => 'Çeviriler',
|
||||
|
||||
// Helper texts
|
||||
'slug_helper' => 'URL\'de görünecek kısım. Örnek: blog-yazisi',
|
||||
|
||||
@@ -26,6 +26,7 @@ return [
|
||||
'form_section_content' => 'İçerik',
|
||||
'form_section_page_settings' => 'Sayfa Ayarları',
|
||||
'form_section_seo_settings' => 'SEO Ayarları',
|
||||
'translations_section' => 'Çeviriler',
|
||||
|
||||
// Form fields
|
||||
'title_field' => 'Başlık',
|
||||
|
||||
Reference in New Issue
Block a user