🌐 Add full i18n system frontend + API (Story 1.3)
Nuxt i18n with lazy-loaded JSON files, localized routes, hreflang SEO tags, LanguageSwitcher component. Laravel SetLocale middleware, HasTranslations trait, API Resources and Controllers for projects/skills with Accept-Language support. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
26
api/app/Http/Controllers/Api/ProjectController.php
Normal file
26
api/app/Http/Controllers/Api/ProjectController.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\ProjectResource;
|
||||
use App\Models\Project;
|
||||
|
||||
class ProjectController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$projects = Project::with('skills')->ordered()->get();
|
||||
|
||||
return ProjectResource::collection($projects)
|
||||
->additional(['meta' => ['lang' => app()->getLocale()]]);
|
||||
}
|
||||
|
||||
public function show(string $slug)
|
||||
{
|
||||
$project = Project::with('skills')->where('slug', $slug)->firstOrFail();
|
||||
|
||||
return (new ProjectResource($project))
|
||||
->additional(['meta' => ['lang' => app()->getLocale()]]);
|
||||
}
|
||||
}
|
||||
22
api/app/Http/Controllers/Api/SkillController.php
Normal file
22
api/app/Http/Controllers/Api/SkillController.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\SkillResource;
|
||||
use App\Models\Skill;
|
||||
|
||||
class SkillController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$skills = Skill::ordered()->get()->groupBy('category');
|
||||
|
||||
$grouped = $skills->map(fn ($group) => SkillResource::collection($group));
|
||||
|
||||
return response()->json([
|
||||
'data' => $grouped,
|
||||
'meta' => ['lang' => app()->getLocale()],
|
||||
]);
|
||||
}
|
||||
}
|
||||
50
api/app/Http/Middleware/SetLocale.php
Normal file
50
api/app/Http/Middleware/SetLocale.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class SetLocale
|
||||
{
|
||||
protected array $supportedLocales = ['fr', 'en'];
|
||||
protected string $fallbackLocale = 'fr';
|
||||
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$locale = $this->parseAcceptLanguage($request->header('Accept-Language'));
|
||||
|
||||
app()->setLocale($locale);
|
||||
$request->attributes->set('locale', $locale);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
protected function parseAcceptLanguage(?string $header): string
|
||||
{
|
||||
if (!$header) {
|
||||
return $this->fallbackLocale;
|
||||
}
|
||||
|
||||
$locales = [];
|
||||
foreach (explode(',', $header) as $part) {
|
||||
$part = trim($part);
|
||||
if (preg_match('/^([a-z]{2})(?:-[A-Z]{2})?(?:;q=([0-9.]+))?$/i', $part, $matches)) {
|
||||
$lang = strtolower($matches[1]);
|
||||
$quality = isset($matches[2]) ? (float) $matches[2] : 1.0;
|
||||
$locales[$lang] = $quality;
|
||||
}
|
||||
}
|
||||
|
||||
arsort($locales);
|
||||
|
||||
foreach (array_keys($locales) as $lang) {
|
||||
if (in_array($lang, $this->supportedLocales)) {
|
||||
return $lang;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->fallbackLocale;
|
||||
}
|
||||
}
|
||||
26
api/app/Http/Resources/ProjectResource.php
Normal file
26
api/app/Http/Resources/ProjectResource.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class ProjectResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'slug' => $this->slug,
|
||||
'title' => $this->getTranslated('title_key'),
|
||||
'description' => $this->getTranslated('description_key'),
|
||||
'short_description' => $this->getTranslated('short_description_key'),
|
||||
'image' => $this->image,
|
||||
'url' => $this->url,
|
||||
'github_url' => $this->github_url,
|
||||
'date_completed' => $this->date_completed?->format('Y-m-d'),
|
||||
'is_featured' => $this->is_featured,
|
||||
'skills' => SkillResource::collection($this->whenLoaded('skills')),
|
||||
];
|
||||
}
|
||||
}
|
||||
26
api/app/Http/Resources/SkillResource.php
Normal file
26
api/app/Http/Resources/SkillResource.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class SkillResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'slug' => $this->slug,
|
||||
'name' => $this->getTranslated('name_key'),
|
||||
'description' => $this->getTranslated('description_key'),
|
||||
'icon' => $this->icon,
|
||||
'category' => $this->category,
|
||||
'max_level' => $this->max_level,
|
||||
'pivot' => $this->when($this->pivot, fn () => [
|
||||
'level_before' => $this->pivot->level_before,
|
||||
'level_after' => $this->pivot->level_after,
|
||||
]),
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user