From 97a6ad2372aa0de9771218858274b28bf58689e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Cmit=20Tun=C3=A7?= Date: Wed, 1 Oct 2025 04:43:27 -0300 Subject: [PATCH] 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. --- .cursorrules | 9 +- .../Resources/Blogs/Pages/CreateBlog.php | 7 + .../Admin/Resources/Blogs/Pages/EditBlog.php | 25 ++ .../Resources/Blogs/Schemas/BlogForm.php | 42 ++++ .../Resources/Components/TranslationTabs.php | 217 ++++++++++++++++++ .../Resources/Pages/Pages/CreatePage.php | 7 + .../Admin/Resources/Pages/Pages/EditPage.php | 15 ++ .../Resources/Pages/Schemas/PageForm.php | 42 ++++ lang/en/blog.php | 1 + lang/en/pages.php | 1 + lang/tr/blog.php | 1 + lang/tr/pages.php | 1 + 12 files changed, 365 insertions(+), 3 deletions(-) create mode 100644 app/Filament/Admin/Resources/Components/TranslationTabs.php diff --git a/.cursorrules b/.cursorrules index 948defd..2c586b7 100644 --- a/.cursorrules +++ b/.cursorrules @@ -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 'Bu alan zorunludur.', 'field_unique' => 'Bu değer zaten kullanılıyor.', ]; -``` + ### Model Kuralları - `$fillable` array'ini tanımla diff --git a/app/Filament/Admin/Resources/Blogs/Pages/CreateBlog.php b/app/Filament/Admin/Resources/Blogs/Pages/CreateBlog.php index fbd5dad..b0c3f01 100644 --- a/app/Filament/Admin/Resources/Blogs/Pages/CreateBlog.php +++ b/app/Filament/Admin/Resources/Blogs/Pages/CreateBlog.php @@ -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()); + } } diff --git a/app/Filament/Admin/Resources/Blogs/Pages/EditBlog.php b/app/Filament/Admin/Resources/Blogs/Pages/EditBlog.php index af1a630..495d912 100644 --- a/app/Filament/Admin/Resources/Blogs/Pages/EditBlog.php +++ b/app/Filament/Admin/Resources/Blogs/Pages/EditBlog.php @@ -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()); + } } diff --git a/app/Filament/Admin/Resources/Blogs/Schemas/BlogForm.php b/app/Filament/Admin/Resources/Blogs/Schemas/BlogForm.php index 13c0a6c..15347c5 100644 --- a/app/Filament/Admin/Resources/Blogs/Schemas/BlogForm.php +++ b/app/Filament/Admin/Resources/Blogs/Schemas/BlogForm.php @@ -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), ]); } } diff --git a/app/Filament/Admin/Resources/Components/TranslationTabs.php b/app/Filament/Admin/Resources/Components/TranslationTabs.php new file mode 100644 index 0000000..b0c61c9 --- /dev/null +++ b/app/Filament/Admin/Resources/Components/TranslationTabs.php @@ -0,0 +1,217 @@ +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) + ]); + } + } + } +} + diff --git a/app/Filament/Admin/Resources/Pages/Pages/CreatePage.php b/app/Filament/Admin/Resources/Pages/Pages/CreatePage.php index 199a18e..d6a37d2 100644 --- a/app/Filament/Admin/Resources/Pages/Pages/CreatePage.php +++ b/app/Filament/Admin/Resources/Pages/Pages/CreatePage.php @@ -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()); + } } diff --git a/app/Filament/Admin/Resources/Pages/Pages/EditPage.php b/app/Filament/Admin/Resources/Pages/Pages/EditPage.php index a8cb597..569c930 100644 --- a/app/Filament/Admin/Resources/Pages/Pages/EditPage.php +++ b/app/Filament/Admin/Resources/Pages/Pages/EditPage.php @@ -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()); + } } diff --git a/app/Filament/Admin/Resources/Pages/Schemas/PageForm.php b/app/Filament/Admin/Resources/Pages/Schemas/PageForm.php index 342a427..ceebbff 100644 --- a/app/Filament/Admin/Resources/Pages/Schemas/PageForm.php +++ b/app/Filament/Admin/Resources/Pages/Schemas/PageForm.php @@ -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), ]); } } diff --git a/lang/en/blog.php b/lang/en/blog.php index 6c38691..8d98641 100644 --- a/lang/en/blog.php +++ b/lang/en/blog.php @@ -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', diff --git a/lang/en/pages.php b/lang/en/pages.php index 85e79c5..d70fc7a 100644 --- a/lang/en/pages.php +++ b/lang/en/pages.php @@ -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', diff --git a/lang/tr/blog.php b/lang/tr/blog.php index 53cf6a6..a16d84f 100644 --- a/lang/tr/blog.php +++ b/lang/tr/blog.php @@ -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', diff --git a/lang/tr/pages.php b/lang/tr/pages.php index e3e2922..2b020aa 100644 --- a/lang/tr/pages.php +++ b/lang/tr/pages.php @@ -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',