Add translation CRUD routes and controller
POST/DELETE for translations gated by can:update,document middleware. Locale validated against SUPPORTED_LOCALES. Default-locale deletion returns 422; duplicate-locale add returns 422. Flash messages added to en/ja lang files (other locales updated in Task 9).
This commit is contained in:
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Middleware\SetLocale;
|
||||||
|
use App\Models\Document;
|
||||||
|
use App\Services\DocumentService;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
class DocumentTranslationController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(private DocumentService $service) {}
|
||||||
|
|
||||||
|
public function store(Request $request, Document $document)
|
||||||
|
{
|
||||||
|
$validated = $request->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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,6 +39,8 @@
|
|||||||
'content_label' => 'Content',
|
'content_label' => 'Content',
|
||||||
'content_placeholder' => 'Write your markdown here...',
|
'content_placeholder' => 'Write your markdown here...',
|
||||||
'saving' => 'Saving...',
|
'saving' => 'Saving...',
|
||||||
|
'translation_added' => 'Translation added.',
|
||||||
|
'translation_deleted' => 'Translation deleted.',
|
||||||
],
|
],
|
||||||
|
|
||||||
// Quick Switcher
|
// Quick Switcher
|
||||||
|
|||||||
@@ -39,6 +39,8 @@
|
|||||||
'content_label' => '本文',
|
'content_label' => '本文',
|
||||||
'content_placeholder' => 'Markdownで記述してください...',
|
'content_placeholder' => 'Markdownで記述してください...',
|
||||||
'saving' => '保存中...',
|
'saving' => '保存中...',
|
||||||
|
'translation_added' => '翻訳を追加しました。',
|
||||||
|
'translation_deleted' => '翻訳を削除しました。',
|
||||||
],
|
],
|
||||||
|
|
||||||
// Quick Switcher
|
// Quick Switcher
|
||||||
|
|||||||
@@ -47,6 +47,15 @@
|
|||||||
Route::get('/{document}/edit', DocumentEditor::class)
|
Route::get('/{document}/edit', DocumentEditor::class)
|
||||||
->middleware('can:update,document')
|
->middleware('can:update,document')
|
||||||
->name('edit');
|
->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');
|
||||||
});
|
});
|
||||||
|
|
||||||
// 公開ルート(動的ルートは最後に)
|
// 公開ルート(動的ルートは最後に)
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\Document;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class DocumentTranslationCrudTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function test_owner_can_add_a_translation(): 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' => '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));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user