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. 测试验证
- 登录租户后台,添加翻译键(如
auth.failed)、指定语言并设置自定义值 - 在租户页面调用
__('auth.failed'),验证是否显示自定义内容 - 切换到其他租户,调用相同翻译键,验证显示默认系统翻译
内容的提问来源于stack exchange,提问作者H.Hassan
相关产品推荐
相关产品推荐

