Skip to content

PR-2: Implement Accept-Language Middleware & Translate Existing Strings #87

@kevalyq

Description

@kevalyq

📌 Parent Epic

#83

🎯 Goal

Implement locale detection middleware that reads Accept-Language header and sets Laravel's application locale accordingly. Extract existing translatable strings, translate them via Translation.io, and ensure all user-facing text is properly internationalized.

📋 Implementation Tasks

  • Create SetLocaleFromHeader middleware
  • Register middleware in api middleware group
  • Test middleware with different Accept-Language headers
  • Extract translatable strings from existing code:
    • Validation error messages
    • Authentication error messages
    • Email templates (password reset)
    • API response messages
  • Run php artisan translation:sync to push strings to Translation.io
  • Translate strings to German in Translation.io dashboard
  • Pull translations with php artisan translation:sync
  • Add tests verifying translations work correctly
  • Update CHANGELOG.md with entry in [Unreleased] → Added section
  • All tests passing (≥80% coverage maintained)
  • PHPStan and Pint checks passing

✅ Acceptance Criteria

  • Middleware correctly parses Accept-Language header
  • Locale is set to de when header contains de with highest priority
  • Locale defaults to en when header is missing or invalid
  • All existing user-facing strings are translatable
  • German translations are complete and functional
  • Tests verify locale switching works correctly
  • Tests verify translations are used (not hardcoded English)
  • No breaking changes to existing API endpoints
  • All tests passing (≥80% coverage)
  • PHPStan level max passing
  • Laravel Pint passing
  • CHANGELOG.md updated
  • LOC count ≤ 600 (excluding tests, or marked with large-pr-approved label if needed)

🔗 Dependencies

📝 Technical Notes

Middleware Implementation

// app/Http/Middleware/SetLocaleFromHeader.php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;

class SetLocaleFromHeader
{
    public function handle(Request $request, Closure $next)
    {
        $supportedLocales = ['en', 'de'];
        
        // Get preferred language from Accept-Language header
        $locale = $request->getPreferredLanguage($supportedLocales);
        
        // Fall back to default if no match
        App::setLocale($locale ?: config('app.locale'));
        
        return $next($request);
    }
}

Registration

// bootstrap/app.php or app/Http/Kernel.php
->withMiddleware(function (Middleware $middleware) {
    $middleware->api(append: [
        \App\Http\Middleware\SetLocaleFromHeader::class,
    ]);
})

Translation Workflow

  1. Mark strings as translatable using Laravel's __() helper:

    // Before
    'message' => 'Email is required'
    
    // After  
    'message' => __('validation.required', ['attribute' => __('attributes.email')])
  2. Run sync to extract strings:

    ddev exec php artisan translation:sync
  3. Translate in Translation.io dashboard: https://translation.io/secpal/api

  4. Pull translations back:

    ddev exec php artisan translation:sync

Areas to Translate

  • app/Http/Controllers/AuthController.php - Error messages
  • resources/lang/en/validation.php - Laravel validation messages (if custom)
  • resources/views/emails/ - Email templates
  • Any API response messages with user-facing text

🧪 Testing Strategy

test('locale is set from Accept-Language header', function () {
    $response = $this->withHeader('Accept-Language', 'de-DE,de;q=0.9')
        ->getJson('/api/v1/health');
    
    expect(App::getLocale())->toBe('de');
});

test('validation errors are translated to German', function () {
    $response = $this->withHeader('Accept-Language', 'de')
        ->postJson('/api/v1/auth/token', []);
    
    $response->assertStatus(422);
    // Verify German error message
    expect($response->json('errors.email.0'))
        ->toContain('E-Mail'); // German: "Das E-Mail-Feld ist erforderlich"
});

test('default locale is English when header is missing', function () {
    $response = $this->postJson('/api/v1/auth/token', []);
    
    expect(App::getLocale())->toBe('en');
});

test('password reset email is translated', function () {
    Queue::fake();
    
    $user = User::factory()->create(['email' => 'test@example.com']);
    
    $this->withHeader('Accept-Language', 'de')
        ->postJson('/api/v1/auth/password/reset-request', [
            'email' => 'test@example.com',
        ]);
    
    // Assert email queued with German locale
    // Verify email content contains German text
});

📝 PR Linking Instructions

When creating the PR for this sub-issue, use this in your PR description:

Fixes #<this-sub-issue-number>
Part of: #83

⚠️ Important:

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Status

    ✅ Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions