Feature: Adding Spatie's Translation Loader

This commit is contained in:
2026-08-06 00:13:13 +03:00
parent 93469d033f
commit 8646dca16a
4 changed files with 135 additions and 2 deletions
+4 -2
View File
@@ -16,7 +16,8 @@
"symfony/yaml": "^7.0", "symfony/yaml": "^7.0",
"lunarphp/table-rate-shipping": "^1.3", "lunarphp/table-rate-shipping": "^1.3",
"lunarphp/search": "*", "lunarphp/search": "*",
"lunarphp/meilisearch": "*" "lunarphp/meilisearch": "*",
"spatie/laravel-translation-loader": "^2.8"
}, },
"require-dev": { "require-dev": {
"fakerphp/faker": "^1.23", "fakerphp/faker": "^1.23",
@@ -39,7 +40,8 @@
}, },
"config": { "config": {
"allow-plugins": { "allow-plugins": {
"pestphp/pest-plugin": true "pestphp/pest-plugin": true,
"php-http/discovery": true
} }
} }
} }
@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Schema::create('language_lines', function (Blueprint $table) {
$table->id();
$table->string('group')->index();
$table->string('key');
$table->json('text');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
Schema::dropIfExists('language_lines');
}
};
+62
View File
@@ -88,3 +88,65 @@ If a shop has only one row in `languages`, the middleware still enforces the pre
URL under `/en/...`) rather than special-casing it away — this keeps behavior identical across URL under `/en/...`) rather than special-casing it away — this keeps behavior identical across
shops and avoids a silent restructuring if a second language is added later. If a shop genuinely shops and avoids a silent restructuring if a second language is added later. If a shop genuinely
never wants locale prefixes, don't apply the `locale` middleware to its routes at all. never wants locale prefixes, don't apply the `locale` middleware to its routes at all.
---
## Storefront UI labels (`__('storefront.*')`)
The `locale` middleware resolves *which* language a request is in — routing/redirects,
`Lunar\Models\Language`, and Lunar's own translatable product/collection content. It has nothing
to do with static UI chrome like "Cart", "Back", "Add to Cart". Those are handled separately by
[`spatie/laravel-translation-loader`](https://github.com/spatie/laravel-translation-loader),
stored in the `language_lines` table.
### Why a separate system, not another `languages`-table lookup
Lunar's translatable fields (`TranslatedText`, `Url`, etc.) are all tied to specific *model
records* — a product's name, a collection's description. UI labels aren't attached to any model;
they're static strings the app itself owns. `laravel-translation-loader` is Laravel's own
`__()`/`trans()` mechanism with a DB-backed source layered on top of the normal file-based one —
no new helper to learn, no bespoke table shape.
**Nothing existing breaks.** The package's `TranslationLoaderManager` *extends* Laravel's
`FileLoader` and merges DB translations on top of file-based ones
(`array_replace_recursive()`) — Filament's own vendor `lang/en/product.php`-style strings
keep working exactly as before. The package registers itself via Laravel's standard Composer
package auto-discovery (`extra.laravel.providers` in its own `composer.json`) — nothing needed
in `CoreServiceProvider` to wire it up.
### Usage
```blade
{{ __('storefront.nav.cart') }}
{{ __('storefront.product.add_to_cart') }}
```
`group` is `storefront` for e-shop UI labels — kept separate from Lunar/Filament's own `lunar::`
namespaced groups so nothing collides. `__()` resolves the translation for whatever
`App::getLocale()` currently is, which `LocaleMiddleware` already sets per-request (see
"Behavior" above) — no extra wiring needed between the two systems.
### Seeding
A starter set of common e-shop labels (`nav.*`, `cart.*`, `product.*`, `auth.*`, `search.*`,
English + Greek) is seeded by `Modules\Core\Command\InstallLunarCommand` (overrides Lunar's own
`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:
```php
use Spatie\TranslationLoader\LanguageLine;
LanguageLine::create([
'group' => 'storefront',
'key' => 'nav.wishlist',
'text' => ['en' => 'Wishlist', 'el' => 'Λίστα Επιθυμιών'],
]);
```
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.
+35
View File
@@ -18,6 +18,7 @@ use Lunar\Models\Product;
use Lunar\Models\ProductType; use Lunar\Models\ProductType;
use Lunar\Models\TaxClass; use Lunar\Models\TaxClass;
use Lunar\Models\TaxZone; use Lunar\Models\TaxZone;
use Spatie\TranslationLoader\LanguageLine;
/** /**
* Overrides Lunar's own lunar:install to skip the interactive prompts (migrate * Overrides Lunar's own lunar:install to skip the interactive prompts (migrate
@@ -241,9 +242,43 @@ class InstallLunarCommand extends Command
} }
}); });
if (! LanguageLine::where('group', 'storefront')->exists()) {
$this->components->info('Seeding storefront label translations');
$this->seedStorefrontLabels();
}
$this->components->info('Publishing Filament assets'); $this->components->info('Publishing Filament assets');
$this->call('filament:assets'); $this->call('filament:assets');
$this->components->info('Lunar default data seeded.'); $this->components->info('Lunar default data seeded.');
} }
private function seedStorefrontLabels(): void
{
$labels = [
'nav.home' => ['en' => 'Home', 'el' => 'Αρχική'],
'nav.products' => ['en' => 'Products', 'el' => 'Προϊόντα'],
'nav.cart' => ['en' => 'Cart', 'el' => 'Καλάθι'],
'nav.account' => ['en' => 'Account', 'el' => 'Λογαριασμός'],
'nav.back' => ['en' => 'Back', 'el' => 'Πίσω'],
'cart.empty' => ['en' => 'Your cart is empty', 'el' => 'Το καλάθι σας είναι άδειο'],
'cart.checkout' => ['en' => 'Checkout', 'el' => 'Ολοκλήρωση Παραγγελίας'],
'cart.total' => ['en' => 'Total', 'el' => 'Σύνολο'],
'cart.remove' => ['en' => 'Remove', 'el' => 'Αφαίρεση'],
'product.add_to_cart' => ['en' => 'Add to Cart', 'el' => 'Προσθήκη στο Καλάθι'],
'product.out_of_stock' => ['en' => 'Out of Stock', 'el' => 'Εξαντλήθηκε'],
'product.price' => ['en' => 'Price', 'el' => 'Τιμή'],
'auth.login' => ['en' => 'Log In', 'el' => 'Σύνδεση'],
'auth.logout' => ['en' => 'Log Out', 'el' => 'Αποσύνδεση'],
'search.placeholder' => ['en' => 'Search products…', 'el' => 'Αναζήτηση προϊόντων…'],
];
foreach ($labels as $key => $text) {
LanguageLine::create([
'group' => 'storefront',
'key' => $key,
'text' => $text,
]);
}
}
} }