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' => '

Ausführlicher Inhalt für Berater.

', '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' => '

Ausführlicher Inhalt für Berater.

', ]); }); 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 d’Aloe 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 d’Aloe 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); });