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>
This commit is contained in:
124
app/Support/TranslatablePayload.php
Normal file
124
app/Support/TranslatablePayload.php
Normal file
@@ -0,0 +1,124 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user