Merge pull request #3 from truncgil/translatable

Translatable
This commit is contained in:
Truncgil Technology
2025-10-01 11:08:11 +03:00
committed by GitHub
38 changed files with 3327 additions and 7 deletions
+146 -2
View File
@@ -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),
]);
}
}
+284
View File
@@ -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
View File
@@ -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',
+124
View File
@@ -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
View File
@@ -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',
+201
View File
@@ -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']);
}
}
+125
View File
@@ -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;
}
}
+105
View File
@@ -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();
}
}
+97
View File
@@ -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');
}
}
+3
View File
@@ -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,
];
/**
+309
View File
@@ -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
View File
@@ -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');
}
};
+78
View File
@@ -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!');
}
}
+352
View File
@@ -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!
+431
View File
@@ -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
+1
View File
@@ -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',
+75
View File
@@ -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',
];
+1
View File
@@ -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',
+1
View File
@@ -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',
+75
View File
@@ -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',
];
+1
View File
@@ -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 -2
View File
@@ -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;