Версия 0.4.0 #7
| @@ -11,6 +11,7 @@ final readonly class Content | |||||||
|         private string $title, |         private string $title, | ||||||
|         private string $content, |         private string $content, | ||||||
|         private Storages $storages, |         private Storages $storages, | ||||||
|  |         private bool $isTranslateAutomatically = false, | ||||||
|     ) { } |     ) { } | ||||||
| 
 | 
 | ||||||
|     public function getLanguageId(): int |     public function getLanguageId(): int | ||||||
| @@ -32,4 +33,9 @@ final readonly class Content | |||||||
|     { |     { | ||||||
|         return $this->storages; |         return $this->storages; | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     public function isTranslateAutomatically(): bool | ||||||
|  |     { | ||||||
|  |         return $this->isTranslateAutomatically; | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -0,0 +1,25 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Dto\Service\ProjectTranslationServiceHash; | ||||||
|  | 
 | ||||||
|  | final class Fields | ||||||
|  | { | ||||||
|  |     private array $fields = []; | ||||||
|  | 
 | ||||||
|  |     public function add(string $name, string $value): self | ||||||
|  |     { | ||||||
|  |         $this->fields[$name] = $value; | ||||||
|  | 
 | ||||||
|  |         return $this; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getFields(): array | ||||||
|  |     { | ||||||
|  |         return $this->fields; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getNames(): array | ||||||
|  |     { | ||||||
|  |         return \array_keys($this->fields); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,18 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Dto\Service\ProjectTranslationServiceHash; | ||||||
|  | 
 | ||||||
|  | final class HashStatusWaiting | ||||||
|  | { | ||||||
|  |     private array $hash = []; | ||||||
|  | 
 | ||||||
|  |     public function add(string $fieldName, bool $isWaiting): void | ||||||
|  |     { | ||||||
|  |         $this->hash[$fieldName] = $isWaiting; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function isStatusWaiting(string $fieldName): bool | ||||||
|  |     { | ||||||
|  |         return $this->hash[$fieldName] ?? false; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,28 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Dto\Service\ProjectTranslationServiceHash; | ||||||
|  | 
 | ||||||
|  | final class Hashes | ||||||
|  | { | ||||||
|  |     private array $hashes = []; | ||||||
|  |     private array $ids = []; | ||||||
|  | 
 | ||||||
|  |     public function add(int $hashId, string $fieldName, string $hash): void | ||||||
|  |     { | ||||||
|  |         $this->hashes[$hashId] = [ | ||||||
|  |             'field' => $fieldName, | ||||||
|  |             'hash'  => $hash, | ||||||
|  |         ]; | ||||||
|  |         $this->ids[] = $hashId; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getHash(int $hashId): ?array | ||||||
|  |     { | ||||||
|  |         return $this->hashes[$hashId] ?? null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getIds(): array | ||||||
|  |     { | ||||||
|  |         return $this->ids; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,37 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Dto\Service\ProjectTranslationServiceHash; | ||||||
|  | 
 | ||||||
|  | final class TranslateFields | ||||||
|  | { | ||||||
|  |     private array $fields = []; | ||||||
|  |     private array $hashes = []; | ||||||
|  | 
 | ||||||
|  |     public function add(int $languageId, string $fieldName, string $hash, int $hashId): self | ||||||
|  |     { | ||||||
|  |         if (!isset($this->fields[$languageId])) { | ||||||
|  |             $this->fields[$languageId] = []; | ||||||
|  |         } | ||||||
|  |         $this->fields[$languageId][] = $fieldName; | ||||||
|  | 
 | ||||||
|  |         if (!isset($this->hashes[$languageId])) { | ||||||
|  |             $this->hashes[$languageId] = []; | ||||||
|  |         } | ||||||
|  |         $this->hashes[$languageId][$fieldName] = [ | ||||||
|  |             'hash' => $hash, | ||||||
|  |             'hashId' => $hashId, | ||||||
|  |         ]; | ||||||
|  | 
 | ||||||
|  |         return $this; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getFields(): array | ||||||
|  |     { | ||||||
|  |         return $this->fields; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getHashesByLanguage(int $languageId): array | ||||||
|  |     { | ||||||
|  |         return $this->hashes[$languageId] ?? []; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,9 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Enums\ProjectTranslationServiceHashes; | ||||||
|  | 
 | ||||||
|  | enum Status: int | ||||||
|  | { | ||||||
|  |     case Waiting = 0; | ||||||
|  |     case Success = 10; | ||||||
|  | } | ||||||
| @@ -0,0 +1,8 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Exceptions\Services\Translate; | ||||||
|  | 
 | ||||||
|  | final class CompletedException extends \Exception | ||||||
|  | { | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @@ -32,6 +32,7 @@ final class StoreUpdateRequest extends FormRequest implements FormRequestDto | |||||||
|             'content.*.title' => ['required', 'string', 'max:255'], |             'content.*.title' => ['required', 'string', 'max:255'], | ||||||
|             'content.*.content' => ['nullable', 'string'], |             'content.*.content' => ['nullable', 'string'], | ||||||
|             'content.*.content_images.*.file' => ['numeric', 'min:1'], |             'content.*.content_images.*.file' => ['numeric', 'min:1'], | ||||||
|  |             'translate-automatically.*' => ['nullable', 'boolean'], | ||||||
|         ]; |         ]; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @@ -60,6 +61,7 @@ final class StoreUpdateRequest extends FormRequest implements FormRequestDto | |||||||
|                 title:      $content['title'], |                 title:      $content['title'], | ||||||
|                 content:    $content['content'] ?? '', |                 content:    $content['content'] ?? '', | ||||||
|                 storages:   $this->contentStorages($content), |                 storages:   $this->contentStorages($content), | ||||||
|  |                 isTranslateAutomatically: (bool) $this->input('translate-automatically.' . $languageId, false), | ||||||
|             )); |             )); | ||||||
|         } |         } | ||||||
|         return $contents; |         return $contents; | ||||||
|   | |||||||
| @@ -0,0 +1,58 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Jobs\Translate; | ||||||
|  | 
 | ||||||
|  | use App\Services\Translate\Project\DocumentationContentService; | ||||||
|  | use Exception; | ||||||
|  | use Illuminate\Contracts\Queue\ShouldBeEncrypted; | ||||||
|  | use Illuminate\Contracts\Queue\ShouldQueue; | ||||||
|  | use Illuminate\Foundation\Bus\Dispatchable; | ||||||
|  | use Illuminate\Foundation\Queue\Queueable; | ||||||
|  | use Illuminate\Queue\Middleware\WithoutOverlapping; | ||||||
|  | 
 | ||||||
|  | final class ProcessProjectDocumentationContent implements ShouldQueue, ShouldBeEncrypted | ||||||
|  | { | ||||||
|  |     use Dispatchable, Queueable; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get the middleware the job should pass through. | ||||||
|  |      * | ||||||
|  |      * @return array<int, object> | ||||||
|  |      */ | ||||||
|  |     public function middleware(): array | ||||||
|  |     { | ||||||
|  |         return [ | ||||||
|  |             (new WithoutOverlapping($this->uniqueId()))->expireAfter(180), | ||||||
|  |         ]; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Create a new job instance. | ||||||
|  |      */ | ||||||
|  |     public function __construct( | ||||||
|  |         private readonly int   $projectDocumentId, | ||||||
|  |         private readonly array $contentIds = [], | ||||||
|  |         private readonly array $exceptLanguages = [], | ||||||
|  |     ) { } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get the unique ID for the job. | ||||||
|  |      */ | ||||||
|  |     public function uniqueId(): string | ||||||
|  |     { | ||||||
|  |         return 'documentation-' . $this->projectDocumentId; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Execute the job. | ||||||
|  |      * @throws Exception | ||||||
|  |      */ | ||||||
|  |     public function handle(DocumentationContentService $documentationContentService): void | ||||||
|  |     { | ||||||
|  |         $result = $documentationContentService->translate($this->projectDocumentId, $this->contentIds, $this->exceptLanguages); | ||||||
|  |         if ($result->isError() && $result->getCode() !== 404) { | ||||||
|  |             cache()->lock($this->uniqueId())->forceRelease(); | ||||||
|  |             throw new Exception($result->getMessage()); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										51
									
								
								app/application/app/Models/ProjectTranslationServiceHash.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								app/application/app/Models/ProjectTranslationServiceHash.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Models; | ||||||
|  | 
 | ||||||
|  | use App\Enums\Morph; | ||||||
|  | use App\Enums\ProjectTranslationServiceHashes\Status; | ||||||
|  | use Illuminate\Database\Eloquent\Factories\HasFactory; | ||||||
|  | use Illuminate\Database\Eloquent\Model; | ||||||
|  | 
 | ||||||
|  | final class ProjectTranslationServiceHash extends Model | ||||||
|  | { | ||||||
|  |     use HasFactory; | ||||||
|  | 
 | ||||||
|  |     protected $table = 'project_translation_service_hashes'; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * The model's default values for attributes. | ||||||
|  |      * | ||||||
|  |      * @var array | ||||||
|  |      */ | ||||||
|  |     protected $attributes = [ | ||||||
|  |         'status' => Status::Waiting, | ||||||
|  |     ]; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * The attributes that are mass assignable. | ||||||
|  |      * | ||||||
|  |      * @var array | ||||||
|  |      */ | ||||||
|  |     protected $fillable = [ | ||||||
|  |         'language_id', | ||||||
|  |         'field', | ||||||
|  |         'status', | ||||||
|  |         'hash', | ||||||
|  |         'morph_type', | ||||||
|  |         'morph_id', | ||||||
|  |     ]; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get the attributes that should be cast. | ||||||
|  |      * | ||||||
|  |      * @return array<string, string> | ||||||
|  |      */ | ||||||
|  |     protected function casts(): array | ||||||
|  |     { | ||||||
|  |         return [ | ||||||
|  |             'status'     => Status::class, | ||||||
|  |             'morph_type' => Morph::class, | ||||||
|  |         ]; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,38 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Repositories; | ||||||
|  | 
 | ||||||
|  | use App\Contracts\Search; | ||||||
|  | use App\Enums\Morph; | ||||||
|  | use App\Models\ProjectTranslationServiceHash; | ||||||
|  | use App\Services\Search\CreateSearchInstanceCommand; | ||||||
|  | use Illuminate\Database\Eloquent\Builder; | ||||||
|  | 
 | ||||||
|  | final readonly class ProjectTranslationServiceHashRepository | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         private CreateSearchInstanceCommand $createSearchInstanceCommand, | ||||||
|  |     ) { } | ||||||
|  | 
 | ||||||
|  |     public function getHashes(Morph $morph, int $morphId, ?array $languages = null, ?array $fields = null): Search | ||||||
|  |     { | ||||||
|  |         $query = ProjectTranslationServiceHash::query() | ||||||
|  |             ->where('morph_type', $morph) | ||||||
|  |             ->where('morph_id', $morphId) | ||||||
|  |             ->whereIn('language_id', $languages) | ||||||
|  |             ->when($languages, function (Builder $query) use ($languages) { | ||||||
|  |                 $query->whereIn('language_id', $languages); | ||||||
|  |             }) | ||||||
|  |             ->when($fields, function (Builder $query) use ($fields) { | ||||||
|  |                 $query->whereIn('field', $fields); | ||||||
|  |             }); | ||||||
|  | 
 | ||||||
|  |         return $this->createSearchInstanceCommand->execute($query); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getHashesByIds(array $ids): Search | ||||||
|  |     { | ||||||
|  |         $query = ProjectTranslationServiceHash::query()->whereIn('id', $ids); | ||||||
|  |         return $this->createSearchInstanceCommand->execute($query); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -5,7 +5,10 @@ namespace App\Services\Admin\Project; | |||||||
| use App\Dto\Builder\Documentation as DocumentationBuilderDto; | use App\Dto\Builder\Documentation as DocumentationBuilderDto; | ||||||
| use App\Dto\QuerySettingsDto; | use App\Dto\QuerySettingsDto; | ||||||
| use App\Dto\Service\Admin\Project\Documentation\StoreUpdate; | use App\Dto\Service\Admin\Project\Documentation\StoreUpdate; | ||||||
|  | use App\Dto\Service\Admin\Project\DocumentationContent\Content; | ||||||
|  | use App\Dto\Service\Admin\Project\DocumentationContent\Contents; | ||||||
| use App\Exceptions\Services\DocumentationContent\StorageCommandException; | use App\Exceptions\Services\DocumentationContent\StorageCommandException; | ||||||
|  | use App\Jobs\Translate\ProcessProjectDocumentationContent; | ||||||
| use App\Models\Documentation; | use App\Models\Documentation; | ||||||
| use App\Models\ProjectLanguage; | use App\Models\ProjectLanguage; | ||||||
| use App\Models\User; | use App\Models\User; | ||||||
| @@ -90,6 +93,7 @@ final class DocumentationService extends Service | |||||||
|             'project' => $project, |             'project' => $project, | ||||||
|             'documentation' => new Documentation(), |             'documentation' => new Documentation(), | ||||||
|             'categories' => $this->documentationCategoryRepository->getForSelect($version, $defaultLanguage), |             'categories' => $this->documentationCategoryRepository->getForSelect($version, $defaultLanguage), | ||||||
|  |             'serviceTranslationEnable' => config('translation_service.enable', false), | ||||||
|         ]); |         ]); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @@ -120,6 +124,7 @@ final class DocumentationService extends Service | |||||||
|             'project' => $project, |             'project' => $project, | ||||||
|             'documentation' => $documentation, |             'documentation' => $documentation, | ||||||
|             'categories' => $this->documentationCategoryRepository->getForSelect($version, $defaultLanguage, null, $withCategories), |             'categories' => $this->documentationCategoryRepository->getForSelect($version, $defaultLanguage, null, $withCategories), | ||||||
|  |             'serviceTranslationEnable' => config('translation_service.enable', false), | ||||||
|         ]); |         ]); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @@ -150,6 +155,10 @@ final class DocumentationService extends Service | |||||||
| 
 | 
 | ||||||
|                 return $documentation; |                 return $documentation; | ||||||
|             }); |             }); | ||||||
|  | 
 | ||||||
|  |             if (\config('translation_service.enable', false)) { | ||||||
|  |                 $this->translateContent($documentation, $data->getContents()); | ||||||
|  |             } | ||||||
|         } catch (StorageCommandException $e) { |         } catch (StorageCommandException $e) { | ||||||
|             return $e->getResultError(); |             return $e->getResultError(); | ||||||
|         } catch (\Throwable $e) { |         } catch (\Throwable $e) { | ||||||
| @@ -192,6 +201,10 @@ final class DocumentationService extends Service | |||||||
| 
 | 
 | ||||||
|                 return $documentation; |                 return $documentation; | ||||||
|             }); |             }); | ||||||
|  | 
 | ||||||
|  |             if (\config('translation_service.enable', false)) { | ||||||
|  |                 $this->translateContent($documentation, $data->getContents()); | ||||||
|  |             } | ||||||
|         } catch (StorageCommandException $e) { |         } catch (StorageCommandException $e) { | ||||||
|             return $e->getResultError(); |             return $e->getResultError(); | ||||||
|         } catch (\Throwable $e) { |         } catch (\Throwable $e) { | ||||||
| @@ -240,4 +253,27 @@ final class DocumentationService extends Service | |||||||
|             'category_id' => $data->getCategoryId(), |             'category_id' => $data->getCategoryId(), | ||||||
|         ]; |         ]; | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     private function translateContent(Documentation $documentation, Contents $contents): void | ||||||
|  |     { | ||||||
|  |         $translateExceptLanguages = []; | ||||||
|  | 
 | ||||||
|  |         $translateLanguages = []; | ||||||
|  |         foreach ($contents->getContents() as $content) { | ||||||
|  |             /** @var Content $content */ | ||||||
|  |             $translateExceptLanguages[] = $content->getLanguageId(); | ||||||
|  |             if ($content->isTranslateAutomatically()) { | ||||||
|  |                 $translateLanguages[] = $content->getLanguageId(); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (empty($translateLanguages)) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $translateContentIds = $documentation->contents()->select('id') | ||||||
|  |                                 ->whereIn('language_id', $translateLanguages) | ||||||
|  |                                 ->get()->pluck('id')->toArray(); | ||||||
|  |         ProcessProjectDocumentationContent::dispatch($documentation->id, $translateContentIds, $translateExceptLanguages); | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -0,0 +1,54 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Services\ProjectTranslationService; | ||||||
|  | 
 | ||||||
|  | use App\Models\Project; | ||||||
|  | use App\Dto\Service\Admin\Project\ServiceTranslate\Translations; | ||||||
|  | use App\Models\ProjectTranslationService; | ||||||
|  | 
 | ||||||
|  | final readonly class ModelSyncCommand | ||||||
|  | { | ||||||
|  |     public function execute(Project $project, Translations $translations): void | ||||||
|  |     { | ||||||
|  |         $insert = []; | ||||||
|  |         $delete = []; | ||||||
|  | 
 | ||||||
|  |         $languages = $project->languages()->with(['serviceTranslate'])->get(); | ||||||
|  |         foreach ($translations->getTranslations() as $translation) { | ||||||
|  |             /* @var \App\Dto\Service\Admin\Project\ServiceTranslate\Translation $translation */ | ||||||
|  |             $language = $languages->firstWhere('id', '=', $translation->getLanguageId()); | ||||||
|  |             if ($language === null) { | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  |             $serviceTranslate = $language->serviceTranslate; | ||||||
|  |             if ($serviceTranslate === null) { | ||||||
|  |                 if ($translation->getCode() !== null) { | ||||||
|  |                     $insert[] = [ | ||||||
|  |                         'language_id'        => $translation->getLanguageId(), | ||||||
|  |                         'code'               => $translation->getCode(), | ||||||
|  |                         'source_language_id' => $translation->getSourceLanguageId(), | ||||||
|  |                     ]; | ||||||
|  |                 } | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if ($translation->getCode() === null) { | ||||||
|  |                 $delete[] = [$serviceTranslate->id]; | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             $serviceTranslate->update([ | ||||||
|  |                 'code'               => $translation->getCode(), | ||||||
|  |                 'source_language_id' => $translation->getSourceLanguageId(), | ||||||
|  |             ]); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (!empty($insert)) { | ||||||
|  |             ProjectTranslationService::query()->insert($insert); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (!empty($delete)) { | ||||||
|  |             ProjectTranslationService::query()->whereIn('id', $delete)->delete(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,18 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Services\ProjectTranslationService; | ||||||
|  | 
 | ||||||
|  | use Illuminate\Support\Str; | ||||||
|  | 
 | ||||||
|  | final readonly class NoTranslateAttributeHandler | ||||||
|  | { | ||||||
|  |     public function handleAddAttribute(string $text): string | ||||||
|  |     { | ||||||
|  |         return Str::replace('<code', '<code translate="no"', $text); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function handleRemoveAttribute(string $text): string | ||||||
|  |     { | ||||||
|  |         return Str::replace('<code translate="no"', '<code', $text); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,40 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Services\ProjectTranslationServiceHash; | ||||||
|  | 
 | ||||||
|  | use App\Dto\Service\ProjectTranslationServiceHash\Hashes; | ||||||
|  | use App\Dto\Service\ProjectTranslationServiceHash\HashStatusWaiting; | ||||||
|  | use App\Enums\ProjectTranslationServiceHashes\Status; | ||||||
|  | use App\Repositories\ProjectTranslationServiceHashRepository; | ||||||
|  | 
 | ||||||
|  | final readonly class CompletionChecker | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         private ProjectTranslationServiceHashRepository     $hashRepository, | ||||||
|  |         private ProjectTranslationServiceHashCommandHandler $commandHandler, | ||||||
|  |     ) { } | ||||||
|  | 
 | ||||||
|  |     public function execute(Hashes $hashesDto): HashStatusWaiting | ||||||
|  |     { | ||||||
|  |         $hashes = $this->hashRepository->getHashesByIds($hashesDto->getIds())->all(); | ||||||
|  |         $hashStatusWaiting = new HashStatusWaiting(); | ||||||
|  |         $hashSuccessIds = []; | ||||||
|  |         foreach ($hashes as $hash) { | ||||||
|  |             $dataHash = $hashesDto->getHash($hash->id); | ||||||
|  |             if ($dataHash === null) { | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             $isWaiting = false; | ||||||
|  |             if ($hash->status === Status::Waiting && $hash->hash === $dataHash['hash'] && $hash->field === $dataHash['field']) { | ||||||
|  |                 $isWaiting = true; | ||||||
|  |                 $hashSuccessIds[] = $hash->id; | ||||||
|  |             } | ||||||
|  |             $hashStatusWaiting->add($hash['field'], $isWaiting); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $this->commandHandler->handleSetStatusById($hashSuccessIds, Status::Success); | ||||||
|  | 
 | ||||||
|  |         return $hashStatusWaiting; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,69 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Services\ProjectTranslationServiceHash; | ||||||
|  | 
 | ||||||
|  | use App\Dto\Service\ProjectTranslationServiceHash\Fields; | ||||||
|  | use App\Dto\Service\ProjectTranslationServiceHash\TranslateFields; | ||||||
|  | use App\Enums\Morph; | ||||||
|  | use App\Enums\ProjectTranslationServiceHashes\Status; | ||||||
|  | use App\Models\ProjectTranslationServiceHash; | ||||||
|  | use App\Repositories\ProjectTranslationServiceHashRepository; | ||||||
|  | 
 | ||||||
|  | final readonly class HashTrackerCommand | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         private ProjectTranslationServiceHashRepository     $hashRepository, | ||||||
|  |         private ProjectTranslationServiceHashCommandHandler $commandHandler, | ||||||
|  |     ) { } | ||||||
|  | 
 | ||||||
|  |     public function execute(Morph $morph, int $morphId, array $languages, Fields $fields): TranslateFields | ||||||
|  |     { | ||||||
|  |         $hashes = $this->hashRepository->getHashes($morph, $morphId, $languages, $fields->getNames())->all(); | ||||||
|  | 
 | ||||||
|  |         $translateFields = new TranslateFields(); | ||||||
|  |         foreach ($fields->getFields() as $fieldName => $fieldValue) { | ||||||
|  |             $textHash = $this->generateHashFromText((string) $fieldValue); | ||||||
|  |             foreach ($languages as $language) { | ||||||
|  |                 $modelHash = $hashes->where('language_id', $language)->firstWhere('field', $fieldName); | ||||||
|  |                 if ($modelHash === null) { | ||||||
|  |                     $modelHash = $this->commandHandler->handleStore([ | ||||||
|  |                         'language_id' => $language, | ||||||
|  |                         'morph_type'  => $morph, | ||||||
|  |                         'morph_id'    => $morphId, | ||||||
|  |                         'field'       => $fieldName, | ||||||
|  |                         'hash'        => $textHash, | ||||||
|  |                         'status'      => Status::Waiting, | ||||||
|  |                     ]); | ||||||
|  |                     $translateFields->add($language, $fieldName, $textHash, $modelHash->id); | ||||||
|  |                     continue; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 if ($modelHash->hash === $textHash && $this->isSuccess($modelHash)) { | ||||||
|  |                     continue; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 $translateFields->add($language, $fieldName, $textHash, $modelHash->id); | ||||||
|  |                 $this->commandHandler->handleUpdate($modelHash, [ | ||||||
|  |                     'hash'   => $textHash, | ||||||
|  |                     'status' => Status::Waiting, | ||||||
|  |                 ]); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $translateFields; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private function generateHashFromText(string $text): string | ||||||
|  |     { | ||||||
|  |         return \hash('sha256', $text); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private function isSuccess(ProjectTranslationServiceHash $modelHash): bool | ||||||
|  |     { | ||||||
|  |         if ($modelHash->status === Status::Success) { | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return ($modelHash->updated_at >= now()->subMinutes(10)); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,26 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Services\ProjectTranslationServiceHash; | ||||||
|  | 
 | ||||||
|  | use App\Enums\ProjectTranslationServiceHashes\Status; | ||||||
|  | use App\Models\ProjectTranslationServiceHash; | ||||||
|  | 
 | ||||||
|  | final readonly class ProjectTranslationServiceHashCommandHandler | ||||||
|  | { | ||||||
|  |     public function handleStore(array $data): ProjectTranslationServiceHash | ||||||
|  |     { | ||||||
|  |         return ProjectTranslationServiceHash::create($data); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function handleUpdate(ProjectTranslationServiceHash $hash, array $data): ProjectTranslationServiceHash | ||||||
|  |     { | ||||||
|  |         $hash->update($data); | ||||||
|  |         $hash->touch(); | ||||||
|  |         return $hash; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function handleSetStatusById(array $ids, Status $status): void | ||||||
|  |     { | ||||||
|  |         ProjectTranslationServiceHash::query()->whereIn('id', $ids)->update(['status' => $status->value]); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,90 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Services\Translate\Completed\Project; | ||||||
|  | 
 | ||||||
|  | use App\Dto\Service\ProjectTranslationServiceHash\Hashes; | ||||||
|  | use App\Exceptions\Services\Translate\CompletedException; | ||||||
|  | use App\Jobs\Translate\ProcessProjectDocumentationContent; | ||||||
|  | use App\Models\Documentation; | ||||||
|  | use App\Models\DocumentationContent; | ||||||
|  | use App\Repositories\DocumentationRepository; | ||||||
|  | use App\Services\ProjectTranslationService\NoTranslateAttributeHandler; | ||||||
|  | use App\Services\ProjectTranslationServiceHash\CompletionChecker; | ||||||
|  | use Illuminate\Support\Facades\DB; | ||||||
|  | use Illuminate\Support\Facades\Log; | ||||||
|  | use KorElf\TranslateLaravel\Contracts\TranslationCompletedListener; | ||||||
|  | 
 | ||||||
|  | final readonly class DocumentationContentService implements TranslationCompletedListener | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         private DocumentationRepository     $documentationRepository, | ||||||
|  |         private NoTranslateAttributeHandler $noTranslateAttributeHandler, | ||||||
|  |         private CompletionChecker           $completionChecker, | ||||||
|  |     ) { } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * @throws CompletedException | ||||||
|  |      */ | ||||||
|  |     public function onTranslationCompleted(array $translatedText, array $data = []): void | ||||||
|  |     { | ||||||
|  |         if ( | ||||||
|  |             !isset($data['projectDocumentId']) | ||||||
|  |             || !isset($data['languageId']) | ||||||
|  |             || !isset($data['hashes']) | ||||||
|  |         ) { | ||||||
|  |             throw new CompletedException('Required data is missing: projectDocumentId, languageId, or hashes.'); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $documentation = $this->documentationRepository->getDocumentationById((int) $data['projectDocumentId']); | ||||||
|  |         if ($documentation === null) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $documentationContent = DB::transaction(function () use ($data, $documentation, $translatedText) { | ||||||
|  |             $values = []; | ||||||
|  |             $hashes = $this->completionChecker->execute( | ||||||
|  |                 $this->getHashes($data['hashes']), | ||||||
|  |             ); | ||||||
|  | 
 | ||||||
|  |             foreach ($translatedText as $translatedTextKey => $translatedTextValue) { | ||||||
|  |                 if ($hashes->isStatusWaiting($translatedTextKey) !== true) { | ||||||
|  |                     continue; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 $translatedTextValue = $this->noTranslateAttributeHandler->handleRemoveAttribute($translatedTextValue); | ||||||
|  |                 $values[$translatedTextKey] = $translatedTextValue; | ||||||
|  |             } | ||||||
|  |             if (\count($values) === 0) { | ||||||
|  |                 return null; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             $documentationContent = $documentation->contents()->where('language_id', $data['languageId'])->first(); | ||||||
|  |             if ($documentationContent !== null) { | ||||||
|  |                 $documentationContent->update($values); | ||||||
|  |                 return $documentationContent; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             $values['language_id'] = $data['languageId']; | ||||||
|  |             return $documentation->contents()->create($values); | ||||||
|  |         }); | ||||||
|  |         if (\config('translation_service.enable', false) && $documentationContent !== null) { | ||||||
|  |             $this->translateContent($documentation, $documentationContent, $data); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private function translateContent(Documentation $documentation, DocumentationContent $documentationContent, array $data): void | ||||||
|  |     { | ||||||
|  |         $translateExceptLanguages = $data['exceptLanguages'] ?? []; | ||||||
|  |         $translateExceptLanguages[] = $data['languageId']; | ||||||
|  |         ProcessProjectDocumentationContent::dispatch($documentation->id, [$documentationContent->id], $translateExceptLanguages); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private function getHashes(array $hashes): Hashes | ||||||
|  |     { | ||||||
|  |         $hashesDto = new Hashes(); | ||||||
|  |         foreach ($hashes as $field => $hash) { | ||||||
|  |             $hashesDto->add((int) $hash['hashId'], $field, $hash['hash']); | ||||||
|  |         } | ||||||
|  |         return $hashesDto; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,95 @@ | |||||||
|  | <?php declare(strict_types=1); | ||||||
|  | 
 | ||||||
|  | namespace App\Services\Translate\Project; | ||||||
|  | 
 | ||||||
|  | use App\Dto\Service\ProjectTranslationServiceHash\Fields; | ||||||
|  | use App\Dto\Service\ProjectTranslationServiceHash\TranslateFields; | ||||||
|  | use App\Enums\Morph; | ||||||
|  | use App\Repositories\DocumentationRepository; | ||||||
|  | use App\Repositories\ProjectTranslationServiceRepository; | ||||||
|  | use App\ServiceResults\ServiceResultError; | ||||||
|  | use App\ServiceResults\ServiceResultSuccess; | ||||||
|  | use App\Services\ProjectTranslationService\NoTranslateAttributeHandler; | ||||||
|  | use App\Services\ProjectTranslationServiceHash\HashTrackerCommand; | ||||||
|  | use App\Services\Service; | ||||||
|  | use App\Services\Translate\Completed\Project\DocumentationContentService as DocumentationContentServiceCompleted; | ||||||
|  | use Illuminate\Support\Facades\DB; | ||||||
|  | use KorElf\TranslateLaravel\DTO\AfterTranslateDto; | ||||||
|  | use KorElf\TranslateLaravel\Facades\Translate; | ||||||
|  | 
 | ||||||
|  | final class DocumentationContentService extends Service | ||||||
|  | { | ||||||
|  |     public function __construct( | ||||||
|  |         private readonly DocumentationRepository             $documentationRepository, | ||||||
|  |         private readonly ProjectTranslationServiceRepository $projectTranslationServiceRepository, | ||||||
|  |         private readonly HashTrackerCommand                  $hashTrackerCommand, | ||||||
|  |         private readonly NoTranslateAttributeHandler         $noTranslateAttributeHandler, | ||||||
|  |     ) {} | ||||||
|  | 
 | ||||||
|  |     public function translate(int $projectDocumentId, array $contentIds, array $exceptLanguages): ServiceResultSuccess | ServiceResultError | ||||||
|  |     { | ||||||
|  |         try { | ||||||
|  |             $documentation = $this->documentationRepository->getDocumentationById($projectDocumentId); | ||||||
|  |             if ($documentation === null) { | ||||||
|  |                 return $this->errNotFound(__('Documentation not found')); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             $contents = $documentation->contents() | ||||||
|  |                 ->whereIn('id', $contentIds) | ||||||
|  |                 ->cursor(); | ||||||
|  | 
 | ||||||
|  |             $sourceLanguagesCode = []; | ||||||
|  |             foreach ($contents as $content) { | ||||||
|  |                 if (!isset($sourceLanguagesCode[$content->language_id])) { | ||||||
|  |                     $sourceLanguagesCode[$content->language_id] = $this->projectTranslationServiceRepository->getLanguageCodeByLanguageId($content->language_id) ?? 'null'; | ||||||
|  |                 } | ||||||
|  |                 $translateIntoLanguage = $this->projectTranslationServiceRepository | ||||||
|  |                     ->getLanguagesBySourceLanguage($content->language_id, $exceptLanguages) | ||||||
|  |                     ->all()->pluck('code', 'language_id')->toArray(); | ||||||
|  | 
 | ||||||
|  |                 if (empty($translateIntoLanguage)) { | ||||||
|  |                     continue; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 $fields = (new Fields()) | ||||||
|  |                     ->add('title', $content->title ?? '') | ||||||
|  |                     ->add('content', $content->content ?? ''); | ||||||
|  |                 $translations = DB::transaction(function () use ($content, $translateIntoLanguage, $fields) { | ||||||
|  |                     return $this->hashTrackerCommand->execute(Morph::DocumentationContent, $content->id, \array_keys($translateIntoLanguage), $fields); | ||||||
|  |                 }); | ||||||
|  |                 /** @var TranslateFields $translations */ | ||||||
|  |                 unset($fields); | ||||||
|  | 
 | ||||||
|  |                 foreach ($translations->getFields() as $languageId => $fields) { | ||||||
|  |                     $params = new \KorElf\TranslateLaravel\DTO\RunTranslateDto(); | ||||||
|  |                     $sourceLanguageCode = null; | ||||||
|  |                     if (isset($sourceLanguagesCode[$content->language_id]) && $sourceLanguagesCode[$content->language_id] !== 'null') { | ||||||
|  |                         $sourceLanguageCode = $sourceLanguagesCode[$content->language_id]; | ||||||
|  |                     } | ||||||
|  |                     foreach ($fields as $field) { | ||||||
|  |                         $text = $this->noTranslateAttributeHandler->handleAddAttribute((string) $content->{$field} ?? ''); | ||||||
|  |                         $params->addParamHtml($field, $text, $translateIntoLanguage[$languageId], $sourceLanguageCode); | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                     $afterTranslateDto = new AfterTranslateDto(DocumentationContentServiceCompleted::class, [ | ||||||
|  |                         'projectDocumentId' => $projectDocumentId, | ||||||
|  |                         'languageId'        => $languageId, | ||||||
|  |                         'hashes'            => $translations->getHashesByLanguage($languageId), | ||||||
|  |                         'exceptLanguages'   => $exceptLanguages, | ||||||
|  |                     ]); | ||||||
|  |                     Translate::runJob($params, $afterTranslateDto); | ||||||
|  |                     unset($params, $afterTranslateDto); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 unset($translations, $translateIntoLanguage); | ||||||
|  |             } | ||||||
|  |             unset($contents, $documentation); | ||||||
|  | 
 | ||||||
|  |         } catch (\Throwable $e) { | ||||||
|  |             \report($e); | ||||||
|  |             return $this->errService($e->getMessage()); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $this->ok(); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,36 @@ | |||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | use Illuminate\Database\Migrations\Migration; | ||||||
|  | use Illuminate\Database\Schema\Blueprint; | ||||||
|  | use Illuminate\Support\Facades\Schema; | ||||||
|  | 
 | ||||||
|  | return new class extends Migration | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Run the migrations. | ||||||
|  |      */ | ||||||
|  |     public function up(): void | ||||||
|  |     { | ||||||
|  |         Schema::create('project_translation_service_hashes', function (Blueprint $table) { | ||||||
|  |             $table->id(); | ||||||
|  |             $table->unsignedBigInteger('language_id')->index(); | ||||||
|  |             $table->foreign('language_id')->references('id')->on('project_languages'); | ||||||
|  |             $table->unsignedInteger('morph_type'); | ||||||
|  |             $table->unsignedBigInteger('morph_id'); | ||||||
|  |             $table->string('field', 255); | ||||||
|  |             $table->unsignedInteger('status')->index()->default(0); | ||||||
|  |             $table->char('hash', 64); | ||||||
|  |             $table->timestamps(); | ||||||
|  | 
 | ||||||
|  |             $table->index(['morph_type', 'morph_id']); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Reverse the migrations. | ||||||
|  |      */ | ||||||
|  |     public function down(): void | ||||||
|  |     { | ||||||
|  |         Schema::dropIfExists('project_translation_service_hashes'); | ||||||
|  |     } | ||||||
|  | }; | ||||||
| @@ -256,6 +256,7 @@ | |||||||
|     "The link has been deleted": "The link has been deleted", |     "The link has been deleted": "The link has been deleted", | ||||||
|     "Language not found": "Language not found", |     "Language not found": "Language not found", | ||||||
|     "Project not found": "Project not found", |     "Project not found": "Project not found", | ||||||
|  |     "Documentation not found": "Documentation not found", | ||||||
|     "Translations successfully updated": "Translations successfully updated", |     "Translations successfully updated": "Translations successfully updated", | ||||||
|     "allowed characters:": "allowed characters:", |     "allowed characters:": "allowed characters:", | ||||||
|     "Documentation version created successfully": "Documentation version created successfully", |     "Documentation version created successfully": "Documentation version created successfully", | ||||||
| @@ -268,5 +269,7 @@ | |||||||
|     "Category successfully created": "Category successfully created", |     "Category successfully created": "Category successfully created", | ||||||
|     "Category updated successfully": "Category updated successfully", |     "Category updated successfully": "Category updated successfully", | ||||||
|     "Category successfully deleted": "Category successfully deleted", |     "Category successfully deleted": "Category successfully deleted", | ||||||
|     "Old Files deleted": "Old Files deleted" |     "Old Files deleted": "Old Files deleted", | ||||||
|  |     "Automatic translation": "Automatic translation", | ||||||
|  |     "The settings were saved successfully": "The settings were saved successfully" | ||||||
| } | } | ||||||
|   | |||||||
| @@ -256,6 +256,7 @@ | |||||||
|     "The link has been deleted": "Ссылка удалена", |     "The link has been deleted": "Ссылка удалена", | ||||||
|     "Language not found": "Язык не найден", |     "Language not found": "Язык не найден", | ||||||
|     "Project not found": "Проект не найден", |     "Project not found": "Проект не найден", | ||||||
|  |     "Documentation not found": "Документация не найдена", | ||||||
|     "Translations successfully updated": "Переводы успешно обновлены", |     "Translations successfully updated": "Переводы успешно обновлены", | ||||||
|     "allowed characters:": "разрешенные символы:", |     "allowed characters:": "разрешенные символы:", | ||||||
|     "Documentation version created successfully": "Версия документации успешно создана", |     "Documentation version created successfully": "Версия документации успешно создана", | ||||||
| @@ -268,5 +269,7 @@ | |||||||
|     "Category successfully created": "Категория успешно создана", |     "Category successfully created": "Категория успешно создана", | ||||||
|     "Category updated successfully": "Категория успешно обновлена", |     "Category updated successfully": "Категория успешно обновлена", | ||||||
|     "Category successfully deleted": "Категория успешно удалена", |     "Category successfully deleted": "Категория успешно удалена", | ||||||
|     "Old Files deleted": "Старые файлы удалены" |     "Old Files deleted": "Старые файлы удалены", | ||||||
|  |     "Automatic translation": "Автоматический перевод", | ||||||
|  |     "The settings were saved successfully": "Настройки успешно сохранены" | ||||||
| } | } | ||||||
|   | |||||||
| @@ -29,7 +29,13 @@ | |||||||
|              role="tabpanel" aria-labelledby="language-{{ $language->id }}-tab"> |              role="tabpanel" aria-labelledby="language-{{ $language->id }}-tab"> | ||||||
|             <x-volt.forms.checkbox :title="__('Edit')" :name="'content-enable-' . $language->id" |             <x-volt.forms.checkbox :title="__('Edit')" :name="'content-enable-' . $language->id" | ||||||
|                                    :user-value="($index === 0) ? 1 : 0" class="content-enable" checkbox-value="1" |                                    :user-value="($index === 0) ? 1 : 0" class="content-enable" checkbox-value="1" | ||||||
|                                    notCheckedValue="0"/> |                                    not-checked-value="0"/> | ||||||
|  |             @if($serviceTranslationEnable) | ||||||
|  |                 <x-volt.forms.checkbox :title="__('Automatic translation')" :name="'translate-automatically[' . $language->id . ']'" | ||||||
|  |                                        :user-value="1" class="language-content" checkbox-value="1" | ||||||
|  |                                        not-checked-value="0"/> | ||||||
|  |             @endif | ||||||
|  | 
 | ||||||
|             <x-volt.forms.input :title="__('validation.attributes.title')" |             <x-volt.forms.input :title="__('validation.attributes.title')" | ||||||
|                                 :name="'content[' . $language->id . '][title]'" type="text" class="language-content" |                                 :name="'content[' . $language->id . '][title]'" type="text" class="language-content" | ||||||
|                                 :disabled="$index !== 0" :value="$content?->title ?? ''" required/> |                                 :disabled="$index !== 0" :value="$content?->title ?? ''" required/> | ||||||
|   | |||||||
| @@ -40,6 +40,15 @@ | |||||||
|                                 @endif |                                 @endif | ||||||
|                             </td> |                             </td> | ||||||
|                             <td> |                             <td> | ||||||
|  |                                 @if($serviceTranslationEnable) | ||||||
|  |                                     @can('settingUpAutomaticTranslation', $project) | ||||||
|  |                                         <a href="{{ route('admin.projects.service-translate.view', $project)  }}" class="btn btn-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="{{ __('Automatic translation') }}"> | ||||||
|  |                                             <svg class="align-text-top" width="16" height="16" data-slot="icon" fill="none" stroke-width="1.5" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"> | ||||||
|  |                                                 <path stroke-linecap="round" stroke-linejoin="round" d="m10.5 21 5.25-11.25L21 21m-9-3h7.5M3 5.621a48.474 48.474 0 0 1 6-.371m0 0c1.12 0 2.233.038 3.334.114M9 5.25V3m3.334 2.364C11.176 10.658 7.69 15.08 3 17.502m9.334-12.138c.896.061 1.785.147 2.666.257m-4.589 8.495a18.023 18.023 0 0 1-3.827-5.802"></path> | ||||||
|  |                                             </svg> | ||||||
|  |                                         </a> | ||||||
|  |                                     @endcan | ||||||
|  |                                 @endif | ||||||
|                                 <a href="{{ route('admin.projects.edit', $project)  }}" class="btn btn-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="{{ __('Edit') }}"> |                                 <a href="{{ route('admin.projects.edit', $project)  }}" class="btn btn-primary" data-bs-toggle="tooltip" data-bs-placement="top" title="{{ __('Edit') }}"> | ||||||
|                                     <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="align-text-top" viewBox="0 0 16 16"> |                                     <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="align-text-top" viewBox="0 0 16 16"> | ||||||
|                                         <path d="M12.854.146a.5.5 0 0 0-.707 0L10.5 1.793 14.207 5.5l1.647-1.646a.5.5 0 0 0 0-.708l-3-3zm.646 6.061L9.793 2.5 3.293 9H3.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.207l6.5-6.5zm-7.468 7.468A.5.5 0 0 1 6 13.5V13h-.5a.5.5 0 0 1-.5-.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.5-.5V10h-.5a.499.499 0 0 1-.175-.032l-.179.178a.5.5 0 0 0-.11.168l-2 5a.5.5 0 0 0 .65.65l5-2a.5.5 0 0 0 .168-.11l.178-.178z"/> |                                         <path d="M12.854.146a.5.5 0 0 0-.707 0L10.5 1.793 14.207 5.5l1.647-1.646a.5.5 0 0 0 0-.708l-3-3zm.646 6.061L9.793 2.5 3.293 9H3.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.207l6.5-6.5zm-7.468 7.468A.5.5 0 0 1 6 13.5V13h-.5a.5.5 0 0 1-.5-.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.5-.5V10h-.5a.499.499 0 0 1-.175-.032l-.179.178a.5.5 0 0 0-.11.168l-2 5a.5.5 0 0 0 .65.65l5-2a.5.5 0 0 0 .168-.11l.178-.178z"/> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user