Laravel多语言API怎么设计

wen PHP项目 4

Laravel多语言API设计:从入门到企业级架构的完整指南

目录导读

  1. 为什么需要多语言API?
  2. 数据库设计:三种主流方案对比
  3. 中间件实现:动态语言切换机制
  4. 翻译文件与Eloquent模型结合
  5. API响应格式化与错误处理
  6. 缓存策略与性能优化
  7. 测试与多语言回退规则
  8. 常见问题问答

为什么需要多语言API?

全球化应用要求API返回的内容(如商品名、错误信息、动态文案)能根据用户的语言偏好自动切换,Laravel内置的Lang类虽然方便,但直接用在API中会遇到两个核心挑战:

Laravel多语言API怎么设计

  • 动态资源:数据库存储的多语言字段(如title_entitle_zh)需要高效查询。
  • 请求上下文:用户语言可能来自Header(Accept-Language)、URL参数或用户设置。

设计原则:将语言逻辑抽象为中间件+翻译器层,避免控制器里写满if-else判断。


数据库设计:三种主流方案对比

方案A:每张表加多语言字段(简单但扩展差)

-- 不推荐,新增语言需要改表结构
CREATE TABLE products (
    id INT,en VARCHAR(255),zh VARCHAR(255),
    description_en TEXT,
    description_zh TEXT
);

方案B:JSON字段存储(Laravel推荐)

// 迁移文件
Schema::create('products', function (Blueprint $table) {
    $table->id();
    $table->json('title')->nullable(); // {"en":"Apple","zh":"苹果"}
    $table->json('description')->nullable();
});
  • 优点:无需额外关联表,查询用->where('title->en', 'Apple')
  • 缺点:JSON列在MySQL中无法直接索引,大量数据查询性能下降。

方案C:独立翻译表(企业级标准)

Schema::create('product_translations', function (Blueprint $table) {
    $table->id();
    $table->foreignId('product_id')->constrained()->cascadeOnDelete();
    $table->string('locale', 5)->index(); // 'en', 'zh-CN'
    $table->string('title', 200);
    $table->text('description');
    $table->unique(['product_id', 'locale']);
});
  • 优点:完美支持多对多、可索引、便于查询特定语言的记录。
  • 推荐用于:商品、文章、分类等核心业务数据。

实战选择:项目初期用JSON字段快速迭代,数据量超10万条后迁移到翻译表,Laravel的spatie/laravel-translatable包正是基于方案B和C混合实现。


中间件实现:动态语言切换

创建一个中间件,从请求头或参数获取语言值,并设置到Laravel的应用区域设置。

// app/Http/Middleware/SetLocale.php
public function handle(Request $request, Closure $next)
{
    // 优先级:Header > URL参数 > 用户模型语言
    $locale = $request->header('Accept-Language', 'en');
    if (in_array($locale, ['zh-CN', 'en', 'ja'])) {
        app()->setLocale($locale);
    }
    // 如果用户已登录,可以从Auth::user()->locale读取用户偏好
    return $next($request);
}

注册中间件:

// Kernel.php
protected $routeMiddleware = [
    'locale' => \App\Http\Middleware\SetLocale::class,
];
// 或全局中间件
protected $middlewareGroups = [
    'api' => ['throttle:60,1', 'locale'],
];

注意点:设置app()->setLocale()只影响Laravel的Lang::get()系统,不影响数据库查询,要实现完整的“当前语言”,还需要在控制器里显式使用当前语言查询。


翻译文件与Eloquent模型结合

1 静态文本翻译(Lang文件)

// resources/lang/en/messages.php
return ['welcome' => 'Welcome to our API'];
// resources/lang/zh-CN/messages.php
return ['welcome' => '欢迎使用我们的API'];

API调用:__('messages.welcome')Lang::get('messages.welcome')

2 动态模型翻译(Eloquent Scope)

为方案C创建translation()关系:

class Product extends Model
{
    public function translations()
    {
        return $this->hasMany(ProductTranslation::class);
    }
    public function scopeWithLocale($query, $locale = null)
    {
        $locale = $locale ?? app()->getLocale();
        return $query->with(['translation' => function ($q) use ($locale) {
            $q->where('locale', $locale);
        }]);
    }
}

控制器中:

