diff --git a/composer.json b/composer.json index 84768af..09a1774 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,8 @@ "symfony/yaml": "^7.0", "lunarphp/table-rate-shipping": "^1.3", "lunarphp/search": "*", - "lunarphp/meilisearch": "*" + "lunarphp/meilisearch": "*", + "spatie/laravel-translation-loader": "^2.8" }, "require-dev": { "fakerphp/faker": "^1.23", @@ -39,7 +40,8 @@ }, "config": { "allow-plugins": { - "pestphp/pest-plugin": true + "pestphp/pest-plugin": true, + "php-http/discovery": true } } } diff --git a/database/migrations/2026_08_05_210535_create_language_lines_table.php b/database/migrations/2026_08_05_210535_create_language_lines_table.php new file mode 100644 index 0000000..d24d12a --- /dev/null +++ b/database/migrations/2026_08_05_210535_create_language_lines_table.php @@ -0,0 +1,34 @@ +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'); + } +}; diff --git a/docs/localization.md b/docs/localization.md index ba3c80e..bf1a7b5 100644 --- a/docs/localization.md +++ b/docs/localization.md @@ -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 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. + +--- + +## 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. diff --git a/src/Command/InstallLunarCommand.php b/src/Command/InstallLunarCommand.php index f74b925..e07d3a3 100644 --- a/src/Command/InstallLunarCommand.php +++ b/src/Command/InstallLunarCommand.php @@ -18,6 +18,7 @@ use Lunar\Models\Product; use Lunar\Models\ProductType; use Lunar\Models\TaxClass; use Lunar\Models\TaxZone; +use Spatie\TranslationLoader\LanguageLine; /** * 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->call('filament:assets'); $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, + ]); + } + } }