<?php
 namespace App\Services\PageBuilder; use App\Models\Page; use App\Models\PageRevision; use App\PageBuilder\ComponentRegistry; use App\Services\TranslationService; use Illuminate\Support\Facades\DB; use Illuminate\Support\Str; class PageContentTranslator { private const NON_TRANSLATABLE_PATTERNS = [ '/^icon$/i', '/icon_class$/i', '/^fa_/i', '/_url$/i', '/^url$/i', '/^link$/i', '/_link$/i', '/^slug$/i', '/^anchor$/i', '/^css_class$/i', '/^image$/i', '/_image$/i', '/^src$/i', '/^embed$/i', '/^code$/i', '/^target$/i', ]; public function __construct( private readonly ComponentRegistry $registry, private readonly TranslationService $translator, ) {} public function translatePage(Page $source, string $targetLocale, ?int $adminId = null): Page { $target = Page::where('translation_group_id', $source->translation_group_id) ->where('locale', $targetLocale) ->first(); return DB::transaction(function () use ($source, $target, $targetLocale, $adminId) { if (! $target) { $target = Page::create([ 'brand_id' => $source->brand_id, 'translation_group_id' => $source->translation_group_id, 'locale' => $targetLocale, 'slug' => $source->slug.'-'.$targetLocale, 'title' => $source->title, 'content' => ['blocks' => []], 'status' => 'draft', 'is_home' => false, 'created_by_admin_id' => $adminId, ]); } else { PageRevision::create([ 'page_id' => $target->id, 'content' => $target->content, 'title' => $target->title, 'slug' => $target->slug, 'meta_title' => $target->meta_title, 'meta_description' => $target->meta_description, 'meta_keywords' => $target->meta_keywords, 'og_image' => $target->og_image, 'canonical_url' => $target->canonical_url, 'robots' => $target->robots, 'schema_json' => $target->schema_json, 'status' => $target->status, 'created_by_admin_id' => $adminId, 'created_at' => now(), ]); } $strings = $this->collectStrings($source); if (empty($strings)) { $target->save(); return $target->fresh(); } $sourceLocale = $source->locale; $translated = $this->translator->translateBatch( array_column($strings, 'text'), $targetLocale, $sourceLocale, ); $newContent = $source->content ?? ['blocks' => []]; $metaUpdates = []; foreach ($strings as $idx => $entry) { $value = $translated[$idx] ?? $entry['text']; if ($entry['kind'] === 'meta') { $metaUpdates[$entry['field']] = $value; } elseif ($entry['kind'] === 'block') { $this->setByPath($newContent['blocks'][$entry['block_idx']]['data'], $entry['path'], $value); } } $target->fill([ 'title' => $metaUpdates['title'] ?? $target->title, 'meta_title' => $metaUpdates['meta_title'] ?? $target->meta_title, 'meta_description' => $metaUpdates['meta_description'] ?? $target->meta_description, 'meta_keywords' => $metaUpdates['meta_keywords'] ?? $target->meta_keywords, 'content' => $newContent, ]); if ($adminId) $target->updated_by_admin_id = $adminId; $target->save(); return $target->fresh(); }); } public function collectStrings(Page $page): array { $out = []; foreach (['title', 'meta_title', 'meta_description', 'meta_keywords'] as $f) { $val = $page->{$f}; if (is_string($val) && trim($val) !== '') { $out[] = ['kind' => 'meta', 'field' => $f, 'text' => $val]; } } $blocks = $page->content['blocks'] ?? []; foreach ($blocks as $i => $block) { $type = $block['type'] ?? null; $data = $block['data'] ?? []; if (! $type) continue; $component = $this->registry->resolve($type); if (! $component) continue; $schema = $component->schema(); $this->walkSchema($schema, $data, [], function (array $path, string $text) use (&$out, $i) { $out[] = [ 'kind' => 'block', 'block_idx' => $i, 'path' => $path, 'text' => $text, ]; }); } return $out; } private function walkSchema(array $schema, array $data, array $path, callable $emit): void { foreach ($schema as $key => $def) { if ($key === 'options') continue; if (! is_array($def)) continue; $type = $def['type'] ?? null; $value = $data[$key] ?? null; $newPath = [...$path, $key]; if ($type === 'repeater') { if (! is_array($value)) continue; $itemSchema = $def['item_schema'] ?? $def['fields'] ?? []; if (! is_array($itemSchema)) continue; foreach ($value as $idx => $item) { if (! is_array($item)) continue; $this->walkSchema($itemSchema, $item, [...$newPath, $idx], $emit); } continue; } if (in_array($type, ['text', 'textarea', 'richtext', 'wysiwyg'], true)) { if (($def['translatable'] ?? null) === false) continue; if ($this->isFieldNonTranslatable($key)) continue; if (is_string($value) && trim($value) !== '') { $emit($newPath, $value); } } } } private function isFieldNonTranslatable(string $name): bool { foreach (self::NON_TRANSLATABLE_PATTERNS as $pattern) { if (preg_match($pattern, $name)) return true; } return false; } private function setByPath(array &$container, array $path, mixed $value): void { $ref = &$container; $last = count($path) - 1; foreach ($path as $i => $segment) { if ($i === $last) { $ref[$segment] = $value; return; } if (! isset($ref[$segment]) || ! is_array($ref[$segment])) { $ref[$segment] = []; } $ref = &$ref[$segment]; } } } 