# Localization Storefront routes can be locale-prefixed (`/el/proionta`, `/en/products`) using a middleware that reads directly from Lunar's `languages` table — the same table the Filament **Languages** resource manages, so there's no separate locale config to keep in sync. --- ## Why prefix every locale, including the default Leaving the default locale bare at the root (`/proionta` for Greek, `/en/products` for English) creates ambiguity: is `/` the language-neutral homepage or specifically the Greek version? It also complicates `hreflang` (needs a self-referencing tag on the root plus a possibly-duplicate `x-default`) and risks duplicate content if a bot or campaign link reaches the root without a language signal. Prefixing every locale avoids this: every URL unambiguously declares its language, `hreflang` tags are symmetrical, and adding a locale later requires no URL restructuring. --- ## Opt-in, not global The middleware is registered as a **named alias** (`locale`), not pushed onto the `web` middleware group. Apply it explicitly to the route group(s) that make up your storefront: ```php // routes/web.php use Illuminate\Support\Facades\Route; Route::middleware('locale')->group(function () { Route::get('/{locale}', HomeController::class); Route::get('/{locale}/proionta', ProductIndexController::class); Route::get('/{locale}/proionta/{slug}', ProductShowController::class); }); ``` It is **not** applied automatically because storefront routes aren't the only routes living under `web` in a shop: - The Filament admin panel (`/boboko*`, see `PanelServiceProvider`) has its own routing/auth concerns and must never be locale-redirected. - Livewire's internal update endpoint (`/livewire/update`) must resolve without a locale prefix. - Webhooks, health checks, and other non-storefront routes shouldn't be touched. If a shop's entire `web.php` *is* the storefront, wrapping the whole file in the group above is fine — just keep admin/Livewire/webhook routes registered outside of it (as they already are). --- ## Behavior `Modules\Core\Localization\LocaleMiddleware`: 1. Reads the first path segment (`request()->segment(1)`). 2. Matches it against `Lunar\Models\Language::code`. - **Match** — `App::setLocale($code)` is set, and `locale` / `language` request attributes are populated for controllers/views to use. - **No match** (missing, wrong, or unknown segment) — redirects to the same path prefixed with a resolved locale: - the best match from the `Accept-Language` header against available language codes, or - the language flagged `default` in the `languages` table, or - 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. --- ## Reading the resolved locale/language downstream ```php // In a controller or view composer $locale = $request->attributes->get('locale'); // e.g. "el" $language = $request->attributes->get('language'); // Lunar\Models\Language instance ``` Use `$language->id` when querying Lunar's translatable content (e.g. `Url::where('language_id', ...)`). --- ## Single-language shops If a shop has only one row in `languages`, the middleware still enforces the prefix (e.g. every 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.