public function show($id)
{
    $product = Product::withLocale()->findOrFail($id);
    return response()->json([
        'id' => $product->id,
        'title' => $product->translation->title ?? $product->fallbackTitle(), // 回退逻辑
    ]);
}

API响应格式化与错误处理

1 统一响应结构

推荐使用Resource类搭配语言包:

// app/Http/Resources/ProductResource.php
public function toArray($request)
{
    return [
        'id' => $this->id,
        'title' => $this->getTranslatedTitle(),
        'description' => LaravelLocalization::getLocalizedURL($this->description),
    ];
}
private function getTranslatedTitle()
{
    $locale = app()->getLocale();
    return $this->translations->firstWhere('locale', $locale)?->title
           ?? $this->translations->first()->title; // 回退到第一个可用语言
}

2 多语言错误消息

在异常处理器中捕获ValidationException并返回本地化消息:

// app/Exceptions/Handler.php
public function register()
{
    $this->renderable(function (ValidationException $e, $request) {
        if ($request->expectsJson()) {
            $messages = $e->validator->errors()->toArray();
            // 自动使用当前语言翻译字段名(可通过Lang文件映射)
            return response()->json(['errors' => $messages], 422);
        }
    });
}

缓存策略与性能优化

1 缓存翻译关系

对于频繁查询的热点数据,使用Redis缓存翻译结果:

public function scopeCachedWithLocale($query, $locale)
{
    $cacheKey = "product_{$this->id}_{$locale}";
    return Cache::remember($cacheKey, 3600, function () use ($query, $locale) {
        return $query->with(['translation' => fn($q) => $q->where('locale', $locale)])->first();
    });
}

2 避免N+1查询

在资源控制器或Resource类中使用预加载

Product::with(['translations' => fn($q) => $q->where('locale', $locale)])->paginate(20);

3 数据库索引优化

如果使用翻译表,务必添加复合索引:

ALTER TABLE product_translations ADD INDEX idx_locale_product (locale, product_id);

测试与多语言回退规则

编写测试用例验证各场景:

public function test_product_shows_fallback_language_when_translation_missing()
{
    $product = Product::factory()->has(ProductTranslation::factory()->forLocale('en'))->create();
    $response = $this->withHeaders(['Accept-Language' => 'fr'])->get("/api/products/{$product->id}");
    $response->assertJsonFragment(['title' => 'Fallback English']);
}

回退逻辑最佳实践

  1. 优先返回用户请求的语言。
  2. 如果不存在,返回默认语言(如en)。
  3. 如果默认语言也没有,返回翻译表的第一个可用记录。
  4. 完全无翻译时返回null或空字符串,并记录日志。

常见问题问答

Q1:如何让用户选择语言后,下次请求自动识别? A:在中间件中检查Auth::user()->locale,若用户登录则优先使用该值,同时可在POST /auth/locale端点接收新语言并更新用户表。

Q2:URL里带语言参数(如/zh-CN/products)和Header哪种更好? A:API优先推荐Header方式(符合RESTful风格),但可能需要为前端兼容提供URL参数方案,Laravel的mcamara/laravel-localization包可以同时支持两种方式。

Q3:大量翻译数据时,JSON字段查询性能如何优化? A:对于JSON字段,可以使用MySQL的虚拟列+索引(如title->>"$.en"生成列),或迁移到翻译表方案,经测试,翻译表方案在10万级数据量下查询性能比JSON字段快20-30%。

Q4:第三方工具推荐? A:除了上述spatie/laravel-translatablemcamara/laravel-localization,还可考虑:

  • nickurt/laravel-languages:管理所有语言配置。
  • dimsav/laravel-translatable:老牌包,支持嵌套翻译。

Q5:如何保证翻译文件与数据库翻译的一致性? A:创建php artisan translations:sync命令,定期扫描数据库所有表,检查是否有未在Lang文件中定义的翻译key,自动添加默认值。


通过以上设计,你的Laravel API可以支持从简单静态文案到复杂业务数据(商品、文章、分类等)的多语言能力,同时兼顾性能、扩展性和代码清晰度,建议从方案B(JSON字段)起步,当数据量和团队协作复杂度上升后,平滑过渡到方案C(翻译表)。

抱歉,评论功能暂时关闭!