stoic multilingual

This commit is contained in:
elvira
2026-09-28 18:17:33 +03:00
parent 023c11019c
commit aa051525a1
20 changed files with 696 additions and 27 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ public function __construct(private readonly ProductService $products) {}
public function index(string $locale)
{
$page = StoicPage::firstWhere('slug', 'home');
$page = StoicPage::localized('home');
if (! $page) {
abort(404);
+1 -1
View File
@@ -45,7 +45,7 @@ public function cookies(string $locale)
private function show(string $slug)
{
$page = StoicPage::firstWhere('slug', $slug);
$page = StoicPage::localized($slug);
if (! $page) {
abort(404);
+17 -1
View File
@@ -2,16 +2,32 @@
namespace App\Models;
use App\Services\Stoic;
use Illuminate\Database\Eloquent\Model;
use JacobJoergensen\LaravelPaper\Attributes\ContentPath;
use JacobJoergensen\LaravelPaper\Attributes\Driver;
use JacobJoergensen\LaravelPaper\Attributes\Timestamps;
use JacobJoergensen\LaravelPaper\Paper;
// Points at the default locale (el), which Stoic writes every field to. Other
// locales hold only the translatable fields — use localized() to get a page
// with those layered on top for the current locale.
#[Driver("markdown")]
#[ContentPath("resources/stoic/content/pages")]
#[ContentPath("resources/stoic/content/el/pages")]
#[Timestamps]
class StoicPage extends Model
{
use Paper;
/**
* The page in the current locale: the default-locale entry, with the
* current locale's non-empty fields (and body) swapped in. Untranslated
* fields fall back to the default locale.
*/
public static function localized(string $slug): ?static
{
$page = static::firstWhere('slug', $slug);
return $page?->forceFill(Stoic::localeOverrides("pages/{$slug}.md"));
}
}
+37
View File
@@ -4,6 +4,7 @@
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Storage;
use Spatie\YamlFrontMatter\YamlFrontMatter;
use Symfony\Component\Yaml\Yaml;
class Stoic
@@ -29,6 +30,42 @@ public static function presets(): array
return static::$presets;
}
// Stoic's default locale (first in i18n.locales), or null when i18n is off.
// Its content folder holds every field; other locales only translatable ones.
public static function defaultLocale(): ?string
{
static::loadConfig();
return static::$config["i18n"]["locales"][0]["code"] ?? null;
}
// The current locale's translated fields for a content file (path relative
// to a locale folder, e.g. "pages/home.md"), with the markdown body as
// `content`. Empty when the current locale is the default one (or i18n is
// off), the file doesn't exist, or a field is left blank — so layering this
// over the default-locale values falls back to them field by field.
public static function localeOverrides(string $file): array
{
$default = static::defaultLocale();
$locale = app()->getLocale();
if ($default === null || $locale === $default) {
return [];
}
$path = resource_path("stoic/content/{$locale}/{$file}");
if (!is_file($path)) {
return [];
}
$document = YamlFrontMatter::parse(file_get_contents($path));
return array_filter(
[...$document->matter(), "content" => trim($document->body())],
fn($value) => filled($value),
);
}
// Returns the public URL for a stoic image at a given preset.
public static function image(string $filename, string $preset): string
{
+44 -18
View File
@@ -6,13 +6,19 @@
/**
* Site-wide, marketing-editable settings from Stoic's `settings` singleton
* (resources/stoic/content/settings.md). Read directly rather than through a
* Paper model: Paper models map to a folder of entries, and this is one file
* at the content root. Legal and transactional details (address, ΑΦΜ, phone)
* (resources/stoic/content/{locale}/settings.md). Read directly rather than
* through a Paper model: Paper models map to a folder of entries, and this is
* one file per locale. Legal and transactional details (address, ΑΦΜ, phone)
* live in boboko-core's StoreDetails instead.
*
* Bound as scoped in AppServiceProvider, so the file is parsed once per
* request (or queued job) and edits in Stoic show up on the next one.
* Stoic writes every field to the default locale's file and only the
* translatable ones (e.g. meta_description) to the others, so the current
* locale's file is layered over the default one; empty values fall through.
*
* Bound as scoped in AppServiceProvider, so files are parsed at most once per
* locale per request (or queued job) and edits in Stoic show up on the next one.
* The locale is read per call, not at construction, so a mail rendered with
* ->locale() still gets its own locale's values.
*/
class StoicSettings
{
@@ -22,26 +28,20 @@ class StoicSettings
'tiktok' => 'TikTok',
];
private array $data;
public function __construct()
{
$path = resource_path('stoic/content/settings.md');
$this->data = is_file($path)
? YamlFrontMatter::parse(file_get_contents($path))->matter()
: [];
}
/** @var array<string, array<string, mixed>> keyed by locale */
private array $data = [];
public function get(string $key, mixed $default = null): mixed
{
return filled($this->data[$key] ?? null) ? $this->data[$key] : $default;
$value = $this->data()[$key] ?? null;
return filled($value) ? $value : $default;
}
/**
* Absolute og:image URL for a Stoic image field value (the page's own, else
* this file's default), or null. Image fields store a comma-separated list;
* only the first is used.
* the default from settings.md), or null. Image fields store a
* comma-separated list; only the first is used.
*/
public function ogImageUrl(?string $pageImage = null): ?string
{
@@ -67,4 +67,30 @@ public function socialLinks(): array
return $links;
}
private function data(): array
{
$locale = app()->getLocale();
return $this->data[$locale] ??= $this->load();
}
private function load(): array
{
$default = Stoic::defaultLocale();
if ($default === null) {
return $this->parse(resource_path('stoic/content/settings.md'));
}
return array_merge(
$this->parse(resource_path("stoic/content/{$default}/settings.md")),
Stoic::localeOverrides('settings.md'),
);
}
private function parse(string $path): array
{
return is_file($path) ? YamlFrontMatter::parse(file_get_contents($path))->matter() : [];
}
}