Englische Fassung der Markenseiten: Merkmale, Leistungen, Abschnitte, Showroom, Bildunterschriften

Diese Markenfelder waren nur einsprachig gespeichert und erschienen auf der englischen Seite
deutsch. Neue Spalte cms_brands.field_translations (additiv) trägt die Übersetzungen je Sprache,
die bisherigen Spalten bleiben die deutsche Fassung; ohne Übersetzung gilt weiter Deutsch.
Marken-Editor bearbeitet die Felder über den DE/EN-Umschalter (Bilder und Rubriken bleiben
sprachübergreifend). BrandTranslationsSeeder spielt die englischen Texte der vier Kernmarken
ein, ohne gepflegte Übersetzungen zu überschreiben. php artisan cms:untranslated listet
Inhalte, die in einer Sprache noch fehlen oder deutsch sind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-10-09 10:08:03 +00:00
parent dc31cdb401
commit 8ddc85422e
8 changed files with 982 additions and 34 deletions

View file

@ -28,6 +28,15 @@ class CmsBrand extends Model
/** @var list<string> */
public const GALLERY_CATEGORIES = [self::GALLERY_PROJECT, self::GALLERY_INSPIRATION, self::GALLERY_COMPANY];
/** Sprache der Spalten highlights, services, sections, showroom und der Bildunterschriften. */
public const BASE_LOCALE = 'de';
/**
* Felder, die nur einsprachig als Spalte existieren und ihre Übersetzungen in `field_translations`
* tragen (09.10.2026). `gallery_captions` sind die Bildunterschriften, nach Bild-Datei.
*/
public const LOCALIZED_FIELDS = ['highlights', 'services', 'sections', 'showroom', 'gallery_captions'];
protected $table = 'cms_brands';
protected $fillable = [
@ -46,6 +55,7 @@ class CmsBrand extends Model
'cta_title',
'cta_text',
'gallery',
'field_translations',
'website_url',
'showroom',
'origin_city',
@ -77,6 +87,7 @@ class CmsBrand extends Model
'services' => 'array',
'sections' => 'array',
'gallery' => 'array',
'field_translations' => 'array',
'is_published' => 'boolean',
'order' => 'integer',
];
@ -359,6 +370,87 @@ class CmsBrand extends Model
/**
* @return array<string, mixed>
*/
/**
* Ein einsprachiges Feld in der gewünschten Sprache: die Übersetzung aus `field_translations`,
* sonst die deutsche Spalte. `gallery_captions` liefert Bild-Datei => Unterschrift.
*/
public function localized(string $field, ?string $locale = null): mixed
{
$locale ??= app()->getLocale();
$base = $field === 'gallery_captions'
? collect(self::normalizeGallery($this->gallery))->mapWithKeys(fn (array $item) => [$item['image'] => $item['caption']])->all()
: $this->getAttribute($field);
if ($locale === self::BASE_LOCALE) {
return $base;
}
$translated = $this->field_translations[$locale][$field] ?? null;
if ($field === 'gallery_captions') {
foreach ($base as $image => $caption) {
if (filled($translated[$image] ?? null)) {
$base[$image] = (string) $translated[$image];
}
}
return $base;
}
return filled($translated) ? $translated : $base;
}
/**
* Schreibt ein einsprachiges Feld: Deutsch in die Spalte, andere Sprachen nach `field_translations`.
* Eine Übersetzung, die genau dem deutschen Stand entspricht, wird nicht gespeichert – sonst
* gälte deutscher Text als übersetzt.
*/
public function setLocalized(string $field, string $locale, mixed $value): void
{
if ($locale === self::BASE_LOCALE) {
if ($field !== 'gallery_captions') {
$this->setAttribute($field, $value);
}
return;
}
$translations = $this->field_translations ?? [];
$base = $this->localized($field, self::BASE_LOCALE);
if ($field === 'gallery_captions') {
$value = array_filter(
(array) $value,
fn (mixed $caption, string $image): bool => filled($caption) && $caption !== ($base[$image] ?? null),
ARRAY_FILTER_USE_BOTH,
);
}
if (blank($value) || $value === $base) {
unset($translations[$locale][$field]);
} else {
$translations[$locale][$field] = $value;
}
$translations = array_filter($translations, fn (mixed $fields): bool => filled($fields));
$this->field_translations = $translations === [] ? null : $translations;
}
/**
* Bildergalerie in der gewünschten Sprache (Bilder und Rubriken sind sprachunabhängig).
*
* @return list<array{image: string, category: string, caption: string}>
*/
public function localizedGallery(?string $locale = null): array
{
$captions = $this->localized('gallery_captions', $locale);
return array_map(
fn (array $item): array => [...$item, 'caption' => (string) ($captions[$item['image']] ?? $item['caption'])],
self::normalizeGallery($this->gallery),
);
}
public function toFrontendArray(): array
{
return [
@ -368,18 +460,18 @@ class CmsBrand extends Model
'tagline' => $this->tagline,
'teaser' => $this->teaser,
'summary' => $this->summary,
'highlights' => $this->highlights ?? [],
'highlights' => $this->localized('highlights') ?? [],
'description' => $this->description,
'logo' => $this->logo,
'image' => $this->image,
'services' => $this->services ?? [],
'sections' => self::normalizeSections($this->sections),
'services' => $this->localized('services') ?? [],
'sections' => self::normalizeSections($this->localized('sections')),
'widget_label' => (string) $this->widget_label,
'cta_title' => (string) $this->cta_title,
'cta_text' => (string) $this->cta_text,
'gallery' => self::normalizeGallery($this->gallery),
'gallery' => $this->localizedGallery(),
'website_url' => $this->website_url,
'showroom' => $this->showroom,
'showroom' => $this->localized('showroom'),
'origin_city' => (string) $this->origin_city,
'origin_distance' => (string) $this->origin_distance,
'origin_role' => (string) $this->origin_role,