feat(i18n): database-backed translation loader with Redis cache
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>main
parent
15f4b2c0d8
commit
ce9e53d206
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App\Domains\Translation\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Translation extends Model
|
||||
{
|
||||
protected $guarded = ['id'];
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
namespace App\Domains\Translation\Providers;
|
||||
|
||||
use App\Domains\Translation\Models\Translation;
|
||||
use Illuminate\Contracts\Translation\Loader;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
|
||||
class DatabaseTranslationLoader implements Loader
|
||||
{
|
||||
public function load($locale, $group, $namespace = null): array
|
||||
{
|
||||
$cacheKey = "lang:{$locale}:{$group}";
|
||||
|
||||
$cached = Redis::hGetAll($cacheKey);
|
||||
if (! empty($cached)) {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
$translations = Translation::where('locale', $locale)
|
||||
->where('group', $group)
|
||||
->pluck('value', 'key')
|
||||
->toArray();
|
||||
|
||||
if (! empty($translations)) {
|
||||
Redis::hMSet($cacheKey, $translations);
|
||||
Redis::expire($cacheKey, 3600);
|
||||
}
|
||||
|
||||
return $translations;
|
||||
}
|
||||
|
||||
public function addNamespace($namespace, $hint): void {}
|
||||
public function addPath($path): void {}
|
||||
public function addJsonPath($path): void {}
|
||||
|
||||
public function namespaces(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
namespace App\Domains\Translation\Services;
|
||||
|
||||
use App\Domains\Translation\Models\Translation;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
|
||||
class TranslationService
|
||||
{
|
||||
public function set(string $locale, string $group, string $key, string $value): Translation
|
||||
{
|
||||
$translation = Translation::updateOrCreate(
|
||||
['locale' => $locale, 'group' => $group, 'key' => $key],
|
||||
['value' => $value, 'namespace' => '*'],
|
||||
);
|
||||
|
||||
Redis::del("lang:{$locale}:{$group}");
|
||||
|
||||
return $translation;
|
||||
}
|
||||
|
||||
public function flushCache(string $locale, string $group): void
|
||||
{
|
||||
Redis::del("lang:{$locale}:{$group}");
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Domains\Translation\Providers\DatabaseTranslationLoader;
|
||||
use App\Domains\Workspace\Models\Workspace;
|
||||
use App\Domains\Workspace\Policies\WorkspacePolicy;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
|
|
@ -14,7 +15,21 @@ class AppServiceProvider extends ServiceProvider
|
|||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
$this->app->singleton('translation.loader', function ($app) {
|
||||
return new DatabaseTranslationLoader;
|
||||
});
|
||||
|
||||
// TranslationServiceProvider is deferred and re-binds translation.loader
|
||||
// when it resolves. We extend 'translator' to swap in our DB loader after
|
||||
// the translator instance is created.
|
||||
$this->app->extend('translator', function ($translator, $app) {
|
||||
$loader = new DatabaseTranslationLoader;
|
||||
$reflection = new \ReflectionProperty($translator, 'loader');
|
||||
$reflection->setAccessible(true);
|
||||
$reflection->setValue($translator, $loader);
|
||||
|
||||
return $translator;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
use App\Domains\Translation\Models\Translation;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
|
||||
beforeEach(function () {
|
||||
Redis::del('lang:de:auth');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
Redis::del('lang:de:auth');
|
||||
});
|
||||
|
||||
it('loads translation from database via __()', function () {
|
||||
Translation::create([
|
||||
'locale' => 'de',
|
||||
'namespace' => '*',
|
||||
'group' => 'auth',
|
||||
'key' => 'login',
|
||||
'value' => 'Anmelden',
|
||||
]);
|
||||
|
||||
App::setLocale('de');
|
||||
// Force reload
|
||||
app('translator')->setLoaded([]);
|
||||
|
||||
expect(__('auth.login'))->toBe('Anmelden');
|
||||
});
|
||||
Loading…
Reference in New Issue