diff --git a/app/Filament/Admin/Resources/Languages/LanguageResource.php b/app/Filament/Admin/Resources/Languages/LanguageResource.php new file mode 100644 index 0000000..fb7c4ea --- /dev/null +++ b/app/Filament/Admin/Resources/Languages/LanguageResource.php @@ -0,0 +1,68 @@ +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'), + ]; + } +} + diff --git a/app/Filament/Admin/Resources/Languages/Pages/CreateLanguage.php b/app/Filament/Admin/Resources/Languages/Pages/CreateLanguage.php new file mode 100644 index 0000000..a651c6c --- /dev/null +++ b/app/Filament/Admin/Resources/Languages/Pages/CreateLanguage.php @@ -0,0 +1,39 @@ +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; + } +} + diff --git a/app/Filament/Admin/Resources/Languages/Pages/EditLanguage.php b/app/Filament/Admin/Resources/Languages/Pages/EditLanguage.php new file mode 100644 index 0000000..91a6a1a --- /dev/null +++ b/app/Filament/Admin/Resources/Languages/Pages/EditLanguage.php @@ -0,0 +1,53 @@ +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; + } +} + diff --git a/app/Filament/Admin/Resources/Languages/Pages/ListLanguages.php b/app/Filament/Admin/Resources/Languages/Pages/ListLanguages.php new file mode 100644 index 0000000..23ac100 --- /dev/null +++ b/app/Filament/Admin/Resources/Languages/Pages/ListLanguages.php @@ -0,0 +1,26 @@ +label(__('language.create')), + ]; + } +} + diff --git a/app/Filament/Admin/Resources/Languages/Schemas/LanguageForm.php b/app/Filament/Admin/Resources/Languages/Schemas/LanguageForm.php new file mode 100644 index 0000000..0eecc68 --- /dev/null +++ b/app/Filament/Admin/Resources/Languages/Schemas/LanguageForm.php @@ -0,0 +1,95 @@ +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), + ]), + ]), + ]; + } +} + diff --git a/app/Filament/Admin/Resources/Languages/Tables/LanguagesTable.php b/app/Filament/Admin/Resources/Languages/Tables/LanguagesTable.php new file mode 100644 index 0000000..b21de0b --- /dev/null +++ b/app/Filament/Admin/Resources/Languages/Tables/LanguagesTable.php @@ -0,0 +1,113 @@ +columns([ + TextColumn::make('flag') + ->label(__('language.table_flag')) + ->sortable() + ->searchable() + ->formatStateUsing(fn (?string $state): string => + '' . ($state ?? '🌐') . '' + ) + ->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, + ])); + } +} + diff --git a/app/Models/Blog.php b/app/Models/Blog.php index d53d08e..adcc672 100644 --- a/app/Models/Blog.php +++ b/app/Models/Blog.php @@ -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', diff --git a/app/Models/Language.php b/app/Models/Language.php new file mode 100644 index 0000000..ca3fe3a --- /dev/null +++ b/app/Models/Language.php @@ -0,0 +1,124 @@ + '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(); + } +} diff --git a/app/Models/Page.php b/app/Models/Page.php index 7823d78..e039d07 100644 --- a/app/Models/Page.php +++ b/app/Models/Page.php @@ -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', diff --git a/app/Models/Translation.php b/app/Models/Translation.php new file mode 100644 index 0000000..b0f4103 --- /dev/null +++ b/app/Models/Translation.php @@ -0,0 +1,201 @@ + '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']); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index ac2ca92..952ef3a 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -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; + } } diff --git a/app/Models/UserLanguage.php b/app/Models/UserLanguage.php new file mode 100644 index 0000000..99701e1 --- /dev/null +++ b/app/Models/UserLanguage.php @@ -0,0 +1,105 @@ + '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(); + } +} diff --git a/app/Policies/LanguagePolicy.php b/app/Policies/LanguagePolicy.php new file mode 100644 index 0000000..95f50ff --- /dev/null +++ b/app/Policies/LanguagePolicy.php @@ -0,0 +1,97 @@ +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'); + } +} diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 89c03aa..07a76d3 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -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, ]; /** diff --git a/app/Traits/HasTranslations.php b/app/Traits/HasTranslations.php new file mode 100644 index 0000000..0c97b24 --- /dev/null +++ b/app/Traits/HasTranslations.php @@ -0,0 +1,309 @@ +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; + } +} + diff --git a/database/seeders/LanguageSeeder.php b/database/seeders/LanguageSeeder.php new file mode 100644 index 0000000..24c9b61 --- /dev/null +++ b/database/seeders/LanguageSeeder.php @@ -0,0 +1,78 @@ + '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!'); + } +} diff --git a/docs/TRANSLATION_QUICK_START.md b/docs/TRANSLATION_QUICK_START.md new file mode 100644 index 0000000..941904d --- /dev/null +++ b/docs/TRANSLATION_QUICK_START.md @@ -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 --}} +