Files
landing-api/app/Support/TranslatablePayload.php
Josep Piguillem 3897113313 Initial commit: Pireon Admin — headless admin panel and public API
Laravel 13 + Filament 5 admin panel and REST API for a non-profit developer
collective, backed by the Pireon OAuth identity server. The public site is a
separate, server-rendered application consuming /api/public/v1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 17:55:27 +02:00

125 lines
3.8 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Support;
use Illuminate\Database\Eloquent\Model;
/**
* Translates between the API's JSON shape and the locale => value maps that
* spatie/laravel-translatable stores.
*
* Responses resolve each translatable field to the request's active locale, so
* a plain client sees plain strings. Clients that need every language ask for
* it explicitly with `?with=translations` and get a `translations` object
* alongside.
*
* Requests accept either shape: a bare string writes the active locale only
* and leaves other languages untouched, while an object writes exactly the
* locales it names.
*/
final class TranslatablePayload
{
/**
* Field values resolved to a single locale, with the configured fallback
* filling any gaps.
*
* @param array<int, string> $fields
* @return array<string, string|null>
*/
public static function resolve(Model $model, array $fields, ?string $locale = null): array
{
$locale ??= app()->getLocale();
$fallback = (string) config('locales.fallback');
$resolved = [];
foreach ($fields as $field) {
$value = $model->getTranslation($field, $locale, useFallbackLocale: false);
if (blank($value) && $locale !== $fallback) {
$value = $model->getTranslation($field, $fallback, useFallbackLocale: false);
}
$resolved[$field] = blank($value) ? null : $value;
}
return $resolved;
}
/**
* The full locale => value map for each field, limited to supported
* locales so retired languages do not leak into responses.
*
* @param array<int, string> $fields
* @return array<string, array<string, string>>
*/
public static function all(Model $model, array $fields): array
{
$supported = array_keys(config('locales.supported'));
$translations = [];
foreach ($fields as $field) {
$translations[$field] = array_filter(
$model->getTranslations($field),
fn (string $locale) => in_array($locale, $supported, true),
ARRAY_FILTER_USE_KEY,
);
}
return $translations;
}
/**
* Normalise an incoming value into a locale => value map.
*
* @param string|array<string, string|null> $value
* @return array<string, string|null>
*/
public static function normalise(string|array $value, ?string $locale = null): array
{
if (is_string($value)) {
return [$locale ?? app()->getLocale() => $value];
}
$supported = array_keys(config('locales.supported'));
return array_filter(
$value,
fn (string $key) => in_array($key, $supported, true),
ARRAY_FILTER_USE_KEY,
);
}
/**
* Apply validated input to a model, merging translatable fields into their
* existing maps instead of replacing them wholesale — a request that only
* sends Catalan must not wipe the Spanish text.
*
* @param array<string, mixed> $data
* @param array<int, string> $translatable
* @return array<string, mixed>
*/
public static function merge(Model $model, array $data, array $translatable): array
{
foreach ($translatable as $field) {
if (! array_key_exists($field, $data)) {
continue;
}
$incoming = self::normalise($data[$field]);
$existing = $model->exists ? $model->getTranslations($field) : [];
// A null value for a locale clears that translation.
$data[$field] = array_filter(
array_merge($existing, $incoming),
fn ($v) => filled($v),
);
}
return $data;
}
}