Feature: Adding Translation Service and Translation Seeder

This commit is contained in:
2026-08-06 12:06:55 +03:00
parent 8646dca16a
commit 1857ced3a2
22 changed files with 650 additions and 27 deletions
+78 -15
View File
@@ -63,10 +63,35 @@ fine — just keep admin/Livewire/webhook routes registered outside of it (as th
- the first language row, as a last resort.
The language list is cached with `Cache::rememberForever()` under `core.localization.languages`
and invalidated automatically by `Modules\Core\Localization\LanguageCacheObserver`, which
observes `Lunar\Models\Language` `saved`/`deleted` events. Adding, editing, or removing a
language via the Filament **Languages** resource clears the cache immediately — no TTL, no
stale reads.
and invalidated automatically. Adding, editing, or removing a language via the Filament
**Languages** resource clears the cache immediately — no TTL, no stale reads.
### How invalidation is wired (event-driven, not the observer itself)
`Modules\Core\Localization\LanguageCacheObserver` observes `Lunar\Models\Language`'s
`created`/`updated`/`deleted` Eloquent events, but it's a thin trigger only — it doesn't do any
invalidation work itself. It dispatches one of three events from
`Modules\Core\Localization\Events` (`LanguageCreated`, `LanguageUpdated` — carrying the old
`code` — or `LanguageDeleted`), and two listeners, wired in
`Modules\Core\Providers\LocalizationServiceProvider`, react:
- **`FlushLanguageCache`** — flushes `core.localization.languages` on all three events.
- **`MigrateTranslationsForRenamedLanguage`** — `LanguageUpdated` only, and only when `code`
actually changed. A renamed `Language::code` (e.g. `el` → `gr`) would otherwise strand every
`LanguageLine`'s translated text under the old, now-unroutable key —
`getTranslationsForGroup('gr', ...)` would silently return nothing for that locale even though
the translated content still exists. This listener moves the `text.{oldCode}` key to
`text.{newCode}` on every affected `LanguageLine` row and flushes both the old and new code's
translation cache for every group touched.
Deleting a `Language` only flushes the language-list cache — `LanguageLine.text` keys for the
deleted code are left in place rather than destructively erased, in case the language is ever
re-added under the same code.
Splitting cache-flush and text-migration into separate listeners (rather than one
`LanguageCacheObserver` method doing both) mirrors the same event → listener pattern used for
`TranslationService`'s writes below — the observer only detects *what happened*, listeners own
*what to do about it*.
---
@@ -133,20 +158,58 @@ English + Greek) is seeded by `Modules\Core\Command\InstallLunarCommand` (overri
`lunar:install`), guarded by `LanguageLine::where('group', 'storefront')->exists()` — same
idempotent pattern as the rest of that command, safe to run unattended on every boot.
**Editing existing labels or adding new ones is a normal Eloquent operation**, not a
re-seed:
### Admin UI
`Modules\Core\Localization\Filament\Resources\LanguageLineResource` (registered in
`CorePlugin`, under the panel's Settings group) lists/searches/filters `language_lines` and
edits each row's `group`, `key`, and one text input per row currently in `lunar_languages` —
the locale columns are generated dynamically from `Language::query()->pluck('code')`, so adding
a third language automatically adds a third input, no resource changes needed.
### `TranslationService` — writes go through here, not the model directly
`Modules\Core\Localization\TranslationService` wraps create/update/delete on `LanguageLine` and
dispatches a domain event after each write, following this project's standard event-driven
pattern (see `modules.md`'s "Splitting Service Providers" / event-listener convention —
the same shape as `Modules\Core\Auth\Events\UserCreated`):
```php
use Spatie\TranslationLoader\LanguageLine;
use Modules\Core\Localization\TranslationService;
LanguageLine::create([
'group' => 'storefront',
'key' => 'nav.wishlist',
'text' => ['en' => 'Wishlist', 'el' => 'Λίστα Επιθυμιών'],
app(TranslationService::class)->create('storefront', 'nav.wishlist', [
'en' => 'Wishlist',
'el' => 'Λίστα Επιθυμιών',
]);
app(TranslationService::class)->update(
$languageLine,
'storefront',
'nav.wishlist',
['en' => 'Wishlist ♥', 'el' => 'Λίστα Επιθυμιών ♥'],
);
app(TranslationService::class)->delete($languageLine);
```
There is currently no Filament resource for editing `language_lines` — labels are edited via
tinker/seeder until one is built. `LanguageLine` caches per group+locale
(`Cache::rememberForever`) and flushes itself automatically on `saved`/`deleted`, so edits take
effect immediately with no manual cache-clear step.
`update()` takes the full `group`/`key`/`text` state, not just `text` — a rename is a normal
update, not a special case. `TranslationCreated`, `TranslationUpdated` (carries the full
`{group, key, text}` snapshot from *before* the update, so a listener can tell a rename from a
text edit), and `TranslationDeleted` are dispatched from `Modules\Core\Localization\Events`. Two
listeners are wired in `Modules\Core\Providers\LocalizationServiceProvider` for all three events:
- **`FlushTranslationCache`** — `LanguageLine::boot()` already flushes the cache for the
*current* group's locales present after a save, but misses two cases on update: locales a save
*removed* from `text` (e.g. dropping the `el` key leaves `storefront.el` stale), and a changed
`group`/`key` (the *old* group's cached array is never told a row left it). This listener
flushes every group+locale combination touched by either the old or new state, so nothing —
including the group a row was renamed away from — can remain stale.
- **`LogTranslationActivity`** — records the change via `Modules\Core\Logging\ActivityLogService`
on the `lunar` activity log channel, same `created`/`updated`/`deleted` shape as every other
domain write in this project. A rename shows up in the log as an `old`/`attributes` diff across
`group`, `key`, and `text` together, not just a text diff.
The Filament resource's Create/Edit/Delete pages route through `TranslationService` (via
`handleRecordCreation`/`handleRecordUpdate`/the delete action's `->action()` override) rather
than Filament's default direct-model calls, so **every** edit made in the admin UI — including a
bare `group`/`key` rename with no `text` change — dispatches `TranslationUpdated` and is both
cache-invalidated and audit-logged.