Make DocumentService locale-aware
createDocument/updateDocument now accept a \$locale parameter and write to document_translations. Adds addTranslation, deleteTranslation, setDefaultLocale (with path/slug regen), distinct-document search, and findByTitle that delegates to WikiLinkResolver.
This commit is contained in:
@@ -182,4 +182,12 @@ public function scopeInDirectory(Builder $query, string $directory): Builder
|
|||||||
$directory = rtrim($directory, '/') . '/';
|
$directory = rtrim($directory, '/') . '/';
|
||||||
return $query->where('path', 'like', $directory . '%');
|
return $query->where('path', 'like', $directory . '%');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync wiki-links for this document. Implementation in Task 7.
|
||||||
|
*/
|
||||||
|
public function syncLinks(): void
|
||||||
|
{
|
||||||
|
// Replaced in Task 7
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,143 +2,185 @@
|
|||||||
|
|
||||||
namespace App\Services;
|
namespace App\Services;
|
||||||
|
|
||||||
use App\Models\Document;
|
|
||||||
use App\Models\RecentDocument;
|
|
||||||
use App\Helpers\SlugHelper;
|
use App\Helpers\SlugHelper;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use App\Models\Document;
|
||||||
|
use App\Models\DocumentTranslation;
|
||||||
|
use App\Models\RecentDocument;
|
||||||
|
use Illuminate\Support\Facades\App;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Str;
|
|
||||||
|
|
||||||
class DocumentService
|
class DocumentService
|
||||||
{
|
{
|
||||||
|
|
||||||
/**
|
|
||||||
* 新しいドキュメントを作成
|
|
||||||
*
|
|
||||||
* @param string $title
|
|
||||||
* @param string $content
|
|
||||||
* @param int|null $userId
|
|
||||||
* @param string|null $directory (deprecated - path is now auto-generated from title)
|
|
||||||
* @return Document
|
|
||||||
*/
|
|
||||||
public function createDocument(
|
public function createDocument(
|
||||||
string $title,
|
string $title,
|
||||||
string $content,
|
string $content,
|
||||||
?int $userId = null,
|
?int $userId = null,
|
||||||
?string $directory = null
|
?string $locale = null,
|
||||||
): Document {
|
): Document {
|
||||||
// タイトルからパスとスラッグを自動生成
|
$locale = $locale ?: App::getLocale();
|
||||||
// 例: "Laravel/Livewire/Components" → path="Laravel/Livewire/Components.md", slug="components"
|
|
||||||
[$path, $slug] = $this->generatePathAndSlug($title);
|
[$path, $slug] = $this->generatePathAndSlug($title);
|
||||||
|
|
||||||
// ドキュメントをDBに作成
|
return DB::transaction(function () use ($title, $content, $userId, $locale, $path, $slug) {
|
||||||
$document = Document::create([
|
$document = Document::create([
|
||||||
'path' => $path,
|
'path' => $path,
|
||||||
'title' => $title,
|
|
||||||
'slug' => $slug,
|
'slug' => $slug,
|
||||||
'content' => $content,
|
'default_locale' => $locale,
|
||||||
'rendered_html' => Document::renderMarkdown($content),
|
|
||||||
'created_by' => $userId,
|
'created_by' => $userId,
|
||||||
'updated_by' => $userId,
|
'updated_by' => $userId,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// リンクを同期
|
DocumentTranslation::create([
|
||||||
|
'document_id' => $document->id,
|
||||||
|
'locale' => $locale,
|
||||||
|
'title' => $title,
|
||||||
|
'content' => $content,
|
||||||
|
'rendered_html' => DocumentTranslation::renderMarkdown($content),
|
||||||
|
'created_by' => $userId,
|
||||||
|
'updated_by' => $userId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$document->load('translations');
|
||||||
$document->syncLinks();
|
$document->syncLinks();
|
||||||
|
|
||||||
return $document;
|
return $document;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* ドキュメントを更新
|
|
||||||
*
|
|
||||||
* @param Document $document
|
|
||||||
* @param string $title
|
|
||||||
* @param string $content
|
|
||||||
* @param int|null $userId
|
|
||||||
* @return Document
|
|
||||||
*/
|
|
||||||
public function updateDocument(
|
public function updateDocument(
|
||||||
Document $document,
|
Document $document,
|
||||||
string $title,
|
string $title,
|
||||||
string $content,
|
string $content,
|
||||||
?int $userId = null
|
?int $userId = null,
|
||||||
|
?string $locale = null,
|
||||||
): Document {
|
): Document {
|
||||||
// タイトルが変更された場合はパスとスラッグを再生成
|
$locale = $locale ?: App::getLocale();
|
||||||
if ($document->title !== $title) {
|
|
||||||
|
return DB::transaction(function () use ($document, $title, $content, $userId, $locale) {
|
||||||
|
$translation = $document->translations()->firstOrNew(['locale' => $locale]);
|
||||||
|
$translation->title = $title;
|
||||||
|
$translation->content = $content;
|
||||||
|
$translation->rendered_html = DocumentTranslation::renderMarkdown($content);
|
||||||
|
$translation->updated_by = $userId;
|
||||||
|
if (!$translation->exists) {
|
||||||
|
$translation->created_by = $userId;
|
||||||
|
}
|
||||||
|
$translation->save();
|
||||||
|
|
||||||
|
$document->updated_by = $userId;
|
||||||
|
|
||||||
|
// Path/slug regenerate only when editing the default-locale translation
|
||||||
|
if ($locale === $document->default_locale) {
|
||||||
[$path, $slug] = $this->generatePathAndSlug($title, $document->id);
|
[$path, $slug] = $this->generatePathAndSlug($title, $document->id);
|
||||||
$document->path = $path;
|
$document->path = $path;
|
||||||
$document->slug = $slug;
|
$document->slug = $slug;
|
||||||
}
|
}
|
||||||
|
|
||||||
$document->title = $title;
|
|
||||||
$document->content = $content;
|
|
||||||
$document->rendered_html = Document::renderMarkdown($content);
|
|
||||||
$document->updated_by = $userId;
|
|
||||||
|
|
||||||
// DBに保存
|
|
||||||
$document->save();
|
$document->save();
|
||||||
|
$document->load('translations');
|
||||||
// リンクを再同期
|
|
||||||
$document->syncLinks();
|
$document->syncLinks();
|
||||||
|
|
||||||
return $document;
|
return $document;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addTranslation(
|
||||||
|
Document $document,
|
||||||
|
string $locale,
|
||||||
|
string $title,
|
||||||
|
string $content,
|
||||||
|
?int $userId = null,
|
||||||
|
): DocumentTranslation {
|
||||||
|
if ($document->translations()->where('locale', $locale)->exists()) {
|
||||||
|
throw new \InvalidArgumentException("Translation for locale '$locale' already exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
return DocumentTranslation::create([
|
||||||
|
'document_id' => $document->id,
|
||||||
|
'locale' => $locale,
|
||||||
|
'title' => $title,
|
||||||
|
'content' => $content,
|
||||||
|
'rendered_html' => DocumentTranslation::renderMarkdown($content),
|
||||||
|
'created_by' => $userId,
|
||||||
|
'updated_by' => $userId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteTranslation(Document $document, string $locale): void
|
||||||
|
{
|
||||||
|
if ($locale === $document->default_locale) {
|
||||||
|
throw new \InvalidArgumentException("Cannot delete default-locale translation '$locale'");
|
||||||
|
}
|
||||||
|
$document->translations()->where('locale', $locale)->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDefaultLocale(Document $document, string $locale): Document
|
||||||
|
{
|
||||||
|
$translation = $document->translations()->where('locale', $locale)->first();
|
||||||
|
if (!$translation) {
|
||||||
|
throw new \InvalidArgumentException("Cannot set default to '$locale': translation does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
return DB::transaction(function () use ($document, $locale, $translation) {
|
||||||
|
$document->default_locale = $locale;
|
||||||
|
[$path, $slug] = $this->generatePathAndSlug($translation->title, $document->id);
|
||||||
|
$document->path = $path;
|
||||||
|
$document->slug = $slug;
|
||||||
|
$document->save();
|
||||||
|
|
||||||
|
return $document->fresh('translations');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* ドキュメントを削除
|
|
||||||
*
|
|
||||||
* @param Document $document
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function deleteDocument(Document $document): bool
|
public function deleteDocument(Document $document): bool
|
||||||
{
|
{
|
||||||
// DBから削除(ソフトデリート)
|
|
||||||
return $document->delete();
|
return $document->delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 全文検索
|
* Locale-agnostic full-text search; returns distinct documents.
|
||||||
*
|
|
||||||
* @param string $query
|
|
||||||
* @param int $limit
|
|
||||||
* @return \Illuminate\Database\Eloquent\Collection
|
|
||||||
*/
|
*/
|
||||||
public function search(string $query, int $limit = 20)
|
public function search(string $query, int $limit = 20)
|
||||||
{
|
{
|
||||||
return Document::search($query)
|
$documentIds = DocumentTranslation::query()
|
||||||
->limit($limit)
|
->search($query)
|
||||||
->get();
|
->limit($limit * 5) // overscan to allow distinct collapse
|
||||||
|
->pluck('document_id')
|
||||||
|
->unique()
|
||||||
|
->values()
|
||||||
|
->take($limit);
|
||||||
|
|
||||||
|
if ($documentIds->isEmpty()) {
|
||||||
|
return Document::query()->whereRaw('1 = 0')->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Document::with('translations')
|
||||||
|
->whereIn('id', $documentIds)
|
||||||
|
->get()
|
||||||
|
->sortBy(fn ($d) => $documentIds->search($d->id))
|
||||||
|
->values();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByTitle(string $title, ?string $locale = null): ?Document
|
||||||
|
{
|
||||||
|
return (new WikiLinkResolver())->resolve($title, $locale ?: App::getLocale());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* ディレクトリツリーを生成
|
|
||||||
*
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function getDirectoryTree(): array
|
public function getDirectoryTree(): array
|
||||||
{
|
{
|
||||||
$documents = Document::orderBy('path')->get();
|
$documents = Document::with('translations')->orderBy('path')->get();
|
||||||
|
|
||||||
$tree = [];
|
$tree = [];
|
||||||
|
|
||||||
foreach ($documents as $document) {
|
foreach ($documents as $document) {
|
||||||
$parts = explode('/', $document->path);
|
$parts = explode('/', $document->path);
|
||||||
$current = &$tree;
|
$current = &$tree;
|
||||||
|
|
||||||
foreach ($parts as $index => $part) {
|
foreach ($parts as $index => $part) {
|
||||||
$isFile = ($index === count($parts) - 1);
|
$isFile = ($index === count($parts) - 1);
|
||||||
|
|
||||||
if ($isFile) {
|
if ($isFile) {
|
||||||
// ファイル
|
|
||||||
if (!isset($current['_files'])) {
|
|
||||||
$current['_files'] = [];
|
|
||||||
}
|
|
||||||
$current['_files'][] = [
|
$current['_files'][] = [
|
||||||
'name' => $part,
|
'name' => $part,
|
||||||
'document' => $document,
|
'document' => $document,
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
// ディレクトリ
|
|
||||||
if (!isset($current[$part])) {
|
if (!isset($current[$part])) {
|
||||||
$current[$part] = [];
|
$current[$part] = [];
|
||||||
}
|
}
|
||||||
@@ -146,67 +188,28 @@ public function getDirectoryTree(): array
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $tree;
|
return $tree;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* ユーザーの最近閲覧したドキュメントを取得
|
|
||||||
*
|
|
||||||
* @param int $userId
|
|
||||||
* @param int $limit
|
|
||||||
* @return \Illuminate\Database\Eloquent\Collection
|
|
||||||
*/
|
|
||||||
public function getRecentDocuments(int $userId, int $limit = 10)
|
public function getRecentDocuments(int $userId, int $limit = 10)
|
||||||
{
|
{
|
||||||
return RecentDocument::getRecentForUser($userId, $limit);
|
return RecentDocument::getRecentForUser($userId, $limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* ドキュメント閲覧を記録
|
|
||||||
*
|
|
||||||
* @param Document $document
|
|
||||||
* @param int $userId
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function recordDocumentAccess(Document $document, int $userId): void
|
public function recordDocumentAccess(Document $document, int $userId): void
|
||||||
{
|
{
|
||||||
RecentDocument::recordAccess($userId, $document->id);
|
RecentDocument::recordAccess($userId, $document->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 指定タイトルのドキュメントを検索
|
|
||||||
*
|
|
||||||
* @param string $title
|
|
||||||
* @return Document|null
|
|
||||||
*/
|
|
||||||
public function findByTitle(string $title): ?Document
|
|
||||||
{
|
|
||||||
return Document::where('title', $title)
|
|
||||||
->orWhere('slug', SlugHelper::generate($title))
|
|
||||||
->first();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 被リンク(バックリンク)を取得
|
|
||||||
*
|
|
||||||
* @param Document $document
|
|
||||||
* @return \Illuminate\Database\Eloquent\Collection
|
|
||||||
*/
|
|
||||||
public function getBacklinks(Document $document)
|
public function getBacklinks(Document $document)
|
||||||
{
|
{
|
||||||
return $document->incomingLinks()
|
return $document->incomingLinks()
|
||||||
->with('sourceDocument')
|
->with('sourceDocument.translations')
|
||||||
->get()
|
->get()
|
||||||
->pluck('sourceDocument')
|
->pluck('sourceDocument')
|
||||||
->filter();
|
->filter();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 壊れたリンク(未作成ページへのリンク)を取得
|
|
||||||
*
|
|
||||||
* @return \Illuminate\Database\Eloquent\Collection
|
|
||||||
*/
|
|
||||||
public function getBrokenLinks()
|
public function getBrokenLinks()
|
||||||
{
|
{
|
||||||
return DB::table('document_links')
|
return DB::table('document_links')
|
||||||
@@ -217,39 +220,13 @@ public function getBrokenLinks()
|
|||||||
->get();
|
->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* タイトルからパスとスラッグを生成
|
|
||||||
* タイトルに含まれる / をディレクトリ区切りとして扱う
|
|
||||||
*
|
|
||||||
* 例: "Laravel/Livewire/Components"
|
|
||||||
* → path = "Laravel/Livewire/Components.md"
|
|
||||||
* → slug = "components" (最後のコンポーネントから生成)
|
|
||||||
*
|
|
||||||
* @param string $title
|
|
||||||
* @param int|null $excludeDocumentId 更新時に除外するドキュメントID
|
|
||||||
* @return array [path, slug]
|
|
||||||
*/
|
|
||||||
private function generatePathAndSlug(string $title, ?int $excludeDocumentId = null): array
|
private function generatePathAndSlug(string $title, ?int $excludeDocumentId = null): array
|
||||||
{
|
{
|
||||||
// タイトルをそのままパスとして使用(.md拡張子を追加)
|
|
||||||
$basePath = $title . '.md';
|
$basePath = $title . '.md';
|
||||||
|
$baseSlug = SlugHelper::generate(basename($title));
|
||||||
// 最後のパスコンポーネント(スラッシュで区切られた最後の部分)からスラッグを生成
|
|
||||||
$lastComponent = basename($title);
|
|
||||||
$baseSlug = SlugHelper::generate($lastComponent);
|
|
||||||
|
|
||||||
// ユニークなパスとスラッグを生成
|
|
||||||
return $this->ensureUniquePath($basePath, $baseSlug, $excludeDocumentId);
|
return $this->ensureUniquePath($basePath, $baseSlug, $excludeDocumentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* パスとスラッグがユニークになるように調整
|
|
||||||
*
|
|
||||||
* @param string $basePath
|
|
||||||
* @param string $baseSlug
|
|
||||||
* @param int|null $excludeDocumentId
|
|
||||||
* @return array [path, slug]
|
|
||||||
*/
|
|
||||||
private function ensureUniquePath(string $basePath, string $baseSlug, ?int $excludeDocumentId = null): array
|
private function ensureUniquePath(string $basePath, string $baseSlug, ?int $excludeDocumentId = null): array
|
||||||
{
|
{
|
||||||
$path = $basePath;
|
$path = $basePath;
|
||||||
@@ -261,46 +238,17 @@ private function ensureUniquePath(string $basePath, string $baseSlug, ?int $excl
|
|||||||
->where(function ($q) use ($path, $slug) {
|
->where(function ($q) use ($path, $slug) {
|
||||||
$q->where('path', $path)->orWhere('slug', $slug);
|
$q->where('path', $path)->orWhere('slug', $slug);
|
||||||
});
|
});
|
||||||
|
|
||||||
if ($excludeDocumentId) {
|
if ($excludeDocumentId) {
|
||||||
$query->where('id', '!=', $excludeDocumentId);
|
$query->where('id', '!=', $excludeDocumentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$query->exists()) {
|
if (!$query->exists()) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
$counter++;
|
$counter++;
|
||||||
// パス: "title.md" → "title-2.md"
|
|
||||||
$path = preg_replace('/\.md$/', "-{$counter}.md", $basePath);
|
$path = preg_replace('/\.md$/', "-{$counter}.md", $basePath);
|
||||||
// スラッグ: "title" → "title-2"
|
|
||||||
$slug = $baseSlug . '-' . $counter;
|
$slug = $baseSlug . '-' . $counter;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [$path, $slug];
|
return [$path, $slug];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 初期ドキュメントを作成
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function createInitialDocuments(): void
|
|
||||||
{
|
|
||||||
// ホームページ
|
|
||||||
$this->createDocument(
|
|
||||||
'Home',
|
|
||||||
"# Welcome to Knowledge Base\n\nThis is your personal knowledge base powered by Markdown.\n\n## Getting Started\n\n- Create new documents using [[wiki-links]]\n- Use Ctrl+K for quick switching\n- Full-text search is available\n\n## Example Links\n\n- [[Getting Started]]\n- [[Documentation]]\n- [[Notes]]",
|
|
||||||
null,
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
// Getting Startedページ
|
|
||||||
$this->createDocument(
|
|
||||||
'Getting Started',
|
|
||||||
"# Getting Started\n\nLearn how to use this knowledge base.\n\n## Creating Documents\n\nClick on any [[wiki-link]] to create a new document.\n\n## Editing\n\nClick the edit button to modify content.\n\nBack to [[Home]]",
|
|
||||||
null,
|
|
||||||
null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit\Services;
|
||||||
|
|
||||||
|
use App\Models\Document;
|
||||||
|
use App\Models\DocumentTranslation;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Services\DocumentService;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\App;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class DocumentServiceTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
private DocumentService $service;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
$this->service = new DocumentService();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_create_document_creates_one_translation_in_given_locale(): void
|
||||||
|
{
|
||||||
|
App::setLocale('en'); // ensure deterministic
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
$doc = $this->service->createDocument('Hello', '# Hi', $user->id, 'en');
|
||||||
|
|
||||||
|
$this->assertSame('en', $doc->default_locale);
|
||||||
|
$this->assertSame('Hello.md', $doc->path);
|
||||||
|
$this->assertSame('hello', $doc->slug);
|
||||||
|
$this->assertCount(1, $doc->translations);
|
||||||
|
$this->assertSame('Hello', $doc->translations->first()->title);
|
||||||
|
$this->assertSame('# Hi', $doc->translations->first()->content);
|
||||||
|
$this->assertStringContainsString('<h1>', $doc->translations->first()->rendered_html);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_update_document_in_default_locale_regenerates_path_and_slug(): void
|
||||||
|
{
|
||||||
|
$doc = $this->service->createDocument('Old', 'body', null, 'en');
|
||||||
|
|
||||||
|
$updated = $this->service->updateDocument($doc, 'New Title', 'body2', null, 'en');
|
||||||
|
|
||||||
|
$this->assertSame('New Title.md', $updated->path);
|
||||||
|
$this->assertSame('new-title', $updated->slug);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_update_document_in_non_default_locale_does_not_change_path(): void
|
||||||
|
{
|
||||||
|
$doc = $this->service->createDocument('English', 'body', null, 'en');
|
||||||
|
$originalPath = $doc->path;
|
||||||
|
$originalSlug = $doc->slug;
|
||||||
|
|
||||||
|
$updated = $this->service->updateDocument($doc, '日本語タイトル', '本文', null, 'ja');
|
||||||
|
|
||||||
|
$this->assertSame($originalPath, $updated->path);
|
||||||
|
$this->assertSame($originalSlug, $updated->slug);
|
||||||
|
$this->assertSame('日本語タイトル', $updated->translationFor('ja', false)->title);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_add_translation_creates_new_locale_row(): void
|
||||||
|
{
|
||||||
|
$doc = $this->service->createDocument('Hello', 'body', null, 'en');
|
||||||
|
|
||||||
|
$this->service->addTranslation($doc, 'ja', 'こんにちは', 'やあ', null);
|
||||||
|
|
||||||
|
$this->assertCount(2, $doc->fresh()->translations);
|
||||||
|
$this->assertSame('こんにちは', $doc->fresh()->translationFor('ja', false)->title);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_add_translation_throws_on_duplicate_locale(): void
|
||||||
|
{
|
||||||
|
$doc = $this->service->createDocument('Hello', 'body', null, 'en');
|
||||||
|
|
||||||
|
$this->expectException(\InvalidArgumentException::class);
|
||||||
|
$this->service->addTranslation($doc, 'en', 'X', 'Y', null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_delete_translation_removes_non_default(): void
|
||||||
|
{
|
||||||
|
$doc = $this->service->createDocument('Hello', 'body', null, 'en');
|
||||||
|
$this->service->addTranslation($doc, 'ja', 'こんにちは', 'やあ', null);
|
||||||
|
|
||||||
|
$this->service->deleteTranslation($doc, 'ja');
|
||||||
|
|
||||||
|
$this->assertNull($doc->fresh()->translationFor('ja', false));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_delete_translation_refuses_default_locale(): void
|
||||||
|
{
|
||||||
|
$doc = $this->service->createDocument('Hello', 'body', null, 'en');
|
||||||
|
|
||||||
|
$this->expectException(\InvalidArgumentException::class);
|
||||||
|
$this->service->deleteTranslation($doc, 'en');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_set_default_locale_requires_existing_translation(): void
|
||||||
|
{
|
||||||
|
$doc = $this->service->createDocument('Hello', 'body', null, 'en');
|
||||||
|
|
||||||
|
$this->expectException(\InvalidArgumentException::class);
|
||||||
|
$this->service->setDefaultLocale($doc, 'ja');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_set_default_locale_regenerates_path_from_new_locale_title(): void
|
||||||
|
{
|
||||||
|
$doc = $this->service->createDocument('Hello', 'body', null, 'en');
|
||||||
|
$this->service->addTranslation($doc, 'ja', 'こんにちは', 'やあ', null);
|
||||||
|
|
||||||
|
$updated = $this->service->setDefaultLocale($doc, 'ja');
|
||||||
|
|
||||||
|
$this->assertSame('ja', $updated->default_locale);
|
||||||
|
$this->assertSame('こんにちは.md', $updated->path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_search_returns_distinct_documents_across_locales(): void
|
||||||
|
{
|
||||||
|
$doc = $this->service->createDocument('Searchword', 'body', null, 'en');
|
||||||
|
$this->service->addTranslation($doc, 'ja', 'Searchword JA', 'Searchword body', null);
|
||||||
|
|
||||||
|
$results = $this->service->search('Searchword');
|
||||||
|
|
||||||
|
$this->assertCount(1, $results); // distinct, even though 2 translations match
|
||||||
|
$this->assertSame($doc->id, $results->first()->id);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user