From 105e14b48d06c27128766c88315866bc5c6c0e82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Cmit=20Tun=C3=A7?= Date: Wed, 1 Oct 2025 04:15:21 -0300 Subject: [PATCH] Add Language Management Module: Created Language resource with CRUD pages, schemas, and models to support the universal translation system. Implemented localization for English and Turkish, ensuring consistent user experience. Added necessary policies and seeder for initial language data, enhancing the overall language management functionality. --- .../Resources/Languages/LanguageResource.php | 68 +++ .../Languages/Pages/CreateLanguage.php | 39 ++ .../Languages/Pages/EditLanguage.php | 53 +++ .../Languages/Pages/ListLanguages.php | 26 ++ .../Languages/Schemas/LanguageForm.php | 95 ++++ .../Languages/Tables/LanguagesTable.php | 113 +++++ app/Models/Blog.php | 14 +- app/Models/Language.php | 124 +++++ app/Models/Page.php | 14 +- app/Models/Translation.php | 201 ++++++++ app/Models/User.php | 125 +++++ app/Models/UserLanguage.php | 105 +++++ app/Policies/LanguagePolicy.php | 97 ++++ app/Providers/AuthServiceProvider.php | 3 + app/Traits/HasTranslations.php | 309 +++++++++++++ database/seeders/LanguageSeeder.php | 78 ++++ docs/TRANSLATION_QUICK_START.md | 352 ++++++++++++++ docs/TRANSLATION_SYSTEM.md | 431 ++++++++++++++++++ lang/en/language.php | 75 +++ lang/tr/language.php | 75 +++ resources/css/citrus.css | 5 +- 21 files changed, 2398 insertions(+), 4 deletions(-) create mode 100644 app/Filament/Admin/Resources/Languages/LanguageResource.php create mode 100644 app/Filament/Admin/Resources/Languages/Pages/CreateLanguage.php create mode 100644 app/Filament/Admin/Resources/Languages/Pages/EditLanguage.php create mode 100644 app/Filament/Admin/Resources/Languages/Pages/ListLanguages.php create mode 100644 app/Filament/Admin/Resources/Languages/Schemas/LanguageForm.php create mode 100644 app/Filament/Admin/Resources/Languages/Tables/LanguagesTable.php create mode 100644 app/Models/Language.php create mode 100644 app/Models/Translation.php create mode 100644 app/Models/UserLanguage.php create mode 100644 app/Policies/LanguagePolicy.php create mode 100644 app/Traits/HasTranslations.php create mode 100644 database/seeders/LanguageSeeder.php create mode 100644 docs/TRANSLATION_QUICK_START.md create mode 100644 docs/TRANSLATION_SYSTEM.md create mode 100644 lang/en/language.php create mode 100644 lang/tr/language.php 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 --}} +

{{ $blog->translate('title') }}

+ +{{-- Belirli dilde --}} +

{{ $blog->translate('title', 'en') }}

+ +{{-- Helper ile --}} +

{{ translate_model($blog, 'title') }}

+ +{{-- Dil seçici --}} +@foreach(available_languages() as $language) + + {{ $language->flag }} {{ $language->name }} + +@endforeach + +{{-- Progress bar --}} +@php + $progress = $blog->getTranslationProgress(); +@endphp + +@foreach($progress as $code => $percentage) +
+ {{ get_language_flag($code) }} +
{{ $percentage }}%
+
+@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! + diff --git a/docs/TRANSLATION_SYSTEM.md b/docs/TRANSLATION_SYSTEM.md new file mode 100644 index 0000000..d8bb42c --- /dev/null +++ b/docs/TRANSLATION_SYSTEM.md @@ -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 + diff --git a/lang/en/language.php b/lang/en/language.php new file mode 100644 index 0000000..d152b74 --- /dev/null +++ b/lang/en/language.php @@ -0,0 +1,75 @@ + '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', +]; + diff --git a/lang/tr/language.php b/lang/tr/language.php new file mode 100644 index 0000000..debca43 --- /dev/null +++ b/lang/tr/language.php @@ -0,0 +1,75 @@ + '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', +]; + diff --git a/resources/css/citrus.css b/resources/css/citrus.css index 1ac5e99..17f6ae9 100644 --- a/resources/css/citrus.css +++ b/resources/css/citrus.css @@ -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;