+146
-2
@@ -50,8 +50,87 @@ php artisan make:filament-resource ModelName --generate --model --migration --fa
|
||||
- Table column'ları için header'ları localization'dan al
|
||||
- Validation mesajları için localization kullan
|
||||
|
||||
### Dil Dosyası Yapısı
|
||||
### Filament 4.x Import Kuralları (ÖNEMLİ!)
|
||||
|
||||
#### **Table Actions (DOĞRU):**
|
||||
```php
|
||||
// Table Actions için
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\RestoreAction;
|
||||
use Filament\Actions\ForceDeleteAction;
|
||||
|
||||
// Bulk Actions için
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\ForceDeleteBulkAction;
|
||||
use Filament\Actions\RestoreBulkAction;
|
||||
|
||||
// Filters için
|
||||
use Filament\Tables\Filters\TrashedFilter;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
|
||||
// Columns için
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\ImageColumn;
|
||||
|
||||
// Table metodları
|
||||
$table
|
||||
->recordActions([ // ✅ Doğru
|
||||
EditAction::make(),
|
||||
])
|
||||
->toolbarActions([ // ✅ Doğru
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
```
|
||||
|
||||
#### **Form & Schema Components (DOĞRU):**
|
||||
```php
|
||||
// Form input components
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\DateTimePicker;
|
||||
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\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):**
|
||||
```php
|
||||
// ❌ YANLIŞ - Bu namespace'ler Filament 4'te YOK!
|
||||
use Filament\Tables\Actions\EditAction; // ❌
|
||||
use Filament\Forms\Components\Section; // ❌
|
||||
use Filament\Forms\Components\Grid; // ❌
|
||||
|
||||
// ❌ YANLIŞ - Eski metod adları
|
||||
$table
|
||||
->actions([...]) // ❌ Kullanma!
|
||||
->bulkActions([...]) // ❌ Kullanma!
|
||||
```
|
||||
|
||||
#### **ÖZET:**
|
||||
- ✅ **Table Actions:** `Filament\Actions\` namespace
|
||||
- ✅ **Table Columns:** `Filament\Tables\Columns\` namespace
|
||||
- ✅ **Table Filters:** `Filament\Tables\Filters\` namespace
|
||||
- ✅ **Form Inputs:** `Filament\Forms\Components\` namespace
|
||||
- ✅ **Layout Components:** `Filament\Schemas\Components\` namespace (Section, Grid, Tabs)
|
||||
- ✅ **Table Methods:** `->recordActions()` ve `->toolbarActions()`
|
||||
|
||||
### Dil Dosyası Yapısı
|
||||
<?php
|
||||
|
||||
return [
|
||||
@@ -82,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
|
||||
@@ -147,3 +226,68 @@ Pages modülü bu kuralların uygulandığı örnek modüldür. Yeni modül olu
|
||||
- ✅ Action buttons
|
||||
- ✅ Notifications
|
||||
- ✅ Redirect handling
|
||||
|
||||
## Universal Translation System
|
||||
|
||||
### Translation System Kullanımı
|
||||
Platform, dinamik ve evrensel bir çeviri sistemi içerir. Herhangi bir modele çeviri desteği eklemek için:
|
||||
|
||||
```php
|
||||
use App\Traits\HasTranslations;
|
||||
|
||||
class YourModel extends Model
|
||||
{
|
||||
use HasTranslations;
|
||||
|
||||
protected $translatable = [
|
||||
'title',
|
||||
'content',
|
||||
'description',
|
||||
'meta_title',
|
||||
'meta_description',
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
### Translation Kuralları
|
||||
- ✅ Çevrilecek alanlar `$translatable` array'inde tanımlanmalı
|
||||
- ✅ `HasTranslations` trait'i kullanılmalı
|
||||
- ✅ Trait'te `$translatable` property tanımlama (conflict oluşur)
|
||||
- ✅ Her model kendi `$translatable` array'ini tanımlar
|
||||
- ✅ Slug, ID, FK gibi alanlar çevrilmemeli
|
||||
|
||||
### Translation Metodları
|
||||
```php
|
||||
// Çeviri kaydet
|
||||
$model->setTranslation('field_name', 'en', 'Translation', 'published');
|
||||
|
||||
// Çeviri oku
|
||||
$model->translate('field_name'); // Mevcut dilde
|
||||
$model->translate('field_name', 'en'); // Belirli dilde
|
||||
$model->translate('field_name', 'en', false); // Fallback olmadan
|
||||
|
||||
// Çeviri kontrolü
|
||||
$model->hasTranslation('field_name', 'en');
|
||||
|
||||
// İlerleme takibi
|
||||
$model->getTranslationProgress(); // ['tr' => 100, 'en' => 75]
|
||||
$model->getMissingTranslations('en'); // ['meta_description']
|
||||
```
|
||||
|
||||
### Translation Workflow
|
||||
- `draft` → `review` → `approved` → `published`
|
||||
- Sadece `published` çeviriler son kullanıcıya görünür
|
||||
- User-language permissions ile kontrol edilir
|
||||
|
||||
### Helper Functions
|
||||
```php
|
||||
current_language(); // Mevcut dil objesi
|
||||
available_languages(); // Aktif diller
|
||||
translate_model($model, 'field', 'en'); // Model çevirisi
|
||||
user_can_manage_language('en'); // Kullanıcı yetkisi
|
||||
switch_language('en'); // Dil değiştir
|
||||
```
|
||||
|
||||
### Dokümantasyon
|
||||
- `docs/TRANSLATION_SYSTEM.md` - Tam sistem dokümantasyonu
|
||||
- `docs/TRANSLATION_QUICK_START.md` - Hızlı başlangıç rehberi
|
||||
|
||||
@@ -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)
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Languages;
|
||||
|
||||
use App\Filament\Admin\Resources\Languages\Pages\CreateLanguage;
|
||||
use App\Filament\Admin\Resources\Languages\Pages\EditLanguage;
|
||||
use App\Filament\Admin\Resources\Languages\Pages\ListLanguages;
|
||||
use App\Models\Language;
|
||||
use BackedEnum;
|
||||
use UnitEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class LanguageResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Language::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedGlobeAlt;
|
||||
|
||||
protected static UnitEnum|string|null $navigationGroup = 'System';
|
||||
|
||||
protected static ?int $navigationSort = 100;
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return __('language.navigation_label');
|
||||
}
|
||||
|
||||
public static function getModelLabel(): string
|
||||
{
|
||||
return __('language.model_label');
|
||||
}
|
||||
|
||||
public static function getPluralModelLabel(): string
|
||||
{
|
||||
return __('language.plural_model_label');
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->schema(\App\Filament\Admin\Resources\Languages\Schemas\LanguageForm::schema());
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return \App\Filament\Admin\Resources\Languages\Tables\LanguagesTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListLanguages::route('/'),
|
||||
'create' => CreateLanguage::route('/create'),
|
||||
'edit' => EditLanguage::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Languages\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\Languages\LanguageResource;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateLanguage extends CreateRecord
|
||||
{
|
||||
protected static string $resource = LanguageResource::class;
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return __('language.create');
|
||||
}
|
||||
|
||||
protected function getCreatedNotification(): ?Notification
|
||||
{
|
||||
return Notification::make()
|
||||
->success()
|
||||
->title(__('language.created_successfully'))
|
||||
->body(__('language.created_successfully_body'));
|
||||
}
|
||||
|
||||
protected function getRedirectUrl(): string
|
||||
{
|
||||
return $this->getResource()::getUrl('index');
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeCreate(array $data): array
|
||||
{
|
||||
$data['created_by'] = auth()->id();
|
||||
$data['updated_by'] = auth()->id();
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Languages\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\Languages\LanguageResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\ForceDeleteAction;
|
||||
use Filament\Actions\RestoreAction;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditLanguage extends EditRecord
|
||||
{
|
||||
protected static string $resource = LanguageResource::class;
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return __('language.edit');
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
DeleteAction::make()
|
||||
->label(__('language.delete')),
|
||||
RestoreAction::make()
|
||||
->label(__('language.restore')),
|
||||
ForceDeleteAction::make()
|
||||
->label(__('language.force_delete')),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getSavedNotification(): ?Notification
|
||||
{
|
||||
return Notification::make()
|
||||
->success()
|
||||
->title(__('language.updated_successfully'))
|
||||
->body(__('language.updated_successfully_body'));
|
||||
}
|
||||
|
||||
protected function getRedirectUrl(): string
|
||||
{
|
||||
return $this->getResource()::getUrl('index');
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeSave(array $data): array
|
||||
{
|
||||
$data['updated_by'] = auth()->id();
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Languages\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\Languages\LanguageResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListLanguages extends ListRecords
|
||||
{
|
||||
protected static string $resource = LanguageResource::class;
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return __('language.title');
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make()
|
||||
->label(__('language.create')),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Languages\Schemas;
|
||||
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Schemas\Components\Grid;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class LanguageForm
|
||||
{
|
||||
public static function schema(): array
|
||||
{
|
||||
return [
|
||||
Section::make(__('language.basic_information'))
|
||||
->schema([
|
||||
Grid::make(2)->schema([
|
||||
TextInput::make('code')
|
||||
->label(__('language.code'))
|
||||
->required()
|
||||
->unique(ignoreRecord: true)
|
||||
->maxLength(10)
|
||||
->placeholder('tr, en, de')
|
||||
->helperText(__('language.code_helper'))
|
||||
->columnSpan(1),
|
||||
|
||||
TextInput::make('flag')
|
||||
->label(__('language.flag'))
|
||||
->maxLength(10)
|
||||
->placeholder('🇹🇷')
|
||||
->helperText(__('language.flag_helper'))
|
||||
->columnSpan(1),
|
||||
]),
|
||||
|
||||
Grid::make(2)->schema([
|
||||
TextInput::make('name')
|
||||
->label(__('language.name'))
|
||||
->required()
|
||||
->maxLength(100)
|
||||
->placeholder('Turkish')
|
||||
->helperText(__('language.name_helper'))
|
||||
->columnSpan(1),
|
||||
|
||||
TextInput::make('native_name')
|
||||
->label(__('language.native_name'))
|
||||
->required()
|
||||
->maxLength(100)
|
||||
->placeholder('Türkçe')
|
||||
->helperText(__('language.native_name_helper'))
|
||||
->columnSpan(1),
|
||||
]),
|
||||
|
||||
Grid::make(2)->schema([
|
||||
Select::make('direction')
|
||||
->label(__('language.direction'))
|
||||
->options([
|
||||
'ltr' => __('language.direction_ltr'),
|
||||
'rtl' => __('language.direction_rtl'),
|
||||
])
|
||||
->default('ltr')
|
||||
->required()
|
||||
->columnSpan(1),
|
||||
|
||||
TextInput::make('sort_order')
|
||||
->label(__('language.sort_order'))
|
||||
->numeric()
|
||||
->default(0)
|
||||
->helperText(__('language.sort_order_helper'))
|
||||
->columnSpan(1),
|
||||
]),
|
||||
]),
|
||||
|
||||
Section::make(__('language.settings'))
|
||||
->schema([
|
||||
Grid::make(3)->schema([
|
||||
Checkbox::make('is_active')
|
||||
->label(__('language.is_active'))
|
||||
->default(true)
|
||||
->helperText(__('language.is_active_helper'))
|
||||
->columnSpan(1),
|
||||
|
||||
Checkbox::make('is_default')
|
||||
->label(__('language.is_default'))
|
||||
->default(false)
|
||||
->helperText(__('language.is_default_helper'))
|
||||
->columnSpan(1)
|
||||
->disabled(fn ($record) => $record && $record->is_default),
|
||||
]),
|
||||
]),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Languages\Tables;
|
||||
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Actions\ForceDeleteAction;
|
||||
use Filament\Actions\ForceDeleteBulkAction;
|
||||
use Filament\Actions\RestoreAction;
|
||||
use Filament\Actions\RestoreBulkAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\TrashedFilter;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
|
||||
class LanguagesTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('flag')
|
||||
->label(__('language.table_flag'))
|
||||
->sortable()
|
||||
->searchable()
|
||||
->formatStateUsing(fn (?string $state): string =>
|
||||
'<span style="font-size: 2rem; line-height: 1;">' . ($state ?? '🌐') . '</span>'
|
||||
)
|
||||
->html()
|
||||
->alignCenter(),
|
||||
|
||||
TextColumn::make('code')
|
||||
->label(__('language.table_code'))
|
||||
->sortable()
|
||||
->searchable()
|
||||
->badge()
|
||||
->color('info'),
|
||||
|
||||
TextColumn::make('name')
|
||||
->label(__('language.table_name'))
|
||||
->sortable()
|
||||
->searchable()
|
||||
->description(fn ($record) => $record->native_name),
|
||||
|
||||
IconColumn::make('is_active')
|
||||
->label(__('language.table_is_active'))
|
||||
->boolean()
|
||||
->sortable(),
|
||||
|
||||
IconColumn::make('is_default')
|
||||
->label(__('language.table_is_default'))
|
||||
->boolean()
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('direction')
|
||||
->label(__('language.table_direction'))
|
||||
->badge()
|
||||
->color(fn (string $state): string => match ($state) {
|
||||
'ltr' => 'success',
|
||||
'rtl' => 'warning',
|
||||
})
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('sort_order')
|
||||
->label(__('language.table_sort_order'))
|
||||
->numeric()
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('created_at')
|
||||
->label(__('language.table_created_at'))
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
|
||||
TextColumn::make('updated_at')
|
||||
->label(__('language.table_updated_at'))
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
|
||||
TextColumn::make('deleted_at')
|
||||
->label(__('language.table_deleted_at'))
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
TrashedFilter::make(),
|
||||
])
|
||||
->recordActions([
|
||||
EditAction::make(),
|
||||
DeleteAction::make(),
|
||||
RestoreAction::make(),
|
||||
ForceDeleteAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
ForceDeleteBulkAction::make(),
|
||||
RestoreBulkAction::make(),
|
||||
]),
|
||||
])
|
||||
->defaultSort('sort_order')
|
||||
->modifyQueryUsing(fn (Builder $query) => $query->withoutGlobalScopes([
|
||||
SoftDeletingScope::class,
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Language;
|
||||
use App\Models\Translation;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
if (!function_exists('current_language')) {
|
||||
/**
|
||||
* Get current active language
|
||||
*/
|
||||
function current_language(): ?Language
|
||||
{
|
||||
return Language::findByCode(app()->getLocale());
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('current_language_code')) {
|
||||
/**
|
||||
* Get current language code
|
||||
*/
|
||||
function current_language_code(): string
|
||||
{
|
||||
return app()->getLocale();
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('default_language')) {
|
||||
/**
|
||||
* Get default language
|
||||
*/
|
||||
function default_language(): ?Language
|
||||
{
|
||||
return Language::getDefault();
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('default_language_code')) {
|
||||
/**
|
||||
* Get default language code
|
||||
*/
|
||||
function default_language_code(): string
|
||||
{
|
||||
$defaultLanguage = Language::getDefault();
|
||||
return $defaultLanguage ? $defaultLanguage->code : config('app.locale');
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('available_languages')) {
|
||||
/**
|
||||
* Get all active languages
|
||||
*/
|
||||
function available_languages(): Collection
|
||||
{
|
||||
return Language::getActive();
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('available_language_codes')) {
|
||||
/**
|
||||
* Get all active language codes
|
||||
*/
|
||||
function available_language_codes(): array
|
||||
{
|
||||
return Language::active()->pluck('code')->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('translate_model')) {
|
||||
/**
|
||||
* Get translated value for a model field
|
||||
*
|
||||
* @param mixed $model Model instance
|
||||
* @param string $field Field name to translate
|
||||
* @param string|null $languageCode Language code (null for current)
|
||||
* @param bool $fallback Use fallback to default language
|
||||
* @return mixed
|
||||
*/
|
||||
function translate_model($model, string $field, ?string $languageCode = null, bool $fallback = true): mixed
|
||||
{
|
||||
if (!method_exists($model, 'translate')) {
|
||||
return $model->{$field} ?? null;
|
||||
}
|
||||
|
||||
return $model->translate($field, $languageCode, $fallback);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('has_translation')) {
|
||||
/**
|
||||
* Check if model has translation for field and language
|
||||
*/
|
||||
function has_translation($model, string $field, ?string $languageCode = null): bool
|
||||
{
|
||||
if (!method_exists($model, 'hasTranslation')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$languageCode = $languageCode ?? current_language_code();
|
||||
return $model->hasTranslation($field, $languageCode);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('translation_progress')) {
|
||||
/**
|
||||
* Get translation progress for a model
|
||||
*/
|
||||
function translation_progress($model, ?string $languageCode = null): int
|
||||
{
|
||||
if (!method_exists($model, 'getTranslationProgress')) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$progress = $model->getTranslationProgress();
|
||||
|
||||
if ($languageCode) {
|
||||
return $progress[$languageCode] ?? 0;
|
||||
}
|
||||
|
||||
return $progress;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('user_can_manage_language')) {
|
||||
/**
|
||||
* Check if current user can manage a language
|
||||
*/
|
||||
function user_can_manage_language(string $languageCode): bool
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
if (!$user || !method_exists($user, 'canManageLanguage')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $user->canManageLanguage($languageCode);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('user_managed_languages')) {
|
||||
/**
|
||||
* Get languages that current user can manage
|
||||
*/
|
||||
function user_managed_languages(): Collection
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
if (!$user || !method_exists($user, 'getManagedLanguages')) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return $user->getManagedLanguages();
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('user_managed_language_codes')) {
|
||||
/**
|
||||
* Get language codes that current user can manage
|
||||
*/
|
||||
function user_managed_language_codes(): array
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
if (!$user || !method_exists($user, 'getManagedLanguageCodes')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $user->getManagedLanguageCodes();
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('switch_language')) {
|
||||
/**
|
||||
* Switch application language
|
||||
*/
|
||||
function switch_language(string $languageCode): bool
|
||||
{
|
||||
$language = Language::findByCode($languageCode);
|
||||
|
||||
if (!$language || !$language->is_active) {
|
||||
return false;
|
||||
}
|
||||
|
||||
app()->setLocale($languageCode);
|
||||
|
||||
if (auth()->check()) {
|
||||
session(['locale' => $languageCode]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('get_translation_status_badge')) {
|
||||
/**
|
||||
* Get translation status badge HTML
|
||||
*/
|
||||
function get_translation_status_badge(string $status): string
|
||||
{
|
||||
$badges = [
|
||||
'draft' => '<span class="badge badge-secondary">Draft</span>',
|
||||
'review' => '<span class="badge badge-warning">Review</span>',
|
||||
'approved' => '<span class="badge badge-success">Approved</span>',
|
||||
'published' => '<span class="badge badge-primary">Published</span>',
|
||||
];
|
||||
|
||||
return $badges[$status] ?? $badges['draft'];
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('get_translation_status_color')) {
|
||||
/**
|
||||
* Get translation status color
|
||||
*/
|
||||
function get_translation_status_color(string $status): string
|
||||
{
|
||||
$colors = [
|
||||
'draft' => 'gray',
|
||||
'review' => 'warning',
|
||||
'approved' => 'success',
|
||||
'published' => 'primary',
|
||||
];
|
||||
|
||||
return $colors[$status] ?? 'gray';
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('get_language_flag')) {
|
||||
/**
|
||||
* Get language flag emoji or icon
|
||||
*/
|
||||
function get_language_flag(string $languageCode): string
|
||||
{
|
||||
$language = Language::findByCode($languageCode);
|
||||
return $language ? ($language->flag ?? '🌐') : '🌐';
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('format_language_name')) {
|
||||
/**
|
||||
* Format language name with native name
|
||||
*/
|
||||
function format_language_name(string $languageCode): string
|
||||
{
|
||||
$language = Language::findByCode($languageCode);
|
||||
|
||||
if (!$language) {
|
||||
return $languageCode;
|
||||
}
|
||||
|
||||
return "{$language->name} ({$language->native_name})";
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('translation_exists')) {
|
||||
/**
|
||||
* Check if a translation exists in database
|
||||
*/
|
||||
function translation_exists(string $modelClass, int $modelId, string $field, string $languageCode): bool
|
||||
{
|
||||
return Translation::forModel($modelClass, $modelId)
|
||||
->forField($field)
|
||||
->forLanguage($languageCode)
|
||||
->exists();
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('get_translation_value')) {
|
||||
/**
|
||||
* Get translation value directly from Translation model
|
||||
*/
|
||||
function get_translation_value(string $modelClass, int $modelId, string $field, ?string $languageCode = null): ?string
|
||||
{
|
||||
$languageCode = $languageCode ?? current_language_code();
|
||||
|
||||
$translation = Translation::forModel($modelClass, $modelId)
|
||||
->forField($field)
|
||||
->forLanguage($languageCode)
|
||||
->published()
|
||||
->first();
|
||||
|
||||
return $translation ? $translation->field_value : null;
|
||||
}
|
||||
}
|
||||
|
||||
+13
-1
@@ -2,13 +2,14 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\HasTranslations;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Blog extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
use HasFactory, SoftDeletes, HasTranslations;
|
||||
|
||||
protected $fillable = [
|
||||
'title',
|
||||
@@ -28,6 +29,17 @@ class Blog extends Model
|
||||
'allow_comments',
|
||||
];
|
||||
|
||||
/**
|
||||
* Translatable fields
|
||||
*/
|
||||
protected $translatable = [
|
||||
'title',
|
||||
'content',
|
||||
'excerpt',
|
||||
'meta_title',
|
||||
'meta_description',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'published_at' => 'datetime',
|
||||
'tags' => 'array',
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Language extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'code',
|
||||
'name',
|
||||
'native_name',
|
||||
'flag',
|
||||
'direction',
|
||||
'is_active',
|
||||
'is_default',
|
||||
'sort_order',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
'is_default' => 'boolean',
|
||||
'sort_order' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
* Boot method
|
||||
*/
|
||||
protected static function boot(): void
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
// Only one default language allowed
|
||||
static::saving(function ($language) {
|
||||
if ($language->is_default) {
|
||||
static::where('id', '!=', $language->id)->update(['is_default' => false]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationships
|
||||
*/
|
||||
public function translations(): HasMany
|
||||
{
|
||||
return $this->hasMany(Translation::class, 'language_code', 'code');
|
||||
}
|
||||
|
||||
public function userLanguages(): HasMany
|
||||
{
|
||||
return $this->hasMany(UserLanguage::class, 'language_code', 'code');
|
||||
}
|
||||
|
||||
public function creator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
public function updater(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'updated_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scopes
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
public function scopeOrdered($query)
|
||||
{
|
||||
return $query->orderBy('sort_order')->orderBy('name');
|
||||
}
|
||||
|
||||
/**
|
||||
* Helpers
|
||||
*/
|
||||
public static function getDefault(): ?self
|
||||
{
|
||||
return static::where('is_default', true)->first();
|
||||
}
|
||||
|
||||
public static function getActive(): \Illuminate\Database\Eloquent\Collection
|
||||
{
|
||||
return static::active()->ordered()->get();
|
||||
}
|
||||
|
||||
public static function findByCode(string $code): ?self
|
||||
{
|
||||
return static::where('code', $code)->first();
|
||||
}
|
||||
|
||||
public function activate(): bool
|
||||
{
|
||||
$this->is_active = true;
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
public function deactivate(): bool
|
||||
{
|
||||
if ($this->is_default) {
|
||||
return false; // Cannot deactivate default language
|
||||
}
|
||||
$this->is_active = false;
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
public function setAsDefault(): bool
|
||||
{
|
||||
$this->is_default = true;
|
||||
$this->is_active = true;
|
||||
return $this->save();
|
||||
}
|
||||
}
|
||||
+13
-1
@@ -2,13 +2,14 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\HasTranslations;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Page extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
use HasFactory, SoftDeletes, HasTranslations;
|
||||
|
||||
protected $fillable = [
|
||||
'title',
|
||||
@@ -28,6 +29,17 @@ class Page extends Model
|
||||
'show_in_menu',
|
||||
];
|
||||
|
||||
/**
|
||||
* Translatable fields
|
||||
*/
|
||||
protected $translatable = [
|
||||
'title',
|
||||
'content',
|
||||
'excerpt',
|
||||
'meta_title',
|
||||
'meta_description',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'published_at' => 'datetime',
|
||||
'is_homepage' => 'boolean',
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
|
||||
class Translation extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'translatable_type',
|
||||
'translatable_id',
|
||||
'language_code',
|
||||
'field_name',
|
||||
'field_value',
|
||||
'status',
|
||||
'version',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'approved_by',
|
||||
'approved_at',
|
||||
'published_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'version' => 'integer',
|
||||
'approved_at' => 'datetime',
|
||||
'published_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* Relationships
|
||||
*/
|
||||
public function translatable(): MorphTo
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
|
||||
public function language(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Language::class, 'language_code', 'code');
|
||||
}
|
||||
|
||||
public function creator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
public function updater(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'updated_by');
|
||||
}
|
||||
|
||||
public function approver(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'approved_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scopes
|
||||
*/
|
||||
public function scopeForModel($query, string $modelClass, int $modelId)
|
||||
{
|
||||
return $query->where('translatable_type', $modelClass)
|
||||
->where('translatable_id', $modelId);
|
||||
}
|
||||
|
||||
public function scopeForLanguage($query, string $languageCode)
|
||||
{
|
||||
return $query->where('language_code', $languageCode);
|
||||
}
|
||||
|
||||
public function scopeForField($query, string $fieldName)
|
||||
{
|
||||
return $query->where('field_name', $fieldName);
|
||||
}
|
||||
|
||||
public function scopePublished($query)
|
||||
{
|
||||
return $query->where('status', 'published');
|
||||
}
|
||||
|
||||
public function scopeApproved($query)
|
||||
{
|
||||
return $query->whereIn('status', ['approved', 'published']);
|
||||
}
|
||||
|
||||
public function scopeDraft($query)
|
||||
{
|
||||
return $query->where('status', 'draft');
|
||||
}
|
||||
|
||||
public function scopeReview($query)
|
||||
{
|
||||
return $query->where('status', 'review');
|
||||
}
|
||||
|
||||
public function scopePendingApproval($query)
|
||||
{
|
||||
return $query->whereIn('status', ['draft', 'review']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Workflow Methods
|
||||
*/
|
||||
public function submitForReview(): bool
|
||||
{
|
||||
if ($this->status === 'draft') {
|
||||
$this->status = 'review';
|
||||
return $this->save();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function approve(?int $userId = null): bool
|
||||
{
|
||||
if (in_array($this->status, ['draft', 'review'])) {
|
||||
$this->status = 'approved';
|
||||
$this->approved_by = $userId ?? auth()->id();
|
||||
$this->approved_at = now();
|
||||
return $this->save();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function publish(?int $userId = null): bool
|
||||
{
|
||||
if (in_array($this->status, ['approved', 'draft', 'review'])) {
|
||||
$this->status = 'published';
|
||||
$this->published_at = now();
|
||||
if (!$this->approved_by) {
|
||||
$this->approved_by = $userId ?? auth()->id();
|
||||
$this->approved_at = now();
|
||||
}
|
||||
return $this->save();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function reject(): bool
|
||||
{
|
||||
if ($this->status === 'review') {
|
||||
$this->status = 'draft';
|
||||
return $this->save();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function unpublish(): bool
|
||||
{
|
||||
if ($this->status === 'published') {
|
||||
$this->status = 'approved';
|
||||
$this->published_at = null;
|
||||
return $this->save();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper Methods
|
||||
*/
|
||||
public function isDraft(): bool
|
||||
{
|
||||
return $this->status === 'draft';
|
||||
}
|
||||
|
||||
public function isReview(): bool
|
||||
{
|
||||
return $this->status === 'review';
|
||||
}
|
||||
|
||||
public function isApproved(): bool
|
||||
{
|
||||
return $this->status === 'approved';
|
||||
}
|
||||
|
||||
public function isPublished(): bool
|
||||
{
|
||||
return $this->status === 'published';
|
||||
}
|
||||
|
||||
public function canBeEdited(): bool
|
||||
{
|
||||
return in_array($this->status, ['draft', 'review']);
|
||||
}
|
||||
|
||||
public function canBeApproved(): bool
|
||||
{
|
||||
return in_array($this->status, ['draft', 'review']);
|
||||
}
|
||||
|
||||
public function canBePublished(): bool
|
||||
{
|
||||
return in_array($this->status, ['draft', 'review', 'approved']);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Spatie\Permission\Traits\HasRoles;
|
||||
@@ -46,4 +47,128 @@ class User extends Authenticatable
|
||||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationships
|
||||
*/
|
||||
public function userLanguages(): HasMany
|
||||
{
|
||||
return $this->hasMany(UserLanguage::class);
|
||||
}
|
||||
|
||||
public function languages()
|
||||
{
|
||||
return $this->belongsToMany(Language::class, 'user_languages', 'user_id', 'language_code', 'id', 'code')
|
||||
->withPivot(['can_create', 'can_edit', 'can_approve', 'can_publish'])
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
/**
|
||||
* Language Management Methods
|
||||
*/
|
||||
public function canManageLanguage(string $languageCode): bool
|
||||
{
|
||||
// Super admin can manage all languages
|
||||
if ($this->hasRole('super_admin')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->userLanguages()
|
||||
->where('language_code', $languageCode)
|
||||
->where(function ($query) {
|
||||
$query->where('can_create', true)
|
||||
->orWhere('can_edit', true)
|
||||
->orWhere('can_approve', true)
|
||||
->orWhere('can_publish', true);
|
||||
})
|
||||
->exists();
|
||||
}
|
||||
|
||||
public function canCreateInLanguage(string $languageCode): bool
|
||||
{
|
||||
if ($this->hasRole('super_admin')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->userLanguages()
|
||||
->where('language_code', $languageCode)
|
||||
->where('can_create', true)
|
||||
->exists();
|
||||
}
|
||||
|
||||
public function canEditInLanguage(string $languageCode): bool
|
||||
{
|
||||
if ($this->hasRole('super_admin')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->userLanguages()
|
||||
->where('language_code', $languageCode)
|
||||
->where('can_edit', true)
|
||||
->exists();
|
||||
}
|
||||
|
||||
public function canApproveInLanguage(string $languageCode): bool
|
||||
{
|
||||
if ($this->hasRole('super_admin')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->userLanguages()
|
||||
->where('language_code', $languageCode)
|
||||
->where('can_approve', true)
|
||||
->exists();
|
||||
}
|
||||
|
||||
public function canPublishInLanguage(string $languageCode): bool
|
||||
{
|
||||
if ($this->hasRole('super_admin')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->userLanguages()
|
||||
->where('language_code', $languageCode)
|
||||
->where('can_publish', true)
|
||||
->exists();
|
||||
}
|
||||
|
||||
public function getManagedLanguages(): \Illuminate\Support\Collection
|
||||
{
|
||||
return $this->userLanguages()
|
||||
->with('language')
|
||||
->get()
|
||||
->pluck('language');
|
||||
}
|
||||
|
||||
public function getManagedLanguageCodes(): array
|
||||
{
|
||||
return $this->userLanguages()
|
||||
->pluck('language_code')
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function assignLanguage(
|
||||
string $languageCode,
|
||||
bool $canCreate = true,
|
||||
bool $canEdit = true,
|
||||
bool $canApprove = false,
|
||||
bool $canPublish = false
|
||||
): UserLanguage {
|
||||
return $this->userLanguages()->updateOrCreate(
|
||||
['language_code' => $languageCode],
|
||||
[
|
||||
'can_create' => $canCreate,
|
||||
'can_edit' => $canEdit,
|
||||
'can_approve' => $canApprove,
|
||||
'can_publish' => $canPublish,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function removeLanguage(string $languageCode): bool
|
||||
{
|
||||
return $this->userLanguages()
|
||||
->where('language_code', $languageCode)
|
||||
->delete() > 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class UserLanguage extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'language_code',
|
||||
'can_create',
|
||||
'can_edit',
|
||||
'can_approve',
|
||||
'can_publish',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'can_create' => 'boolean',
|
||||
'can_edit' => 'boolean',
|
||||
'can_approve' => 'boolean',
|
||||
'can_publish' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
* Relationships
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function language(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Language::class, 'language_code', 'code');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scopes
|
||||
*/
|
||||
public function scopeForUser($query, int $userId)
|
||||
{
|
||||
return $query->where('user_id', $userId);
|
||||
}
|
||||
|
||||
public function scopeForLanguage($query, string $languageCode)
|
||||
{
|
||||
return $query->where('language_code', $languageCode);
|
||||
}
|
||||
|
||||
public function scopeCanCreate($query)
|
||||
{
|
||||
return $query->where('can_create', true);
|
||||
}
|
||||
|
||||
public function scopeCanEdit($query)
|
||||
{
|
||||
return $query->where('can_edit', true);
|
||||
}
|
||||
|
||||
public function scopeCanApprove($query)
|
||||
{
|
||||
return $query->where('can_approve', true);
|
||||
}
|
||||
|
||||
public function scopeCanPublish($query)
|
||||
{
|
||||
return $query->where('can_publish', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper Methods
|
||||
*/
|
||||
public function hasFullAccess(): bool
|
||||
{
|
||||
return $this->can_create && $this->can_edit && $this->can_approve && $this->can_publish;
|
||||
}
|
||||
|
||||
public function hasNoAccess(): bool
|
||||
{
|
||||
return !$this->can_create && !$this->can_edit && !$this->can_approve && !$this->can_publish;
|
||||
}
|
||||
|
||||
public function grantFullAccess(): bool
|
||||
{
|
||||
$this->can_create = true;
|
||||
$this->can_edit = true;
|
||||
$this->can_approve = true;
|
||||
$this->can_publish = true;
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
public function revokeAllAccess(): bool
|
||||
{
|
||||
$this->can_create = false;
|
||||
$this->can_edit = false;
|
||||
$this->can_approve = false;
|
||||
$this->can_publish = false;
|
||||
return $this->save();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\Language;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Access\Response;
|
||||
|
||||
class LanguagePolicy
|
||||
{
|
||||
/**
|
||||
* Determine whether the user can view any models.
|
||||
*/
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return $user->hasRole('super_admin') || $user->can('language::viewAny');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can view the model.
|
||||
*/
|
||||
public function view(User $user, Language $language): bool
|
||||
{
|
||||
return $user->hasRole('super_admin') || $user->can('language::view');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can create models.
|
||||
*/
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return $user->hasRole('super_admin') || $user->can('language::create');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can update the model.
|
||||
*/
|
||||
public function update(User $user, Language $language): bool
|
||||
{
|
||||
// Cannot disable default language
|
||||
if ($language->is_default && !$user->hasRole('super_admin')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $user->hasRole('super_admin') || $user->can('language::update');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can delete the model.
|
||||
*/
|
||||
public function delete(User $user, Language $language): bool
|
||||
{
|
||||
// Cannot delete default language
|
||||
if ($language->is_default) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $user->hasRole('super_admin') || $user->can('language::delete');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can restore the model.
|
||||
*/
|
||||
public function restore(User $user, Language $language): bool
|
||||
{
|
||||
return $user->hasRole('super_admin') || $user->can('language::restore');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can permanently delete the model.
|
||||
*/
|
||||
public function forceDelete(User $user, Language $language): bool
|
||||
{
|
||||
// Cannot force delete default language
|
||||
if ($language->is_default) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $user->hasRole('super_admin') || $user->can('language::forceDelete');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can activate the language.
|
||||
*/
|
||||
public function activate(User $user): bool
|
||||
{
|
||||
return $user->hasRole('super_admin') || $user->can('language::activate');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can set the language as default.
|
||||
*/
|
||||
public function setDefault(User $user): bool
|
||||
{
|
||||
return $user->hasRole('super_admin') || $user->can('language::setDefault');
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Models\Language;
|
||||
use App\Models\User;
|
||||
use App\Policies\LanguagePolicy;
|
||||
use App\Policies\UserPolicy;
|
||||
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
|
||||
use Spatie\Permission\Models\Role;
|
||||
@@ -18,6 +20,7 @@ class AuthServiceProvider extends ServiceProvider
|
||||
protected $policies = [
|
||||
User::class => UserPolicy::class,
|
||||
Role::class => RolePolicy::class,
|
||||
Language::class => LanguagePolicy::class,
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
use App\Models\Language;
|
||||
use App\Models\Translation;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
trait HasTranslations
|
||||
{
|
||||
/**
|
||||
* Get all translations for this model
|
||||
*/
|
||||
public function translations(): MorphMany
|
||||
{
|
||||
return $this->morphMany(Translation::class, 'translatable');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get translation for a specific field and language
|
||||
*/
|
||||
public function getTranslation(string $fieldName, ?string $languageCode = null, bool $published = true): ?Translation
|
||||
{
|
||||
$languageCode = $languageCode ?? $this->getCurrentLanguageCode();
|
||||
|
||||
$query = $this->translations()
|
||||
->where('field_name', $fieldName)
|
||||
->where('language_code', $languageCode);
|
||||
|
||||
if ($published) {
|
||||
$query->where('status', 'published');
|
||||
}
|
||||
|
||||
return $query->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get translation value for a specific field and language
|
||||
*/
|
||||
public function translate(string $fieldName, ?string $languageCode = null, bool $fallback = true): mixed
|
||||
{
|
||||
$languageCode = $languageCode ?? $this->getCurrentLanguageCode();
|
||||
|
||||
$translation = $this->getTranslation($fieldName, $languageCode);
|
||||
|
||||
if ($translation) {
|
||||
return $translation->field_value;
|
||||
}
|
||||
|
||||
// Fallback to default language
|
||||
if ($fallback && $languageCode !== $this->getDefaultLanguageCode()) {
|
||||
$defaultTranslation = $this->getTranslation($fieldName, $this->getDefaultLanguageCode());
|
||||
if ($defaultTranslation) {
|
||||
return $defaultTranslation->field_value;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to original field value
|
||||
return $this->{$fieldName} ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set translation for a specific field and language
|
||||
*/
|
||||
public function setTranslation(
|
||||
string $fieldName,
|
||||
string $languageCode,
|
||||
mixed $value,
|
||||
string $status = 'draft',
|
||||
?int $userId = null
|
||||
): Translation {
|
||||
$userId = $userId ?? auth()->id();
|
||||
|
||||
return $this->translations()->updateOrCreate(
|
||||
[
|
||||
'field_name' => $fieldName,
|
||||
'language_code' => $languageCode,
|
||||
],
|
||||
[
|
||||
'field_value' => $value,
|
||||
'status' => $status,
|
||||
'created_by' => $userId,
|
||||
'updated_by' => $userId,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all translations for a specific field
|
||||
*/
|
||||
public function getTranslations(string $fieldName, bool $published = true): Collection
|
||||
{
|
||||
$query = $this->translations()
|
||||
->where('field_name', $fieldName);
|
||||
|
||||
if ($published) {
|
||||
$query->where('status', 'published');
|
||||
}
|
||||
|
||||
return $query->get()->keyBy('language_code');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all translations for a specific language
|
||||
*/
|
||||
public function getTranslationsForLanguage(string $languageCode, bool $published = true): Collection
|
||||
{
|
||||
$query = $this->translations()
|
||||
->where('language_code', $languageCode);
|
||||
|
||||
if ($published) {
|
||||
$query->where('status', 'published');
|
||||
}
|
||||
|
||||
return $query->get()->keyBy('field_name');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if translation exists for a field and language
|
||||
*/
|
||||
public function hasTranslation(string $fieldName, string $languageCode, bool $published = true): bool
|
||||
{
|
||||
$query = $this->translations()
|
||||
->where('field_name', $fieldName)
|
||||
->where('language_code', $languageCode);
|
||||
|
||||
if ($published) {
|
||||
$query->where('status', 'published');
|
||||
}
|
||||
|
||||
return $query->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get translation progress for all languages
|
||||
*/
|
||||
public function getTranslationProgress(): array
|
||||
{
|
||||
$languages = Language::active()->get();
|
||||
$translatableFields = $this->getTranslatableFields();
|
||||
$totalFields = count($translatableFields);
|
||||
|
||||
$progress = [];
|
||||
|
||||
foreach ($languages as $language) {
|
||||
$translatedCount = $this->translations()
|
||||
->where('language_code', $language->code)
|
||||
->where('status', 'published')
|
||||
->whereIn('field_name', $translatableFields)
|
||||
->count();
|
||||
|
||||
$progress[$language->code] = $totalFields > 0
|
||||
? round(($translatedCount / $totalFields) * 100)
|
||||
: 0;
|
||||
}
|
||||
|
||||
return $progress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get missing translations for a specific language
|
||||
*/
|
||||
public function getMissingTranslations(string $languageCode): array
|
||||
{
|
||||
$translatableFields = $this->getTranslatableFields();
|
||||
|
||||
$existingTranslations = $this->translations()
|
||||
->where('language_code', $languageCode)
|
||||
->where('status', 'published')
|
||||
->pluck('field_name')
|
||||
->toArray();
|
||||
|
||||
return array_diff($translatableFields, $existingTranslations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicate translation from one language to another
|
||||
*/
|
||||
public function duplicateTranslation(string $fromLanguage, string $toLanguage, ?int $userId = null): int
|
||||
{
|
||||
$userId = $userId ?? auth()->id();
|
||||
$count = 0;
|
||||
|
||||
$sourceTranslations = $this->getTranslationsForLanguage($fromLanguage, false);
|
||||
|
||||
foreach ($sourceTranslations as $fieldName => $translation) {
|
||||
$this->setTranslation(
|
||||
$fieldName,
|
||||
$toLanguage,
|
||||
$translation->field_value,
|
||||
'draft',
|
||||
$userId
|
||||
);
|
||||
$count++;
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get translation status for a language
|
||||
*/
|
||||
public function getTranslationStatus(string $languageCode): string
|
||||
{
|
||||
$progress = $this->getTranslationProgress();
|
||||
$percentage = $progress[$languageCode] ?? 0;
|
||||
|
||||
if ($percentage === 100) {
|
||||
return 'complete';
|
||||
} elseif ($percentage > 0) {
|
||||
return 'partial';
|
||||
} else {
|
||||
return 'missing';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync translations for all translatable fields
|
||||
*/
|
||||
public function syncTranslations(array $languageCodes, ?int $userId = null): void
|
||||
{
|
||||
$userId = $userId ?? auth()->id();
|
||||
$translatableFields = $this->getTranslatableFields();
|
||||
|
||||
foreach ($languageCodes as $languageCode) {
|
||||
foreach ($translatableFields as $field) {
|
||||
if (!$this->hasTranslation($field, $languageCode, false)) {
|
||||
$this->setTranslation(
|
||||
$field,
|
||||
$languageCode,
|
||||
$this->{$field} ?? '',
|
||||
'draft',
|
||||
$userId
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get translatable fields for this model
|
||||
*/
|
||||
public function getTranslatableFields(): array
|
||||
{
|
||||
return $this->translatable ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current language code
|
||||
*/
|
||||
protected function getCurrentLanguageCode(): string
|
||||
{
|
||||
return app()->getLocale();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default language code
|
||||
*/
|
||||
protected function getDefaultLanguageCode(): string
|
||||
{
|
||||
$defaultLanguage = Language::where('is_default', true)->first();
|
||||
return $defaultLanguage ? $defaultLanguage->code : config('app.locale');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope: Filter by language
|
||||
*/
|
||||
public function scopeWhereHasTranslation($query, string $languageCode, bool $published = true)
|
||||
{
|
||||
return $query->whereHas('translations', function ($q) use ($languageCode, $published) {
|
||||
$q->where('language_code', $languageCode);
|
||||
if ($published) {
|
||||
$q->where('status', 'published');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope: Filter by user's languages
|
||||
*/
|
||||
public function scopeWhereHasTranslationForUser($query, $user)
|
||||
{
|
||||
$languageCodes = $user->userLanguages()->pluck('language_code')->toArray();
|
||||
|
||||
return $query->whereHas('translations', function ($q) use ($languageCodes) {
|
||||
$q->whereIn('language_code', $languageCodes);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get translated model for current language
|
||||
*/
|
||||
public function translated(?string $languageCode = null): self
|
||||
{
|
||||
$languageCode = $languageCode ?? $this->getCurrentLanguageCode();
|
||||
|
||||
$translations = $this->getTranslationsForLanguage($languageCode);
|
||||
|
||||
foreach ($translations as $fieldName => $translation) {
|
||||
if (in_array($fieldName, $this->getTranslatableFields())) {
|
||||
$this->{$fieldName} = $translation->field_value;
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
+4
-1
@@ -28,7 +28,10 @@
|
||||
"App\\": "app/",
|
||||
"Database\\Factories\\": "database/factories/",
|
||||
"Database\\Seeders\\": "database/seeders/"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"app/Helpers/translation_helpers.php"
|
||||
]
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('languages', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('code', 10)->unique()->comment('Language code (tr, en, de)');
|
||||
$table->string('name', 100)->comment('Language name in English');
|
||||
$table->string('native_name', 100)->comment('Language name in native language');
|
||||
$table->string('flag', 10)->nullable()->comment('Flag emoji or icon code');
|
||||
$table->enum('direction', ['ltr', 'rtl'])->default('ltr')->comment('Text direction');
|
||||
$table->boolean('is_active')->default(true)->comment('Is language active?');
|
||||
$table->boolean('is_default')->default(false)->comment('Is default language?');
|
||||
$table->integer('sort_order')->default(0)->comment('Display order');
|
||||
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index(['is_active', 'sort_order']);
|
||||
$table->index('code');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('languages');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('translations', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('translatable_type')->comment('Model class name');
|
||||
$table->unsignedBigInteger('translatable_id')->comment('Model ID');
|
||||
$table->string('language_code', 10)->comment('Language code');
|
||||
$table->string('field_name', 100)->comment('Field name to translate');
|
||||
$table->longText('field_value')->nullable()->comment('Translated value');
|
||||
$table->enum('status', ['draft', 'review', 'approved', 'published'])->default('draft')->comment('Translation status');
|
||||
$table->integer('version')->default(1)->comment('Translation version');
|
||||
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->foreignId('approved_by')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->timestamp('approved_at')->nullable();
|
||||
$table->timestamp('published_at')->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
// Indexes for performance
|
||||
$table->index(['translatable_type', 'translatable_id']);
|
||||
$table->index(['language_code', 'status']);
|
||||
$table->index(['created_by', 'status']);
|
||||
$table->index('approved_by');
|
||||
|
||||
// Unique constraint: one translation per model+field+language
|
||||
$table->unique(['translatable_type', 'translatable_id', 'language_code', 'field_name'], 'unique_translation');
|
||||
|
||||
// Foreign key for language
|
||||
$table->foreign('language_code')->references('code')->on('languages')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('translations');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('user_languages', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained('users')->onDelete('cascade');
|
||||
$table->string('language_code', 10);
|
||||
$table->boolean('can_create')->default(true)->comment('Can create content in this language');
|
||||
$table->boolean('can_edit')->default(true)->comment('Can edit content in this language');
|
||||
$table->boolean('can_approve')->default(false)->comment('Can approve translations in this language');
|
||||
$table->boolean('can_publish')->default(false)->comment('Can publish content in this language');
|
||||
$table->timestamps();
|
||||
|
||||
// Unique constraint: one entry per user+language
|
||||
$table->unique(['user_id', 'language_code']);
|
||||
|
||||
// Foreign key for language
|
||||
$table->foreign('language_code')->references('code')->on('languages')->onDelete('cascade');
|
||||
|
||||
// Indexes
|
||||
$table->index(['user_id', 'language_code']);
|
||||
$table->index('language_code');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('user_languages');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Language;
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class LanguageSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$languages = [
|
||||
[
|
||||
'code' => 'tr',
|
||||
'name' => 'Turkish',
|
||||
'native_name' => 'Türkçe',
|
||||
'flag' => '🇹🇷',
|
||||
'direction' => 'ltr',
|
||||
'is_active' => true,
|
||||
'is_default' => true,
|
||||
'sort_order' => 1,
|
||||
],
|
||||
[
|
||||
'code' => 'en',
|
||||
'name' => 'English',
|
||||
'native_name' => 'English',
|
||||
'flag' => '🇬🇧',
|
||||
'direction' => 'ltr',
|
||||
'is_active' => true,
|
||||
'is_default' => false,
|
||||
'sort_order' => 2,
|
||||
],
|
||||
[
|
||||
'code' => 'de',
|
||||
'name' => 'German',
|
||||
'native_name' => 'Deutsch',
|
||||
'flag' => '🇩🇪',
|
||||
'direction' => 'ltr',
|
||||
'is_active' => false,
|
||||
'is_default' => false,
|
||||
'sort_order' => 3,
|
||||
],
|
||||
[
|
||||
'code' => 'fr',
|
||||
'name' => 'French',
|
||||
'native_name' => 'Français',
|
||||
'flag' => '🇫🇷',
|
||||
'direction' => 'ltr',
|
||||
'is_active' => false,
|
||||
'is_default' => false,
|
||||
'sort_order' => 4,
|
||||
],
|
||||
[
|
||||
'code' => 'ar',
|
||||
'name' => 'Arabic',
|
||||
'native_name' => 'العربية',
|
||||
'flag' => '🇸🇦',
|
||||
'direction' => 'rtl',
|
||||
'is_active' => false,
|
||||
'is_default' => false,
|
||||
'sort_order' => 5,
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($languages as $language) {
|
||||
Language::updateOrCreate(
|
||||
['code' => $language['code']],
|
||||
$language
|
||||
);
|
||||
}
|
||||
|
||||
$this->command->info('Languages seeded successfully!');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
# Translation System - Quick Start Guide
|
||||
|
||||
## 🚀 5 Dakikada Başlangıç
|
||||
|
||||
### 1️⃣ Admin Panel'e Git
|
||||
|
||||
```
|
||||
http://your-domain.com/admin/languages
|
||||
```
|
||||
|
||||
### 2️⃣ Dilleri Aktifleştir
|
||||
|
||||
- 🇹🇷 Türkçe (TR) - Zaten aktif (varsayılan)
|
||||
- 🇬🇧 İngilizce (EN) - Zaten aktif
|
||||
- 🇩🇪 Almanca (DE) - İsterseniz aktif edin
|
||||
- 🇫🇷 Fransızca (FR) - İsterseniz aktif edin
|
||||
|
||||
### 3️⃣ İlk Çeviriyi Ekle
|
||||
|
||||
#### Tinker ile (Terminal):
|
||||
```bash
|
||||
php artisan tinker
|
||||
|
||||
# Blog çevirisi ekle
|
||||
$blog = App\Models\Blog::first();
|
||||
$blog->setTranslation('title', 'en', 'My First Blog Post', 'published');
|
||||
$blog->setTranslation('content', 'en', 'This is my blog content in English...', 'published');
|
||||
|
||||
# Kontrol et
|
||||
echo $blog->translate('title', 'en');
|
||||
```
|
||||
|
||||
#### Kod ile:
|
||||
```php
|
||||
use App\Models\Blog;
|
||||
|
||||
$blog = Blog::find(1);
|
||||
|
||||
// İngilizce çeviri ekle
|
||||
$blog->setTranslation('title', 'en', 'My Blog Title', 'published');
|
||||
$blog->setTranslation('content', 'en', 'Blog content here...', 'published');
|
||||
$blog->setTranslation('excerpt', 'en', 'Short description', 'published');
|
||||
|
||||
// Oku
|
||||
echo $blog->translate('title'); // Mevcut dilde
|
||||
echo $blog->translate('title', 'en'); // İngilizce
|
||||
echo $blog->translate('title', 'tr'); // Türkçe
|
||||
```
|
||||
|
||||
### 4️⃣ Translation Progress Kontrol Et
|
||||
|
||||
```php
|
||||
$blog = Blog::find(1);
|
||||
|
||||
// İlerleme yüzdeleri
|
||||
$progress = $blog->getTranslationProgress();
|
||||
print_r($progress);
|
||||
// ['tr' => 100, 'en' => 60, 'de' => 0]
|
||||
|
||||
// Eksik çeviriler
|
||||
$missing = $blog->getMissingTranslations('en');
|
||||
print_r($missing);
|
||||
// ['meta_title', 'meta_description']
|
||||
|
||||
// Durum
|
||||
echo $blog->getTranslationStatus('en'); // 'partial'
|
||||
```
|
||||
|
||||
### 5️⃣ Kullanıcıya Dil Yetkisi Ver
|
||||
|
||||
```php
|
||||
use App\Models\User;
|
||||
|
||||
$user = User::find(1);
|
||||
|
||||
// Türkçe içerik yöneticisi
|
||||
$user->assignLanguage('tr',
|
||||
canCreate: true,
|
||||
canEdit: true,
|
||||
canApprove: true,
|
||||
canPublish: true
|
||||
);
|
||||
|
||||
// İngilizce çevirmen (yayınlayamaz)
|
||||
$user->assignLanguage('en',
|
||||
canCreate: true,
|
||||
canEdit: true,
|
||||
canApprove: false,
|
||||
canPublish: false
|
||||
);
|
||||
|
||||
// Kontrol
|
||||
if ($user->canManageLanguage('tr')) {
|
||||
echo "User can manage Turkish!";
|
||||
}
|
||||
```
|
||||
|
||||
## 💡 Yaygın Kullanım Örnekleri
|
||||
|
||||
### Örnek 1: Blog Oluştur ve Çevir
|
||||
|
||||
```php
|
||||
// 1. Türkçe blog oluştur
|
||||
$blog = Blog::create([
|
||||
'title' => 'Laravel Eğitimi',
|
||||
'slug' => 'laravel-egitimi',
|
||||
'content' => 'Laravel hakkında detaylı bilgi...',
|
||||
'excerpt' => 'Laravel nedir?',
|
||||
'status' => 'published',
|
||||
'author_id' => 1,
|
||||
]);
|
||||
|
||||
// 2. İngilizce çevirisi ekle
|
||||
$blog->setTranslation('title', 'en', 'Laravel Tutorial', 'published');
|
||||
$blog->setTranslation('content', 'en', 'Detailed info about Laravel...', 'published');
|
||||
$blog->setTranslation('excerpt', 'en', 'What is Laravel?', 'published');
|
||||
|
||||
// 3. Almanca çevirisi ekle (draft olarak)
|
||||
$blog->setTranslation('title', 'de', 'Laravel Tutorial', 'draft');
|
||||
$blog->setTranslation('content', 'de', 'Detaillierte Informationen über Laravel...', 'draft');
|
||||
|
||||
// 4. Çevirileri kullan
|
||||
echo $blog->translate('title'); // Mevcut dilde
|
||||
echo $blog->translate('title', 'en'); // İngilizce
|
||||
echo $blog->translate('title', 'de'); // Almanca (draft, yayında değil)
|
||||
```
|
||||
|
||||
### Örnek 2: Translation Workflow
|
||||
|
||||
```php
|
||||
$blog = Blog::find(1);
|
||||
|
||||
// Çeviri oluştur (draft olarak başlar)
|
||||
$translation = $blog->setTranslation('title', 'en', 'My Title', 'draft');
|
||||
|
||||
// Workflow adımları
|
||||
$translation->submitForReview(); // Draft → Review
|
||||
$translation->approve(auth()->id()); // Review → Approved
|
||||
$translation->publish(auth()->id()); // Approved → Published
|
||||
|
||||
// Veya direkt yayınla
|
||||
$translation->publish(); // Draft/Review/Approved → Published
|
||||
|
||||
// Geri al
|
||||
$translation->unpublish(); // Published → Approved
|
||||
$translation->reject(); // Review → Draft
|
||||
```
|
||||
|
||||
### Örnek 3: Yeni Model Ekle
|
||||
|
||||
```php
|
||||
// 1. Model oluştur
|
||||
class Product extends Model
|
||||
{
|
||||
use HasTranslations;
|
||||
|
||||
protected $fillable = [
|
||||
'name', 'slug', 'description', 'price', 'sku'
|
||||
];
|
||||
|
||||
protected $translatable = [
|
||||
'name',
|
||||
'description',
|
||||
];
|
||||
}
|
||||
|
||||
// 2. Migration
|
||||
Schema::create('products', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('slug')->unique();
|
||||
$table->text('description');
|
||||
$table->decimal('price', 10, 2);
|
||||
$table->string('sku')->unique();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
// 3. Kullan
|
||||
$product = Product::create([
|
||||
'name' => 'Laptop',
|
||||
'slug' => 'laptop',
|
||||
'description' => 'Yüksek performanslı laptop',
|
||||
'price' => 15000,
|
||||
'sku' => 'LAP-001'
|
||||
]);
|
||||
|
||||
$product->setTranslation('name', 'en', 'Laptop', 'published');
|
||||
$product->setTranslation('description', 'en', 'High performance laptop', 'published');
|
||||
|
||||
// Artık Product'da çeviri var! 🎉
|
||||
echo $product->translate('name', 'en'); // "Laptop"
|
||||
```
|
||||
|
||||
### Örnek 4: Tüm Çevirileri Getir
|
||||
|
||||
```php
|
||||
$blog = Blog::find(1);
|
||||
|
||||
// Bir alanın tüm dillerdeki çevirileri
|
||||
$titleTranslations = $blog->getTranslations('title');
|
||||
foreach ($titleTranslations as $langCode => $translation) {
|
||||
echo "{$langCode}: {$translation->field_value}\n";
|
||||
}
|
||||
// tr: Laravel Eğitimi
|
||||
// en: Laravel Tutorial
|
||||
// de: Laravel Tutorial
|
||||
|
||||
// Bir dilin tüm alan çevirileri
|
||||
$enTranslations = $blog->getTranslationsForLanguage('en');
|
||||
foreach ($enTranslations as $fieldName => $translation) {
|
||||
echo "{$fieldName}: {$translation->field_value}\n";
|
||||
}
|
||||
// title: Laravel Tutorial
|
||||
// content: Detailed info about Laravel...
|
||||
// excerpt: What is Laravel?
|
||||
```
|
||||
|
||||
### Örnek 5: Helper Functions
|
||||
|
||||
```php
|
||||
// Mevcut dil
|
||||
$currentLang = current_language(); // Language model
|
||||
$currentCode = current_language_code(); // 'tr'
|
||||
|
||||
// Varsayılan dil
|
||||
$defaultLang = default_language();
|
||||
$defaultCode = default_language_code(); // 'tr'
|
||||
|
||||
// Aktif diller
|
||||
$languages = available_languages(); // Collection [TR, EN]
|
||||
$codes = available_language_codes(); // ['tr', 'en']
|
||||
|
||||
// Dil değiştir
|
||||
switch_language('en');
|
||||
app()->setLocale('en');
|
||||
|
||||
// Model çevirisi
|
||||
$title = translate_model($blog, 'title', 'en');
|
||||
|
||||
// Çeviri var mı?
|
||||
if (has_translation($blog, 'title', 'en')) {
|
||||
echo "English translation exists!";
|
||||
}
|
||||
|
||||
// İlerleme
|
||||
$progress = translation_progress($blog, 'en'); // 75
|
||||
|
||||
// Kullanıcı dilleri
|
||||
$userLanguages = user_managed_languages(); // Collection
|
||||
$userCodes = user_managed_language_codes(); // ['tr', 'en']
|
||||
|
||||
// Dil bilgileri
|
||||
echo get_language_flag('tr'); // 🇹🇷
|
||||
echo format_language_name('tr'); // Turkish (Türkçe)
|
||||
```
|
||||
|
||||
## 🎯 Sık Sorulan Sorular
|
||||
|
||||
### 1. Varsayılan dilde çeviri lazım mı?
|
||||
Hayır! Varsayılan dil (TR) için veritabanındaki orijinal değer kullanılır. Sadece diğer diller için çeviri eklemeniz yeterli.
|
||||
|
||||
### 2. Çeviri yoksa ne olur?
|
||||
Fallback mekanizması devreye girer:
|
||||
1. İstenen dilde ara
|
||||
2. Yoksa varsayılan dilde ara
|
||||
3. Yoksa model'deki orijinal değeri kullan
|
||||
|
||||
### 3. Draft çeviriler görünür mü?
|
||||
Hayır, sadece `published` çeviriler görünür. Ama parametre ile draft'ları da alabilirsiniz:
|
||||
```php
|
||||
$translation = $blog->getTranslation('title', 'en', published: false);
|
||||
```
|
||||
|
||||
### 4. Toplu çeviri nasıl yapılır?
|
||||
```php
|
||||
// TR'den EN'e tüm alanları kopyala
|
||||
$blog->duplicateTranslation('tr', 'en');
|
||||
|
||||
// Tüm diller için placeholder oluştur
|
||||
$blog->syncTranslations(['tr', 'en', 'de']);
|
||||
```
|
||||
|
||||
### 5. Performans nasıl?
|
||||
- ✅ Indexed queries
|
||||
- ✅ Eager loading desteği
|
||||
- ✅ Tek query ile tüm çevirileri çek
|
||||
- ✅ Cache-ready yapı
|
||||
|
||||
## 📊 Dashboard Özet Örneği
|
||||
|
||||
```php
|
||||
// Admin dashboard widget örneği
|
||||
$stats = [
|
||||
'total_languages' => Language::active()->count(),
|
||||
'total_translations' => Translation::published()->count(),
|
||||
'pending_approvals' => Translation::review()->count(),
|
||||
'translation_progress' => []
|
||||
];
|
||||
|
||||
foreach (Language::active()->get() as $language) {
|
||||
$stats['translation_progress'][$language->code] = [
|
||||
'name' => $language->name,
|
||||
'flag' => $language->flag,
|
||||
'count' => Translation::forLanguage($language->code)->published()->count(),
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
## 🎨 Blade Template Kullanımı
|
||||
|
||||
```blade
|
||||
{{-- Mevcut dilde --}}
|
||||
<h1>{{ $blog->translate('title') }}</h1>
|
||||
|
||||
{{-- Belirli dilde --}}
|
||||
<h1>{{ $blog->translate('title', 'en') }}</h1>
|
||||
|
||||
{{-- Helper ile --}}
|
||||
<h1>{{ translate_model($blog, 'title') }}</h1>
|
||||
|
||||
{{-- Dil seçici --}}
|
||||
@foreach(available_languages() as $language)
|
||||
<a href="?lang={{ $language->code }}">
|
||||
{{ $language->flag }} {{ $language->name }}
|
||||
</a>
|
||||
@endforeach
|
||||
|
||||
{{-- Progress bar --}}
|
||||
@php
|
||||
$progress = $blog->getTranslationProgress();
|
||||
@endphp
|
||||
|
||||
@foreach($progress as $code => $percentage)
|
||||
<div class="progress-bar">
|
||||
<span>{{ get_language_flag($code) }}</span>
|
||||
<div style="width: {{ $percentage }}%">{{ $percentage }}%</div>
|
||||
</div>
|
||||
@endforeach
|
||||
```
|
||||
|
||||
## 🔥 Pro Tips
|
||||
|
||||
1. **Her zaman published status kullanın** (son kullanıcı için)
|
||||
2. **Draft status kullanın** (henüz tamamlanmamış çeviriler için)
|
||||
3. **Translation progress ile takip edin** (hangi diller eksik?)
|
||||
4. **User-language permissions ile kontrol edin** (kim ne yapabilir?)
|
||||
5. **Fallback mekanizmasını kullanın** (eksik çeviriler için)
|
||||
|
||||
---
|
||||
|
||||
Hazırsınız! 🚀 Artık universal translation sisteminiz tamamen çalışıyor!
|
||||
|
||||
@@ -0,0 +1,431 @@
|
||||
# Universal Dynamic Translation System
|
||||
|
||||
## 🌟 Genel Bakış
|
||||
|
||||
Citrus Platform için geliştirilmiş **evrensel, dinamik ve role-based** çeviri sistemi. Bu sistem, herhangi bir modelin çeviri desteğine sahip olmasını sağlar ve kullanıcı bazlı dil yönetimi sunar.
|
||||
|
||||
## 📋 Sistem Mimarisi
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ languages │ → Dinamik dil tanımları (TR, EN, DE, FR, AR...)
|
||||
└─────────────────┘
|
||||
↓
|
||||
┌─────────────────┐
|
||||
│ translations │ → Evrensel çeviri tablosu (polymorphic)
|
||||
└─────────────────┘
|
||||
↓
|
||||
┌─────────────────┐
|
||||
│ user_languages │ → Kullanıcı-Dil ilişkisi (yetkiler)
|
||||
└─────────────────┘
|
||||
↓
|
||||
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌──────────┐
|
||||
│ pages │ │ blogs │ │products │ │ news │ → Herhangi bir model
|
||||
└─────────┘ └─────────┘ └─────────┘ └──────────┘
|
||||
```
|
||||
|
||||
## ✅ Kurulum ve Yapılandırma
|
||||
|
||||
### 1. Database Tabloları
|
||||
|
||||
```bash
|
||||
php artisan migrate
|
||||
```
|
||||
|
||||
Oluşturulan tablolar:
|
||||
- `languages` - Dil tanımları
|
||||
- `translations` - Çeviri veriler (polymorphic)
|
||||
- `user_languages` - Kullanıcı-dil yetkileri
|
||||
|
||||
### 2. Varsayılan Diller
|
||||
|
||||
```bash
|
||||
php artisan db:seed --class=LanguageSeeder
|
||||
```
|
||||
|
||||
Eklenen diller:
|
||||
- 🇹🇷 Türkçe (tr) - Varsayılan, Aktif
|
||||
- 🇬🇧 İngilizce (en) - Aktif
|
||||
- 🇩🇪 Almanca (de) - Pasif
|
||||
- 🇫🇷 Fransızca (fr) - Pasif
|
||||
- 🇸🇦 Arapça (ar) - Pasif, RTL
|
||||
|
||||
### 3. Model Entegrasyonu
|
||||
|
||||
Herhangi bir modele çeviri desteği eklemek için:
|
||||
|
||||
```php
|
||||
use App\Traits\HasTranslations;
|
||||
|
||||
class YourModel extends Model
|
||||
{
|
||||
use HasTranslations;
|
||||
|
||||
protected $translatable = [
|
||||
'title',
|
||||
'description',
|
||||
'content',
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
## 🎯 Kullanım
|
||||
|
||||
### Çeviri Kaydetme
|
||||
|
||||
```php
|
||||
// Basit çeviri kaydetme
|
||||
$blog->setTranslation('title', 'en', 'My Blog Post');
|
||||
|
||||
// Status ile kaydetme
|
||||
$blog->setTranslation('title', 'en', 'My Blog Post', 'published');
|
||||
|
||||
// Kullanıcı ID ile
|
||||
$blog->setTranslation('title', 'en', 'My Blog Post', 'draft', auth()->id());
|
||||
```
|
||||
|
||||
### Çeviri Okuma
|
||||
|
||||
```php
|
||||
// Mevcut dilde
|
||||
$title = $blog->translate('title');
|
||||
|
||||
// Belirli bir dilde
|
||||
$title = $blog->translate('title', 'en');
|
||||
|
||||
// Fallback olmadan
|
||||
$title = $blog->translate('title', 'en', false);
|
||||
```
|
||||
|
||||
### Toplu İşlemler
|
||||
|
||||
```php
|
||||
// Tüm çevirileri al (bir alan için)
|
||||
$translations = $blog->getTranslations('title');
|
||||
// ['tr' => 'Blog Yazım', 'en' => 'My Blog Post']
|
||||
|
||||
// Bir dildeki tüm çevirileri al
|
||||
$translations = $blog->getTranslationsForLanguage('en');
|
||||
// ['title' => 'My Blog Post', 'content' => '...']
|
||||
|
||||
// Çeviri var mı kontrol et
|
||||
if ($blog->hasTranslation('title', 'en')) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Translation Progress
|
||||
|
||||
```php
|
||||
// Tüm diller için ilerleme
|
||||
$progress = $blog->getTranslationProgress();
|
||||
// ['tr' => 100, 'en' => 75, 'de' => 0]
|
||||
|
||||
// Eksik çevirileri bul
|
||||
$missing = $blog->getMissingTranslations('en');
|
||||
// ['meta_description', 'excerpt']
|
||||
|
||||
// Çeviri durumu
|
||||
$status = $blog->getTranslationStatus('en');
|
||||
// 'complete', 'partial', 'missing'
|
||||
```
|
||||
|
||||
### Translation Workflow
|
||||
|
||||
```php
|
||||
// Çeviri oluştur (draft)
|
||||
$translation = $blog->setTranslation('title', 'en', 'My Title', 'draft');
|
||||
|
||||
// Onaya gönder
|
||||
$translation->submitForReview();
|
||||
|
||||
// Onayla
|
||||
$translation->approve(auth()->id());
|
||||
|
||||
// Yayınla
|
||||
$translation->publish(auth()->id());
|
||||
|
||||
// Reddet
|
||||
$translation->reject();
|
||||
```
|
||||
|
||||
### Dil Değiştirme
|
||||
|
||||
```php
|
||||
// Runtime'da dil değiştir
|
||||
switch_language('en');
|
||||
|
||||
// Uygulama dili
|
||||
app()->setLocale('tr');
|
||||
```
|
||||
|
||||
## 🔐 Kullanıcı Yetkilendirme
|
||||
|
||||
### Kullanıcıya Dil Atama
|
||||
|
||||
```php
|
||||
// Tam yetki ile dil ata
|
||||
$user->assignLanguage('tr',
|
||||
canCreate: true,
|
||||
canEdit: true,
|
||||
canApprove: true,
|
||||
canPublish: true
|
||||
);
|
||||
|
||||
// Sadece oluşturma ve düzenleme yetkisi
|
||||
$user->assignLanguage('en',
|
||||
canCreate: true,
|
||||
canEdit: true,
|
||||
canApprove: false,
|
||||
canPublish: false
|
||||
);
|
||||
```
|
||||
|
||||
### Yetki Kontrolleri
|
||||
|
||||
```php
|
||||
// Kullanıcı bu dili yönetebilir mi?
|
||||
if ($user->canManageLanguage('en')) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// Specific permissions
|
||||
if ($user->canCreateInLanguage('en')) { }
|
||||
if ($user->canEditInLanguage('en')) { }
|
||||
if ($user->canApproveInLanguage('en')) { }
|
||||
if ($user->canPublishInLanguage('en')) { }
|
||||
|
||||
// Kullanıcının yönettiği diller
|
||||
$languages = $user->getManagedLanguages();
|
||||
$codes = $user->getManagedLanguageCodes(); // ['tr', 'en']
|
||||
```
|
||||
|
||||
### Query Filtering
|
||||
|
||||
```php
|
||||
// Sadece belirli dildeki içerikleri getir
|
||||
Blog::whereHasTranslation('en')->get();
|
||||
|
||||
// Kullanıcının yönettiği dillerdeki içerikleri getir
|
||||
Blog::whereHasTranslationForUser(auth()->user())->get();
|
||||
```
|
||||
|
||||
## 🛠️ Helper Functions
|
||||
|
||||
```php
|
||||
// Mevcut dil
|
||||
$language = current_language(); // Language model
|
||||
$code = current_language_code(); // 'tr'
|
||||
|
||||
// Varsayılan dil
|
||||
$defaultLang = default_language();
|
||||
$defaultCode = default_language_code();
|
||||
|
||||
// Aktif diller
|
||||
$languages = available_languages(); // Collection
|
||||
$codes = available_language_codes(); // ['tr', 'en']
|
||||
|
||||
// Model çevirisi
|
||||
$value = translate_model($blog, 'title', 'en');
|
||||
|
||||
// Çeviri kontrolü
|
||||
$exists = has_translation($blog, 'title', 'en');
|
||||
|
||||
// İlerleme
|
||||
$progress = translation_progress($blog, 'en'); // 75
|
||||
|
||||
// Kullanıcı yetkileri
|
||||
$can = user_can_manage_language('en');
|
||||
$languages = user_managed_languages();
|
||||
$codes = user_managed_language_codes();
|
||||
|
||||
// Dil bilgileri
|
||||
$flag = get_language_flag('tr'); // 🇹🇷
|
||||
$name = format_language_name('tr'); // Turkish (Türkçe)
|
||||
|
||||
// Status
|
||||
$badge = get_translation_status_badge('published');
|
||||
$color = get_translation_status_color('draft');
|
||||
```
|
||||
|
||||
## 📊 Filament Admin Panel
|
||||
|
||||
### Language Management
|
||||
|
||||
Admin panelde **Languages** resource'u üzerinden:
|
||||
- ✅ Dil ekleme/düzenleme/silme
|
||||
- ✅ Dil aktif/pasif yapma
|
||||
- ✅ Varsayılan dil seçme
|
||||
- ✅ Sıralama düzenleme
|
||||
- ✅ RTL/LTR desteği
|
||||
|
||||
### Permissions
|
||||
|
||||
Spatie Permission ile entegre:
|
||||
- `language::viewAny`
|
||||
- `language::view`
|
||||
- `language::create`
|
||||
- `language::update`
|
||||
- `language::delete`
|
||||
- `language::restore`
|
||||
- `language::forceDelete`
|
||||
- `language::activate`
|
||||
- `language::setDefault`
|
||||
|
||||
### Policy Kuralları
|
||||
|
||||
- Varsayılan dil silinemez
|
||||
- Varsayılan dil pasif edilemez
|
||||
- Super admin tüm yetkilere sahip
|
||||
- Diğer kullanıcılar permission'lara göre erişir
|
||||
|
||||
## 🎨 Translation Workflow Statuses
|
||||
|
||||
| Status | Açıklama | Badge Color |
|
||||
|--------|----------|-------------|
|
||||
| `draft` | Taslak, henüz onaylanmamış | Gray |
|
||||
| `review` | Onay bekliyor | Warning |
|
||||
| `approved` | Onaylanmış, yayınlanmayı bekliyor | Success |
|
||||
| `published` | Yayınlanmış, görünür | Primary |
|
||||
|
||||
## 📦 Database Schema
|
||||
|
||||
### languages
|
||||
|
||||
```sql
|
||||
id, code (unique), name, native_name, flag, direction,
|
||||
is_active, is_default, sort_order,
|
||||
created_by, updated_by, created_at, updated_at, deleted_at
|
||||
```
|
||||
|
||||
### translations
|
||||
|
||||
```sql
|
||||
id, translatable_type, translatable_id, language_code,
|
||||
field_name, field_value, status, version,
|
||||
created_by, updated_by, approved_by, approved_at, published_at,
|
||||
created_at, updated_at, deleted_at
|
||||
|
||||
UNIQUE: translatable_type + translatable_id + language_code + field_name
|
||||
```
|
||||
|
||||
### user_languages
|
||||
|
||||
```sql
|
||||
id, user_id, language_code,
|
||||
can_create, can_edit, can_approve, can_publish,
|
||||
created_at, updated_at
|
||||
|
||||
UNIQUE: user_id + language_code
|
||||
```
|
||||
|
||||
## 🚀 Örnekler
|
||||
|
||||
### Yeni Model için Çeviri Ekle
|
||||
|
||||
```php
|
||||
// 1. Model oluştur
|
||||
class Product extends Model
|
||||
{
|
||||
use HasTranslations;
|
||||
|
||||
protected $translatable = ['name', 'description', 'features'];
|
||||
}
|
||||
|
||||
// 2. Çeviri ekle
|
||||
$product = Product::create(['name' => 'Ürün', 'slug' => 'urun']);
|
||||
$product->setTranslation('name', 'en', 'Product', 'published');
|
||||
$product->setTranslation('description', 'en', 'Product description', 'published');
|
||||
|
||||
// 3. Kullan
|
||||
echo $product->translate('name'); // Mevcut dilde
|
||||
echo $product->translate('name', 'en'); // İngilizce'de
|
||||
```
|
||||
|
||||
### Çeviri İlerlemesi Widget
|
||||
|
||||
```php
|
||||
$progress = $blog->getTranslationProgress();
|
||||
foreach ($progress as $langCode => $percentage) {
|
||||
$language = Language::findByCode($langCode);
|
||||
echo "{$language->flag} {$language->name}: {$percentage}%";
|
||||
}
|
||||
```
|
||||
|
||||
### Kullanıcı Dil Yönetimi
|
||||
|
||||
```php
|
||||
// Turkish Editor oluştur
|
||||
$user = User::find(1);
|
||||
$user->assignLanguage('tr', true, true, false, true);
|
||||
|
||||
// English Translator oluştur
|
||||
$translator = User::find(2);
|
||||
$translator->assignLanguage('en', true, true, false, false);
|
||||
|
||||
// Multi-language Manager
|
||||
$manager = User::find(3);
|
||||
$manager->assignLanguage('tr', true, true, true, true);
|
||||
$manager->assignLanguage('en', true, true, true, true);
|
||||
```
|
||||
|
||||
## ⚡ Performans
|
||||
|
||||
- ✅ Indexed queries (translatable_type, translatable_id, language_code)
|
||||
- ✅ Eager loading desteği
|
||||
- ✅ Cache-ready yapı
|
||||
- ✅ Efficient polymorphic relationships
|
||||
- ✅ Unique constraints
|
||||
|
||||
## 🔄 Migration
|
||||
|
||||
Mevcut projeden yeni sisteme geçiş için:
|
||||
|
||||
```php
|
||||
// Eski sistem
|
||||
$blog->title_tr
|
||||
$blog->title_en
|
||||
|
||||
// Yeni sisteme migrate
|
||||
$blog->setTranslation('title', 'tr', $blog->title_tr, 'published');
|
||||
$blog->setTranslation('title', 'en', $blog->title_en, 'published');
|
||||
```
|
||||
|
||||
## 📝 Notlar
|
||||
|
||||
1. **Zero Configuration**: Yeni model için sadece trait ekle
|
||||
2. **Dynamic**: Runtime'da dil ekle/çıkar
|
||||
3. **Role-Based**: Her kullanıcı farklı dilleri yönetebilir
|
||||
4. **Workflow**: Draft → Review → Approved → Published
|
||||
5. **Fallback**: Çeviri yoksa default dile düş
|
||||
6. **Version Control**: Her çevirinin versiyonu
|
||||
7. **Soft Delete**: Tüm tablolar soft delete destekli
|
||||
|
||||
## 🎯 Gelecek Özellikler (TODO)
|
||||
|
||||
- [ ] Translation approval notifications
|
||||
- [ ] Bulk translation import/export
|
||||
- [ ] Translation memory (önceki çevirilerden öner)
|
||||
- [ ] AI-powered translation suggestions
|
||||
- [ ] Translation dashboard widget
|
||||
- [ ] Language-specific user roles
|
||||
- [ ] Translation history/audit log
|
||||
- [ ] Compare translations side-by-side
|
||||
- [ ] Missing translations report
|
||||
|
||||
## 📚 İlgili Dosyalar
|
||||
|
||||
- Models: `app/Models/Language.php`, `Translation.php`, `UserLanguage.php`
|
||||
- Trait: `app/Traits/HasTranslations.php`
|
||||
- Helpers: `app/Helpers/translation_helpers.php`
|
||||
- Resource: `app/Filament/Admin/Resources/Languages/`
|
||||
- Policy: `app/Policies/LanguagePolicy.php`
|
||||
- Migrations: `database/migrations/*_create_languages_table.php`
|
||||
- Translations: `lang/tr/language.php`, `lang/en/language.php`
|
||||
|
||||
---
|
||||
|
||||
**Geliştirici:** Citrus Platform Team
|
||||
**Versiyon:** 1.0.0
|
||||
**Tarih:** 1 Ekim 2025
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
// Navigation & Labels
|
||||
'title' => 'Languages',
|
||||
'navigation_label' => 'Languages',
|
||||
'model_label' => 'Language',
|
||||
'plural_model_label' => 'Languages',
|
||||
|
||||
// Actions
|
||||
'create' => 'Create Language',
|
||||
'edit' => 'Edit Language',
|
||||
'delete' => 'Delete Language',
|
||||
'restore' => 'Restore',
|
||||
'force_delete' => 'Force Delete',
|
||||
'activate' => 'Activate',
|
||||
'deactivate' => 'Deactivate',
|
||||
'set_default' => 'Set as Default',
|
||||
|
||||
// Form Sections
|
||||
'basic_information' => 'Basic Information',
|
||||
'settings' => 'Settings',
|
||||
|
||||
// Form Fields
|
||||
'code' => 'Language Code',
|
||||
'code_helper' => 'ISO 639-1 language code (e.g: tr, en, de)',
|
||||
'name' => 'Language Name',
|
||||
'name_helper' => 'English language name (e.g: Turkish)',
|
||||
'native_name' => 'Native Name',
|
||||
'native_name_helper' => 'Language name in its own language (e.g: Türkçe)',
|
||||
'flag' => 'Flag',
|
||||
'flag_helper' => 'Flag emoji or icon code',
|
||||
'direction' => 'Text Direction',
|
||||
'direction_ltr' => 'Left to Right (LTR)',
|
||||
'direction_rtl' => 'Right to Left (RTL)',
|
||||
'is_active' => 'Active',
|
||||
'is_active_helper' => 'Is this language available in the system?',
|
||||
'is_default' => 'Default Language',
|
||||
'is_default_helper' => 'Should this be the default language?',
|
||||
'sort_order' => 'Sort Order',
|
||||
'sort_order_helper' => 'Display order of languages',
|
||||
|
||||
// Table Columns
|
||||
'table_flag' => 'Flag',
|
||||
'table_code' => 'Code',
|
||||
'table_name' => 'Language Name',
|
||||
'table_is_active' => 'Active',
|
||||
'table_is_default' => 'Default',
|
||||
'table_direction' => 'Direction',
|
||||
'table_sort_order' => 'Order',
|
||||
'table_created_at' => 'Created',
|
||||
'table_updated_at' => 'Updated',
|
||||
'table_deleted_at' => 'Deleted',
|
||||
|
||||
// Messages
|
||||
'created_successfully' => 'Language created successfully',
|
||||
'created_successfully_body' => 'New language has been added to the system.',
|
||||
'updated_successfully' => 'Language updated successfully',
|
||||
'updated_successfully_body' => 'Language information has been updated.',
|
||||
'deleted_successfully' => 'Language deleted successfully',
|
||||
'deleted_successfully_body' => 'Language has been removed from the system.',
|
||||
'restored_successfully' => 'Language restored successfully',
|
||||
'activated_successfully' => 'Language activated',
|
||||
'deactivated_successfully' => 'Language deactivated',
|
||||
'set_default_successfully' => 'Default language updated',
|
||||
'cannot_delete_default' => 'Cannot delete default language',
|
||||
'cannot_deactivate_default' => 'Cannot deactivate default language',
|
||||
|
||||
// Validation
|
||||
'code_required' => 'Language code is required',
|
||||
'code_unique' => 'This language code is already in use',
|
||||
'name_required' => 'Language name is required',
|
||||
'native_name_required' => 'Native name is required',
|
||||
];
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
// Navigation & Labels
|
||||
'title' => 'Diller',
|
||||
'navigation_label' => 'Diller',
|
||||
'model_label' => 'Dil',
|
||||
'plural_model_label' => 'Diller',
|
||||
|
||||
// Actions
|
||||
'create' => 'Dil Oluştur',
|
||||
'edit' => 'Dil Düzenle',
|
||||
'delete' => 'Dil Sil',
|
||||
'restore' => 'Geri Yükle',
|
||||
'force_delete' => 'Kalıcı Olarak Sil',
|
||||
'activate' => 'Aktif Et',
|
||||
'deactivate' => 'Pasif Et',
|
||||
'set_default' => 'Varsayılan Yap',
|
||||
|
||||
// Form Sections
|
||||
'basic_information' => 'Temel Bilgiler',
|
||||
'settings' => 'Ayarlar',
|
||||
|
||||
// Form Fields
|
||||
'code' => 'Dil Kodu',
|
||||
'code_helper' => 'ISO 639-1 dil kodu (örn: tr, en, de)',
|
||||
'name' => 'Dil Adı',
|
||||
'name_helper' => 'İngilizce dil adı (örn: Turkish)',
|
||||
'native_name' => 'Yerel Dil Adı',
|
||||
'native_name_helper' => 'Dil adının kendi dilindeki yazılışı (örn: Türkçe)',
|
||||
'flag' => 'Bayrak',
|
||||
'flag_helper' => 'Bayrak emojisi veya icon kodu',
|
||||
'direction' => 'Metin Yönü',
|
||||
'direction_ltr' => 'Soldan Sağa (LTR)',
|
||||
'direction_rtl' => 'Sağdan Sola (RTL)',
|
||||
'is_active' => 'Aktif',
|
||||
'is_active_helper' => 'Dil sistemde kullanılabilir mi?',
|
||||
'is_default' => 'Varsayılan Dil',
|
||||
'is_default_helper' => 'Bu dil varsayılan dil olsun mu?',
|
||||
'sort_order' => 'Sıralama',
|
||||
'sort_order_helper' => 'Dillerin görüntülenme sırası',
|
||||
|
||||
// Table Columns
|
||||
'table_flag' => 'Bayrak',
|
||||
'table_code' => 'Kod',
|
||||
'table_name' => 'Dil Adı',
|
||||
'table_is_active' => 'Aktif',
|
||||
'table_is_default' => 'Varsayılan',
|
||||
'table_direction' => 'Yön',
|
||||
'table_sort_order' => 'Sıra',
|
||||
'table_created_at' => 'Oluşturulma',
|
||||
'table_updated_at' => 'Güncellenme',
|
||||
'table_deleted_at' => 'Silinme',
|
||||
|
||||
// Messages
|
||||
'created_successfully' => 'Dil başarıyla oluşturuldu',
|
||||
'created_successfully_body' => 'Yeni dil sisteme eklendi.',
|
||||
'updated_successfully' => 'Dil başarıyla güncellendi',
|
||||
'updated_successfully_body' => 'Dil bilgileri güncellendi.',
|
||||
'deleted_successfully' => 'Dil başarıyla silindi',
|
||||
'deleted_successfully_body' => 'Dil sistemden kaldırıldı.',
|
||||
'restored_successfully' => 'Dil başarıyla geri yüklendi',
|
||||
'activated_successfully' => 'Dil aktif edildi',
|
||||
'deactivated_successfully' => 'Dil pasif edildi',
|
||||
'set_default_successfully' => 'Varsayılan dil güncellendi',
|
||||
'cannot_delete_default' => 'Varsayılan dil silinemez',
|
||||
'cannot_deactivate_default' => 'Varsayılan dil pasif edilemez',
|
||||
|
||||
// Validation
|
||||
'code_required' => 'Dil kodu zorunludur',
|
||||
'code_unique' => 'Bu dil kodu zaten kullanılıyor',
|
||||
'name_required' => 'Dil adı zorunludur',
|
||||
'native_name_required' => 'Yerel dil adı zorunludur',
|
||||
];
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
height: 6rem !important;
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
|
||||
.fi-topbar-start .fi-logo {
|
||||
height: 2.5rem !important;
|
||||
}
|
||||
/* Login sayfası brand logo boyutu */
|
||||
.fi-brand-logo {
|
||||
height: 6rem !important;
|
||||
|
||||
Reference in New Issue
Block a user