You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Laravel多租户应用中实现租户独立自定义lang文件的方案问询

实现Laravel多租户的租户级自定义翻译功能

1. 数据库设计:存储租户专属翻译

首先创建数据表存储每个租户的自定义翻译,确保数据隔离。

生成迁移文件

执行命令创建迁移:

php artisan make:migration create_tenant_translations_table

编写迁移代码

在生成的迁移文件中定义表结构:

Schema::create('tenant_translations', function (Blueprint $table) {
    $table->id();
    $table->unsignedBigInteger('tenant_id');
    $table->string('locale');
    $table->string('key');
    $table->text('value');
    $table->timestamps();

    // 唯一约束:同一租户同一语言下翻译键不能重复
    $table->unique(['tenant_id', 'locale', 'key']);
    $table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('cascade');
});

创建模型

生成对应模型并设置可填充字段:

php artisan make:model TenantTranslation
class TenantTranslation extends Model
{
    protected $fillable = ['tenant_id', 'locale', 'key', 'value'];
}

2. 自定义翻译加载器:覆盖Laravel默认逻辑

通过自定义加载器,优先加载租户的自定义翻译,不存在时回退到默认lang目录的文件。

创建加载器类

在app/Translation目录下创建TenantTranslationLoader.php:

namespace App\Translation;

use App\Models\TenantTranslation;
use Illuminate\Contracts\Translation\Loader;
use Illuminate\Translation\ArrayLoader;

class TenantTranslationLoader implements Loader
{
    protected $defaultLoader;

    public function __construct(ArrayLoader $defaultLoader)
    {
        $this->defaultLoader = $defaultLoader;
    }

    public function load($locale, $group, $namespace = null)
    {
        // 先加载系统默认翻译
        $defaultTranslations = $this->defaultLoader->load($locale, $group, $namespace);

        // 多租户环境下,加载当前租户的自定义翻译并覆盖默认值
        if (app()->has('tenant') && $tenant = app('tenant')) {
            $tenantTranslations = TenantTranslation::where('tenant_id', $tenant->id)
                ->where('locale', $locale)
                ->get()
                ->pluck('value', 'key')
                ->toArray();

            foreach ($tenantTranslations as $key => $value) {
                // 处理带分组的翻译键(如'auth.failed')
                if (str_contains($key, '.')) {
                    [$currentGroup, $currentKey] = explode('.', $key, 2);
                    if ($currentGroup === $group) {
                        $defaultTranslations[$currentKey] = $value;
                    }
                } else {
                    $defaultTranslations[$key] = $value;
                }
            }
        }

        return $defaultTranslations;
    }

    // 委托接口其他方法给默认加载器
    public function addNamespace($namespace, $hint)
    {
        $this->defaultLoader->addNamespace($namespace, $hint);
    }

    public function namespaces()
    {
        return $this->defaultLoader->namespaces();
    }

    public function addJsonPath($path)
    {
        $this->defaultLoader->addJsonPath($path);
    }

    public function jsonPaths()
    {
        return $this->defaultLoader->jsonPaths();
    }
}

替换默认加载器

在AppServiceProvider的boot方法中替换系统翻译加载器:

use App\Translation\TenantTranslationLoader;
use Illuminate\Translation\ArrayLoader;
use Illuminate\Translation\Translator;

public function boot()
{
    // 替换翻译加载器
    $this->app->extend('translation.loader', function ($app) {
        $defaultLoader = new ArrayLoader();
        // 继承原加载器的命名空间和JSON路径配置
        foreach (Lang::getLoader()->namespaces() as $namespace => $hint) {
            $defaultLoader->addNamespace($namespace, $hint);
        }
        foreach (Lang::getLoader()->jsonPaths() as $path) {
            $defaultLoader->addJsonPath($path);
        }
        return new TenantTranslationLoader($defaultLoader);
    });

    // 重新初始化翻译器
    $this->app->singleton('translator', function ($app) {
        $loader = $app['translation.loader'];
        $translator = new Translator($loader, $app['config']['app.locale']);
        $translator->setFallback($app['config']['app.fallback_locale']);
        return $translator;
    });
}

3. 后台配置功能:租户管理翻译

创建控制器和视图,让租户可以在后台增删改查自定义翻译。

创建租户翻译控制器

在app/Http/Controllers/Tenant目录下创建TranslationController.php:

namespace App\Http\Controllers\Tenant;

use App\Models\TenantTranslation;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;

class TranslationController extends Controller
{
    public function index(Request $request)
    {
        $locale = $request->get('locale', app()->getLocale());
        $translations = TenantTranslation::where('tenant_id', auth()->user()->tenant_id)
            ->where('locale', $locale)
            ->paginate(20);

        return view('tenant.translations.index', compact('translations', 'locale'));
    }

