13-05-2026 implementation FR

This commit is contained in:
Kevin Adametz 2026-05-13 17:33:52 +02:00
parent 245c281541
commit 70240d2b6a
61 changed files with 4472 additions and 2 deletions

View file

@ -0,0 +1,278 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Schema;
uses(Tests\TestCase::class);
beforeEach(function () {
Schema::dropIfExists('trans_products');
Schema::dropIfExists('products');
Schema::dropIfExists('trans_ingredients');
Schema::dropIfExists('ingredients');
Schema::dropIfExists('dashboard_news');
Schema::dropIfExists('trans_languages');
Schema::create('trans_languages', function (Blueprint $table) {
$table->id();
$table->string('language');
$table->string('name')->nullable();
$table->timestamps();
});
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->text('copy')->nullable();
$table->text('description')->nullable();
$table->text('usage')->nullable();
$table->text('ingredients')->nullable();
});
Schema::create('trans_products', function (Blueprint $table) {
$table->id();
$table->string('language');
$table->unsignedBigInteger('product_id');
$table->string('key')->nullable();
$table->text('value')->nullable();
$table->timestamps();
});
Schema::create('ingredients', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->text('inci')->nullable();
$table->text('effect')->nullable();
});
Schema::create('trans_ingredients', function (Blueprint $table) {
$table->id();
$table->string('language');
$table->unsignedBigInteger('ingredient_id');
$table->string('key')->nullable();
$table->text('value')->nullable();
$table->timestamps();
});
Schema::create('dashboard_news', function (Blueprint $table) {
$table->id();
$table->string('title')->nullable();
$table->text('teaser')->nullable();
$table->text('content')->nullable();
$table->json('trans_title')->nullable();
$table->json('trans_teaser')->nullable();
$table->json('trans_content')->nullable();
$table->boolean('active')->default(true);
$table->timestamps();
});
});
it('backfills missing french product translations without overwriting existing values', function () {
DB::table('products')->insert([
'id' => 1,
'name' => 'Aloe Vera Gel',
'copy' => 'Deutsche Kurzbeschreibung',
'description' => 'Deutsche Beschreibung',
'usage' => null,
'ingredients' => 'Aloe Vera',
]);
DB::table('trans_products')->insert([
'language' => 'fr',
'product_id' => 1,
'key' => 'name',
'value' => 'Nom existant',
'created_at' => now(),
'updated_at' => now(),
]);
$this->artisan('translation:backfill-french-db', [
'--driver' => 'copy-source',
'--models' => 'products',
])
->expectsOutput('Processing products...')
->expectsOutput('Status products: 1 Datensätze, 5 Felder.')
->expectsOutput('Datensatz 1/1: products#1')
->expectsOutput(' - products#1.name: vorhandene Übersetzung, übersprungen.')
->expectsOutput(' - products#1.copy: übernehme Quelle...')
->expectsOutput(' - products#1.copy: gespeichert (erstellt).')
->expectsOutput(' - products#1.usage: Quelle leer, übersprungen.')
->assertSuccessful();
expect(DB::table('trans_languages')->where('language', 'fr')->value('name'))->toBe('Französisch')
->and(DB::table('trans_products')->where('language', 'fr')->where('product_id', 1)->where('key', 'name')->value('value'))->toBe('Nom existant')
->and(DB::table('trans_products')->where('language', 'fr')->where('product_id', 1)->where('key', 'copy')->value('value'))->toBe('Deutsche Kurzbeschreibung')
->and(DB::table('trans_products')->where('language', 'fr')->where('product_id', 1)->where('key', 'description')->value('value'))->toBe('Deutsche Beschreibung')
->and(DB::table('trans_products')->where('language', 'fr')->where('product_id', 1)->where('key', 'usage')->exists())->toBeFalse();
});
it('uses openai for translatable fields and copies inci values unchanged', function () {
config()->set('services.openai.api_key', 'test-key');
config()->set('services.openai.model', 'gpt-5.4-mini');
Http::fake(function (Request $request) {
$text = $request['messages'][1]['content'];
return Http::response([
'choices' => [
[
'message' => [
'content' => str_contains($text, 'Pflege')
? 'FR __MIVITA_TRANSLATION_TOKEN_1__ __MIVITA_TRANSLATION_TOKEN_0__ Pflege'
: 'FR Beruhigende Wirkung',
],
],
],
]);
});
DB::table('ingredients')->insert([
'id' => 1,
'name' => 'MIVITA :amount Pflege',
'inci' => 'Aloe Barbadensis Leaf Juice',
'effect' => 'Beruhigende Wirkung',
]);
$this->artisan('translation:backfill-french-db', [
'--driver' => 'openai',
'--models' => 'ingredients',
])->assertSuccessful();
expect(DB::table('trans_ingredients')->where('language', 'fr')->where('ingredient_id', 1)->where('key', 'name')->value('value'))->toBe('FR MIVITA :amount Pflege')
->and(DB::table('trans_ingredients')->where('language', 'fr')->where('ingredient_id', 1)->where('key', 'inci')->value('value'))->toBe('Aloe Barbadensis Leaf Juice')
->and(DB::table('trans_ingredients')->where('language', 'fr')->where('ingredient_id', 1)->where('key', 'effect')->value('value'))->toBe('FR Beruhigende Wirkung');
Http::assertSentCount(2);
Http::assertSent(fn (Request $request): bool => $request->hasHeader('Authorization', 'Bearer test-key')
&& $request->url() === 'https://api.openai.com/v1/chat/completions'
&& $request['model'] === 'gpt-5.4-mini');
});
it('backfills dashboard news json translations', function () {
DB::table('dashboard_news')->insert([
'id' => 1,
'title' => 'Neue Aktion',
'teaser' => 'Kurzer Hinweis für das Dashboard',
'content' => '<p>Ausführlicher Inhalt für Berater.</p>',
'trans_title' => json_encode(['en' => 'Existing English title']),
'trans_teaser' => json_encode(['fr' => 'Teaser existant']),
'trans_content' => null,
'active' => true,
'created_at' => now(),
'updated_at' => now(),
]);
$this->artisan('translation:backfill-french-db', [
'--driver' => 'copy-source',
'--models' => 'dashboard_news',
])
->expectsOutput('Processing dashboard_news...')
->expectsOutput('Status dashboard_news: 1 Datensätze, 3 Felder.')
->expectsOutput('Datensatz 1/1: dashboard_news#1')
->expectsOutput(' - dashboard_news#1.title: übernehme Quelle...')
->expectsOutput(' - dashboard_news#1.title: gespeichert (erstellt).')
->expectsOutput(' - dashboard_news#1.teaser: vorhandene Übersetzung, übersprungen.')
->expectsOutput(' - dashboard_news#1.content: übernehme Quelle...')
->expectsOutput(' - dashboard_news#1.content: gespeichert (erstellt).')
->assertSuccessful();
$news = DB::table('dashboard_news')->where('id', 1)->first();
expect(json_decode($news->trans_title, true))->toMatchArray([
'en' => 'Existing English title',
'fr' => 'Neue Aktion',
])
->and(json_decode($news->trans_teaser, true))->toMatchArray([
'fr' => 'Teaser existant',
])
->and(json_decode($news->trans_content, true))->toMatchArray([
'fr' => '<p>Ausführlicher Inhalt für Berater.</p>',
]);
});
it('updates dashboard news json translations when overwrite is enabled', function () {
DB::table('dashboard_news')->insert([
'id' => 1,
'title' => 'Aktualisierte Aktion',
'teaser' => null,
'content' => null,
'trans_title' => json_encode(['fr' => 'Ancien titre']),
'trans_teaser' => null,
'trans_content' => null,
'active' => true,
'created_at' => now(),
'updated_at' => now(),
]);
$this->artisan('translation:backfill-french-db', [
'--driver' => 'copy-source',
'--models' => 'dashboard_news',
'--overwrite' => true,
])
->expectsOutput(' - dashboard_news#1.title: gespeichert (aktualisiert).')
->assertSuccessful();
$translations = json_decode(DB::table('dashboard_news')->where('id', 1)->value('trans_title'), true);
expect($translations['fr'])->toBe('Aktualisierte Aktion');
});
it('prints sample translations for the api test mode', function () {
config()->set('services.openai.api_key', 'test-key');
config()->set('services.openai.model', 'gpt-5.4-mini');
Http::fakeSequence()
->push(['choices' => [['message' => ['content' => 'Gel dAloe Vera pour les soins quotidiens de la peau.']]]])
->push(['choices' => [['message' => ['content' => 'Le conseiller peut recommander un abonnement adapté à son client.']]]])
->push(['choices' => [['message' => ['content' => 'Description du produit MIVITA avec :amount ml de contenu et paiement PayPal.']]]]);
$this->artisan('translation:backfill-french-db', [
'--test-api' => true,
])
->expectsOutput('OpenAI translation API test')
->expectsOutput('Model: gpt-5.4-mini')
->expectsOutput('Language: de -> fr')
->expectsOutput('[1] DE: Aloe Vera Gel für die tägliche Pflege der Haut.')
->expectsOutput('[1] FR: Gel dAloe Vera pour les soins quotidiens de la peau.')
->expectsOutput('[2] DE: Der Berater kann seinem Kunden ein passendes Abo empfehlen.')
->expectsOutput('[2] FR: Le conseiller peut recommander un abonnement adapté à son client.')
->expectsOutput('[3] DE: MIVITA Produktbeschreibung mit :amount ml Inhalt und PayPal Zahlung.')
->expectsOutput('[3] FR: Description du produit MIVITA avec :amount ml de contenu et paiement PayPal.')
->expectsOutput('API test completed.')
->assertSuccessful();
Http::assertSentCount(3);
});
it('prints a helpful message when openai quota is exceeded', function () {
config()->set('services.openai.api_key', 'test-key');
config()->set('services.openai.model', 'gpt-5.4-mini');
Http::fake([
'*' => Http::response([
'error' => [
'message' => 'You exceeded your current quota, please check your plan and billing details.',
'type' => 'insufficient_quota',
'code' => 'insufficient_quota',
],
], 429),
]);
$this->artisan('translation:backfill-french-db', [
'--test-api' => true,
])
->expectsOutput('OpenAI translation API test')
->expectsOutput('Model: gpt-5.4-mini')
->expectsOutput('Language: de -> fr')
->expectsOutput('OpenAI API request failed with HTTP 429.')
->expectsOutput('Code: insufficient_quota')
->expectsOutput('Type: insufficient_quota')
->expectsOutput('Message: You exceeded your current quota, please check your plan and billing details.')
->expectsOutput('Bitte prüfe im OpenAI Dashboard das Billing, das Projekt-Budget, Usage-Limits und ob der OPENAI_API_KEY zum richtigen Projekt gehört.')
->assertFailed();
Http::assertSentCount(1);
});