Compare commits

..
4 Commits
5 changed files with 69 additions and 12 deletions
+10
View File
@@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [0.5.2] - 2026-08-26
### Fixed
- `Modules\Core\Localization\LocaleMiddleware`'s shared view data only ever surfaced a single alternate locale (`altLocale`/`altLocaleUrl`, found via `firstWhere('code', '!=', $current)`) — correct by coincidence for a 2-language store, but silently dropped every locale past the first "other" one found for a 3+ language store, with no error. Replaced with `altLocales`, a collection of every other configured language (`code`, `name`, `url` for the current route each), so a language switcher or `hreflang` tags scale to any number of locales. Documented in `docs/localization.md` ("Shared view data — language switcher and `hreflang` tags").
## [0.5.1] - 2026-08-25
### Added
- `Modules\Core\Search\ProductIndexer` now indexes `channel_ids` (filterable) — Lunar's base indexer only marks `status` as filterable, not channel assignment, so storefront search couldn't otherwise scope results to products actually assigned and enabled on the current sales channel. Computed from `$product->channels()->wherePivot('enabled', true)`. Ported from an older `Products` branch whose remote had been deleted; the branch's other, now-superseded `ProductIndexer` changes were dropped in favor of the richer indexer already on `master` (collections, price, variants, reviews — see `0.5.0`).
## [0.5.0] - 2026-08-24
### Added
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "boboko/core",
"description": "Core module — authentication and shared panel behaviour",
"type": "library",
"version": "0.5.0",
"version": "0.5.2",
"autoload": {
"psr-4": {
"Modules\\Core\\": "src/"
+27
View File
@@ -105,6 +105,33 @@ $language = $request->attributes->get('language'); // Lunar\Models\Language in
Use `$language->id` when querying Lunar's translatable content (e.g. `Url::where('language_id', ...)`).
### Shared view data — language switcher and `hreflang` tags
The middleware also shares two variables with every view, via `View::share()`, so a layout's
language switcher or `hreflang` tags don't have to recompute the language list themselves:
```blade
{{-- current locale --}}
{{ $currentLocale }} {{-- e.g. "el" --}}
{{-- every OTHER configured language, each with its own URL for the current page --}}
@foreach ($altLocales as $altLocale)
<a href="{{ $altLocale['url'] }}" hreflang="{{ $altLocale['code'] }}">{{ $altLocale['name'] }}</a>
@endforeach
```
`$altLocales` is a **collection**, not a single value — deliberately, so it scales to any number
of configured languages rather than assuming exactly two. Each entry is a plain array:
| Key | Description |
|---|---|
| `code` | The language's `Lunar\Models\Language::code` (e.g. `en`) |
| `name` | The language's display name |
| `url` | The **current route**, re-generated with that language's code — via `route($routeName, [...])` when the current request matched a named route, or a bare `/{code}` fallback otherwise |
A 3+ language store gets one `$altLocales` entry per additional language automatically — nothing
about this shape assumes or special-cases a two-language store.
---
## Single-language shops
+23 -11
View File
@@ -62,24 +62,36 @@ class LocaleMiddleware
}
/**
* Shares the current/alternate locale (and the alternate's URL) with all
* views, so the header language switcher and layout hreflang tags don't
* have to recompute it.
* Shares the current locale and every OTHER available locale (each with its
* own URL for the current page) with all views, so the header language
* switcher and layout hreflang tags don't have to recompute it.
*
* `altLocales` is a collection, not a single value — firstWhere('code', '!=',
* ...) would only ever surface one alternate, which happens to look correct
* with exactly 2 configured languages (there's only one "other" to find) but
* silently drops every locale past the first for a 3+ language store, with no
* error, just fewer switcher options than actually configured. A view iterates
* `$altLocales` to render as many links/dropdown entries as there are
* alternates, whether that's 1 or 10.
*/
private function shareLocaleViewData(Request $request, Language $language, Collection $languages): void
{
$altLanguage = $languages->firstWhere('code', '!=', $language->code);
$route = $request->route();
$routeName = $route?->getName();
$altLocales = $languages
->reject(fn (Language $other) => $other->code === $language->code)
->map(fn (Language $other) => [
'code' => $other->code,
'name' => $other->name,
'url' => $routeName
? route($routeName, array_merge($route->parameters(), ['locale' => $other->code]))
: url('/'.$other->code),
])
->values();
View::share('currentLocale', $language->code);
View::share('altLocale', $altLanguage?->code);
View::share(
'altLocaleUrl',
$altLanguage && $routeName
? route($routeName, array_merge($route->parameters(), ['locale' => $altLanguage->code]))
: ($altLanguage ? url('/'.$altLanguage->code) : null),
);
View::share('altLocales', $altLocales);
}
private function redirectToLocalizedUrl(Request $request, Collection $languages): Response
+8
View File
@@ -27,6 +27,9 @@ use Spatie\MediaLibrary\MediaCollections\Models\Media;
* - reviews: public-safe fields only (see mapReview() — reviewer_email is deliberately
* excluded, it's PII with no storefront use), including staff replies, plus an
* average rating
* - channel_ids (filterable) — Lunar's base indexer only indexes "status" as
* filterable, not channel assignment, so search results can't otherwise be
* scoped to products actually assigned+enabled on the current sales channel
*
* A review is created/edited independently of its product (Modules\Core\Providers\
* ReviewServiceProvider re-indexes the product on review create/update/delete), so
@@ -49,6 +52,7 @@ class ProductIndexer extends BaseProductIndexer
'collections',
'price',
'slugs',
'channel_ids',
];
}
@@ -83,6 +87,10 @@ class ProductIndexer extends BaseProductIndexer
$data['reviews'] = $reviews->map(fn (ProductReview $review) => $this->mapReview($review))->all();
$data['review_count'] = $reviews->count();
$data['average_rating'] = $reviews->isEmpty() ? null : round($reviews->avg('rating'), 1);
$data['channel_ids'] = $model->channels()
->wherePivot('enabled', true)
->pluck('id')
->toArray();
return $data;
}