    public function create()
    {
        return view('tenant.translations.create');
    }

    public function store(Request $request)
    {
        $request->validate([
            'locale' => 'required|string|max:5',
            'key' => 'required|string|unique:tenant_translations,key,NULL,id,tenant_id,'.auth()->user()->tenant_id.',locale,'.$request->locale,
            'value' => 'required|string',
        ]);

        TenantTranslation::create([
            'tenant_id' => auth()->user()->tenant_id,
            'locale' => $request->locale,
            'key' => $request->key,
            'value' => $request->value,
        ]);

        return redirect()->route('tenant.translations.index')->with('success', '翻译已添加');
    }

    public function edit(TenantTranslation $translation)
    {
        $this->authorize('update', $translation);
        return view('tenant.translations.edit', compact('translation'));
    }

    public function update(Request $request, TenantTranslation $translation)
    {
        $this->authorize('update', $translation);

        $request->validate([
            'value' => 'required|string',
        ]);

        $translation->update(['value' => $request->value]);

        return redirect()->route('tenant.translations.index')->with('success', '翻译已更新');
    }

    public function destroy(TenantTranslation $translation)
    {
        $this->authorize('delete', $translation);
        $translation->delete();

        return redirect()->route('tenant.translations.index')->with('success', '翻译已删除');
    }
}

添加租户路由

在routes/tenant.php中添加路由(需配合多租户路由中间件):

use App\Http\Controllers\Tenant\TranslationController;

Route::middleware(['auth', 'tenant'])->group(function () {
    Route::resource('translations', TranslationController::class);
});

创建视图文件

创建以下Blade视图:

  • resources/views/tenant/translations/index.blade.php:翻译列表页
  • resources/views/tenant/translations/create.blade.php:添加翻译表单
  • resources/views/tenant/translations/edit.blade.php:编辑翻译表单

示例列表页代码:

<div class="container">
    <h1>自定义翻译管理</h1>
    <div class="mb-3">
        <a href="{{ route('tenant.translations.create') }}" class="btn btn-primary">添加新翻译</a>
        <form action="{{ route('tenant.translations.index') }}" method="get" class="d-inline">
            <select name="locale" onchange="this.form.submit()" class="form-select d-inline w-auto">
                @foreach(config('app.locales') as $locale)
                    <option value="{{ $locale }}" {{ $locale == app()->getLocale() ? 'selected' : '' }}>{{ $locale }}</option>
                @endforeach
            </select>
        </form>
    </div>
    @if(session('success'))
        <div class="alert alert-success">{{ session('success') }}</div>
    @endif
    <table class="table">
        <thead>
            <tr>
                <th>语言</th>
                <th>翻译键</th>
                <th>翻译内容</th>
                <th>操作</th>
            </tr>
        </thead>
        <tbody>
            @foreach($translations as $translation)
                <tr>
                    <td>{{ $translation->locale }}</td>
                    <td>{{ $translation->key }}</td>
                    <td>{{ $translation->value }}</td>
                    <td>
                        <a href="{{ route('tenant.translations.edit', $translation) }}" class="btn btn-sm btn-outline-primary">编辑</a>
                        <form action="{{ route('tenant.translations.destroy', $translation) }}" method="post" class="d-inline">
                            @csrf
                            @method('DELETE')
                            <button type="submit" class="btn btn-sm btn-outline-danger" onclick="return confirm('确定删除?')">删除</button>
                        </form>
                    </td>
                </tr>
            @endforeach
        </tbody>
    </table>
    {{ $translations->links() }}
</div>

4. 权限控制(可选)

创建翻译模型的权限策略,确保租户只能操作自己的翻译:

// app/Policies/TenantTranslationPolicy.php
namespace App\Policies;

use App\Models\TenantTranslation;
use App\Models\User;

class TenantTranslationPolicy
{
    public function viewAny(User $user)
    {
        return true;
    }

    public function view(User $user, TenantTranslation $translation)
    {
        return $user->tenant_id === $translation->tenant_id;
    }

    public function create(User $user)
    {
        return true;
    }

    public function update(User $user, TenantTranslation $translation)
    {
        return $user->tenant_id === $translation->tenant_id;
    }

    public function delete(User $user, TenantTranslation $translation)
    {
        return $user->tenant_id === $translation->tenant_id;
    }
}

在AuthServiceProvider中注册策略:

protected $policies = [
    TenantTranslation::class => TenantTranslationPolicy::class,
];

5. 测试验证

  1. 登录租户后台,添加翻译键(如auth.failed)、指定语言并设置自定义值
  2. 在租户页面调用__('auth.failed'),验证是否显示自定义内容
  3. 切换到其他租户,调用相同翻译键,验证显示默认系统翻译

内容的提问来源于stack exchange,提问作者H.Hassan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.09 16:15:56