diff --git a/src/app/Http/Controllers/DocumentTranslationController.php b/src/app/Http/Controllers/DocumentTranslationController.php new file mode 100644 index 0000000..2b7eb85 --- /dev/null +++ b/src/app/Http/Controllers/DocumentTranslationController.php @@ -0,0 +1,54 @@ +validate([ + 'locale' => ['required', 'string', 'in:' . implode(',', array_keys(SetLocale::SUPPORTED_LOCALES))], + 'title' => ['required', 'string', 'max:255'], + 'content' => ['required', 'string'], + ]); + + try { + $this->service->addTranslation( + $document, + $validated['locale'], + $validated['title'], + $validated['content'], + Auth::id(), + ); + } catch (\InvalidArgumentException $e) { + return response()->json(['message' => $e->getMessage()], 422); + } + + return redirect()->route('documents.show', $document) + ->with('message', __('messages.documents.translation_added')); + } + + public function destroy(Document $document, string $locale) + { + if (!array_key_exists($locale, SetLocale::SUPPORTED_LOCALES)) { + abort(404); + } + + try { + $this->service->deleteTranslation($document, $locale); + } catch (\InvalidArgumentException $e) { + return response()->json(['message' => $e->getMessage()], 422); + } + + return redirect()->route('documents.show', $document) + ->with('message', __('messages.documents.translation_deleted')); + } +} diff --git a/src/lang/en/messages.php b/src/lang/en/messages.php index bef3a45..af317d8 100644 --- a/src/lang/en/messages.php +++ b/src/lang/en/messages.php @@ -39,6 +39,8 @@ 'content_label' => 'Content', 'content_placeholder' => 'Write your markdown here...', 'saving' => 'Saving...', + 'translation_added' => 'Translation added.', + 'translation_deleted' => 'Translation deleted.', ], // Quick Switcher diff --git a/src/lang/ja/messages.php b/src/lang/ja/messages.php index 87b42ee..e8ac3e0 100644 --- a/src/lang/ja/messages.php +++ b/src/lang/ja/messages.php @@ -39,6 +39,8 @@ 'content_label' => '本文', 'content_placeholder' => 'Markdownで記述してください...', 'saving' => '保存中...', + 'translation_added' => '翻訳を追加しました。', + 'translation_deleted' => '翻訳を削除しました。', ], // Quick Switcher diff --git a/src/routes/web.php b/src/routes/web.php index ce6f662..efa7735 100644 --- a/src/routes/web.php +++ b/src/routes/web.php @@ -47,6 +47,15 @@ Route::get('/{document}/edit', DocumentEditor::class) ->middleware('can:update,document') ->name('edit'); + Route::post('/{document}/translations', [\App\Http\Controllers\DocumentTranslationController::class, 'store']) + ->middleware('can:update,document') + ->name('translations.store'); + Route::delete('/{document}/translations/{locale}', [\App\Http\Controllers\DocumentTranslationController::class, 'destroy']) + ->middleware('can:update,document') + ->name('translations.destroy'); + Route::get('/{document}/translations/{locale}/edit', \App\Livewire\DocumentEditor::class) + ->middleware('can:update,document') + ->name('translations.edit'); }); // 公開ルート(動的ルートは最後に) diff --git a/src/tests/Feature/DocumentTranslationCrudTest.php b/src/tests/Feature/DocumentTranslationCrudTest.php new file mode 100644 index 0000000..d14e4dc --- /dev/null +++ b/src/tests/Feature/DocumentTranslationCrudTest.php @@ -0,0 +1,94 @@ +create(); + $doc = Document::factory()->create(['default_locale' => 'en', 'created_by' => $owner->id]); + + $response = $this->actingAs($owner)->post( + route('documents.translations.store', $doc), + ['locale' => 'ja', 'title' => 'こんにちは', 'content' => '本文'] + ); + + $response->assertRedirect(); + $this->assertNotNull($doc->fresh()->translationFor('ja', false)); + } + + public function test_non_owner_cannot_add_translation(): void + { + $owner = User::factory()->create(); + $other = User::factory()->create(); + $doc = Document::factory()->create(['default_locale' => 'en', 'created_by' => $owner->id]); + + $response = $this->actingAs($other)->post( + route('documents.translations.store', $doc), + ['locale' => 'ja', 'title' => 'こんにちは', 'content' => '本文'] + ); + + $response->assertForbidden(); + } + + public function test_invalid_locale_is_rejected(): void + { + $owner = User::factory()->create(); + $doc = Document::factory()->create(['default_locale' => 'en', 'created_by' => $owner->id]); + + $response = $this->actingAs($owner)->post( + route('documents.translations.store', $doc), + ['locale' => 'xx', 'title' => 'X', 'content' => 'Y'] + ); + + $response->assertSessionHasErrors('locale'); + } + + public function test_duplicate_locale_returns_422(): void + { + $owner = User::factory()->create(); + $doc = Document::factory()->create(['default_locale' => 'en', 'created_by' => $owner->id]); + + $response = $this->actingAs($owner)->post( + route('documents.translations.store', $doc), + ['locale' => 'en', 'title' => 'X', 'content' => 'Y'] + ); + + $response->assertStatus(422); + } + + public function test_owner_can_delete_non_default_translation(): void + { + $owner = User::factory()->create(); + $doc = Document::factory()->create(['default_locale' => 'en', 'created_by' => $owner->id]); + app(\App\Services\DocumentService::class)->addTranslation($doc, 'ja', 'JA', 'body', $owner->id); + + $response = $this->actingAs($owner)->delete( + route('documents.translations.destroy', ['document' => $doc, 'locale' => 'ja']) + ); + + $response->assertRedirect(); + $this->assertNull($doc->fresh()->translationFor('ja', false)); + } + + public function test_default_locale_translation_cannot_be_deleted(): void + { + $owner = User::factory()->create(); + $doc = Document::factory()->create(['default_locale' => 'en', 'created_by' => $owner->id]); + + $response = $this->actingAs($owner)->delete( + route('documents.translations.destroy', ['document' => $doc, 'locale' => 'en']) + ); + + $response->assertStatus(422); + $this->assertNotNull($doc->fresh()->translationFor('en', false)); + } +}